diff --git a/Android.mk b/Android.mk index 566b36fc1aba2c044c8aefd98e2bdcf32a8eb163..949216762768987c8c513620f02fe6879f9b2340 100644 --- a/Android.mk +++ b/Android.mk @@ -100,9 +100,11 @@ LOCAL_SRC_FILES += \ core/java/android/bluetooth/IBluetoothCallback.aidl \ core/java/android/bluetooth/IBluetoothHeadset.aidl \ core/java/android/bluetooth/IBluetoothPbap.aidl \ + core/java/android/content/IClipboard.aidl \ core/java/android/content/IContentService.aidl \ core/java/android/content/IIntentReceiver.aidl \ core/java/android/content/IIntentSender.aidl \ + core/java/android/content/IOnPrimaryClipChangedListener.aidl \ core/java/android/content/ISyncAdapter.aidl \ core/java/android/content/ISyncContext.aidl \ core/java/android/content/ISyncStatusObserver.aidl \ @@ -129,7 +131,6 @@ LOCAL_SRC_FILES += \ 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 \ core/java/android/view/IApplicationToken.aidl \ @@ -159,6 +160,9 @@ 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 \ + core/java/com/android/internal/widget/IRemoteViewsFactory.aidl \ + location/java/android/location/ICountryDetector.aidl \ + location/java/android/location/ICountryListener.aidl \ location/java/android/location/IGeocodeProvider.aidl \ location/java/android/location/IGpsStatusListener.aidl \ location/java/android/location/IGpsStatusProvider.aidl \ @@ -348,7 +352,7 @@ framework_docs_LOCAL_JAVA_LIBRARIES := \ framework \ framework_docs_LOCAL_MODULE_CLASS := JAVA_LIBRARIES -framework_docs_LOCAL_DROIDDOC_HTML_DIR := $(LOCAL_PATH)/docs/html $(OUT_DOCS)/gen +framework_docs_LOCAL_DROIDDOC_HTML_DIR := docs/html # The since flag (-since N.xml API_LEVEL) is used to add API Level information # to the reference documentation. Must be in order of oldest to newest. framework_docs_LOCAL_DROIDDOC_OPTIONS := \ @@ -361,8 +365,9 @@ framework_docs_LOCAL_DROIDDOC_OPTIONS := \ -since ./frameworks/base/api/6.xml 6 \ -since ./frameworks/base/api/7.xml 7 \ -since ./frameworks/base/api/8.xml 8 \ - -werror -hide 13 \ - -overview $(LOCAL_PATH)/core/java/overview.html + -since ./frameworks/base/api/current.xml HC \ + -werror -hide 113 \ + -overview $(LOCAL_PATH)/core/java/overview.html framework_docs_LOCAL_ADDITIONAL_JAVA_DIR:= $(call intermediates-dir-for,JAVA_LIBRARIES,framework) @@ -420,7 +425,9 @@ web_docs_sample_code_flags := \ -samplecode $(sample_dir)/WiktionarySimple \ resources/samples/WiktionarySimple "Wiktionary (Simplified)" \ -samplecode $(sample_dir)/VoiceRecognitionService \ - resources/samples/VoiceRecognitionService "Voice Recognition Service" + resources/samples/VoiceRecognitionService "Voice Recognition Service" \ + -samplecode $(sample_dir)/XmlAdapters \ + resources/samples/XmlAdapters "XML Adapters" ## SDK version identifiers used in the published docs # major[.minor] version for current SDK. (full releases only) @@ -455,11 +462,13 @@ LOCAL_DROIDDOC_OPTIONS:=\ -nodocs LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR:=build/tools/droiddoc/templates-sdk -LOCAL_DROIDDOC_CUSTOM_ASSET_DIR:=assets-sdk + +LOCAL_UNINSTALLABLE_MODULE := true include $(BUILD_DROIDDOC) -$(full_target): $(framework_built) +# $(gen), i.e. framework.aidl, is also needed while building against the current stub. +$(full_target): $(framework_built) $(gen) $(INTERNAL_PLATFORM_API_FILE): $(full_target) $(call dist-for-goals,sdk,$(INTERNAL_PLATFORM_API_FILE)) @@ -482,7 +491,8 @@ LOCAL_DROIDDOC_OPTIONS:=\ -parsecomments LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR:=build/tools/droiddoc/templates-sdk -LOCAL_DROIDDOC_CUSTOM_ASSET_DIR:=assets-sdk + +LOCAL_UNINSTALLABLE_MODULE := true include $(BUILD_DROIDDOC) @@ -513,14 +523,13 @@ LOCAL_DROIDDOC_OPTIONS:=\ -proofread $(OUT_DOCS)/$(LOCAL_MODULE)-proofread.txt \ -todo $(OUT_DOCS)/$(LOCAL_MODULE)-docs-todo.html \ -sdkvalues $(OUT_DOCS) \ - -hdf android.whichdoc offline + -hdf android.whichdoc offline ifeq ($(framework_docs_SDK_PREVIEW),true) LOCAL_DROIDDOC_OPTIONS += -hdf sdk.preview true endif LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR:=build/tools/droiddoc/templates-sdk -LOCAL_DROIDDOC_CUSTOM_ASSET_DIR:=assets-sdk include $(BUILD_DROIDDOC) @@ -533,7 +542,6 @@ $(static_doc_index_redirect): \ $(full_target): $(static_doc_index_redirect) $(full_target): $(framework_built) - # ==== docs for the web (on the google app engine server) ======================= include $(CLEAR_VARS) @@ -557,12 +565,11 @@ LOCAL_DROIDDOC_OPTIONS:= \ -hdf template.showLanguageMenu true LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR:=build/tools/droiddoc/templates-sdk -LOCAL_DROIDDOC_CUSTOM_ASSET_DIR:=assets-sdk include $(BUILD_DROIDDOC) # explicitly specify that online-sdk depends on framework-res and any generated docs -$(full_target): framework-res-package-target $(ALL_GENERATED_DOCS) +$(full_target): framework-res-package-target # ==== docs that have all of the stuff that's @hidden ======================= include $(CLEAR_VARS) @@ -579,11 +586,10 @@ LOCAL_ADDITIONAL_DEPENDENCIES:=$(framework_docs_LOCAL_ADDITIONAL_DEPENDENCIES) LOCAL_MODULE := hidden LOCAL_DROIDDOC_OPTIONS:=\ $(framework_docs_LOCAL_DROIDDOC_OPTIONS) \ - -title "Android SDK - Including hidden APIs." -# -hidden + -title "Android SDK - Including hidden APIs." +# -hidden LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR:=build/tools/droiddoc/templates-sdk -LOCAL_DROIDDOC_CUSTOM_ASSET_DIR:=assets-sdk include $(BUILD_DROIDDOC) @@ -593,10 +599,14 @@ include $(BUILD_DROIDDOC) ext_dirs := \ ../../external/nist-sip/java \ ../../external/apache-http/src \ - ../../external/tagsoup/src + ../../external/tagsoup/src \ + ../../external/libphonenumber/java/src ext_src_files := $(call all-java-files-under,$(ext_dirs)) +ext_res_dirs := \ + ../../external/libphonenumber/java/src + # ==== the library ========================================= include $(CLEAR_VARS) @@ -604,6 +614,7 @@ LOCAL_SRC_FILES := $(ext_src_files) LOCAL_NO_STANDARD_LIBRARIES := true LOCAL_JAVA_LIBRARIES := core +LOCAL_JAVA_RESOURCE_DIRS := $(ext_res_dirs) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := ext diff --git a/CleanSpec.mk b/CleanSpec.mk index 5618eaad20b2139f2a287f24938a22abe52d460e..e5f1f90e46153fa7f4d02fdeee953ec1b60ca2a8 100644 --- a/CleanSpec.mk +++ b/CleanSpec.mk @@ -67,7 +67,20 @@ $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/SHARED_LIBRARIES/libequalizerte $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/SHARED_LIBRARIES/libreverb_intermediates) $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/SHARED_LIBRARIES/libreverbtest_intermediates) $(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/lib/soundfx/) +$(call add-clean-step, find . -type f -name "*.rs" -print0 | xargs -0 touch) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/SHARED_LIBRARIES/libandroid_runtime_intermediates) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/lib/libandroid_runtime.so) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/symbols/system/lib/libandroid_runtime.so) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/lib/libandroid_runtime.so) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/SHARED_LIBRARIES/libhwui_intermediates) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/lib/libhwui.so) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/symbols/system/lib/libhwui.so) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/lib/libhwui.so) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/os/storage/*) +$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/content/IClipboard.P) +$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/telephony/java/com/android/internal/telephony/ITelephonyRegistry.P) +$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android_stubs_current_intermediates) +$(call add-clean-step, rm -rf out/target/common/docs/api-stubs*) # ************************************************ # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST diff --git a/api/9.xml b/api/9.xml index 9ff1287595670fbbed799d2659e927b7a2624b4f..3e7653c8f2e59396e2b01da0d16f7211c292deaf 100644 --- a/api/9.xml +++ b/api/9.xml @@ -15845,17 +15845,6 @@ visibility="public" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + - - + - - - - + - - + - - - - + - - + + - + + - + + - + + - + - - - - + + + + + + + - + - - + + - - + - + - + + - + + - + + - + - - - - + - - - - + + - + + - - - - - - - - - - + - - + - - - - - - + - + + - + - - - - - + - - + - + + - - + - + - - - - + + - - - - + - + - + - + - + - - + + - - - - + - + - + + + - + - - - - + + + + - + - + + + - + - - - - + + - + - + - - + - - - + - + - - + - - + + + + + - - + + + + + - + - + - - + - + - + - - + - - - + - + - - + - - - - - - - - - - - - - - - - - - - - + - - + - - - - + - + - - - - - - + + + - - + - + - - - - + - + - - + - - - - - - + - + + - - + - + + - - - + - + + + + - + + - + + + + - - + - + - - + + + - - + + + - - - + - - + + + + + + - - - - - + + + + + + + - + + + + + + - + + - - + - + - + - - + + + + + - + - - + + + + + - - - - - - - + - + - - - - + + - + - - + - + - + - - + - - - - - - - - - - - - - - - - - - - - - + - - + - - + - - + - - + - + - - - - + + - - - - + - + + + + + - + - - - - - + - + + + - + - + + + + - - + + + - + - - - - - + - - - - - - - + + - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - + - - + - - + - - - - - + + + - - + - - + - - - - + - - + - - - + + + - - + + + - - - - - + - - - - + + + + + - - - - - - + - - + + + + + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + - - + - - + - - - - - - - + + + - - + + + + + + + - - + + + - - + - + + + - - - - - + + + - - + - - - + + + - - - + + + - - + + + - - + - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + + + + + - - - - - - - - - - - - - - - - - + - - + - - - - - - + - - + + + - - - - - + - - + + + - - + - - - - + - - - - - - - - - - - - - - - - - - - - + - - - - + + + + + - - + + + + + + - - + + + + + + + + + + - - + - - + + + + + - - + - - + - - + + + + + + + - - - + + + - - - + + + - - + + + + + - - + - - + - + + + - - - - - - - - - - - - - + + + - - + + + - - + + + - - + - - + + + - - + + + + + - - + + + - - + - - + - - + - - + - - + - - + - - - + - - - - - + - - - - - - - - - - - - - + - - + - - + - - + - - + + + - - + - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + - - - - - + - + - + + + + + - - - - - - + - - + - + + + - - - - + - - - - - - - - - - - - + - + - - - - + - - - - - + - - + - - + - - + + + + + - - - - - - - - - - - - - + - - - - + - - + + + + + - - + - + - - + - - - - + - + - - + + + + + - - + + + + + + + - - - - - - - - + - + - - + - + - - + + + + + - - + + + - - - - - - - - - - - + - + - - - - - - + - + - - - - + - - - - + - - - - + - - + - + - - - - - + - - - - - + - + - - + - + + + - - - - - - + - - - - + - + - + - - - - - - - - - - - - - + - - - - - - - - + - - - - + - - - - - - - - - + - - + + + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - + + + - - + + + - - + - - + + - + + - + + - - - + + - + + + + - + + - + + + + - + + + + - + + + + - + + + + - - + - - + - - + - - - - - - + - - + - - - - + - - + - + + - - + - + - - - - + - - - - - - + - - - - + - - - - + - - - - + - - + - - + - - + - + - - + - - - - + - + - - + + + + + - + + + + + + - + + + + - + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + - - + - - + - - + + + + + + + - - + - - + - + + + + - - - - - - + - - - - + - - - - + - - - - + + + + + + + + + + + + + + - - - - - - - - + - - - + - - + - - - - - - - - + - - - - - - - - - - - - - - - - - + - - + - - - - - + + + + + - - - - - + + + - - + + + - - + + + - - - + - - + + + - - - - - - - - + - - - + + - + + + + - + + - - + - - - - - - - - - - - - + - - - - + + - - - - + + - - - - + - - - - - - - - + - - - - - - + + + - - - - - - - - - + - - - - - - - - + - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - + - - - - + + - - - - + + - - - - + - - - - - + + - - + - + - - - + - - + - - + - - + + - - + + - - - - - - - - + - + - + - + - - + - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - + - - + - - - - - - + - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - - - + - - - - + - - - - + - - + - - - - + - - + - - - - + - - + + - - + + - - + - + + - - + + + - - - + + + + + + + + + - - - - + - - - - - - + - - - - + - - - - - - - - + - - - - - - - - + - - + - - + - - + - - + - - + - - + - - - + - - + + - - - - - - + + - - - + + + + + + + - - - - + - - - - + - - - - + - - - + + + + + + + - - - + - + + + + + + - - + + + + + + + - - - + + + + + + + + + + + - + + - - - + + + + + + + + + + + + + + + + + + - + + + + - - - + - + + + + + + + + + + + - + + - - - - - + + + + + - + + + + + + - - + + + - + + + + - - - + + + + + - - + + + + + - + + + - - - + + + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - + + + + + + + + + + + - + - + + + + + + + - - + + + + - + + - + + + + + + - - - - - - - - + - - + - - + - - - + + + - + - + - - + - + - + - - + - - + - - + + + + + - + + + + + + - + + + + + + + + - - + + + - + + + + - + + + + - + + + + - + + - - - - + - - - - - - - - - + + + - - + + + + + + + + + - - + + + + + - - + + + + + + + - - - - + - + - + - - + + + + + - - + - - + - - - + + + - + - - - - - + + + + - - - - - - - - + - - + - - + + + + + + + + - + - + - + - - + + + + + + + + + + + + + + + + + - + - + - - - + + + + + + + + + - - + + + + + + + + - - - - - - + + + + + + + + + + - - + + + + + + + + - - + + + - - + + + + + - - + + + + + + + - - + + + + + - - + - - + - - + - - + + + - - + + + - - + + + - - + + + - - + + + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - + - + - + - - - - + - + + - - + - + + + - - - - - - + - - - - - + + + + + + + - - - - - - - + - + - - - - - - + - + - + + + + + + - - - - - - + - + - + + + + - - + + + - - - + + - - + - - - - - - + - + - - - - - - - - + - - - - - - - - + - + - + - - - - - - + - - - - - - - - + - - + - - + + + - - + - - - + + + - + + + - - + + + - + - - + - + - - - + - + - - - - - - - - - + - - - + + + - + - - + - - - + - - + + + - + + - + + - - + + + - - + - + + + + - + + + + - - + + + - - + + + - - + - - + - - + - - + - - + - - - - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - - - - - - + + + - + + + - - + + - - - - - - - + + + + - - + - - + - - - - - - - - - - + - + + - - - - + - + - - - - - - - - - - - - - - - - - - - - - + + + + + - - + + + + + - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + + + + + - - + - - + - - + - - - + - + + + + + + + + + + + - - - + + + + + - - - - - + + + + + + + + + - + + - + - - + + + + + + + + + + + + + + - + + - - - - - - - - - - - - + + + + - + + + + + + - - - - - - + + - + + + + + + - - - - + - - - - + + + - - - + + + - - + + + - + - + - + - + + + + + + - - + + + - - + + + + + - - - - - + - - + - + - - - - - - - - - + - - + - - - - - - - + - - - + - - - - - - - - - - - - - - + - - + + + - - - + + + - + + + - - + + + + + + + - + + - - + + + - - + - + - - - + + + + + + + + - - - - - - - - - - - - - + - + + + - + - + - - - - - - - + - + - + + + + + - - - - + - - - - - - + - + - - + - - + - - - - + + + + + + + + - + + - - + - - - + + + - - + + + + + - - + - - + - - + + + + + - - + + + + + - - + + + + + + + + + + + - + - - - - - - + - - - - - - - - - - + - + + - - - - - + - - + + + + - + + + + - + + + + + + - + + - - - - - - - + - - - - - - - + - - + + + - - + - + + + - - - - - + + + - - + + - - + - + + - + + + + - + + - + + - - + - + + + + + + + + + + + + + - - - - + + + - - - - - - - - + - + - + - - + + + + + - - - - + + + - - - - + - - - - + - + - - - - - - + - - + - + - - - - - - - - - - - - - + + + - + - + - - - - - - + + + - - + - + - + + - + + - + + - + + - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + + + + - + + + + + - - - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + - - + - + + + + + + + + + - - + - + + + + + + + - - - - + - - - - - - - + - + - - - - - - - - - - - - - - - + - - - + + + - + + + + + - + + - - + - - + - - + + + - - + - - + - - - + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + - - + - - + - - + - - + - - + - - + - + + + - - - - + + + + - - - - - - - - - - - + + + + + + + + - - - - - - + - - - - - + + + + - - - - - + - - - - - - - - - - - - - - - + - - - - - - + + - - - - - + - - - + + + + - - - - - + + - - - + + + - - - + + - - - - - - - - - - - + - - - + - - - + - + - - - - - - - - + - - - - + - - - - - + + + + + + + + - - - - - - - - - - - - - - - + - + - + - - + + + + + - - - - - + - - + - - + + - - - - - + - - + - - - - - - + + + + + + + - - + - - - - - + + + - - - + - - + - - - - - + - - + - - - - - - - + - - + + + + + + + - - + - + - + - + + + + + + + + + + - - + + + - + + + + + + + + - - - - + - + - - + + + - + - + + + + + + + + + + - + + - - - + + + - - - - + - + - - + + + - + + - + + - - - - - - - - + - - - - - - + - - - - - - - - - + + + - + + + + + + + + + + - - + - - - - + - - + - + + + + + - - - - + - - - + + + - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + - - + - - - - - - + - - - - + - - - - + - - - - - - + - - - - - - - - - + - - - + - - + - - - - + - - + - - - - - - + - - - - - - - - - - - - + + - - + + + - - + + + + - + + + + + + - + + + + - + + + + + + + + + + - + + + + + + + + - - + + + + + + + + + - - + + - + + - + + + + - - + - + - + + + - - + + + + + + + - + + + + + + + + + + + + - - - + + + + + - - + + + + + - - - - - + - - - - + - - + + + - - + + + + - - - - + + + + + + + + + + + + - - - - + + + - - + + + + + - - + - - + - - + - - + + + - - - + + + - - - + - + + - + + - - + - - - - + - - - - + + + - + + + - - + + + - - + + + + + + + - - + + + + + + + + + - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - + - - + + + + - - + + + - - + - - + + + - - + + + - - + + + - - + + + - - + + + + + + + + + + + - - + - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + - - - + - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - + - - - - - - + - - - - - - - - - - - - - - + + + + + + - - + + - - + - - - + + + + + - + - + - - + - + - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - + - - - - - - + - + - + - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - + - + - - - + + + - - + - - + + + - - + + + - - - - - - - - - - - - - + - - + + + - + + - + - - + + + + - - + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + - - + + + + + - + + + + - + - - - + - + + + - + - - - - - - - + - - + - - - - - - - - - - - - + - - - - - - + - + - - - - + - - - + - - - - - - - + - - - - - - + - - - - - - + + + + + + - - - - - - + + - + + + + + + + - - - + + + - - - - - - + + + + - - + + + - + + - - + - - - - + - + - + - - - - - - + + + - + - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - + - + + + + + + + + + - + + + + - - + + + - + + + + - + + + + - - + - + + + - - - - + - + - - - + + + + + + + + + - + + + + - + - + - - + + + - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - + - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - - - - - - - + - - - - - - + - - - - - - + - - - - + - + - - - - + - - + - + - - + - + - - + - + - - + + + - - + + + - - - - - - - - - - - - - - + - - - + - - - - + - + - - - - - - + - - - - - - - - - - + - - + - - + - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - - + + - - - - + - + + - - - - + - + + - - - - + - - + - - - - + - - - - - - - - - + - - - - - - - - - - - + - - + - + - - - - - - + - - - - - + - - - - - + - - - - + - + - - - - - - - - - - + - + - - - - + - + - - - - + - + - - + + + - - - - + - + - - - - + - + - - + + + - - - - + - + - - + - + - - - - - - - - - - + - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + - + + + + + - - - - - - - - - - - - - + + + + + + + + + + - - - - - - + - - + + + + + + + + + + + - - + + + + + + + - + - - - - - - - - - + + + + + + + - - - + + - - - + + - + - - - - - - - - - + + - - - - + - - - - - + - + + + - - + + + - + - - - - - - - + - + + + - - - - + - - - - + - + - - - + + - - + - - - - - - - - + - - + - + - + + + + - - + + + - + + + + + - + + - + - - - - - + - - - - - - - - - - + - + - - + - - - - + - + - - - - + + - + - + - - + - + - + - + - - - - - - - - + - + + - - - - - - - - - - - - - - - - - - - + - + - - - - + - - + - - - + + + - - - - - - - - - - - + - - - - - - - + - + - + - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + + + - - + + + + + + + - - - - - + - - + - + - + + - + - - - - - + + + - + + - + + - - + + - + + + + - + + + + - + + + + - - + - + - - + + + - - + - + - - + + + - - - - - + - - + - - + - - + + + + + + + + + + + + + + + + + + - - + + - - - + - + - + + - - + - - + - + - - - - + - + + + - + - - - - - - - - - - + + - + + + - - - - + - - - - - - - - - - + - + - + + - - + - - - - - - + - - - - - - - - - - + - - - - + - - - - - - + + + + + + + + + + + - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + - - - - + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - + - - + - - + - - + - - + - - + - - + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - - - + + + - + + - + - - - - + + - - + + - + + - + + - + + - - + + + - - - - + + + + + - - - - - - - + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + - - - - - - + - - - - - - - - + - - - - - - - - + - - + - - - - - - + - - - - - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - - - + - - - - - - - - - - - - - - - - + - - - - + - - - - - - - - - - - - - - + - - - - + - - - - - - + - - - - - - + - - - - + - - - - - - - - + - - - - - - - - - - - - - - + - - - - + - - - - + - - - - + - - - - - + - + - - + - - + - - - - - - - + + - + + + - - - - + - + + + + - - - - - - - + + + + + - + + + - - - + - - - - - - - - - + - + + - - - - - - - - - + + - + - + - - - + - + + + + + + + - + - - - - - - + + - + + - - - - + - + - - + - - + + + + + - - - - - - + + + + + + + - - - - - - - - + - - + - - - - + - - - - + - - - - - - + - - - - - - + - - - - - - - - - - + - + + + - - - - + + + + - + + - - - - + - + + - - - - + - - + - + + - - - + - - - - + - - + - - + - - - - + - + + - - + + + + + + + - - + - + + + + + + + + + - - + - - + - - + + + + + - + + - - + + + + + + - - - - - - - + - - + + + + - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + - - - - - - - - - + - + - - - - - - - - - - - - + - - + + - - - - - - + + - - - - - + - - + + + + + + + + + + + - + + + + - - + + + - + + + - - - - - + - + + + + + + + - + - - - - - - + - - + - + - - + + - - - + - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - + - + + - - - - - - + + + - + - + - + - - - - - - - - - + - - + - - - + - + - - - - - - - - - - - - + - - - + - - + - + - + - - + - - - + - - + - - - + - - + - + - + - - + - - - - - + - - - - - - + + - + - + - - + + + + + - - - - - - + - + - + + + + + + + + + + + - - - + + - - + + - - - - - - + + - + + + + + - - + - - + - + - - - - - - - + - + + + + + + + - - + + + - - + + + + + + + - + + + + + + - + + + - - + - + - - - - - + - - - - - - - - - - + + + - - + + + - - + + + - - + - + - - + - + - - + - + - - + - - + - + + - - - - - - - - - - - - + + + - - + + + - - + + + - + + + + - - + + + - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + + + + + + - - + + - - + + + + + + + + + - - + + + - - + - - + - - + + + + + - - + - - + + + + + - - - - + - - - - + - - + + + + - - - - - + - - + + - - + - - + - - + - - + - - + - - + - - + - - + - - + + + + + - - - - - - - - - - - + - - + + - - + - - + + + - - + - - + - - + + + + + - - - - - - - - - - + - - - - + + + + - - - - + + + - - + + + + + + + - - + + + - + - - - - + - - + + + + + + + - - + + + - - + + + + + + + - - + + + + + - - + - - + + + + + + + + + + + - - + + + + + + + + + + + + - - + + - - + - - + + + - - + - - + + + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - - - + - - + - - + - - - - - - + - - - - - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -43557,289 +54083,13 @@ deprecated="not deprecated" visibility="public" > - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - + - - - - + - - + - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - - - - + - - + - - - - - + - - - + - - + - - - - - - + - - - - - - - - - - - - - - - - - - - - + - - - - - - + - - - + - - - - - - - - - - - - - + - + - - + - - - - + - - - + + + - - + + - + - - + + + - - - - + - - - - + - + + - - - - + - - - - + - - - - - - + - + + + - - - - - - - - - + + + + + - + - - - - - - - + - - - - - + - + - - + + - - - - - - - - + + - - - - + - - - + - - + - - - - - - - - - - - - - + - + - - - - - - - - - - - + - - - - + - - - - - - + - - - - - + + - + + - - - - + - - + + + - - - - - - - + - - - - - - + - - - - - - + - - - - - - + - - - - - - + - - - - + - - - - - + + - - - + + + + + + + + + - - + + + + + + + + + - - + + + + + + + - + - + - - + + + - - - - - - - - - - - - - - - - - - - - - + - - - - - - + - - + + + - - - - - - - - - - - - - - + + + + - + + - - - - - - + - - + - - - - - - + - - - - - - + - - + - - - - - + - + - - + + + + + + - - - - - - - + + + + + + + + + + + - + + + + - + + - + + - + + - + + + + - + - + - - - - - - - - - - + - - + + - - + + + - + + + + + - - - - - - + - - + + + - + + - - + - + + + + + - - - + + + - - + + + + + - - + + + + + - - + + + + + - - + + + - - + + + - - + + + + + - - + + + + + - - + + + + + + + - - - + + + + + - + + + + + - + - - - + + + - + - - + - - - - - + + + - - + + + + + - - - - - - + + + - - - + + + - - + + + + + - + - - + - - + - + + + - + + - - - - - - - - - - - - - - - - - - - + + + - - + + + - - + + + + + + + - - + + + - - + + + - - + + + + + - - + + + + + - - + + + + + + + - - + + + - - + + + + + + + - - + + + + + + + - - + + + + + + + - - + + + - - + + + + + + + - - + + + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + + + - - + - - + - - + + + + + + + - - + + + + + + + - - + + + - - + - - + + + + + - - + + + + + + + - - + + + + + - - + + + + + - - + + + + + + + + + - - + + + + + - - + + + + + + + - - - + + + - - - + + + - - + + + + + - + - - + + + + + + + - - + - + + + - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - + + + - - + + - - + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - + - + - + - + - - - - - - + - - - - - - - - - + - + + + - - + + - - + - - + + + - - - + + + + - - + + + + - - - - + - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + - - - - - - - - - + - - - - - - - + + + - + + - - - - - - + - - - - - + + + - - - - - + + + + - - - + - + - - + + - - + - - + - - - - + + + - + + - - - - - - - - - - + + - - - - + + - + - + + + - - + - - - - - - - - - - - - - - + + + - - - - - - - + + + - - - + + + + + - + + + + - + - - - - + + + - - - + - - - - + - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + + + + - + - - + + + + + - - + + + + + + + - - - - - - - - - - - + + + - - + + + - - + - + - + - - + + + - - + + + - - + - + + + - - + - + - - + - + - + - + + + + + + + + + + - - + + - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - + - - - - + - + - - + - + + + + + - - - - + - - + - + - + + + + + + + + + + - + - + - + + + - - + + + + - - - - - - - - - - - - - - - - + - + + - - + - + + + - - - - - - - - + + + + - - - - - - + - - - + - - + - - - - - - - + - - - - - - - - - + - - - - + - - + + + - - - - + - - - - + - - - - - - - - - - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - + + - - - - + + + + + + - - + + - - - + + + - - - + + + - - + + + - - - - + + + - - - - - + - - + + + + + - - + + + + + - - + + + + + - - + - - + + + + + - - + + + + + + + - - + + + + + + + - - + + + + + - - + + + + + - - + + + + + - - - + + + + + - - - + + + + + + + + + - + - - + + + + + - + - + + + - - - - - + + + + + - - + + + + + - + - - + + + + + + + - - - - + + + - - - + + + + + + + - - - + + + + + - - - + - + - - + + + - + - - + + + + + + + - + + + + + + + + - + + + + + + + + - - - - + + + - - - + - + - - - - - - + + + + + - - - + + + + + - - - + - + - - + + + - + + + - - + + + + + + + - + + + + + + - + + + + - + + - - + + + + + + + + + + + - + + + + + + - + + - + + + + + + - - + - + + + + + - - - + + + + + + + - + + + - - - + - - - - - + + + - + + + + + - + - - - - - + - + + + + + + + + + - + + + + - - + - - - - + + + - - - - + - + + + + + - - - - + - - - - - - - - - + - + - - - - + + + - - - - + - + - - - - - - - + + + + + - - + + + - - - + - + + + + + - - - - - + + - + + - - + - + + + + - - - + + - - - - - - - - - - - + - - - - - - + + + + + + - - - - - - + + + - + - - - - - - - - - - - - - - - - - + - + - - + + + + - - + - - - - - - - + - - - - + + + - - - - - + + + - - + - - - - + - + - - - + - - + - - - - + - - - - + + - + - + + - - + - - - - + - - - - - + - - + - - + - - + - - + - - + + + - - + - - + - - + + + - - + + + - - + - - + - - + - - + + + - - + - - + + + - - + + + + + - - + + + - - + + + - - + - - + + + - - + + + + + - - + + + - - + + + - - - - - - + - - - - + + + + - - - - + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - + + - - + + + - - + + + - - + - - - - + - - + + + - - + + + - + + + - - - - - - - + - - + + - - + - - - + - - + + - + + + - - - - - - - - - + + + - - - + + - - + + + - - - - - - - - - + - - - - - + - - - - - - - - + + + - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - + - - - - - - + - - - - - - - - - + - - - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - - - - - - - + - - - - - - + - - - - + + + - - - - - - + - + - - - - - - + - - - - - - - - + - - + + + + + + + + + + + - + + + + - - + - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + - - - + + + - - - - - - - + + + + - - + - - - - - - - - - - - - + - + - + + + - - - - - - - - - - + - + - - + - + - - + - + - - + + + - - + - + - - - - - - - - - - - + + + - - - + - + - - + - + - - + - + - - + - + - - + + + - + + + + - - + - + - + + - - - - - + + + + + - - + + + + + - - + - + + + - - + + + - + + + + + + - - + - + + - - - - - - - + + + + - - - - + + - + - + + - - - - - - - - - + + + - + - + - + - + - + - + - @@ -53547,8 +63546,8 @@ visibility="public" > - @@ -53575,7 +63574,7 @@ native="false" synchronized="false" static="false" - final="true" + final="false" deprecated="not deprecated" visibility="public" > @@ -53591,26 +63590,13 @@ visibility="public" > - - - - @@ -53621,14 +63607,14 @@ native="false" synchronized="false" static="false" - final="true" + final="false" deprecated="not deprecated" visibility="public" > - + @@ -53658,7 +63644,7 @@ native="false" synchronized="false" static="false" - final="true" + final="false" deprecated="not deprecated" visibility="public" > @@ -53669,7 +63655,7 @@ native="false" synchronized="false" static="false" - final="true" + final="false" deprecated="not deprecated" visibility="public" > @@ -53680,7 +63666,7 @@ native="false" synchronized="false" static="false" - final="true" + final="false" deprecated="not deprecated" visibility="public" > @@ -53691,7 +63677,7 @@ native="false" synchronized="false" static="false" - final="true" + final="false" deprecated="not deprecated" visibility="public" > @@ -53699,30 +63685,6 @@ - - - - - - - - - - - + - + - - - - + - - + - - + + - - - - - + - + - - - + - - - - + + + + - - - - - - - - + - + + + - - + + + - - + + + - - + + + + + - + + + + + + - - + + + - - + + + + + + + - - + + + + + + + - - + + + + + - - + + + + + - - + + + + + - - - - - - + - - - + - - - - - - - + + + - - - + + + + + + + + + - + + + + + - - + + + + + - - + + + + + + + - - - - + - - + + + + + + + + + - - + + + + + - - + + + + + - - - - - - + - + + + + + - + + + + + + - - + - + + + + + - - - + + - - + - + - + + + + - - + - + + - - + + + - - + + + - - - - + - + + - + + - - + - + + + + + + - - + + + - - + + + - - + + + + + - + + + + + + + + - - + - - + + + - + + + + - + + - + + + + + + - + + + + - + + + + - + + + + + + + + + + - - + - - - - + - - - - + - - + - - + + - - - + - - + + + + + + + - - + + + - - + + + - + + + + - - + + + - - + - + - - + + + - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + + + - + + - - - - - - + - - + - - + - - - - + + - + - - - - + + + + + + + + + - - - - - + - + + - - - - - - - - - + - - - + - - - + - - - + - + + - - - - + - + + - - - - - - + - - - - - - - - - + - + - - - + + - - + - - - - - - - - - - - + - + + - - - - - - + - - - - - - + - - - - - - + - - - - + - - - - - - + - + + + + - + - - - - - - - + - - - - - - - - - - + + + + - - - - - - - - + - + - - + + + - - - - - - - - - - - - + - + - - + + - - - - + - + - - + + - - - - - - + - + - - + + - - - - - - + - + - - + + - - - - - - + - - + + + + - - - - + - - - - - - - - - + - - - - + - + - - + + - - - + - - + + + + + + + - + - - + + + + + - - - - - - - - - - + - + - - - - - - + - - + + + - + + + + - - - + + + + - - - - - - - - + + + - - - + - - - - - - - - - - - + + + - - - - - - - - + - - - - + - + - + - - - - - - - - - - - - - - - - - - - - + + + - - - - + - + + + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - + - + - + + + - - + - + - + - + - - - - - - - - - - - - - - - - - - - - - + - + - - + - + + + - - + - - - - - + + + + + - - + - + - + + + - - + - + - - + - + - - + + + + + - - + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + - - + - + + + + + + + + + + + + + + + - - + - - + - + + + + + - - - - - - - - - + - + - + + + - - + - + + + + + - - - - + - - - - + - - - - + - - - - + - - - - + - - + - - + + - - + + + + + + + - + + + + + + + + - - - - + - + + + + - - - + - - - - - - + - - - - + + + + + + + + + + + + + + - - - - - + + + + + + + - + - - - - + - + - - + + - - - - - - - - + - + - - + + - - - - - - - - + - + - - + + - - - - + - + - + - - - + + + - + - - - - + - + - - + + - - - - - - - - + - + - - + + - - - - - - - - + - + - + - - + + + + + + + + - - + - - + + + + + + + + + - - - - - - - - - - + - + - - - - + - - + + + - + - + + + + + - - - - - - - - - + + + + + + + - + + + + - + + + + - + + - + + + + - - - + - - + - + - - - + + + - + - + - + - + + + - + - - + + + + + - + + - + + + + - + + + + + + - - + + + + + + - - + - - - - + - + + + - - - - + - - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + - - + - + - + + + + + + + + + + + - + + - - + - - - - - - + - - - - + - + - - - - + - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - + + + + - - - - - - - - - - - - - - - - - + + + - + + - + - + - - + - - - - - - + + + + + + + + + + + + - + + - + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - + + - + - + - + + + - + - + - - + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + - - - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + - - - + + + - - + + + + + - + - - - + - + + + + + - - + + + - + - - - + + + - + - - + + + + + - + - - - + + + - + + + - - + - + - - - + - + + + - - + - + - - - - + - - - + - + - + - - + + + + + - - + - - - + + + - + - - - - - + - - - + - - - + - + + + + + + + - + - + - - + + + + + - + - + - - + - + - + - - + - - - - - - - - - - + - - - - - - - - + + + + + - - - - - - + - - - - - - + - - - - - - + - - - - + - - - - - - + - - - - + - - + + + + - - + + + + + + + + + + + + + + + + - - - - + - - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - + - + - - - - + + - - - - - - - + @@ -62514,16 +72747,6 @@ - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - + + + - - + - + - + - - - + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -78434,7 +88683,7 @@ type="float" transient="false" volatile="false" - value="0.0010f" + value="0.001f" static="true" final="true" deprecated="not deprecated" @@ -80316,6 +90565,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -97825,6 +108291,21 @@ visibility="public" > + + + + + + + + + + @@ -99517,7 +110020,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -99539,7 +110042,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -121369,6 +131872,17 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -131344,7 +142300,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -131357,7 +142313,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -131370,7 +142326,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -131381,11 +142337,131 @@ synchronized="false" static="false" final="false" + deprecated="deprecated" + visibility="public" +> + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -135803,6 +147351,28 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -138265,6 +149922,10 @@ > + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -138731,6 +150478,386 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -139618,7 +151821,7 @@ value="1" static="true" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -139765,6 +151968,17 @@ visibility="public" > + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + @@ -172120,6 +184197,18 @@ deprecated="not deprecated" visibility="public" > + + + + + + @@ -172157,6 +184246,18 @@ deprecated="not deprecated" visibility="public" > + + + + + + @@ -173165,7 +185266,7 @@ abstract="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + - - + - + + + + + + + + + - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -203588,17 +217355,6 @@ visibility="public" > - - + + + + @@ -204191,19 +217960,6 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -206574,6 +220433,17 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + - - - - - + - - + - + - + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - + + + + - - + + + - - + + + - - + + + - - + - - + - + + + + + + + + - - + - - + - - + - + + + - - + - - + - - + - - + + - - + + + - + + + + + + + + + + + + + + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - + - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -413576,7 +429374,7 @@ abstract="true" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -416497,7 +432295,7 @@ abstract="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > : write profiling data to \n" + " -w: wait for instrumentation to finish before returning\n" + "\n" + + " run a test package against an application: am instrument [flags] /\n" + + " -e [,]\n" + + " -w wait for the test to finish (required)\n" + + " -r use with -e perf true to generate raw output for performance measurements\n" + + "\n" + " start profiling: am profile start \n" + " stop profiling: am profile stop\n" + + " dump heap: am dumpheap [flags] \n" + + " -n: dump native heap instead of managed heap\n" + "\n" + " specifications include these flags:\n" + " [-a ] [-d ] [-t ]\n" + @@ -600,7 +655,10 @@ public class Am { " [-e|--es ...]\n" + " [--esn ...]\n" + " [--ez ...]\n" + - " [-e|--ei ...]\n" + + " [--ei ...]\n" + + " [--el ...]\n" + + " [--eia [, [,] [-f ]\n" + " [--grant-read-uri-permission] [--grant-write-uri-permission]\n" + " [--debug-log-resolution]\n" + diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index 7a7f8ed352152b6541f078fceafc962b97457981..9fe1fb8412730f4419fd63a80d36dcbeece90a66 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -49,6 +49,9 @@ #include "BootAnimation.h" +#define USER_BOOTANIMATION_FILE "/data/local/bootanimation.zip" +#define SYSTEM_BOOTANIMATION_FILE "/system/media/bootanimation.zip" + namespace android { // --------------------------------------------------------------------------- @@ -244,12 +247,12 @@ status_t BootAnimation::readyToRun() { mFlingerSurfaceControl = control; mFlingerSurface = s; - 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; + mAndroidAnimation = true; + if ((access(USER_BOOTANIMATION_FILE, R_OK) == 0) || + (access(SYSTEM_BOOTANIMATION_FILE, R_OK) == 0)) { + if ((mZip.open(USER_BOOTANIMATION_FILE) != NO_ERROR) || + (mZip.open(SYSTEM_BOOTANIMATION_FILE) != NO_ERROR)) { + mAndroidAnimation = false; } } diff --git a/cmds/dumpstate/dumpstate.c b/cmds/dumpstate/dumpstate.c index 6e9caaf6867c863172e9023a67393e02167e45d1..a09666e8155f93fffff7e971726f5e41d16ea8ff 100644 --- a/cmds/dumpstate/dumpstate.c +++ b/cmds/dumpstate/dumpstate.c @@ -39,6 +39,8 @@ static char cmdline_buf[16384] = "(unknown)"; static const char *dump_traces_path = NULL; +static char screenshot_path[PATH_MAX] = ""; + /* dumps the current system state to stdout */ static void dumpstate() { time_t now = time(NULL); @@ -76,6 +78,12 @@ static void dumpstate() { dump_file("SLAB INFO", "/proc/slabinfo"); dump_file("ZONEINFO", "/proc/zoneinfo"); + if (screenshot_path[0]) { + LOGI("taking screenshot\n"); + run_command(NULL, 5, "su", "root", "screenshot", screenshot_path, NULL); + LOGI("wrote screenshot: %s\n", screenshot_path); + } + run_command("SYSTEM LOG", 20, "logcat", "-v", "time", "-d", "*:v", NULL); /* show the traces we collected in main(), if that was done */ @@ -103,7 +111,12 @@ static void dumpstate() { dump_file("NETWORK ROUTES", "/proc/net/route"); dump_file("ARP CACHE", "/proc/net/arp"); + run_command("WIFI NETWORKS", 20, + "su", "root", "wpa_cli", "list_networks", NULL); + #ifdef FWDUMP_bcm4329 + run_command("DUMP WIFI STATUS", 20, + "su", "root", "dhdutil", "-i", "eth0", "dump", NULL); run_command("DUMP WIFI FIRMWARE LOG", 60, "su", "root", "dhdutil", "-i", "eth0", "upload", "/data/local/tmp/wlan_crash.dump", NULL); #endif @@ -164,18 +177,25 @@ static void dumpstate() { } static void usage() { - fprintf(stderr, "usage: dumpstate [-d] [-o file] [-s] [-z]\n" - " -d: append date to filename (requires -o)\n" + fprintf(stderr, "usage: dumpstate [-b soundfile] [-e soundfile] [-o file [-d] [-p] [-z]] [-s]\n" " -o: write to file (instead of stdout)\n" + " -d: append date to filename (requires -o)\n" + " -z: gzip output (requires -o)\n" + " -p: capture screenshot to filename.png (requires -o)\n" " -s: write output to control socket (for init)\n" - " -z: gzip output (requires -o)\n"); + " -b: play sound file instead of vibrate, at beginning of job\n" + " -e: play sound file instead of vibrate, at end of job\n" + ); } int main(int argc, char *argv[]) { int do_add_date = 0; int do_compress = 0; char* use_outfile = 0; + char* begin_sound = 0; + char* end_sound = 0; int use_socket = 0; + int do_fb = 0; LOGI("begin\n"); @@ -191,13 +211,16 @@ int main(int argc, char *argv[]) { dump_traces_path = dump_vm_traces(); int c; - while ((c = getopt(argc, argv, "dho:svz")) != -1) { + while ((c = getopt(argc, argv, "b:de:ho:svzp")) != -1) { switch (c) { + case 'b': begin_sound = optarg; break; case 'd': do_add_date = 1; break; + case 'e': end_sound = optarg; break; case 'o': use_outfile = optarg; break; case 's': use_socket = 1; break; case 'v': break; // compatibility no-op case 'z': do_compress = 6; break; + case 'p': do_fb = 1; break; case '?': printf("\n"); case 'h': usage(); @@ -244,6 +267,10 @@ int main(int argc, char *argv[]) { strftime(date, sizeof(date), "-%Y-%m-%d-%H-%M-%S", localtime(&now)); strlcat(path, date, sizeof(path)); } + if (do_fb) { + strlcpy(screenshot_path, path, sizeof(screenshot_path)); + strlcat(screenshot_path, ".png", sizeof(screenshot_path)); + } strlcat(path, ".txt", sizeof(path)); if (do_compress) strlcat(path, ".gz", sizeof(path)); strlcpy(tmp_path, path, sizeof(tmp_path)); @@ -251,16 +278,18 @@ int main(int argc, char *argv[]) { gzip_pid = redirect_to_file(stdout, tmp_path, do_compress); } - /* bzzzzzz */ - if (vibrator) { + if (begin_sound) { + play_sound(begin_sound); + } else if (vibrator) { fputs("150", vibrator); fflush(vibrator); } dumpstate(); - /* bzzz bzzz bzzz */ - if (vibrator) { + if (end_sound) { + play_sound(end_sound); + } else if (vibrator) { int i; for (i = 0; i < 3; i++) { fputs("75\n", vibrator); diff --git a/cmds/dumpstate/dumpstate.h b/cmds/dumpstate/dumpstate.h index 682eafdbab67573966aa4f33eec1b874dad8b94e..83b1d11c38ab2e7a1a126d319df64ba62710081c 100644 --- a/cmds/dumpstate/dumpstate.h +++ b/cmds/dumpstate/dumpstate.h @@ -44,4 +44,7 @@ void for_each_pid(void (*func)(int, const char *), const char *header); /* Displays a blocked processes in-kernel wait channel */ void show_wchan(int pid, const char *name); +/* Play a sound via Stagefright */ +void play_sound(const char* path); + #endif /* _DUMPSTATE_H_ */ diff --git a/cmds/dumpstate/utils.c b/cmds/dumpstate/utils.c index c7a78ccecef8ab70e828b25a2f26a93e1858d15b..f92acbbbf3328b4d970cbf9fc19ca216a5fe4fc2 100644 --- a/cmds/dumpstate/utils.c +++ b/cmds/dumpstate/utils.c @@ -429,3 +429,7 @@ const char *dump_vm_traces() { rename(anr_traces_path, traces_path); return dump_traces_path; } + +void play_sound(const char* path) { + run_command(NULL, 5, "/system/bin/stagefright", "-o", "-a", path, NULL); +} diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java index 9b8b0ac4477adc50d707d22cb91cc906b478759c..040421a8a9d8fd9da942dfb25bad5537c6470cbd 100644 --- a/cmds/pm/src/com/android/commands/pm/Pm.java +++ b/cmds/pm/src/com/android/commands/pm/Pm.java @@ -233,7 +233,7 @@ public final class Pm { for (int i=0; i() { @@ -909,6 +909,9 @@ public final class Pm { System.err.println("The list instrumentation command prints all instrumentations,"); System.err.println("or only those that target a specified package. Options:"); System.err.println(" -f: see their associated file."); + System.err.println("(Use this command to list all test packages, or use "); + System.err.println(" to list the test packages for a particular application. The -f "); + System.err.println(" option lists the .apk file for the test package.)"); System.err.println(""); System.err.println("The list features command prints all features of the system."); System.err.println(""); diff --git a/cmds/screenshot/Android.mk b/cmds/screenshot/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..99c7aeb85c1a0278e1a69f0410ea94e41b769911 --- /dev/null +++ b/cmds/screenshot/Android.mk @@ -0,0 +1,16 @@ +ifneq ($(TARGET_SIMULATOR),true) + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := screenshot.c + +LOCAL_MODULE := screenshot + +LOCAL_SHARED_LIBRARIES := libcutils libz +LOCAL_STATIC_LIBRARIES := libpng +LOCAL_C_INCLUDES += external/zlib + +include $(BUILD_EXECUTABLE) + +endif diff --git a/cmds/screenshot/screenshot.c b/cmds/screenshot/screenshot.c new file mode 100644 index 0000000000000000000000000000000000000000..048636c64e537dbf42686396bc4f7bb08b30561b --- /dev/null +++ b/cmds/screenshot/screenshot.c @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "private/android_filesystem_config.h" + +#define LOG_TAG "screenshot" +#include + +void take_screenshot(FILE *fb_in, FILE *fb_out) { + int fb; + char imgbuf[0x10000]; + struct fb_var_screeninfo vinfo; + png_structp png; + png_infop info; + unsigned int r,c,rowlen; + unsigned int bytespp,offset; + + fb = fileno(fb_in); + if(fb < 0) { + LOGE("failed to open framebuffer\n"); + return; + } + fb_in = fdopen(fb, "r"); + + if(ioctl(fb, FBIOGET_VSCREENINFO, &vinfo) < 0) { + LOGE("failed to get framebuffer info\n"); + return; + } + fcntl(fb, F_SETFD, FD_CLOEXEC); + + png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (png == NULL) { + LOGE("failed png_create_write_struct\n"); + fclose(fb_in); + return; + } + + png_init_io(png, fb_out); + info = png_create_info_struct(png); + if (info == NULL) { + LOGE("failed png_create_info_struct\n"); + png_destroy_write_struct(&png, NULL); + fclose(fb_in); + return; + } + if (setjmp(png_jmpbuf(png))) { + LOGE("failed png setjmp\n"); + png_destroy_write_struct(&png, NULL); + fclose(fb_in); + return; + } + + bytespp = vinfo.bits_per_pixel / 8; + png_set_IHDR(png, info, + vinfo.xres, vinfo.yres, vinfo.bits_per_pixel / 4, + PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + png_write_info(png, info); + + rowlen=vinfo.xres * bytespp; + if (rowlen > sizeof(imgbuf)) { + LOGE("crazy rowlen: %d\n", rowlen); + png_destroy_write_struct(&png, NULL); + fclose(fb_in); + return; + } + + offset = vinfo.xoffset * bytespp + vinfo.xres * vinfo.yoffset * bytespp; + fseek(fb_in, offset, SEEK_SET); + + for(r=0; rAnimatorListeners added to them. + */ +public abstract class Animator implements Cloneable { + + + /** + * The set of listeners to be sent events through the life of an animation. + */ + ArrayList mListeners = null; + + /** + * Starts this animation. If the animation has a nonzero startDelay, the animation will start + * running after that delay elapses. Note that the animation does not start synchronously with + * this call, because all animation events are posted to a central timing loop so that animation + * times are all synchronized on a single timing pulse on the UI thread. So the animation will + * start the next time that event handler processes events. + */ + public void start() { + } + + /** + * Cancels the animation. Unlike {@link #end()}, cancel() causes the animation to + * stop in its tracks, sending an {@link android.animation.Animator.AnimatorListener#onAnimationCancel(Animator)} to + * its listeners, followed by an {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)} message. + */ + public void cancel() { + } + + /** + * Ends the animation. This causes the animation to assign the end value of the property being + * animated, then calling the {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)} method on + * its listeners. + */ + public void end() { + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + * + * @return the number of milliseconds to delay running the animation + */ + public abstract long getStartDelay(); + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + + * @param startDelay The amount of the delay, in milliseconds + */ + public abstract void setStartDelay(long startDelay); + + + /** + * Sets the length of the animation. + * + * @param duration The length of the animation, in milliseconds. + */ + public abstract void setDuration(long duration); + + /** + * Gets the length of the animation. + * + * @return The length of the animation, in milliseconds. + */ + public abstract long getDuration(); + + /** + * The time interpolator used in calculating the elapsed fraction of this animation. The + * interpolator determines whether the animation runs with linear or non-linear motion, + * such as acceleration and deceleration. The default value is + * {@link android.view.animation.AccelerateDecelerateInterpolator} + * + * @param value the interpolator to be used by this animation + */ + public abstract void setInterpolator(Interpolator value); + + /** + * Returns whether this Animator is currently running (having been started and not yet ended). + * @return Whether the Animator is running. + */ + public abstract boolean isRunning(); + + /** + * Adds a listener to the set of listeners that are sent events through the life of an + * animation, such as start, repeat, and end. + * + * @param listener the listener to be added to the current set of listeners for this animation. + */ + public void addListener(AnimatorListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(); + } + mListeners.add(listener); + } + + /** + * Removes a listener from the set listening to this animation. + * + * @param listener the listener to be removed from the current set of listeners for this + * animation. + */ + public void removeListener(AnimatorListener listener) { + if (mListeners == null) { + return; + } + mListeners.remove(listener); + if (mListeners.size() == 0) { + mListeners = null; + } + } + + /** + * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently + * listening for events on this Animator object. + * + * @return ArrayList The set of listeners. + */ + public ArrayList getListeners() { + return mListeners; + } + + /** + * Removes all listeners from this object. This is equivalent to calling + * getListeners() followed by calling clear() on the + * returned list of listeners. + */ + public void removeAllListeners() { + if (mListeners != null) { + mListeners.clear(); + mListeners = null; + } + } + + @Override + public Animator clone() { + try { + final Animator anim = (Animator) super.clone(); + if (mListeners != null) { + ArrayList oldListeners = mListeners; + anim.mListeners = new ArrayList(); + int numListeners = oldListeners.size(); + for (int i = 0; i < numListeners; ++i) { + anim.mListeners.add(oldListeners.get(i)); + } + } + return anim; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + + /** + * This method tells the object to use appropriate information to extract + * starting values for the animation. For example, a AnimatorSet object will pass + * this call to its child objects to tell them to set up the values. A + * ObjectAnimator object will use the information it has about its target object + * and PropertyValuesHolder objects to get the start values for its properties. + * An ValueAnimator object will ignore the request since it does not have enough + * information (such as a target object) to gather these values. + */ + public void setupStartValues() { + } + + /** + * This method tells the object to use appropriate information to extract + * ending values for the animation. For example, a AnimatorSet object will pass + * this call to its child objects to tell them to set up the values. A + * ObjectAnimator object will use the information it has about its target object + * and PropertyValuesHolder objects to get the start values for its properties. + * An ValueAnimator object will ignore the request since it does not have enough + * information (such as a target object) to gather these values. + */ + public void setupEndValues() { + } + + /** + * Sets the target object whose property will be animated by this animation. Not all subclasses + * operate on target objects (for example, {@link ValueAnimator}, but this method + * is on the superclass for the convenience of dealing generically with those subclasses + * that do handle targets. + * + * @param target The object being animated + */ + public void setTarget(Object target) { + } + + /** + *

An animation listener receives notifications from an animation. + * Notifications indicate animation related events, such as the end or the + * repetition of the animation.

+ */ + public static interface AnimatorListener { + /** + *

Notifies the start of the animation.

+ * + * @param animation The started animation. + */ + void onAnimationStart(Animator animation); + + /** + *

Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which reached its end. + */ + void onAnimationEnd(Animator animation); + + /** + *

Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which was canceled. + */ + void onAnimationCancel(Animator animation); + + /** + *

Notifies the repetition of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationRepeat(Animator animation); + } +} diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java new file mode 100644 index 0000000000000000000000000000000000000000..0016459cda1059330d9472f4cb3c6dc6d221bfb5 --- /dev/null +++ b/core/java/android/animation/AnimatorInflater.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.animation; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.content.res.Resources.NotFoundException; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.animation.AnimationUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * This class is used to instantiate menu XML files into Animator objects. + *

+ * For performance reasons, menu inflation relies heavily on pre-processing of + * XML files that is done at build time. Therefore, it is not currently possible + * to use MenuInflater with an XmlPullParser over a plain XML file at runtime; + * it only works with an XmlPullParser returned from a compiled resource (R. + * something file.) + */ +public class AnimatorInflater { + + /** + * These flags are used when parsing AnimatorSet objects + */ + private static final int TOGETHER = 0; + private static final int SEQUENTIALLY = 1; + + /** + * Enum values used in XML attributes to indicate the value for mValueType + */ + private static final int VALUE_TYPE_FLOAT = 0; + private static final int VALUE_TYPE_INT = 1; + private static final int VALUE_TYPE_DOUBLE = 2; + private static final int VALUE_TYPE_COLOR = 3; + private static final int VALUE_TYPE_CUSTOM = 4; + + /** + * Loads an {@link Animator} object from a resource + * + * @param context Application context used to access resources + * @param id The resource id of the animation to load + * @return The animator object reference by the specified id + * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded + */ + public static Animator loadAnimator(Context context, int id) + throws NotFoundException { + + XmlResourceParser parser = null; + try { + parser = context.getResources().getAnimation(id); + return createAnimatorFromXml(context, parser); + } catch (XmlPullParserException ex) { + Resources.NotFoundException rnf = + new Resources.NotFoundException("Can't load animation resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(ex); + throw rnf; + } catch (IOException ex) { + Resources.NotFoundException rnf = + new Resources.NotFoundException("Can't load animation resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(ex); + throw rnf; + } finally { + if (parser != null) parser.close(); + } + } + + private static Animator createAnimatorFromXml(Context c, XmlPullParser parser) + throws XmlPullParserException, IOException { + + return createAnimatorFromXml(c, parser, Xml.asAttributeSet(parser), null, 0); + } + + private static Animator createAnimatorFromXml(Context c, XmlPullParser parser, + AttributeSet attrs, AnimatorSet parent, int sequenceOrdering) + throws XmlPullParserException, IOException { + + Animator anim = null; + ArrayList childAnims = null; + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + + while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + + if (name.equals("objectAnimator")) { + anim = loadObjectAnimator(c, attrs); + } else if (name.equals("animator")) { + anim = loadAnimator(c, attrs, null); + } else if (name.equals("set")) { + anim = new AnimatorSet(); + TypedArray a = c.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AnimatorSet); + int ordering = a.getInt(com.android.internal.R.styleable.AnimatorSet_ordering, + TOGETHER); + createAnimatorFromXml(c, parser, attrs, (AnimatorSet) anim, ordering); + a.recycle(); + } else { + throw new RuntimeException("Unknown animator name: " + parser.getName()); + } + + if (parent != null) { + if (childAnims == null) { + childAnims = new ArrayList(); + } + childAnims.add(anim); + } + } + if (parent != null && childAnims != null) { + Animator[] animsArray = new Animator[childAnims.size()]; + int index = 0; + for (Animator a : childAnims) { + animsArray[index++] = a; + } + if (sequenceOrdering == TOGETHER) { + parent.playTogether(animsArray); + } else { + parent.playSequentially(animsArray); + } + } + + return anim; + + } + + private static ObjectAnimator loadObjectAnimator(Context context, AttributeSet attrs) + throws NotFoundException { + + ObjectAnimator anim = new ObjectAnimator(); + + loadAnimator(context, attrs, anim); + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.PropertyAnimator); + + String propertyName = a.getString(com.android.internal.R.styleable.PropertyAnimator_propertyName); + + anim.setPropertyName(propertyName); + + a.recycle(); + + return anim; + } + + /** + * Creates a new animation whose parameters come from the specified context and + * attributes set. + * + * @param context the application environment + * @param attrs the set of attributes holding the animation parameters + */ + private static ValueAnimator loadAnimator(Context context, AttributeSet attrs, ValueAnimator anim) + throws NotFoundException { + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Animator); + + long duration = a.getInt(com.android.internal.R.styleable.Animator_duration, 0); + + long startDelay = a.getInt(com.android.internal.R.styleable.Animator_startOffset, 0); + + int valueType = a.getInt(com.android.internal.R.styleable.Animator_valueType, + VALUE_TYPE_FLOAT); + + Object valueFrom = null; + Object valueTo = null; + TypeEvaluator evaluator = null; + + switch (valueType) { + case VALUE_TYPE_FLOAT: + if (a.hasValue(com.android.internal.R.styleable.Animator_valueFrom)) { + valueFrom = a.getFloat(com.android.internal.R.styleable.Animator_valueFrom, 0f); + } + if (a.hasValue(com.android.internal.R.styleable.Animator_valueTo)) { + valueTo = a.getFloat(com.android.internal.R.styleable.Animator_valueTo, 0f); + } + break; + case VALUE_TYPE_COLOR: + evaluator = new RGBEvaluator(); + // fall through to pick up values + case VALUE_TYPE_INT: + if (a.hasValue(com.android.internal.R.styleable.Animator_valueFrom)) { + valueFrom = a.getInteger(com.android.internal.R.styleable.Animator_valueFrom, 0); + } + if (a.hasValue(com.android.internal.R.styleable.Animator_valueTo)) { + valueTo = a.getInteger(com.android.internal.R.styleable.Animator_valueTo, 0); + } + break; + case VALUE_TYPE_DOUBLE: + if (a.hasValue(com.android.internal.R.styleable.Animator_valueFrom)) { + valueFrom = (Double)((Float)(a.getFloat(com.android.internal.R.styleable.Animator_valueFrom, 0f))).doubleValue(); + } + if (a.hasValue(com.android.internal.R.styleable.Animator_valueTo)) { + valueTo = (Double)((Float)a.getFloat(com.android.internal.R.styleable.Animator_valueTo, 0f)).doubleValue(); + } + break; + case VALUE_TYPE_CUSTOM: + // TODO: How to get an 'Object' value? + if (a.hasValue(com.android.internal.R.styleable.Animator_valueFrom)) { + valueFrom = a.getFloat(com.android.internal.R.styleable.Animator_valueFrom, 0f); + } + if (a.hasValue(com.android.internal.R.styleable.Animator_valueTo)) { + valueTo = a.getFloat(com.android.internal.R.styleable.Animator_valueTo, 0f); + } + break; + } + + if (anim == null) { + anim = new ValueAnimator(duration, valueFrom, valueTo); + } else { + anim.setDuration(duration); + anim.setValues(valueFrom, valueTo); + } + + anim.setStartDelay(startDelay); + + if (a.hasValue(com.android.internal.R.styleable.Animator_repeatCount)) { + anim.setRepeatCount( + a.getInt(com.android.internal.R.styleable.Animator_repeatCount, 0)); + } + if (a.hasValue(com.android.internal.R.styleable.Animator_repeatMode)) { + anim.setRepeatMode( + a.getInt(com.android.internal.R.styleable.Animator_repeatMode, + ValueAnimator.RESTART)); + } + if (evaluator != null) { + anim.setEvaluator(evaluator); + } + + final int resID = + a.getResourceId(com.android.internal.R.styleable.Animator_interpolator, 0); + if (resID > 0) { + anim.setInterpolator(AnimationUtils.loadInterpolator(context, resID)); + } + a.recycle(); + + return anim; + } +} diff --git a/core/java/android/animation/AnimatorListenerAdapter.java b/core/java/android/animation/AnimatorListenerAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..e5d70a4f017d4bd0bec7a1c8b558bd92fcf53f35 --- /dev/null +++ b/core/java/android/animation/AnimatorListenerAdapter.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +/** + * This adapter class provides empty implementations of the methods from {@link android.animation.Animator.AnimatorListener}. + * Any custom listener that cares only about a subset of the methods of this listener can + * simply subclass this adapter class instead of implementing the interface directly. + */ +public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener { + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationCancel(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationEnd(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationRepeat(Animator animation) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationStart(Animator animation) { + } + +} diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java new file mode 100644 index 0000000000000000000000000000000000000000..a8385e4f5f9976e6d9afb73497080902cd5fa72a --- /dev/null +++ b/core/java/android/animation/AnimatorSet.java @@ -0,0 +1,939 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This class plays a set of {@link Animator} objects in the specified order. Animations + * can be set up to play together, in sequence, or after a specified delay. + * + *

There are two different approaches to adding animations to a AnimatorSet: + * either the {@link AnimatorSet#playTogether(Animator[]) playTogether()} or + * {@link AnimatorSet#playSequentially(Animator[]) playSequentially()} methods can be called to add + * a set of animations all at once, or the {@link AnimatorSet#play(Animator)} can be + * used in conjunction with methods in the {@link AnimatorSet.Builder Builder} + * class to add animations + * one by one.

+ * + *

It is possible to set up a AnimatorSet with circular dependencies between + * its animations. For example, an animation a1 could be set up to start before animation a2, a2 + * before a3, and a3 before a1. The results of this configuration are undefined, but will typically + * result in none of the affected animations being played. Because of this (and because + * circular dependencies do not make logical sense anyway), circular dependencies + * should be avoided, and the dependency flow of animations should only be in one direction. + */ +public final class AnimatorSet extends Animator { + + /** + * Internal variables + * NOTE: This object implements the clone() method, making a deep copy of any referenced + * objects. As other non-trivial fields are added to this class, make sure to add logic + * to clone() to make deep copies of them. + */ + + /** + * Tracks animations currently being played, so that we know what to + * cancel or end when cancel() or end() is called on this AnimatorSet + */ + private ArrayList mPlayingSet = new ArrayList(); + + /** + * Contains all nodes, mapped to their respective Animators. When new + * dependency information is added for an Animator, we want to add it + * to a single node representing that Animator, not create a new Node + * if one already exists. + */ + private HashMap mNodeMap = new HashMap(); + + /** + * Set of all nodes created for this AnimatorSet. This list is used upon + * starting the set, and the nodes are placed in sorted order into the + * sortedNodes collection. + */ + private ArrayList mNodes = new ArrayList(); + + /** + * The sorted list of nodes. This is the order in which the animations will + * be played. The details about when exactly they will be played depend + * on the dependency relationships of the nodes. + */ + private ArrayList mSortedNodes = new ArrayList(); + + /** + * Flag indicating whether the nodes should be sorted prior to playing. This + * flag allows us to cache the previous sorted nodes so that if the sequence + * is replayed with no changes, it does not have to re-sort the nodes again. + */ + private boolean mNeedsSort = true; + + private AnimatorSetListener mSetListener = null; + + /** + * Flag indicating that the AnimatorSet has been canceled (by calling cancel() or end()). + * This flag is used to avoid starting other animations when currently-playing + * child animations of this AnimatorSet end. + */ + boolean mCanceled = false; + + // The amount of time in ms to delay starting the animation after start() is called + private long mStartDelay = 0; + + + // How long the child animations should last in ms. The default value is negative, which + // simply means that there is no duration set on the AnimatorSet. When a real duration is + // set, it is passed along to the child animations. + private long mDuration = -1; + + + /** + * Sets up this AnimatorSet to play all of the supplied animations at the same time. + * + * @param items The animations that will be started simultaneously. + */ + public void playTogether(Animator... items) { + if (items != null) { + mNeedsSort = true; + Builder builder = play(items[0]); + for (int i = 1; i < items.length; ++i) { + builder.with(items[i]); + } + } + } + + /** + * Sets up this AnimatorSet to play each of the supplied animations when the + * previous animation ends. + * + * @param items The aniamtions that will be started one after another. + */ + public void playSequentially(Animator... items) { + if (items != null) { + mNeedsSort = true; + if (items.length == 1) { + play(items[0]); + } else { + for (int i = 0; i < items.length - 1; ++i) { + play(items[i]).before(items[i+1]); + } + } + } + } + + /** + * Returns the current list of child Animator objects controlled by this + * AnimatorSet. This is a copy of the internal list; modifications to the returned list + * will not affect the AnimatorSet, although changes to the underlying Animator objects + * will affect those objects being managed by the AnimatorSet. + * + * @return ArrayList The list of child animations of this AnimatorSet. + */ + public ArrayList getChildAnimations() { + ArrayList childList = new ArrayList(); + for (Node node : mNodes) { + childList.add(node.animation); + } + return childList; + } + + /** + * Sets the target object for all current {@link #getChildAnimations() child animations} + * of this AnimatorSet that take targets ({@link ObjectAnimator} and + * AnimatorSet). + * + * @param target The object being animated + */ + @Override + public void setTarget(Object target) { + for (Node node : mNodes) { + Animator animation = node.animation; + if (animation instanceof AnimatorSet) { + ((AnimatorSet)animation).setTarget(target); + } else if (animation instanceof ObjectAnimator) { + ((ObjectAnimator)animation).setTarget(target); + } + } + } + + /** + * Sets the Interpolator for all current {@link #getChildAnimations() child animations} + * of this AnimatorSet. + * + * @param interpolator the interpolator to be used by each child animation of this AnimatorSet + */ + @Override + public void setInterpolator(Interpolator interpolator) { + for (Node node : mNodes) { + node.animation.setInterpolator(interpolator); + } + } + + /** + * This method creates a Builder object, which is used to + * set up playing constraints. This initial play() method + * tells the Builder the animation that is the dependency for + * the succeeding commands to the Builder. For example, + * calling play(a1).with(a2) sets up the AnimatorSet to play + * a1 and a2 at the same time, + * play(a1).before(a2) sets up the AnimatorSet to play + * a1 first, followed by a2, and + * play(a1).after(a2) sets up the AnimatorSet to play + * a2 first, followed by a1. + * + *

Note that play() is the only way to tell the + * Builder the animation upon which the dependency is created, + * so successive calls to the various functions in Builder + * will all refer to the initial parameter supplied in play() + * as the dependency of the other animations. For example, calling + * play(a1).before(a2).before(a3) will play both a2 + * and a3 when a1 ends; it does not set up a dependency between + * a2 and a3.

+ * + * @param anim The animation that is the dependency used in later calls to the + * methods in the returned Builder object. A null parameter will result + * in a null Builder return value. + * @return Builder The object that constructs the AnimatorSet based on the dependencies + * outlined in the calls to play and the other methods in the + * BuilderNote that canceling a AnimatorSet also cancels all of the animations that it is + * responsible for.

+ */ + @SuppressWarnings("unchecked") + @Override + public void cancel() { + mCanceled = true; + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationCancel(this); + } + } + if (mSortedNodes.size() > 0) { + for (Node node : mSortedNodes) { + node.animation.cancel(); + } + } + } + + /** + * {@inheritDoc} + * + *

Note that ending a AnimatorSet also ends all of the animations that it is + * responsible for.

+ */ + @Override + public void end() { + mCanceled = true; + if (mSortedNodes.size() != mNodes.size()) { + // hasn't been started yet - sort the nodes now, then end them + sortNodes(); + for (Node node : mSortedNodes) { + if (mSetListener == null) { + mSetListener = new AnimatorSetListener(this); + } + node.animation.addListener(mSetListener); + } + } + if (mSortedNodes.size() > 0) { + for (Node node : mSortedNodes) { + node.animation.end(); + } + } + } + + /** + * Returns true if any of the child animations of this AnimatorSet have been started and have not + * yet ended. + * @return Whether this AnimatorSet has been started and has not yet ended. + */ + @Override + public boolean isRunning() { + for (Node node : mNodes) { + if (node.animation.isRunning()) { + return true; + } + } + return false; + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + * + * @return the number of milliseconds to delay running the animation + */ + @Override + public long getStartDelay() { + return mStartDelay; + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + + * @param startDelay The amount of the delay, in milliseconds + */ + @Override + public void setStartDelay(long startDelay) { + mStartDelay = startDelay; + } + + /** + * Gets the length of each of the child animations of this AnimatorSet. This value may + * be less than 0, which indicates that no duration has been set on this AnimatorSet + * and each of the child animations will use their own duration. + * + * @return The length of the animation, in milliseconds, of each of the child + * animations of this AnimatorSet. + */ + @Override + public long getDuration() { + return mDuration; + } + + /** + * Sets the length of each of the current child animations of this AnimatorSet. By default, + * each child animation will use its own duration. If the duration is set on the AnimatorSet, + * then each child animation inherits this duration. + * + * @param duration The length of the animation, in milliseconds, of each of the child + * animations of this AnimatorSet. + */ + @Override + public void setDuration(long duration) { + if (duration < 0) { + throw new IllegalArgumentException("duration must be a value of zero or greater"); + } + for (Node node : mNodes) { + // TODO: don't set the duration of the timing-only nodes created by AnimatorSet to + // insert "play-after" delays + node.animation.setDuration(duration); + } + mDuration = duration; + } + + /** + * {@inheritDoc} + * + *

Starting this AnimatorSet will, in turn, start the animations for which + * it is responsible. The details of when exactly those animations are started depends on + * the dependency relationships that have been set up between the animations. + */ + @SuppressWarnings("unchecked") + @Override + public void start() { + mCanceled = false; + + // First, sort the nodes (if necessary). This will ensure that sortedNodes + // contains the animation nodes in the correct order. + sortNodes(); + + // nodesToStart holds the list of nodes to be started immediately. We don't want to + // start the animations in the loop directly because we first need to set up + // dependencies on all of the nodes. For example, we don't want to start an animation + // when some other animation also wants to start when the first animation begins. + final ArrayList nodesToStart = new ArrayList(); + for (Node node : mSortedNodes) { + if (mSetListener == null) { + mSetListener = new AnimatorSetListener(this); + } + if (node.dependencies == null || node.dependencies.size() == 0) { + nodesToStart.add(node); + } else { + for (Dependency dependency : node.dependencies) { + dependency.node.animation.addListener( + new DependencyListener(this, node, dependency.rule)); + } + node.tmpDependencies = (ArrayList) node.dependencies.clone(); + } + node.animation.addListener(mSetListener); + } + // Now that all dependencies are set up, start the animations that should be started. + if (mStartDelay <= 0) { + for (Node node : nodesToStart) { + node.animation.start(); + mPlayingSet.add(node.animation); + } + } else { + // TODO: Need to cancel out of the delay appropriately + ValueAnimator delayAnim = new ValueAnimator(mStartDelay, 0f, 1f); + delayAnim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator anim) { + for (Node node : nodesToStart) { + node.animation.start(); + mPlayingSet.add(node.animation); + } + } + }); + } + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationStart(this); + } + } + } + + @Override + public AnimatorSet clone() { + final AnimatorSet anim = (AnimatorSet) super.clone(); + /* + * The basic clone() operation copies all items. This doesn't work very well for + * AnimatorSet, because it will copy references that need to be recreated and state + * that may not apply. What we need to do now is put the clone in an uninitialized + * state, with fresh, empty data structures. Then we will build up the nodes list + * manually, as we clone each Node (and its animation). The clone will then be sorted, + * and will populate any appropriate lists, when it is started. + */ + anim.mNeedsSort = true; + anim.mCanceled = false; + anim.mPlayingSet = new ArrayList(); + anim.mNodeMap = new HashMap(); + anim.mNodes = new ArrayList(); + anim.mSortedNodes = new ArrayList(); + + // Walk through the old nodes list, cloning each node and adding it to the new nodemap. + // One problem is that the old node dependencies point to nodes in the old AnimatorSet. + // We need to track the old/new nodes in order to reconstruct the dependencies in the clone. + HashMap nodeCloneMap = new HashMap(); // + for (Node node : mNodes) { + Node nodeClone = node.clone(); + nodeCloneMap.put(node, nodeClone); + anim.mNodes.add(nodeClone); + anim.mNodeMap.put(nodeClone.animation, nodeClone); + // Clear out the dependencies in the clone; we'll set these up manually later + nodeClone.dependencies = null; + nodeClone.tmpDependencies = null; + nodeClone.nodeDependents = null; + nodeClone.nodeDependencies = null; + // clear out any listeners that were set up by the AnimatorSet; these will + // be set up when the clone's nodes are sorted + ArrayList cloneListeners = nodeClone.animation.getListeners(); + if (cloneListeners != null) { + ArrayList listenersToRemove = null; + for (AnimatorListener listener : cloneListeners) { + if (listener instanceof AnimatorSetListener) { + if (listenersToRemove == null) { + listenersToRemove = new ArrayList(); + } + listenersToRemove.add(listener); + } + } + if (listenersToRemove != null) { + for (AnimatorListener listener : listenersToRemove) { + cloneListeners.remove(listener); + } + } + } + } + // Now that we've cloned all of the nodes, we're ready to walk through their + // dependencies, mapping the old dependencies to the new nodes + for (Node node : mNodes) { + Node nodeClone = nodeCloneMap.get(node); + if (node.dependencies != null) { + for (Dependency dependency : node.dependencies) { + Node clonedDependencyNode = nodeCloneMap.get(dependency.node); + Dependency cloneDependency = new Dependency(clonedDependencyNode, + dependency.rule); + nodeClone.addDependency(cloneDependency); + } + } + } + + return anim; + } + + /** + * This class is the mechanism by which animations are started based on events in other + * animations. If an animation has multiple dependencies on other animations, then + * all dependencies must be satisfied before the animation is started. + */ + private static class DependencyListener implements AnimatorListener { + + private AnimatorSet mAnimatorSet; + + // The node upon which the dependency is based. + private Node mNode; + + // The Dependency rule (WITH or AFTER) that the listener should wait for on + // the node + private int mRule; + + public DependencyListener(AnimatorSet animatorSet, Node node, int rule) { + this.mAnimatorSet = animatorSet; + this.mNode = node; + this.mRule = rule; + } + + /** + * Ignore cancel events for now. We may want to handle this eventually, + * to prevent follow-on animations from running when some dependency + * animation is canceled. + */ + public void onAnimationCancel(Animator animation) { + } + + /** + * An end event is received - see if this is an event we are listening for + */ + public void onAnimationEnd(Animator animation) { + if (mRule == Dependency.AFTER) { + startIfReady(animation); + } + } + + /** + * Ignore repeat events for now + */ + public void onAnimationRepeat(Animator animation) { + } + + /** + * A start event is received - see if this is an event we are listening for + */ + public void onAnimationStart(Animator animation) { + if (mRule == Dependency.WITH) { + startIfReady(animation); + } + } + + /** + * Check whether the event received is one that the node was waiting for. + * If so, mark it as complete and see whether it's time to start + * the animation. + * @param dependencyAnimation the animation that sent the event. + */ + private void startIfReady(Animator dependencyAnimation) { + if (mAnimatorSet.mCanceled) { + // if the parent AnimatorSet was canceled, then don't start any dependent anims + return; + } + Dependency dependencyToRemove = null; + for (Dependency dependency : mNode.tmpDependencies) { + if (dependency.rule == mRule && + dependency.node.animation == dependencyAnimation) { + // rule fired - remove the dependency and listener and check to + // see whether it's time to start the animation + dependencyToRemove = dependency; + dependencyAnimation.removeListener(this); + break; + } + } + mNode.tmpDependencies.remove(dependencyToRemove); + if (mNode.tmpDependencies.size() == 0) { + // all dependencies satisfied: start the animation + mNode.animation.start(); + mAnimatorSet.mPlayingSet.add(mNode.animation); + } + } + + } + + private class AnimatorSetListener implements AnimatorListener { + + private AnimatorSet mAnimatorSet; + + AnimatorSetListener(AnimatorSet animatorSet) { + mAnimatorSet = animatorSet; + } + + public void onAnimationCancel(Animator animation) { + if (mPlayingSet.size() == 0) { + if (mListeners != null) { + for (AnimatorListener listener : mListeners) { + listener.onAnimationCancel(mAnimatorSet); + } + } + } + } + + @SuppressWarnings("unchecked") + public void onAnimationEnd(Animator animation) { + animation.removeListener(this); + mPlayingSet.remove(animation); + Node animNode = mAnimatorSet.mNodeMap.get(animation); + animNode.done = true; + ArrayList sortedNodes = mAnimatorSet.mSortedNodes; + boolean allDone = true; + for (Node node : sortedNodes) { + if (!node.done) { + allDone = false; + break; + } + } + if (allDone) { + // If this was the last child animation to end, then notify listeners that this + // AnimatorSet has ended + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationEnd(mAnimatorSet); + } + } + } + } + + // Nothing to do + public void onAnimationRepeat(Animator animation) { + } + + // Nothing to do + public void onAnimationStart(Animator animation) { + } + + } + + /** + * This method sorts the current set of nodes, if needed. The sort is a simple + * DependencyGraph sort, which goes like this: + * - All nodes without dependencies become 'roots' + * - while roots list is not null + * - for each root r + * - add r to sorted list + * - remove r as a dependency from any other node + * - any nodes with no dependencies are added to the roots list + */ + private void sortNodes() { + if (mNeedsSort) { + mSortedNodes.clear(); + ArrayList roots = new ArrayList(); + for (Node node : mNodes) { + if (node.dependencies == null || node.dependencies.size() == 0) { + roots.add(node); + } + } + ArrayList tmpRoots = new ArrayList(); + while (roots.size() > 0) { + for (Node root : roots) { + mSortedNodes.add(root); + if (root.nodeDependents != null) { + for (Node node : root.nodeDependents) { + node.nodeDependencies.remove(root); + if (node.nodeDependencies.size() == 0) { + tmpRoots.add(node); + } + } + } + } + roots.clear(); + roots.addAll(tmpRoots); + tmpRoots.clear(); + } + mNeedsSort = false; + if (mSortedNodes.size() != mNodes.size()) { + throw new IllegalStateException("Circular dependencies cannot exist" + + " in AnimatorSet"); + } + } else { + // Doesn't need sorting, but still need to add in the nodeDependencies list + // because these get removed as the event listeners fire and the dependencies + // are satisfied + for (Node node : mNodes) { + if (node.dependencies != null && node.dependencies.size() > 0) { + for (Dependency dependency : node.dependencies) { + if (node.nodeDependencies == null) { + node.nodeDependencies = new ArrayList(); + } + if (!node.nodeDependencies.contains(dependency.node)) { + node.nodeDependencies.add(dependency.node); + } + } + } + node.done = false; + } + } + } + + /** + * Dependency holds information about the node that some other node is + * dependent upon and the nature of that dependency. + * + */ + private static class Dependency { + static final int WITH = 0; // dependent node must start with this dependency node + static final int AFTER = 1; // dependent node must start when this dependency node finishes + + // The node that the other node with this Dependency is dependent upon + public Node node; + + // The nature of the dependency (WITH or AFTER) + public int rule; + + public Dependency(Node node, int rule) { + this.node = node; + this.rule = rule; + } + } + + /** + * A Node is an embodiment of both the Animator that it wraps as well as + * any dependencies that are associated with that Animation. This includes + * both dependencies upon other nodes (in the dependencies list) as + * well as dependencies of other nodes upon this (in the nodeDependents list). + */ + private static class Node implements Cloneable { + public Animator animation; + + /** + * These are the dependencies that this node's animation has on other + * nodes. For example, if this node's animation should begin with some + * other animation ends, then there will be an item in this node's + * dependencies list for that other animation's node. + */ + public ArrayList dependencies = null; + + /** + * tmpDependencies is a runtime detail. We use the dependencies list for sorting. + * But we also use the list to keep track of when multiple dependencies are satisfied, + * but removing each dependency as it is satisfied. We do not want to remove + * the dependency itself from the list, because we need to retain that information + * if the AnimatorSet is launched in the future. So we create a copy of the dependency + * list when the AnimatorSet starts and use this tmpDependencies list to track the + * list of satisfied dependencies. + */ + public ArrayList tmpDependencies = null; + + /** + * nodeDependencies is just a list of the nodes that this Node is dependent upon. + * This information is used in sortNodes(), to determine when a node is a root. + */ + public ArrayList nodeDependencies = null; + + /** + * nodeDepdendents is the list of nodes that have this node as a dependency. This + * is a utility field used in sortNodes to facilitate removing this node as a + * dependency when it is a root node. + */ + public ArrayList nodeDependents = null; + + /** + * Flag indicating whether the animation in this node is finished. This flag + * is used by AnimatorSet to check, as each animation ends, whether all child animations + * are done and it's time to send out an end event for the entire AnimatorSet. + */ + public boolean done = false; + + /** + * Constructs the Node with the animation that it encapsulates. A Node has no + * dependencies by default; dependencies are added via the addDependency() + * method. + * + * @param animation The animation that the Node encapsulates. + */ + public Node(Animator animation) { + this.animation = animation; + } + + /** + * Add a dependency to this Node. The dependency includes information about the + * node that this node is dependency upon and the nature of the dependency. + * @param dependency + */ + public void addDependency(Dependency dependency) { + if (dependencies == null) { + dependencies = new ArrayList(); + nodeDependencies = new ArrayList(); + } + dependencies.add(dependency); + if (!nodeDependencies.contains(dependency.node)) { + nodeDependencies.add(dependency.node); + } + Node dependencyNode = dependency.node; + if (dependencyNode.nodeDependents == null) { + dependencyNode.nodeDependents = new ArrayList(); + } + dependencyNode.nodeDependents.add(this); + } + + @Override + public Node clone() { + try { + Node node = (Node) super.clone(); + node.animation = (Animator) animation.clone(); + return node; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + } + + /** + * The Builder object is a utility class to facilitate adding animations to a + * AnimatorSet along with the relationships between the various animations. The + * intention of the Builder methods, along with the {@link + * AnimatorSet#play(Animator) play()} method of AnimatorSet is to make it possible to + * express the dependency relationships of animations in a natural way. Developers can also use + * the {@link AnimatorSet#playTogether(Animator[]) playTogether()} and {@link + * AnimatorSet#playSequentially(Animator[]) playSequentially()} methods if these suit the need, + * but it might be easier in some situations to express the AnimatorSet of animations in pairs. + *

+ *

The Builder object cannot be constructed directly, but is rather constructed + * internally via a call to {@link AnimatorSet#play(Animator)}.

+ *

+ *

For example, this sets up a AnimatorSet to play anim1 and anim2 at the same time, anim3 to + * play when anim2 finishes, and anim4 to play when anim3 finishes:

+ *
+     *     AnimatorSet s = new AnimatorSet();
+     *     s.play(anim1).with(anim2);
+     *     s.play(anim2).before(anim3);
+     *     s.play(anim4).after(anim3);
+     * 
+ *

+ *

Note in the example that both {@link Builder#before(Animator)} and {@link + * Builder#after(Animator)} are used. These are just different ways of expressing the same + * relationship and are provided to make it easier to say things in a way that is more natural, + * depending on the situation.

+ *

+ *

It is possible to make several calls into the same Builder object to express + * multiple relationships. However, note that it is only the animation passed into the initial + * {@link AnimatorSet#play(Animator)} method that is the dependency in any of the successive + * calls to the Builder object. For example, the following code starts both anim2 + * and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and + * anim3: + *

+     *   AnimatorSet s = new AnimatorSet();
+     *   s.play(anim1).before(anim2).before(anim3);
+     * 
+ * If the desired result is to play anim1 then anim2 then anim3, this code expresses the + * relationship correctly:

+ *
+     *   AnimatorSet s = new AnimatorSet();
+     *   s.play(anim1).before(anim2);
+     *   s.play(anim2).before(anim3);
+     * 
+ *

+ *

Note that it is possible to express relationships that cannot be resolved and will not + * result in sensible results. For example, play(anim1).after(anim1) makes no + * sense. In general, circular dependencies like this one (or more indirect ones where a depends + * on b, which depends on c, which depends on a) should be avoided. Only create AnimatorSets + * that can boil down to a simple, one-way relationship of animations starting with, before, and + * after other, different, animations.

+ */ + public class Builder { + + /** + * This tracks the current node being processed. It is supplied to the play() method + * of AnimatorSet and passed into the constructor of Builder. + */ + private Node mCurrentNode; + + /** + * package-private constructor. Builders are only constructed by AnimatorSet, when the + * play() method is called. + * + * @param anim The animation that is the dependency for the other animations passed into + * the other methods of this Builder object. + */ + Builder(Animator anim) { + mCurrentNode = mNodeMap.get(anim); + if (mCurrentNode == null) { + mCurrentNode = new Node(anim); + mNodeMap.put(anim, mCurrentNode); + mNodes.add(mCurrentNode); + } + } + + /** + * Sets up the given animation to play at the same time as the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this Builder object. + * + * @param anim The animation that will play when the animation supplied to the + * {@link AnimatorSet#play(Animator)} method starts. + */ + public void with(Animator anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH); + node.addDependency(dependency); + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this Builder object + * ends. + * + * @param anim The animation that will play when the animation supplied to the + * {@link AnimatorSet#play(Animator)} method ends. + */ + public void before(Animator anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER); + node.addDependency(dependency); + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this Builder object + * to start when the animation supplied in this method call ends. + * + * @param anim The animation whose end will cause the animation supplied to the + * {@link AnimatorSet#play(Animator)} method to play. + */ + public void after(Animator anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(node, Dependency.AFTER); + mCurrentNode.addDependency(dependency); + } + + /** + * Sets up the animation supplied in the + * {@link AnimatorSet#play(Animator)} call that created this Builder object + * to play when the given amount of time elapses. + * + * @param delay The number of milliseconds that should elapse before the + * animation starts. + */ + public void after(long delay) { + // setup dummy ValueAnimator just to run the clock + after(new ValueAnimator(delay, 0f, 1f)); + } + + } + +} diff --git a/core/java/android/animation/DoubleEvaluator.java b/core/java/android/animation/DoubleEvaluator.java new file mode 100644 index 0000000000000000000000000000000000000000..e46eb372e5541294be87527fe6325743ad670d0a --- /dev/null +++ b/core/java/android/animation/DoubleEvaluator.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +/** + * This evaluator can be used to perform type interpolation between double values. + */ +public class DoubleEvaluator implements TypeEvaluator { + /** + * This function returns the result of linearly interpolating the start and end values, with + * fraction representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0), + * where x0 is startValue, x1 is endValue, + * and t is fraction. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type double or + * Double + * @param endValue The end value; should be of type double or + * Double + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + double startDouble = ((Number) startValue).doubleValue(); + return startDouble + fraction * (((Number) endValue).doubleValue() - startDouble); + } +} \ No newline at end of file diff --git a/core/java/android/animation/FloatEvaluator.java b/core/java/android/animation/FloatEvaluator.java new file mode 100644 index 0000000000000000000000000000000000000000..9e2054d0dc02d5086dd806be060469dffc72f0bf --- /dev/null +++ b/core/java/android/animation/FloatEvaluator.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +/** + * This evaluator can be used to perform type interpolation between float values. + */ +public class FloatEvaluator implements TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * fraction representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0), + * where x0 is startValue, x1 is endValue, + * and t is fraction. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type float or + * Float + * @param endValue The end value; should be of type float or Float + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + float startFloat = ((Number) startValue).floatValue(); + return startFloat + fraction * (((Number) endValue).floatValue() - startFloat); + } +} \ No newline at end of file diff --git a/core/java/android/animation/IntEvaluator.java b/core/java/android/animation/IntEvaluator.java new file mode 100644 index 0000000000000000000000000000000000000000..7288927f783b2ea55eb05cb6971bfa5238f768db --- /dev/null +++ b/core/java/android/animation/IntEvaluator.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +/** + * This evaluator can be used to perform type interpolation between int values. + */ +public class IntEvaluator implements TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * fraction representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0), + * where x0 is startValue, x1 is endValue, + * and t is fraction. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type int or + * Integer + * @param endValue The end value; should be of type int or Integer + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + int startInt = ((Number) startValue).intValue(); + return (int) (startInt + fraction * (((Number) endValue).intValue() - startInt)); + } +} \ No newline at end of file diff --git a/core/java/android/animation/Keyframe.java b/core/java/android/animation/Keyframe.java new file mode 100644 index 0000000000000000000000000000000000000000..192ba5c78d02fbcc5a5bd7f2d9cb4c5d90d2765a --- /dev/null +++ b/core/java/android/animation/Keyframe.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import android.view.animation.Interpolator; + +/** + * This class holds a time/value pair for an animation. The Keyframe class is used + * by {@link ValueAnimator} to define the values that the animation target will have over the course + * of the animation. As the time proceeds from one keyframe to the other, the value of the + * target object will animate between the value at the previous keyframe and the value at the + * next keyframe. Each keyframe also holds an option {@link android.view.animation.Interpolator} + * object, which defines the time interpolation over the intervalue preceding the keyframe. + */ +public class Keyframe implements Cloneable { + /** + * The time at which mValue will hold true. + */ + private float mFraction; + + /** + * The value of the animation at the time mFraction. + */ + private Object mValue; + + /** + * The type of the value in this Keyframe. This type is determined at construction time, + * based on the type of the value object passed into the constructor. + */ + private Class mValueType; + + /** + * The optional time interpolator for the interval preceding this keyframe. A null interpolator + * (the default) results in linear interpolation over the interval. + */ + private Interpolator mInterpolator = null; + + /** + * Private constructor, called from the public constructors with the additional + * valueType parameter. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + * @param valueType The type of the value object. This is used by the + * {@link #getValue()} functionm, which is queried by {@link ValueAnimator} to determine + * the type of {@link TypeEvaluator} to use to interpolate between values. + */ + private Keyframe(float fraction, Object value, Class valueType) { + mFraction = fraction; + mValue = value; + mValueType = valueType; + } + + /** + * Constructs a Keyframe object with the given time and value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public Keyframe(float fraction, Object value) { + this(fraction, value, (value != null) ? value.getClass() : Object.class); + } + + /** + * Constructs a Keyframe object with the given time and float value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public Keyframe(float fraction, Float value) { + this(fraction, value, Float.class); + } + + /** + * Constructs a Keyframe object with the given time and integer value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public Keyframe(float fraction, Integer value) { + this(fraction, value, Integer.class); + } + + /** + * Constructs a Keyframe object with the given time and double value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public Keyframe(float fraction, Double value) { + this(fraction, value, Double.class); + } + + /** + * Constructs a Keyframe object with the given time and integer value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public Keyframe(float fraction, int value) { + this(fraction, value, int.class); + } + + /** + * Constructs a Keyframe object with the given time and float value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public Keyframe(float fraction, float value) { + this(fraction, value, float.class); + } + + /** + * Constructs a Keyframe object with the given time and double value. The time defines the + * time, as a proportion of an overall animation's duration, at which the value will hold true + * for the animation. The value for the animation between keyframes will be calculated as + * an interpolation between the values at those keyframes. + * + * @param fraction The time, expressed as a value between 0 and 1, representing the fraction + * of time elapsed of the overall animation duration. + * @param value The value that the object will animate to as the animation time approaches + * the time in this keyframe, and the the value animated from as the time passes the time in + * this keyframe. + */ + public Keyframe(float fraction, double value) { + this(fraction, value, double.class); + } + + /** + * Gets the value for this Keyframe. + * + * @return The value for this Keyframe. + */ + public Object getValue() { + return mValue; + } + + /** + * Sets the value for this Keyframe. + * + * @param value value for this Keyframe. + */ + public void setValue(Object value) { + mValue = value; + } + + /** + * Gets the time for this keyframe, as a fraction of the overall animation duration. + * + * @return The time associated with this keyframe, as a fraction of the overall animation + * duration. This should be a value between 0 and 1. + */ + public float getFraction() { + return mFraction; + } + + /** + * Sets the time for this keyframe, as a fraction of the overall animation duration. + * + * @param fraction time associated with this keyframe, as a fraction of the overall animation + * duration. This should be a value between 0 and 1. + */ + public void setFraction(float fraction) { + mFraction = fraction; + } + + /** + * Gets the optional interpolator for this Keyframe. A value of null indicates + * that there is no interpolation, which is the same as linear interpolation. + * + * @return The optional interpolator for this Keyframe. + */ + public Interpolator getInterpolator() { + return mInterpolator; + } + + /** + * Sets the optional interpolator for this Keyframe. A value of null indicates + * that there is no interpolation, which is the same as linear interpolation. + * + * @return The optional interpolator for this Keyframe. + */ + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + /** + * Gets the type of keyframe. This information is used by ValueAnimator to determine the type of + * {@link TypeEvaluator} to use when calculating values between keyframes. The type is based + * on the type of Keyframe created. + * + * @return The type of the value stored in the Keyframe. + */ + public Class getType() { + return mValueType; + } + + @Override + public Keyframe clone() { + Keyframe kfClone = new Keyframe(mFraction, mValue, mValueType); + kfClone.setInterpolator(mInterpolator); + return kfClone; + } +} diff --git a/core/java/android/animation/KeyframeSet.java b/core/java/android/animation/KeyframeSet.java new file mode 100644 index 0000000000000000000000000000000000000000..af47a158efde5778f30db1b0f53f5f7539ac5d92 --- /dev/null +++ b/core/java/android/animation/KeyframeSet.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import java.util.ArrayList; + +import android.view.animation.Interpolator; + +/** + * This class holds a collection of Keyframe objects and is called by ValueAnimator to calculate + * values between those keyframes for a given animation. The class internal to the animation + * package because it is an implementation detail of how Keyframes are stored and used. + */ +class KeyframeSet { + + private int mNumKeyframes; + + ArrayList mKeyframes; + + public KeyframeSet(Keyframe... keyframes) { + mKeyframes = new ArrayList(); + for (Keyframe keyframe : keyframes) { + mKeyframes.add(keyframe); + } + mNumKeyframes = mKeyframes.size(); + } + + /** + * Gets the animated value, given the elapsed fraction of the animation (interpolated by the + * animation's interpolator) and the evaluator used to calculate in-between values. This + * function maps the input fraction to the appropriate keyframe interval and a fraction + * between them and returns the interpolated value. Note that the input fraction may fall + * outside the [0-1] bounds, if the animation's interpolator made that happen (e.g., a + * spring interpolation that might send the fraction past 1.0). We handle this situation by + * just using the two keyframes at the appropriate end when the value is outside those bounds. + * + * @param fraction The elapsed fraction of the animation + * @param evaluator The type evaluator to use when calculating the interpolated values. + * @return The animated value. + */ + public Object getValue(float fraction, TypeEvaluator evaluator) { + // TODO: special-case 2-keyframe common case + + if (fraction <= 0f) { + final Keyframe prevKeyframe = mKeyframes.get(0); + final Keyframe nextKeyframe = mKeyframes.get(1); + final Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevKeyframe.getFraction()) / + (nextKeyframe.getFraction() - prevKeyframe.getFraction()); + return evaluator.evaluate(intervalFraction, prevKeyframe.getValue(), + nextKeyframe.getValue()); + } else if (fraction >= 1f) { + final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2); + final Keyframe nextKeyframe = mKeyframes.get(mNumKeyframes - 1); + final Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevKeyframe.getFraction()) / + (nextKeyframe.getFraction() - prevKeyframe.getFraction()); + return evaluator.evaluate(intervalFraction, prevKeyframe.getValue(), + nextKeyframe.getValue()); + } + Keyframe prevKeyframe = mKeyframes.get(0); + for (int i = 1; i < mNumKeyframes; ++i) { + Keyframe nextKeyframe = mKeyframes.get(i); + if (fraction < nextKeyframe.getFraction()) { + final Interpolator interpolator = nextKeyframe.getInterpolator(); + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + float intervalFraction = (fraction - prevKeyframe.getFraction()) / + (nextKeyframe.getFraction() - prevKeyframe.getFraction()); + return evaluator.evaluate(intervalFraction, prevKeyframe.getValue(), + nextKeyframe.getValue()); + } + prevKeyframe = nextKeyframe; + } + // shouldn't get here + return mKeyframes.get(mNumKeyframes - 1).getValue(); + } +} diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java new file mode 100644 index 0000000000000000000000000000000000000000..69ad67ed83be44564ebf9926af722857055ff02a --- /dev/null +++ b/core/java/android/animation/LayoutTransition.java @@ -0,0 +1,782 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This class enables automatic animations on layout changes in ViewGroup objects. To enable + * transitions for a layout container, create a LayoutTransition object and set it on any + * ViewGroup by calling {@link ViewGroup#setLayoutTransition(LayoutTransition)}. This will cause + * default animations to run whenever items are added to or removed from that container. To specify + * custom animations, use the {@link LayoutTransition#setAnimator(int, Animator) + * setAnimator()} method. + * + *

One of the core concepts of these transition animations is that there are two core + * changes that cause the transition and four different animations that run because of + * those changes. The changes that trigger the transition are items being added to a container + * (referred to as an "appearing" transition) or removed from a container (also known as + * "disappearing"). The animations that run due to those events are one that animates + * items being added, one that animates items being removed, and two that animate the other + * items in the container that change due to the add/remove occurrence. Users of + * the transition may want different animations for the changing items depending on whether + * they are changing due to anappearing or disappearing event, so there is one animation for + * each of these variations of the changing event. Most of the API of this class is concerned + * with setting up the basic properties of the animations used in these four situations, + * or with setting up custom animations for any or all of the four.

+ * + *

The animations specified for the transition, both the defaults and any custom animations + * set on the transition object, are templates only. That is, these animations exist to hold the + * basic animation properties, such as the duration, start delay, and properties being animated. + * But the actual target object, as well as the start and end values for those properties, are + * set automatically in the process of setting up the transition each time it runs. Each of the + * animations is cloned from the original copy and the clone is then populated with the dynamic + * values of the target being animated (such as one of the items in a layout container that is + * moving as a result of the layout event) as well as the values that are changing (such as the + * position and size of that object). The actual values that are pushed to each animation + * depends on what properties are specified for the animation. For example, the default + * CHANGE_APPEARING animation animates left, top, right, + * and bottom. Values for these properties are updated with the pre- and post-layout + * values when the transition begins. Custom animations will be similarly populated with + * the target and values being animated, assuming they use ObjectAnimator objects with + * property names that are known on the target object.

+ */ +public class LayoutTransition { + + /** + * A flag indicating the animation that runs on those items that are changing + * due to a new item appearing in the container. + */ + public static final int CHANGE_APPEARING = 0; + + /** + * A flag indicating the animation that runs on those items that are changing + * due to a new item disappearing from the container. + */ + public static final int CHANGE_DISAPPEARING = 1; + + /** + * A flag indicating the animation that runs on those items that are changing + * due to a new item appearing in the container. + */ + public static final int APPEARING = 2; + + /** + * A flag indicating the animation that runs on those items that are changing + * due to a new item appearing in the container. + */ + public static final int DISAPPEARING = 3; + + /** + * These variables hold the animations that are currently used to run the transition effects. + * These animations are set to defaults, but can be changed to custom animations by + * calls to setAnimator(). + */ + private Animator mDisappearingAnim = null; + private Animator mAppearingAnim = null; + private Animator mChangingAppearingAnim = null; + private Animator mChangingDisappearingAnim = null; + + /** + * These are the default animations, defined in the constructor, that will be used + * unless the user specifies custom animations. + */ + private static ObjectAnimator defaultChangeIn; + private static ObjectAnimator defaultChangeOut; + private static ObjectAnimator defaultFadeIn; + private static ObjectAnimator defaultFadeOut; + + /** + * The default duration used by all animations. + */ + private static long DEFAULT_DURATION = 300; + + /** + * The durations of the four different animations + */ + private long mChangingAppearingDuration = DEFAULT_DURATION; + private long mChangingDisappearingDuration = DEFAULT_DURATION; + private long mAppearingDuration = DEFAULT_DURATION; + private long mDisappearingDuration = DEFAULT_DURATION; + + /** + * The start delays of the four different animations. Note that the default behavior of + * the appearing item is the default duration, since it should wait for the items to move + * before fading it. Same for the changing animation when disappearing; it waits for the item + * to fade out before moving the other items. + */ + private long mAppearingDelay = DEFAULT_DURATION; + private long mDisappearingDelay = 0; + private long mChangingAppearingDelay = 0; + private long mChangingDisappearingDelay = DEFAULT_DURATION; + + /** + * The inter-animation delays used on the two changing animations + */ + private long mChangingAppearingStagger = 0; + private long mChangingDisappearingStagger = 0; + + /** + * The default interpolators used for the animations + */ + private Interpolator mAppearingInterpolator = new AccelerateDecelerateInterpolator(); + private Interpolator mDisappearingInterpolator = new AccelerateDecelerateInterpolator(); + private Interpolator mChangingAppearingInterpolator = new DecelerateInterpolator(); + private Interpolator mChangingDisappearingInterpolator = new DecelerateInterpolator(); + + /** + * This hashmap is used to store the animations that are currently running as part of + * the transition. The reason for this is that a further layout event should cause + * existing animations to stop where they are prior to starting new animations. So + * we cache all of the current animations in this map for possible cancellation on + * another layout event. + */ + private HashMap currentAnimations = new HashMap(); + + /** + * This hashmap is used to track the listeners that have been added to the children of + * a container. When a layout change occurs, an animation is created for each View, so that + * the pre-layout values can be cached in that animation. Then a listener is added to the + * view to see whether the layout changes the bounds of that view. If so, the animation + * is set with the final values and then run. If not, the animation is not started. When + * the process of setting up and running all appropriate animations is done, we need to + * remove these listeners and clear out the map. + */ + private HashMap layoutChangeListenerMap = + new HashMap(); + + /** + * Used to track the current delay being assigned to successive animations as they are + * started. This value is incremented for each new animation, then zeroed before the next + * transition begins. + */ + private long staggerDelay; + + /** + * The set of listeners that should be notified when APPEARING/DISAPPEARING transitions + * start and end. + */ + private ArrayList mListeners; + + + /** + * Constructs a LayoutTransition object. By default, the object will listen to layout + * events on any ViewGroup that it is set on and will run default animations for each + * type of layout event. + */ + public LayoutTransition() { + if (defaultChangeIn == null) { + // "left" is just a placeholder; we'll put real properties/values in when needed + PropertyValuesHolder pvhLeft = new PropertyValuesHolder("left", 0, 1); + PropertyValuesHolder pvhTop = new PropertyValuesHolder("top", 0, 1); + PropertyValuesHolder pvhRight = new PropertyValuesHolder("right", 0, 1); + PropertyValuesHolder pvhBottom = new PropertyValuesHolder("bottom", 0, 1); + defaultChangeIn = new ObjectAnimator(DEFAULT_DURATION, this, + pvhLeft, pvhTop, pvhRight, pvhBottom); + defaultChangeIn.setStartDelay(mChangingAppearingDelay); + defaultChangeIn.setInterpolator(mChangingAppearingInterpolator); + defaultChangeOut = defaultChangeIn.clone(); + defaultChangeOut.setStartDelay(mChangingDisappearingDelay); + defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator); + defaultFadeIn = + new ObjectAnimator(DEFAULT_DURATION, this, "alpha", 0f, 1f); + defaultFadeIn.setStartDelay(mAppearingDelay); + defaultFadeIn.setInterpolator(mAppearingInterpolator); + defaultFadeOut = + new ObjectAnimator(DEFAULT_DURATION, this, "alpha", 1f, 0f); + defaultFadeOut.setStartDelay(mDisappearingDelay); + defaultFadeOut.setInterpolator(mDisappearingInterpolator); + } + mChangingAppearingAnim = defaultChangeIn; + mChangingDisappearingAnim = defaultChangeOut; + mAppearingAnim = defaultFadeIn; + mDisappearingAnim = defaultFadeOut; + } + + /** + * Sets the duration to be used by all animations of this transition object. If you want to + * set the duration of just one of the animations in particular, use the + * {@link #setDuration(int, long)} method. + * + * @param duration The length of time, in milliseconds, that the transition animations + * should last. + */ + public void setDuration(long duration) { + mChangingAppearingDuration = duration; + mChangingDisappearingDuration = duration; + mAppearingDuration = duration; + mDisappearingDuration = duration; + } + + /** + * Sets the start delay on one of the animation objects used by this transition. The + * transitionType parameter determines the animation whose start delay + * is being set. + * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose start + * delay is being set. + * @param delay The length of time, in milliseconds, to delay before starting the animation. + * @see Animator#setStartDelay(long) + */ + public void setStartDelay(int transitionType, long delay) { + switch (transitionType) { + case CHANGE_APPEARING: + mChangingAppearingDelay = delay; + break; + case CHANGE_DISAPPEARING: + mChangingDisappearingDelay = delay; + break; + case APPEARING: + mAppearingDelay = delay; + break; + case DISAPPEARING: + mDisappearingDelay = delay; + break; + } + } + + /** + * Gets the start delay on one of the animation objects used by this transition. The + * transitionType parameter determines the animation whose start delay + * is returned. + * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose start + * delay is returned. + * @return long The start delay of the specified animation. + * @see Animator#getStartDelay() + */ + public long getStartDelay(int transitionType) { + switch (transitionType) { + case CHANGE_APPEARING: + return mChangingAppearingDuration; + case CHANGE_DISAPPEARING: + return mChangingDisappearingDuration; + case APPEARING: + return mAppearingDuration; + case DISAPPEARING: + return mDisappearingDuration; + } + // shouldn't reach here + return 0; + } + + /** + * Sets the duration on one of the animation objects used by this transition. The + * transitionType parameter determines the animation whose duration + * is being set. + * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose + * duration is being set. + * @param duration The length of time, in milliseconds, that the specified animation should run. + * @see Animator#setDuration(long) + */ + public void setDuration(int transitionType, long duration) { + switch (transitionType) { + case CHANGE_APPEARING: + mChangingAppearingDuration = duration; + break; + case CHANGE_DISAPPEARING: + mChangingDisappearingDuration = duration; + break; + case APPEARING: + mAppearingDuration = duration; + break; + case DISAPPEARING: + mDisappearingDuration = duration; + break; + } + } + + /** + * Gets the duration on one of the animation objects used by this transition. The + * transitionType parameter determines the animation whose duration + * is returned. + * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose + * duration is returned. + * @return long The duration of the specified animation. + * @see Animator#getDuration() + */ + public long getDuration(int transitionType) { + switch (transitionType) { + case CHANGE_APPEARING: + return mChangingAppearingDuration; + case CHANGE_DISAPPEARING: + return mChangingDisappearingDuration; + case APPEARING: + return mAppearingDuration; + case DISAPPEARING: + return mDisappearingDuration; + } + // shouldn't reach here + return 0; + } + + /** + * Sets the length of time to delay between starting each animation during one of the + * CHANGE animations. + * + * @param transitionType A value of {@link #CHANGE_APPEARING} or @link #CHANGE_DISAPPEARING}. + * @param duration The length of time, in milliseconds, to delay before launching the next + * animation in the sequence. + */ + public void setStagger(int transitionType, long duration) { + switch (transitionType) { + case CHANGE_APPEARING: + mChangingAppearingStagger = duration; + break; + case CHANGE_DISAPPEARING: + mChangingDisappearingStagger = duration; + break; + // noop other cases + } + } + + /** + * Tets the length of time to delay between starting each animation during one of the + * CHANGE animations. + * + * @param transitionType A value of {@link #CHANGE_APPEARING} or @link #CHANGE_DISAPPEARING}. + * @return long The length of time, in milliseconds, to delay before launching the next + * animation in the sequence. + */ + public long getStagger(int transitionType) { + switch (transitionType) { + case CHANGE_APPEARING: + return mChangingAppearingStagger; + case CHANGE_DISAPPEARING: + return mChangingDisappearingStagger; + } + // shouldn't reach here + return 0; + } + + /** + * Sets the interpolator on one of the animation objects used by this transition. The + * transitionType parameter determines the animation whose interpolator + * is being set. + * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose + * duration is being set. + * @param interpolator The interpolator that the specified animation should use. + * @see Animator#setInterpolator(android.view.animation.Interpolator) + */ + public void setInterpolator(int transitionType, Interpolator interpolator) { + switch (transitionType) { + case CHANGE_APPEARING: + mChangingAppearingInterpolator = interpolator; + break; + case CHANGE_DISAPPEARING: + mChangingDisappearingInterpolator = interpolator; + break; + case APPEARING: + mAppearingInterpolator = interpolator; + break; + case DISAPPEARING: + mDisappearingInterpolator = interpolator; + break; + } + } + + /** + * Gets the interpolator on one of the animation objects used by this transition. The + * transitionType parameter determines the animation whose interpolator + * is returned. + * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose + * duration is being set. + * @return Interpolator The interpolator that the specified animation uses. + * @see Animator#setInterpolator(android.view.animation.Interpolator) + */ + public Interpolator getInterpolator(int transitionType) { + switch (transitionType) { + case CHANGE_APPEARING: + return mChangingAppearingInterpolator; + case CHANGE_DISAPPEARING: + return mChangingDisappearingInterpolator; + case APPEARING: + return mAppearingInterpolator; + case DISAPPEARING: + return mDisappearingInterpolator; + } + // shouldn't reach here + return null; + } + + /** + * Sets the animation used during one of the transition types that may run. Any + * Animator object can be used, but to be most useful in the context of layout + * transitions, the animation should either be a ObjectAnimator or a AnimatorSet + * of animations including PropertyAnimators. Also, these ObjectAnimator objects + * should be able to get and set values on their target objects automatically. For + * example, a ObjectAnimator that animates the property "left" is able to set and get the + * left property from the View objects being animated by the layout + * transition. The transition works by setting target objects and properties + * dynamically, according to the pre- and post-layoout values of those objects, so + * having animations that can handle those properties appropriately will work best + * for custom animation. The dynamic setting of values is only the case for the + * CHANGE animations; the APPEARING and DISAPPEARING animations are simply run with + * the values they have. + * + *

It is also worth noting that any and all animations (and their underlying + * PropertyValuesHolder objects) will have their start and end values set according + * to the pre- and post-layout values. So, for example, a custom animation on "alpha" + * as the CHANGE_APPEARING animation will inherit the real value of alpha on the target + * object (presumably 1) as its starting and ending value when the animation begins. + * Animations which need to use values at the beginning and end that may not match the + * values queried when the transition begins may need to use a different mechanism + * than a standard ObjectAnimator object.

+ * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose + * duration is being set. + * @param animator The animation being assigned. + */ + public void setAnimator(int transitionType, Animator animator) { + switch (transitionType) { + case CHANGE_APPEARING: + mChangingAppearingAnim = (animator != null) ? animator : defaultChangeIn; + break; + case CHANGE_DISAPPEARING: + mChangingDisappearingAnim = (animator != null) ? animator : defaultChangeOut; + break; + case APPEARING: + mAppearingAnim = (animator != null) ? animator : defaultFadeIn; + break; + case DISAPPEARING: + mDisappearingAnim = (animator != null) ? animator : defaultFadeOut; + break; + } + } + + /** + * Gets the animation used during one of the transition types that may run. + * + * @param transitionType one of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, + * {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the animation whose + * duration is being set. + * @return Animator The animation being used for the given transition type. + * @see #setAnimator(int, Animator) + */ + public Animator getAnimator(int transitionType) { + switch (transitionType) { + case CHANGE_APPEARING: + return mChangingAppearingAnim; + case CHANGE_DISAPPEARING: + return mChangingDisappearingAnim; + case APPEARING: + return mAppearingAnim; + case DISAPPEARING: + return mDisappearingAnim; + } + // shouldn't reach here + return null; + } + + /** + * This function sets up runs animations on all of the views that change during layout. + * For every child in the parent, we create a change animation of the appropriate + * type (appearing or disappearing) and ask it to populate its start values from its + * target view. We add layout listeners to all child views and listen for changes. For + * those views that change, we populate the end values for those animations and start them. + * Animations are not run on unchanging views. + * + * @param parent The container which is undergoing an appearing or disappearing change. + * @param newView The view being added to or removed from the parent. + * @param changeReason A value of APPEARING or DISAPPEARING, indicating whether the + * transition is occuring because an item is being added to or removed from the parent. + */ + private void runChangeTransition(final ViewGroup parent, View newView, final int changeReason) { + // reset the inter-animation delay, in case we use it later + staggerDelay = 0; + + final ViewTreeObserver observer = parent.getViewTreeObserver(); // used for later cleanup + if (!observer.isAlive()) { + // If the observer's not in a good state, skip the transition + return; + } + int numChildren = parent.getChildCount(); + + for (int i = 0; i < numChildren; ++i) { + final View child = parent.getChildAt(i); + + // only animate the views not being added or removed + if (child != newView) { + + // If there's an animation running on this view already, cancel it + Animator currentAnimation = currentAnimations.get(child); + if (currentAnimation != null) { + currentAnimation.cancel(); + currentAnimations.remove(child); + } + + // Make a copy of the appropriate animation + final Animator anim = (changeReason == APPEARING) ? + mChangingAppearingAnim.clone() : + mChangingDisappearingAnim.clone(); + + // Set the target object for the animation + anim.setTarget(child); + + // A ObjectAnimator (or AnimatorSet of them) can extract start values from + // its target object + anim.setupStartValues(); + + // Add a listener to track layout changes on this view. If we don't get a callback, + // then there's nothing to animate. + View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() { + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + + // Cache the animation in case we need to cancel it later + currentAnimations.put(child, anim); + + // Tell the animation to extract end values from the changed object + anim.setupEndValues(); + + long startDelay; + long duration; + if (changeReason == APPEARING) { + startDelay = mChangingAppearingDelay + staggerDelay; + staggerDelay += mChangingAppearingStagger; + duration = mChangingAppearingDuration; + } else { + startDelay = mChangingDisappearingDelay + staggerDelay; + staggerDelay += mChangingDisappearingStagger; + duration = mChangingDisappearingDuration; + } + anim.setStartDelay(startDelay); + anim.setDuration(duration); + + // Remove the animation from the cache when it ends + anim.addListener(new AnimatorListenerAdapter() { + private boolean canceled = false; + public void onAnimationCancel(Animator animator) { + // we remove canceled animations immediately, not here + canceled = true; + } + public void onAnimationEnd(Animator animator) { + if (!canceled) { + currentAnimations.remove(child); + } + } + }); + if (anim instanceof ObjectAnimator) { + ((ObjectAnimator) anim).setCurrentPlayTime(0); + } + anim.start(); + + // this only removes listeners whose views changed - must clear the + // other listeners later + child.removeOnLayoutChangeListener(this); + layoutChangeListenerMap.remove(child); + } + }; + child.addOnLayoutChangeListener(listener); + // cache the listener for later removal + layoutChangeListenerMap.put(child, listener); + } + } + // This is the cleanup step. When we get this rendering event, we know that all of + // the appropriate animations have been set up and run. Now we can clear out the + // layout listeners. + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + parent.getViewTreeObserver().removeOnPreDrawListener(this); + int numChildren = parent.getChildCount(); + for (int i = 0; i < numChildren; ++i) { + final View child = parent.getChildAt(i); + child.removeOnLayoutChangeListener(layoutChangeListenerMap.get(child)); + } + layoutChangeListenerMap.clear(); + return true; + } + }); + } + + /** + * This method runs the animation that makes an added item appear. + * + * @param parent The ViewGroup to which the View is being added. + * @param child The View being added to the ViewGroup. + */ + private void runAppearingTransition(final ViewGroup parent, final View child) { + Animator anim = mAppearingAnim.clone(); + anim.setTarget(child); + anim.setStartDelay(mAppearingDelay); + anim.setDuration(mAppearingDuration); + if (anim instanceof ObjectAnimator) { + ((ObjectAnimator) anim).setCurrentPlayTime(0); + } + if (mListeners != null) { + anim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd() { + for (TransitionListener listener : mListeners) { + listener.endTransition(LayoutTransition.this, parent, child, APPEARING); + } + } + }); + } + anim.start(); + } + + /** + * This method runs the animation that makes a removed item disappear. + * + * @param parent The ViewGroup from which the View is being removed. + * @param child The View being removed from the ViewGroup. + */ + private void runDisappearingTransition(final ViewGroup parent, final View child) { + Animator anim = mDisappearingAnim.clone(); + anim.setStartDelay(mDisappearingDelay); + anim.setDuration(mDisappearingDuration); + anim.setTarget(child); + if (mListeners != null) { + anim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd() { + for (TransitionListener listener : mListeners) { + listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING); + } + } + }); + } + if (anim instanceof ObjectAnimator) { + ((ObjectAnimator) anim).setCurrentPlayTime(0); + } + anim.start(); + } + + /** + * This method is called by ViewGroup when a child view is about to be added to the + * container. This callback starts the process of a transition; we grab the starting + * values, listen for changes to all of the children of the container, and start appropriate + * animations. + * + * @param parent The ViewGroup to which the View is being added. + * @param child The View being added to the ViewGroup. + */ + public void childAdd(ViewGroup parent, View child) { + if (mListeners != null) { + for (TransitionListener listener : mListeners) { + listener.startTransition(this, parent, child, APPEARING); + } + } + runChangeTransition(parent, child, APPEARING); + runAppearingTransition(parent, child); + } + + /** + * This method is called by ViewGroup when a child view is about to be removed from the + * container. This callback starts the process of a transition; we grab the starting + * values, listen for changes to all of the children of the container, and start appropriate + * animations. + * + * @param parent The ViewGroup from which the View is being removed. + * @param child The View being removed from the ViewGroup. + */ + public void childRemove(ViewGroup parent, View child) { + if (mListeners != null) { + for (TransitionListener listener : mListeners) { + listener.startTransition(this, parent, child, DISAPPEARING); + } + } + runChangeTransition(parent, child, DISAPPEARING); + runDisappearingTransition(parent, child); + } + + /** + * Add a listener that will be called when the bounds of the view change due to + * layout processing. + * + * @param listener The listener that will be called when layout bounds change. + */ + public void addTransitionListener(TransitionListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(); + } + mListeners.add(listener); + } + + /** + * Remove a listener for layout changes. + * + * @param listener The listener for layout bounds change. + */ + public void removeTransitionListener(TransitionListener listener) { + if (mListeners == null) { + return; + } + mListeners.remove(listener); + } + + /** + * Gets the current list of listeners for layout changes. + * @return + */ + public List getTransitionListeners() { + return mListeners; + } + + /** + * This interface is used for listening to starting and ending events for transitions. + */ + public interface TransitionListener { + + /** + * This event is sent to listeners when an APPEARING or DISAPPEARING transition + * begins. + * + * @param transition The LayoutTransition sending out the event. + * @param container The ViewGroup on which the transition is playing. + * @param view The View object being added or removed from its parent. + * @param transitionType The type of transition that is beginning, either + * {@link android.animation.LayoutTransition#APPEARING} or + * {@link android.animation.LayoutTransition#DISAPPEARING}. + */ + public void startTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType); + + /** + * This event is sent to listeners when an APPEARING or DISAPPEARING transition ends. + * + * @param transition The LayoutTransition sending out the event. + * @param container The ViewGroup on which the transition is playing. + * @param view The View object being added or removed from its parent. + * @param transitionType The type of transition that is ending, either + * {@link android.animation.LayoutTransition#APPEARING} or + * {@link android.animation.LayoutTransition#DISAPPEARING}. + */ + public void endTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType); + } + +} \ No newline at end of file diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java new file mode 100644 index 0000000000000000000000000000000000000000..6cb90bebb18c566d0232ffc44f48f1b3e4932b45 --- /dev/null +++ b/core/java/android/animation/ObjectAnimator.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import android.util.Log; + +import java.lang.reflect.Method; + +/** + * This subclass of {@link ValueAnimator} provides support for animating properties on target objects. + * The constructors of this class take parameters to define the target object that will be animated + * as well as the name of the property that will be animated. Appropriate set/get functions + * are then determined internally and the animation will call these functions as necessary to + * animate the property. + */ +public final class ObjectAnimator extends ValueAnimator { + + // The target object on which the property exists, set in the constructor + private Object mTarget; + + private String mPropertyName; + + /** + * Sets the name of the property that will be animated. This name is used to derive + * a setter function that will be called to set animated values. + * For example, a property name of foo will result + * in a call to the function setFoo() on the target object. If either + * valueFrom or valueTo is null, then a getter function will + * also be derived and called. + * + *

Note that the setter function derived from this property name + * must take the same parameter type as the + * valueFrom and valueTo properties, otherwise the call to + * the setter function will fail.

+ * + *

If this ObjectAnimator has been set up to animate several properties together, + * using more than one PropertyValuesHolder objects, then setting the propertyName simply + * sets the propertyName in the first of those PropertyValuesHolder objects.

+ * + * @param propertyName The name of the property being animated. + */ + public void setPropertyName(String propertyName) { + if (mValues != null) { + // mValues should always be non-null + PropertyValuesHolder valuesHolder = mValues[0]; + String oldName = valuesHolder.getPropertyName(); + valuesHolder.setPropertyName(propertyName); + mValuesMap.remove(oldName); + mValuesMap.put(propertyName, valuesHolder); + } + mPropertyName = propertyName; + } + + /** + * Gets the name of the property that will be animated. This name will be used to derive + * a setter function that will be called to set animated values. + * For example, a property name of foo will result + * in a call to the function setFoo() on the target object. If either + * valueFrom or valueTo is null, then a getter function will + * also be derived and called. + */ + public String getPropertyName() { + return mPropertyName; + } + + /** + * Determine the setter or getter function using the JavaBeans convention of setFoo or + * getFoo for a property named 'foo'. This function figures out what the name of the + * function should be and uses reflection to find the Method with that name on the + * target object. + * + * @param prefix "set" or "get", depending on whether we need a setter or getter. + * @return Method the method associated with mPropertyName. + */ + private Method getPropertyFunction(String prefix, Class valueType) { + // TODO: faster implementation... + Method returnVal = null; + String firstLetter = mPropertyName.substring(0, 1); + String theRest = mPropertyName.substring(1); + firstLetter = firstLetter.toUpperCase(); + String setterName = prefix + firstLetter + theRest; + Class args[] = null; + if (valueType != null) { + args = new Class[1]; + args[0] = valueType; + } + try { + returnVal = mTarget.getClass().getMethod(setterName, args); + } catch (NoSuchMethodException e) { + Log.e("ObjectAnimator", + "Couldn't find setter/getter for property " + mPropertyName + ": " + e); + } + return returnVal; + } + + /** + * Creates a new ObjectAnimator object. This default constructor is primarily for + * use internally; the other constructors which take parameters are more generally + * useful. + */ + public ObjectAnimator() { + } + + /** + * A constructor that takes a single property name and set of values. This constructor is + * used in the simple case of animating a single property. + * + * @param duration The length of the animation, in milliseconds. + * @param target The object whose property is to be animated. This object should + * have a public method on it called setName(), where name is + * the value of the propertyName parameter. + * @param propertyName The name of the property being animated. + * @param values The set of values to animate between. If there is only one value, it + * is assumed to be the final value being animated to, and the initial value will be + * derived on the fly. + */ + public ObjectAnimator(long duration, Object target, String propertyName, T...values) { + super(duration, (T[]) values); + mTarget = target; + setPropertyName(propertyName); + } + + /** + * A constructor that takes PropertyValueHolder values. This constructor should + * be used when animating several properties at once with the same ObjectAnimator, since + * PropertyValuesHolder allows you to associate a set of animation values with a property + * name. + * + * @param duration The length of the animation, in milliseconds. + * @param target The object whose property is to be animated. This object should + * have public methods on it called setName(), where name is + * the name of the property passed in as the propertyName parameter for + * each of the PropertyValuesHolder objects. + * @param values The PropertyValuesHolder objects which hold each the property name and values + * to animate that property between. + */ + public ObjectAnimator(long duration, Object target, PropertyValuesHolder...values) { + super(duration); + setValues(values); + mTarget = target; + } + + /** + * This function is called immediately before processing the first animation + * frame of an animation. If there is a nonzero startDelay, the + * function is called after that delay ends. + * It takes care of the final initialization steps for the + * animation. This includes setting mEvaluator, if the user has not yet + * set it up, and the setter/getter methods, if the user did not supply + * them. + * + *

Overriders of this method should call the superclass method to cause + * internal mechanisms to be set up correctly.

+ */ + @Override + void initAnimation() { + if (!mInitialized) { + // mValueType may change due to setter/getter setup; do this before calling super.init(), + // which uses mValueType to set up the default type evaluator. + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setupSetterAndGetter(mTarget); + } + super.initAnimation(); + } + } + + + /** + * The target object whose property will be animated by this animation + * + * @return The object being animated + */ + public Object getTarget() { + return mTarget; + } + + /** + * Sets the target object whose property will be animated by this animation + * + * @param target The object being animated + */ + @Override + public void setTarget(Object target) { + mTarget = target; + } + + @Override + public void setupStartValues() { + initAnimation(); + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setupStartValue(mTarget); + } + } + + @Override + public void setupEndValues() { + initAnimation(); + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setupEndValue(mTarget); + } + } + + /** + * This method is called with the elapsed fraction of the animation during every + * animation frame. This function turns the elapsed fraction into an interpolated fraction + * and then into an animated value (from the evaluator. The function is called mostly during + * animation updates, but it is also called when the end() + * function is called, to set the final value on the property. + * + *

Overrides of this method must call the superclass to perform the calculation + * of the animated value.

+ * + * @param fraction The elapsed fraction of the animation. + */ + @Override + void animateValue(float fraction) { + super.animateValue(fraction); + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].setAnimatedValue(mTarget); + } + } + + @Override + public ObjectAnimator clone() { + final ObjectAnimator anim = (ObjectAnimator) super.clone(); + return anim; + } +} diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..1d46123db4375f6b33fece8d14b14e80dba73748 --- /dev/null +++ b/core/java/android/animation/PropertyValuesHolder.java @@ -0,0 +1,579 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This class holds information about a property and the values that that property + * should take on during an animation. PropertyValuesHolder objects can be used to create + * animations with ValueAnimator or ObjectAnimator that operate on several different properties + * in parallel. + */ +public class PropertyValuesHolder implements Cloneable { + + /** + * The name of the property associated with the values. This need not be a real property, + * unless this object is being used with ObjectAnimator. But this is the name by which + * aniamted values are looked up with getAnimatedValue(String) in ValueAnimator. + */ + private String mPropertyName; + + /** + * The setter function, if needed. ObjectAnimator hands off this functionality to + * PropertyValuesHolder, since it holds all of the per-property information. This + * property can be manually set via setSetter(). Otherwise, it is automatically + * derived when the animation starts in setupSetterAndGetter() if using ObjectAnimator. + */ + private Method mSetter = null; + + /** + * The getter function, if needed. ObjectAnimator hands off this functionality to + * PropertyValuesHolder, since it holds all of the per-property information. This + * property can be manually set via setSetter(). Otherwise, it is automatically + * derived when the animation starts in setupSetterAndGetter() if using ObjectAnimator. + * The getter is only derived and used if one of the values is null. + */ + private Method mGetter = null; + + /** + * The type of values supplied. This information is used both in deriving the setter/getter + * functions and in deriving the type of TypeEvaluator. + */ + private Class mValueType; + + /** + * The set of keyframes (time/value pairs) that define this animation. + */ + private KeyframeSet mKeyframeSet = null; + + + // type evaluators for the three primitive types handled by this implementation + private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); + private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); + private static final TypeEvaluator sDoubleEvaluator = new DoubleEvaluator(); + + // We try several different types when searching for appropriate setter/getter functions. + // The caller may have supplied values in a type that does not match the setter/getter + // functions (such as the integers 0 and 1 to represent floating point values for alpha). + // Also, the use of generics in constructors means that we end up with the Object versions + // of primitive types (Float vs. float). But most likely, the setter/getter functions + // will take primitive types instead. + // So we supply an ordered array of other types to try before giving up. + private static Class[] FLOAT_VARIANTS = {float.class, Float.class, double.class, int.class, + Double.class, Integer.class}; + private static Class[] INTEGER_VARIANTS = {int.class, Integer.class, float.class, double.class, + Float.class, Double.class}; + private static Class[] DOUBLE_VARIANTS = {double.class, Double.class, float.class, int.class, + Float.class, Integer.class}; + + // These maps hold all property entries for a particular class. This map + // is used to speed up property/setter/getter lookups for a given class/property + // combination. No need to use reflection on the combination more than once. + private static final HashMap> sSetterPropertyMap = + new HashMap>(); + private static final HashMap> sGetterPropertyMap = + new HashMap>(); + + // This lock is used to ensure that only one thread is accessing the property maps + // at a time. + private ReentrantReadWriteLock propertyMapLock = new ReentrantReadWriteLock(); + + // Used to pass single value to varargs parameter in setter invocation + private Object[] mTmpValueArray = new Object[1]; + + /** + * The type evaluator used to calculate the animated values. This evaluator is determined + * automatically based on the type of the start/end objects passed into the constructor, + * but the system only knows about the primitive types int, double, and float. Any other + * type will need to set the evaluator to a custom evaluator for that type. + */ + private TypeEvaluator mEvaluator; + + /** + * The value most recently calculated by calculateValue(). This is set during + * that function and might be retrieved later either by ValueAnimator.animatedValue() or + * by the property-setting logic in ObjectAnimator.animatedValue(). + */ + private Object mAnimatedValue; + + /** + * Constructs a PropertyValuesHolder object with just a set of values. This constructor + * is typically not used when animating objects with ObjectAnimator, because that + * object needs distinct and meaningful property names. Simpler animations of one + * set of values using ValueAnimator may use this constructor, however, because no + * distinguishing name is needed. + * @param values The set of values to animate between. If there is only one value, it + * is assumed to be the final value being animated to, and the initial value will be + * derived on the fly. + */ + public PropertyValuesHolder(T...values) { + this(null, values); + } + + /** + * Constructs a PropertyValuesHolder object with the specified property name and set of + * values. These values can be of any type, but the type should be consistent so that + * an appropriate {@link android.animation.TypeEvaluator} can be found that matches + * the common type. + *

If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling a getter function + * on the object. Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction + * {@link ObjectAnimator}, and with a getter function either + * derived automatically from propertyName or set explicitly via + * {@link #setGetter(java.lang.reflect.Method)}, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * @param propertyName The name of the property associated with this set of values. This + * can be the actual property name to be used when using a ObjectAnimator object, or + * just a name used to get animated values, such as if this object is used with an + * ValueAnimator object. + * @param values The set of values to animate between. + */ + public PropertyValuesHolder(String propertyName, T... values) { + mPropertyName = propertyName; + setValues(values); + } + + /** + * Sets the values being animated between. + * If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling a getter function + * on the object. Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction + * {@link ObjectAnimator}, and with a getter function either + * derived automatically from propertyName or set explicitly via + * {@link #setGetter(java.lang.reflect.Method)}, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * @param values The set of values to animate between. + */ + public void setValues(T... values) { + int numKeyframes = values.length; + for (int i = 0; i < numKeyframes; ++i) { + if (values[i] != null) { + Class thisValueType = values[i].getClass(); + if (mValueType == null) { + mValueType = thisValueType; + } else { + if (thisValueType != mValueType) { + if (mValueType == Integer.class && + (thisValueType == Float.class || thisValueType == Double.class)) { + mValueType = thisValueType; + } else if (mValueType == Float.class && thisValueType == Double.class) { + mValueType = thisValueType; + } + } + } + } + } + Keyframe keyframes[] = new Keyframe[Math.max(numKeyframes,2)]; + if (mValueType.equals(Keyframe.class)) { + mValueType = ((Keyframe)values[0]).getType(); + for (int i = 0; i < numKeyframes; ++i) { + keyframes[i] = (Keyframe)values[i]; + } + } else { + if (numKeyframes == 1) { + keyframes[0] = new Keyframe(0f, (Object) null); + keyframes[1] = new Keyframe(1f, values[0]); + } else { + keyframes[0] = new Keyframe(0f, values[0]); + for (int i = 1; i < numKeyframes; ++i) { + if (values[i] != null && (values[i].getClass() != mValueType)) { + + } + keyframes[i] = new Keyframe((float) i / (numKeyframes - 1), values[i]); + } + } + } + mKeyframeSet = new KeyframeSet(keyframes); + } + + + + /** + * Determine the setter or getter function using the JavaBeans convention of setFoo or + * getFoo for a property named 'foo'. This function figures out what the name of the + * function should be and uses reflection to find the Method with that name on the + * target object. + * + * @param targetClass The class to search for the method + * @param prefix "set" or "get", depending on whether we need a setter or getter. + * @param valueType The type of the parameter (in the case of a setter). This type + * is derived from the values set on this PropertyValuesHolder. This type is used as + * a first guess at the parameter type, but we check for methods with several different + * types to avoid problems with slight mis-matches between supplied values and actual + * value types used on the setter. + * @return Method the method associated with mPropertyName. + */ + private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) { + // TODO: faster implementation... + Method returnVal = null; + String firstLetter = mPropertyName.substring(0, 1); + String theRest = mPropertyName.substring(1); + firstLetter = firstLetter.toUpperCase(); + String methodName = prefix + firstLetter + theRest; + Class args[] = null; + if (valueType == null) { + try { + returnVal = targetClass.getMethod(methodName, args); + } catch (NoSuchMethodException e) { + Log.e("PropertyValuesHolder", + "Couldn't find no-arg method for property " + mPropertyName + ": " + e); + } + } else { + args = new Class[1]; + Class typeVariants[]; + if (mValueType.equals(Float.class)) { + typeVariants = FLOAT_VARIANTS; + } else if (mValueType.equals(Integer.class)) { + typeVariants = INTEGER_VARIANTS; + } else if (mValueType.equals(Double.class)) { + typeVariants = DOUBLE_VARIANTS; + } else { + typeVariants = new Class[1]; + typeVariants[0] = mValueType; + } + for (Class typeVariant : typeVariants) { + args[0] = typeVariant; + try { + returnVal = targetClass.getMethod(methodName, args); + // change the value type to suit + mValueType = typeVariant; + return returnVal; + } catch (NoSuchMethodException e) { + // Swallow the error and keep trying other variants + } + } + // If we got here, then no appropriate function was found + Log.e("PropertyValuesHolder", + "Couldn't find setter/getter for property " + mPropertyName + + "with value type "+ mValueType); + } + + return returnVal; + } + + + /** + * Returns the setter or getter requested. This utility function checks whether the + * requested method exists in the propertyMapMap cache. If not, it calls another + * utility function to request the Method from the targetClass directly. + * @param targetClass The Class on which the requested method should exist. + * @param propertyMapMap The cache of setters/getters derived so far. + * @param prefix "set" or "get", for the setter or getter. + * @param valueType The type of parameter passed into the method (null for getter). + * @return Method the method associated with mPropertyName. + */ + private Method setupSetterOrGetter(Class targetClass, + HashMap> propertyMapMap, + String prefix, Class valueType) { + Method setterOrGetter = null; + try { + // Have to lock property map prior to reading it, to guard against + // another thread putting something in there after we've checked it + // but before we've added an entry to it + // TODO: can we store the setter/getter per Class instead of per Object? + propertyMapLock.writeLock().lock(); + HashMap propertyMap = propertyMapMap.get(targetClass); + if (propertyMap != null) { + setterOrGetter = propertyMap.get(mPropertyName); + } + if (setterOrGetter == null) { + setterOrGetter = getPropertyFunction(targetClass, prefix, valueType); + if (propertyMap == null) { + propertyMap = new HashMap(); + propertyMapMap.put(targetClass, propertyMap); + } + propertyMap.put(mPropertyName, setterOrGetter); + } + } finally { + propertyMapLock.writeLock().unlock(); + } + return setterOrGetter; + } + + /** + * Utility function to get the setter from targetClass + * @param targetClass The Class on which the requested method should exist. + */ + private void setupSetter(Class targetClass) { + mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType); + } + + /** + * Utility function to get the getter from targetClass + */ + private void setupGetter(Class targetClass) { + mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null); + } + + /** + * Internal function (called from ObjectAnimator) to set up the setter and getter + * prior to running the animation. If the setter has not been manually set for this + * object, it will be derived automatically given the property name, target object, and + * types of values supplied. If no getter has been set, it will be supplied iff any of the + * supplied values was null. If there is a null value, then the getter (supplied or derived) + * will be called to set those null values to the current value of the property + * on the target object. + * @param target The object on which the setter (and possibly getter) exist. + */ + void setupSetterAndGetter(Object target) { + Class targetClass = target.getClass(); + if (mSetter == null) { + setupSetter(targetClass); + } + for (Keyframe kf : mKeyframeSet.mKeyframes) { + if (kf.getValue() == null) { + if (mGetter == null) { + setupGetter(targetClass); + } + try { + kf.setValue((T) mGetter.invoke(target)); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + } + } + + /** + * Utility function to set the value stored in a particular Keyframe. The value used is + * whatever the value is for the property name specified in the keyframe on the target object. + * + * @param target The target object from which the current value should be extracted. + * @param kf The keyframe which holds the property name and value. + */ + private void setupValue(Object target, Keyframe kf) { + try { + if (mGetter == null) { + Class targetClass = target.getClass(); + setupGetter(targetClass); + } + kf.setValue((T) mGetter.invoke(target)); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + + /** + * This function is called by ObjectAnimator when setting the start values for an animation. + * The start values are set according to the current values in the target object. The + * property whose value is extracted is whatever is specified by the propertyName of this + * PropertyValuesHolder object. + * + * @param target The object which holds the start values that should be set. + */ + void setupStartValue(Object target) { + setupValue(target, mKeyframeSet.mKeyframes.get(0)); + } + + /** + * This function is called by ObjectAnimator when setting the end values for an animation. + * The end values are set according to the current values in the target object. The + * property whose value is extracted is whatever is specified by the propertyName of this + * PropertyValuesHolder object. + * + * @param target The object which holds the start values that should be set. + */ + void setupEndValue(Object target) { + setupValue(target, mKeyframeSet.mKeyframes.get(mKeyframeSet.mKeyframes.size() - 1)); + } + + @Override + public PropertyValuesHolder clone() { + ArrayList keyframes = mKeyframeSet.mKeyframes; + int numKeyframes = mKeyframeSet.mKeyframes.size(); + Keyframe[] newKeyframes = new Keyframe[numKeyframes]; + for (int i = 0; i < numKeyframes; ++i) { + newKeyframes[i] = keyframes.get(i).clone(); + } + PropertyValuesHolder pvhClone = new PropertyValuesHolder(mPropertyName, + (Object[]) newKeyframes); + return pvhClone; + } + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * @param target The target object on which the value is set + */ + void setAnimatedValue(Object target) { + if (mSetter != null) { + try { + mTmpValueArray[0] = mAnimatedValue; + mSetter.invoke(target, mTmpValueArray); + } catch (InvocationTargetException e) { + Log.e("PropertyValuesHolder", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyValuesHolder", e.toString()); + } + } + } + + /** + * Internal function, called by ValueAnimator, to set up the TypeEvaluator that will be used + * to calculate animated values. + */ + void init() { + if (mEvaluator == null) { + mEvaluator = (mValueType == int.class || mValueType == Integer.class) ? sIntEvaluator : + (mValueType == double.class || mValueType == Double.class) ? sDoubleEvaluator : + sFloatEvaluator; + } + } + + /** + * The TypeEvaluator will the automatically determined based on the type of values + * supplied to PropertyValuesHolder. The evaluator can be manually set, however, if so + * desired. This may be important in cases where either the type of the values supplied + * do not match the way that they should be interpolated between, or if the values + * are of a custom type or one not currently understood by the animation system. Currently, + * only values of type float, double, and int (and their Object equivalents, Float, Double, + * and Integer) are correctly interpolated; all other types require setting a TypeEvaluator. + * @param evaluator + */ + public void setEvaluator(TypeEvaluator evaluator) { + mEvaluator = evaluator; + } + + /** + * Function used to calculate the value according to the evaluator set up for + * this PropertyValuesHolder object. This function is called by ValueAnimator.animateValue(). + * + * @param fraction The elapsed, interpolated fraction of the animation. + * @return The calculated value at this point in the animation. + */ + Object calculateValue(float fraction) { + mAnimatedValue = mKeyframeSet.getValue(fraction, mEvaluator); + return mAnimatedValue; + } + + /** + * Sets the Method that is called with the animated values calculated + * during the animation. Setting the setter method is an alternative to supplying a + * {@link #setPropertyName(String) propertyName} from which the method is derived. This + * approach is more direct, and is especially useful when a function must be called that does + * not correspond to the convention of setName(). For example, if a function + * called offset() is to be called with the animated values, there is no way + * to tell ObjectAnimator how to call that function simply through a property + * name, so a setter method should be supplied instead. + * + *

Note that the setter function must take the same parameter type as the + * valueFrom and valueTo properties, otherwise the call to + * the setter function will fail.

+ * + * @param setter The setter method that should be called with the animated values. + */ + public void setSetter(Method setter) { + mSetter = setter; + } + + /** + * Gets the Method that is called with the animated values calculated + * during the animation. + */ + public Method getSetter() { + return mSetter; + } + + /** + * Sets the Method that is called to get unsupplied valueFrom or + * valueTo properties. Setting the getter method is an alternative to supplying a + * {@link #setPropertyName(String) propertyName} from which the method is derived. This + * approach is more direct, and is especially useful when a function must be called that does + * not correspond to the convention of setName(). For example, if a function + * called offset() is to be called to get an initial value, there is no way + * to tell ObjectAnimator how to call that function simply through a property + * name, so a getter method should be supplied instead. + * + *

Note that the getter method is only called whether supplied here or derived + * from the property name, if one of valueFrom or valueTo are + * null. If both of those values are non-null, then there is no need to get one of the + * values and the getter is not called. + * + *

Note that the getter function must return the same parameter type as the + * valueFrom and valueTo properties (whichever of them are + * non-null), otherwise the call to the getter function will fail.

+ * + * @param getter The getter method that should be called to get initial animation values. + */ + public void setGetter(Method getter) { + mGetter = getter; + } + + /** + * Gets the Method that is called to get unsupplied valueFrom or + * valueTo properties. + */ + public Method getGetter() { + return mGetter; + } + + /** + * Sets the name of the property that will be animated. This name is used to derive + * a setter function that will be called to set animated values. + * For example, a property name of foo will result + * in a call to the function setFoo() on the target object. If either + * valueFrom or valueTo is null, then a getter function will + * also be derived and called. + * + *

Note that the setter function derived from this property name + * must take the same parameter type as the + * valueFrom and valueTo properties, otherwise the call to + * the setter function will fail.

+ * + * @param propertyName The name of the property being animated. + */ + public void setPropertyName(String propertyName) { + mPropertyName = propertyName; + } + + /** + * Gets the name of the property that will be animated. This name will be used to derive + * a setter function that will be called to set animated values. + * For example, a property name of foo will result + * in a call to the function setFoo() on the target object. If either + * valueFrom or valueTo is null, then a getter function will + * also be derived and called. + */ + public String getPropertyName() { + return mPropertyName; + } + + /** + * Internal function, called by ValueAnimator and ObjectAnimator, to retrieve the value + * most recently calculated in calculateValue(). + * @return + */ + Object getAnimatedValue() { + return mAnimatedValue; + } +} \ No newline at end of file diff --git a/core/java/android/animation/RGBEvaluator.java b/core/java/android/animation/RGBEvaluator.java new file mode 100644 index 0000000000000000000000000000000000000000..bae0af0c8b36b3e5b381ef36f8f29c8329b4372a --- /dev/null +++ b/core/java/android/animation/RGBEvaluator.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +/** + * This evaluator can be used to perform type interpolation between integer + * values that represent ARGB colors. + */ +public class RGBEvaluator implements TypeEvaluator { + + /** + * This function returns the calculated in-between value for a color + * given integers that represent the start and end values in the four + * bytes of the 32-bit int. Each channel is separately linearly interpolated + * and the resulting calculated values are recombined into the return value. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue A 32-bit int value representing colors in the + * separate bytes of the parameter + * @param endValue A 32-bit int value representing colors in the + * separate bytes of the parameter + * @return A value that is calculated to be the linearly interpolated + * result, derived by separating the start and end values into separate + * color channels and interpolating each one separately, recombining the + * resulting values in the same way. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + int startInt = (Integer) startValue; + int startA = (startInt >> 24); + int startR = (startInt >> 16) & 0xff; + int startG = (startInt >> 8) & 0xff; + int startB = startInt & 0xff; + + int endInt = (Integer) endValue; + int endA = (endInt >> 24); + int endR = (endInt >> 16) & 0xff; + int endG = (endInt >> 8) & 0xff; + int endB = endInt & 0xff; + + return (int)((startA + (int)(fraction * (endA - startA))) << 24) | + (int)((startR + (int)(fraction * (endR - startR))) << 16) | + (int)((startG + (int)(fraction * (endG - startG))) << 8) | + (int)((startB + (int)(fraction * (endB - startB)))); + } +} \ No newline at end of file diff --git a/core/java/android/animation/TypeEvaluator.java b/core/java/android/animation/TypeEvaluator.java new file mode 100644 index 0000000000000000000000000000000000000000..fa49175ce846f8a7dc278b9025c7a76aaa5dcb7d --- /dev/null +++ b/core/java/android/animation/TypeEvaluator.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +/** + * Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators + * allow developers to create animations on arbitrary property types, by allowing them to supply + * custom evaulators for types that are not automatically understood and used by the animation + * system. + * + * @see ValueAnimator#setEvaluator(TypeEvaluator) + */ +public interface TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * fraction representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0), + * where x0 is startValue, x1 is endValue, + * and t is fraction. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value. + * @param endValue The end value. + * @return A linear interpolation between the start and end values, given the + * fraction parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue); + +} \ No newline at end of file diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java new file mode 100755 index 0000000000000000000000000000000000000000..1e2bbccb74477330fa1480a4af04d6b4f33d68f7 --- /dev/null +++ b/core/java/android/animation/ValueAnimator.java @@ -0,0 +1,981 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This class provides a simple timing engine for running animations + * which calculate animated values and set them on target objects. + * + *

There is a single timing pulse that all animations use. It runs in a + * custom handler to ensure that property changes happen on the UI thread.

+ * + *

By default, ValueAnimator uses non-linear time interpolation, via the + * {@link AccelerateDecelerateInterpolator} class, which accelerates into and decelerates + * out of an animation. This behavior can be changed by calling + * {@link ValueAnimator#setInterpolator(Interpolator)}.

+ */ +public class ValueAnimator extends Animator { + + /** + * Internal constants + */ + + /* + * The default amount of time in ms between animation frames + */ + private static final long DEFAULT_FRAME_DELAY = 30; + + /** + * Messages sent to timing handler: START is sent when an animation first begins, FRAME is sent + * by the handler to itself to process the next animation frame + */ + private static final int ANIMATION_START = 0; + private static final int ANIMATION_FRAME = 1; + + /** + * Values used with internal variable mPlayingState to indicate the current state of an + * animation. + */ + private static final int STOPPED = 0; // Not yet playing + private static final int RUNNING = 1; // Playing normally + private static final int CANCELED = 2; // cancel() called - need to end it + private static final int ENDED = 3; // end() called - need to end it + private static final int SEEKED = 4; // Seeked to some time value + + /** + * Internal variables + * NOTE: This object implements the clone() method, making a deep copy of any referenced + * objects. As other non-trivial fields are added to this class, make sure to add logic + * to clone() to make deep copies of them. + */ + + // The first time that the animation's animateFrame() method is called. This time is used to + // determine elapsed time (and therefore the elapsed fraction) in subsequent calls + // to animateFrame() + private long mStartTime; + + /** + * Set when setCurrentPlayTime() is called. If negative, animation is not currently seeked + * to a value. + */ + private long mSeekTime = -1; + + // The static sAnimationHandler processes the internal timing loop on which all animations + // are based + private static AnimationHandler sAnimationHandler; + + // The static list of all active animations + private static final ArrayList sAnimations = new ArrayList(); + + // The set of animations to be started on the next animation frame + private static final ArrayList sPendingAnimations = new ArrayList(); + + // The time interpolator to be used if none is set on the animation + private static final Interpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator(); + + // type evaluators for the three primitive types handled by this implementation + private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); + private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); + private static final TypeEvaluator sDoubleEvaluator = new DoubleEvaluator(); + + /** + * Used to indicate whether the animation is currently playing in reverse. This causes the + * elapsed fraction to be inverted to calculate the appropriate values. + */ + private boolean mPlayingBackwards = false; + + /** + * This variable tracks the current iteration that is playing. When mCurrentIteration exceeds the + * repeatCount (if repeatCount!=INFINITE), the animation ends + */ + private int mCurrentIteration = 0; + + /** + * Tracks whether a startDelay'd animation has begun playing through the startDelay. + */ + private boolean mStartedDelay = false; + + /** + * Tracks the time at which the animation began playing through its startDelay. This is + * different from the mStartTime variable, which is used to track when the animation became + * active (which is when the startDelay expired and the animation was added to the active + * animations list). + */ + private long mDelayStartTime; + + /** + * Flag that represents the current state of the animation. Used to figure out when to start + * an animation (if state == STOPPED). Also used to end an animation that + * has been cancel()'d or end()'d since the last animation frame. Possible values are + * STOPPED, RUNNING, ENDED, CANCELED. + */ + private int mPlayingState = STOPPED; + + /** + * Internal collections used to avoid set collisions as animations start and end while being + * processed. + */ + private static final ArrayList sEndingAnims = new ArrayList(); + private static final ArrayList sDelayedAnims = new ArrayList(); + private static final ArrayList sReadyAnims = new ArrayList(); + + /** + * Flag that denotes whether the animation is set up and ready to go. Used to + * set up animation that has not yet been started. + */ + boolean mInitialized = false; + + // + // Backing variables + // + + // How long the animation should last in ms + private long mDuration; + + // The amount of time in ms to delay starting the animation after start() is called + private long mStartDelay = 0; + + // The number of milliseconds between animation frames + private static long sFrameDelay = DEFAULT_FRAME_DELAY; + + // The number of times the animation will repeat. The default is 0, which means the animation + // will play only once + private int mRepeatCount = 0; + + /** + * The type of repetition that will occur when repeatMode is nonzero. RESTART means the + * animation will start from the beginning on every new cycle. REVERSE means the animation + * will reverse directions on each iteration. + */ + private int mRepeatMode = RESTART; + + /** + * The time interpolator to be used. The elapsed fraction of the animation will be passed + * through this interpolator to calculate the interpolated fraction, which is then used to + * calculate the animated values. + */ + private Interpolator mInterpolator = sDefaultInterpolator; + + /** + * The set of listeners to be sent events through the life of an animation. + */ + private ArrayList mUpdateListeners = null; + + /** + * The property/value sets being animated. + */ + PropertyValuesHolder[] mValues; + + /** + * A hashmap of the PropertyValuesHolder objects. This map is used to lookup animated values + * by property name during calls to getAnimatedValue(String). + */ + HashMap mValuesMap; + + /** + * Public constants + */ + + /** + * When the animation reaches the end and repeatCount is INFINITE + * or a positive value, the animation restarts from the beginning. + */ + public static final int RESTART = 1; + /** + * When the animation reaches the end and repeatCount is INFINITE + * or a positive value, the animation reverses direction on every iteration. + */ + public static final int REVERSE = 2; + /** + * This value used used with the {@link #setRepeatCount(int)} property to repeat + * the animation indefinitely. + */ + public static final int INFINITE = -1; + + /** + * Creates a new ValueAnimator object. This default constructor is primarily for + * use internally; the other constructors which take parameters are more generally + * useful. + */ + public ValueAnimator() { + } + + /** + * Constructs an ValueAnimator object with the specified duration and set of + * values. If the values are a set of PropertyValuesHolder objects, then these objects + * define the potentially multiple properties being animated and the values the properties are + * animated between. Otherwise, the values define a single set of values animated between. + * + * @param duration The length of the animation, in milliseconds. + * @param values The set of values to animate between. If these values are not + * PropertyValuesHolder objects, then there should be more than one value, since the values + * determine the interval to animate between. + */ + public ValueAnimator(long duration, T...values) { + mDuration = duration; + if (values.length > 0) { + setValues(values); + } + } + + /** + * Sets the values, per property, being animated between. This function is called internally + * by the constructors of ValueAnimator that take a list of values. But an ValueAnimator can + * be constructed without values and this method can be called to set the values manually + * instead. + * + * @param values The set of values, per property, being animated between. + */ + public void setValues(PropertyValuesHolder... values) { + int numValues = values.length; + mValues = values; + mValuesMap = new HashMap(numValues); + for (int i = 0; i < numValues; ++i) { + PropertyValuesHolder valuesHolder = (PropertyValuesHolder) values[i]; + mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder); + } + } + + /** + * Returns the values that this ValueAnimator animates between. These values are stored in + * PropertyValuesHolder objects, even if the ValueAnimator was created with a simple list + * of value objects instead. + * + * @return PropertyValuesHolder[] An array of PropertyValuesHolder objects which hold the + * values, per property, that define the animation. + */ + public PropertyValuesHolder[] getValues() { + return mValues; + } + + /** + * Sets the values to animate between for this animation. If values is + * a set of PropertyValuesHolder objects, these objects will become the set of properties + * animated and the values that those properties are animated between. Otherwise, this method + * will set only one set of values for the ValueAnimator. Also, if the values are not + * PropertyValuesHolder objects and if there are already multiple sets of + * values defined for this ValueAnimator via + * more than one PropertyValuesHolder objects, this method will set the values for + * the first of those objects. + * + * @param values The set of values to animate between. + */ + public void setValues(T... values) { + if (mValues == null || mValues.length == 0) { + setValues(new PropertyValuesHolder[]{ + new PropertyValuesHolder("", (Object[])values)}); + } else { + PropertyValuesHolder valuesHolder = mValues[0]; + valuesHolder.setValues(values); + } + } + + /** + * This function is called immediately before processing the first animation + * frame of an animation. If there is a nonzero startDelay, the + * function is called after that delay ends. + * It takes care of the final initialization steps for the + * animation. + * + *

Overrides of this method should call the superclass method to ensure + * that internal mechanisms for the animation are set up correctly.

+ */ + void initAnimation() { + if (!mInitialized) { + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].init(); + } + mCurrentIteration = 0; + mInitialized = true; + } + } + + + /** + * Sets the length of the animation. + * + * @param duration The length of the animation, in milliseconds. + */ + public void setDuration(long duration) { + mDuration = duration; + } + + /** + * Gets the length of the animation. + * + * @return The length of the animation, in milliseconds. + */ + public long getDuration() { + return mDuration; + } + + /** + * Sets the position of the animation to the specified point in time. This time should + * be between 0 and the total duration of the animation, including any repetition. If + * the animation has not yet been started, then it will not advance forward after it is + * set to this time; it will simply set the time to this value and perform any appropriate + * actions based on that time. If the animation is already running, then setCurrentPlayTime() + * will set the current playing time to this value and continue playing from that point. + * + * @param playTime The time, in milliseconds, to which the animation is advanced or rewound. + */ + public void setCurrentPlayTime(long playTime) { + initAnimation(); + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + if (mPlayingState != RUNNING) { + mSeekTime = playTime; + mPlayingState = SEEKED; + } + mStartTime = currentTime - playTime; + animationFrame(currentTime); + } + + /** + * Gets the current position of the animation in time, which is equal to the current + * time minus the time that the animation started. An animation that is not yet started will + * return a value of zero. + * + * @return The current position in time of the animation. + */ + public long getCurrentPlayTime() { + if (!mInitialized || mPlayingState == STOPPED) { + return 0; + } + return AnimationUtils.currentAnimationTimeMillis() - mStartTime; + } + + /** + * This custom, static handler handles the timing pulse that is shared by + * all active animations. This approach ensures that the setting of animation + * values will happen on the UI thread and that all animations will share + * the same times for calculating their values, which makes synchronizing + * animations possible. + * + */ + private static class AnimationHandler extends Handler { + /** + * There are only two messages that we care about: ANIMATION_START and + * ANIMATION_FRAME. The START message is sent when an animation's start() + * method is called. It cannot start synchronously when start() is called + * because the call may be on the wrong thread, and it would also not be + * synchronized with other animations because it would not start on a common + * timing pulse. So each animation sends a START message to the handler, which + * causes the handler to place the animation on the active animations queue and + * start processing frames for that animation. + * The FRAME message is the one that is sent over and over while there are any + * active animations to process. + */ + @Override + public void handleMessage(Message msg) { + boolean callAgain = true; + switch (msg.what) { + // TODO: should we avoid sending frame message when starting if we + // were already running? + case ANIMATION_START: + if (sAnimations.size() > 0 || sDelayedAnims.size() > 0) { + callAgain = false; + } + // pendingAnims holds any animations that have requested to be started + // We're going to clear sPendingAnimations, but starting animation may + // cause more to be added to the pending list (for example, if one animation + // starting triggers another starting). So we loop until sPendingAnimations + // is empty. + while (sPendingAnimations.size() > 0) { + ArrayList pendingCopy = + (ArrayList) sPendingAnimations.clone(); + sPendingAnimations.clear(); + int count = pendingCopy.size(); + for (int i = 0; i < count; ++i) { + ValueAnimator anim = pendingCopy.get(i); + // If the animation has a startDelay, place it on the delayed list + if (anim.mStartDelay == 0 || anim.mPlayingState == ENDED || + anim.mPlayingState == CANCELED) { + anim.startAnimation(); + } else { + sDelayedAnims.add(anim); + } + } + } + // fall through to process first frame of new animations + case ANIMATION_FRAME: + // currentTime holds the common time for all animations processed + // during this frame + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + // First, process animations currently sitting on the delayed queue, adding + // them to the active animations if they are ready + int numDelayedAnims = sDelayedAnims.size(); + for (int i = 0; i < numDelayedAnims; ++i) { + ValueAnimator anim = sDelayedAnims.get(i); + if (anim.delayedAnimationFrame(currentTime)) { + sReadyAnims.add(anim); + } + } + int numReadyAnims = sReadyAnims.size(); + if (numReadyAnims > 0) { + for (int i = 0; i < numReadyAnims; ++i) { + ValueAnimator anim = sReadyAnims.get(i); + anim.startAnimation(); + sDelayedAnims.remove(anim); + } + sReadyAnims.clear(); + } + + // Now process all active animations. The return value from animationFrame() + // tells the handler whether it should now be ended + int numAnims = sAnimations.size(); + for (int i = 0; i < numAnims; ++i) { + ValueAnimator anim = sAnimations.get(i); + if (anim.animationFrame(currentTime)) { + sEndingAnims.add(anim); + } + } + if (sEndingAnims.size() > 0) { + for (int i = 0; i < sEndingAnims.size(); ++i) { + sEndingAnims.get(i).endAnimation(); + } + sEndingAnims.clear(); + } + + // If there are still active or delayed animations, call the handler again + // after the frameDelay + if (callAgain && (!sAnimations.isEmpty() || !sDelayedAnims.isEmpty())) { + sendEmptyMessageDelayed(ANIMATION_FRAME, sFrameDelay); + } + break; + } + } + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + * + * @return the number of milliseconds to delay running the animation + */ + public long getStartDelay() { + return mStartDelay; + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + + * @param startDelay The amount of the delay, in milliseconds + */ + public void setStartDelay(long startDelay) { + this.mStartDelay = startDelay; + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * @return the requested time between frames, in milliseconds + */ + public static long getFrameDelay() { + return sFrameDelay; + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * @param frameDelay the requested time between frames, in milliseconds + */ + public static void setFrameDelay(long frameDelay) { + sFrameDelay = frameDelay; + } + + /** + * The most recent value calculated by this ValueAnimator when there is just one + * property being animated. This value is only sensible while the animation is running. The main + * purpose for this read-only property is to retrieve the value from the ValueAnimator + * during a call to {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}, which + * is called during each animation frame, immediately after the value is calculated. + * + * @return animatedValue The value most recently calculated by this ValueAnimator for + * the single property being animated. If there are several properties being animated + * (specified by several PropertyValuesHolder objects in the constructor), this function + * returns the animated value for the first of those objects. + */ + public Object getAnimatedValue() { + if (mValues != null && mValues.length > 0) { + return mValues[0].getAnimatedValue(); + } + // Shouldn't get here; should always have values unless ValueAnimator was set up wrong + return null; + } + + /** + * The most recent value calculated by this ValueAnimator for propertyName. + * The main purpose for this read-only property is to retrieve the value from the + * ValueAnimator during a call to + * {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}, which + * is called during each animation frame, immediately after the value is calculated. + * + * @return animatedValue The value most recently calculated for the named property + * by this ValueAnimator. + */ + public Object getAnimatedValue(String propertyName) { + PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName); + if (valuesHolder != null) { + return valuesHolder.getAnimatedValue(); + } else { + // At least avoid crashing if called with bogus propertyName + return null; + } + } + + /** + * Sets how many times the animation should be repeated. If the repeat + * count is 0, the animation is never repeated. If the repeat count is + * greater than 0 or {@link #INFINITE}, the repeat mode will be taken + * into account. The repeat count is 0 by default. + * + * @param value the number of times the animation should be repeated + */ + public void setRepeatCount(int value) { + mRepeatCount = value; + } + /** + * Defines how many times the animation should repeat. The default value + * is 0. + * + * @return the number of times the animation should repeat, or {@link #INFINITE} + */ + public int getRepeatCount() { + return mRepeatCount; + } + + /** + * Defines what this animation should do when it reaches the end. This + * setting is applied only when the repeat count is either greater than + * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}. + * + * @param value {@link #RESTART} or {@link #REVERSE} + */ + public void setRepeatMode(int value) { + mRepeatMode = value; + } + + /** + * Defines what this animation should do when it reaches the end. + * + * @return either one of {@link #REVERSE} or {@link #RESTART} + */ + public int getRepeatMode() { + return mRepeatMode; + } + + /** + * Adds a listener to the set of listeners that are sent update events through the life of + * an animation. This method is called on all listeners for every frame of the animation, + * after the values for the animation have been calculated. + * + * @param listener the listener to be added to the current set of listeners for this animation. + */ + public void addUpdateListener(AnimatorUpdateListener listener) { + if (mUpdateListeners == null) { + mUpdateListeners = new ArrayList(); + } + mUpdateListeners.add(listener); + } + + /** + * Removes a listener from the set listening to frame updates for this animation. + * + * @param listener the listener to be removed from the current set of update listeners + * for this animation. + */ + public void removeUpdateListener(AnimatorUpdateListener listener) { + if (mUpdateListeners == null) { + return; + } + mUpdateListeners.remove(listener); + if (mUpdateListeners.size() == 0) { + mUpdateListeners = null; + } + } + + + /** + * The time interpolator used in calculating the elapsed fraction of this animation. The + * interpolator determines whether the animation runs with linear or non-linear motion, + * such as acceleration and deceleration. The default value is + * {@link android.view.animation.AccelerateDecelerateInterpolator} + * + * @param value the interpolator to be used by this animation + */ + @Override + public void setInterpolator(Interpolator value) { + if (value != null) { + mInterpolator = value; + } + } + + /** + * Returns the timing interpolator that this ValueAnimator uses. + * + * @return The timing interpolator for this ValueAnimator. + */ + public Interpolator getInterpolator() { + return mInterpolator; + } + + /** + * The type evaluator to be used when calculating the animated values of this animation. + * The system will automatically assign a float, int, or double evaluator based on the type + * of startValue and endValue in the constructor. But if these values + * are not one of these primitive types, or if different evaluation is desired (such as is + * necessary with int values that represent colors), a custom evaluator needs to be assigned. + * For example, when running an animation on color values, the {@link RGBEvaluator} + * should be used to get correct RGB color interpolation. + * + *

If this ValueAnimator has only one set of values being animated between, this evaluator + * will be used for that set. If there are several sets of values being animated, which is + * the case if PropertyValuesHOlder objects were set on the ValueAnimator, then the evaluator + * is assigned just to the first PropertyValuesHolder object.

+ * + * @param value the evaluator to be used this animation + */ + public void setEvaluator(TypeEvaluator value) { + if (value != null && mValues != null && mValues.length > 0) { + mValues[0].setEvaluator(value); + } + } + + /** + * Start the animation playing. This version of start() takes a boolean flag that indicates + * whether the animation should play in reverse. The flag is usually false, but may be set + * to true if called from the reverse() method/ + * + * @param playBackwards Whether the ValueAnimator should start playing in reverse. + */ + private void start(boolean playBackwards) { + mPlayingBackwards = playBackwards; + if ((mStartDelay == 0) && (Thread.currentThread() == Looper.getMainLooper().getThread())) { + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationStart(this); + } + } + // This sets the initial value of the animation, prior to actually starting it running + setCurrentPlayTime(getCurrentPlayTime()); + } + mPlayingState = STOPPED; + mStartedDelay = false; + sPendingAnimations.add(this); + if (sAnimationHandler == null) { + sAnimationHandler = new AnimationHandler(); + } + // TODO: does this put too many messages on the queue if the handler + // is already running? + sAnimationHandler.sendEmptyMessage(ANIMATION_START); + } + + @Override + public void start() { + start(false); + } + + @Override + public void cancel() { + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationCancel(this); + } + } + // Just set the CANCELED flag - this causes the animation to end the next time a frame + // is processed. + mPlayingState = CANCELED; + } + + @Override + public void end() { + if (!sAnimations.contains(this) && !sPendingAnimations.contains(this)) { + // Special case if the animation has not yet started; get it ready for ending + mStartedDelay = false; + sPendingAnimations.add(this); + if (sAnimationHandler == null) { + sAnimationHandler = new AnimationHandler(); + } + sAnimationHandler.sendEmptyMessage(ANIMATION_START); + } + // Just set the ENDED flag - this causes the animation to end the next time a frame + // is processed. + mPlayingState = ENDED; + } + + @Override + public boolean isRunning() { + // ENDED or CANCELED indicate that it has been ended or canceled, but not processed yet + return (mPlayingState == RUNNING || mPlayingState == ENDED || mPlayingState == CANCELED); + } + + /** + * Plays the ValueAnimator in reverse. If the animation is already running, + * it will stop itself and play backwards from the point reached when reverse was called. + * If the animation is not currently running, then it will start from the end and + * play backwards. This behavior is only set for the current animation; future playing + * of the animation will use the default behavior of playing forward. + */ + public void reverse() { + mPlayingBackwards = !mPlayingBackwards; + if (mPlayingState == RUNNING) { + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + long currentPlayTime = currentTime - mStartTime; + long timeLeft = mDuration - currentPlayTime; + mStartTime = currentTime - timeLeft; + } else { + start(true); + } + } + + /** + * Called internally to end an animation by removing it from the animations list. Must be + * called on the UI thread. + */ + private void endAnimation() { + sAnimations.remove(this); + mPlayingState = STOPPED; + if (mListeners != null) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationEnd(this); + } + } + } + + /** + * Called internally to start an animation by adding it to the active animations list. Must be + * called on the UI thread. + */ + private void startAnimation() { + initAnimation(); + sAnimations.add(this); + if (mStartDelay > 0 && mListeners != null) { + // Listeners were already notified in start() if startDelay is 0; this is + // just for delayed animations + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationStart(this); + } + } + } + + /** + * Internal function called to process an animation frame on an animation that is currently + * sleeping through its startDelay phase. The return value indicates whether it + * should be woken up and put on the active animations queue. + * + * @param currentTime The current animation time, used to calculate whether the animation + * has exceeded its startDelay and should be started. + * @return True if the animation's startDelay has been exceeded and the animation + * should be added to the set of active animations. + */ + private boolean delayedAnimationFrame(long currentTime) { + if (mPlayingState == CANCELED || mPlayingState == ENDED) { + // end the delay, process an animation frame to actually cancel it + return true; + } + if (!mStartedDelay) { + mStartedDelay = true; + mDelayStartTime = currentTime; + } else { + long deltaTime = currentTime - mDelayStartTime; + if (deltaTime > mStartDelay) { + // startDelay ended - start the anim and record the + // mStartTime appropriately + mStartTime = currentTime - (deltaTime - mStartDelay); + mPlayingState = RUNNING; + return true; + } + } + return false; + } + + /** + * This internal function processes a single animation frame for a given animation. The + * currentTime parameter is the timing pulse sent by the handler, used to calculate the + * elapsed duration, and therefore + * the elapsed fraction, of the animation. The return value indicates whether the animation + * should be ended (which happens when the elapsed time of the animation exceeds the + * animation's duration, including the repeatCount). + * + * @param currentTime The current time, as tracked by the static timing handler + * @return true if the animation's duration, including any repetitions due to + * repeatCount has been exceeded and the animation should be ended. + */ + private boolean animationFrame(long currentTime) { + boolean done = false; + + if (mPlayingState == STOPPED) { + mPlayingState = RUNNING; + if (mSeekTime < 0) { + mStartTime = currentTime; + } else { + mStartTime = currentTime - mSeekTime; + // Now that we're playing, reset the seek time + mSeekTime = -1; + } + } + switch (mPlayingState) { + case RUNNING: + case SEEKED: + float fraction = (float)(currentTime - mStartTime) / mDuration; + if (fraction >= 1f) { + if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) { + // Time to repeat + if (mListeners != null) { + for (AnimatorListener listener : mListeners) { + listener.onAnimationRepeat(this); + } + } + ++mCurrentIteration; + if (mRepeatMode == REVERSE) { + mPlayingBackwards = mPlayingBackwards ? false : true; + } + // TODO: doesn't account for fraction going Wayyyyy over 1, like 2+ + fraction = fraction - 1f; + mStartTime += mDuration; + } else { + done = true; + fraction = Math.min(fraction, 1.0f); + } + } + if (mPlayingBackwards) { + fraction = 1f - fraction; + } + animateValue(fraction); + break; + case ENDED: + // The final value set on the target varies, depending on whether the animation + // was supposed to repeat an odd number of times + if (mRepeatCount > 0 && (mRepeatCount & 0x01) == 1) { + animateValue(0f); + } else { + animateValue(1f); + } + // Fall through to set done flag + case CANCELED: + done = true; + mPlayingState = STOPPED; + break; + } + + return done; + } + + /** + * This method is called with the elapsed fraction of the animation during every + * animation frame. This function turns the elapsed fraction into an interpolated fraction + * and then into an animated value (from the evaluator. The function is called mostly during + * animation updates, but it is also called when the end() + * function is called, to set the final value on the property. + * + *

Overrides of this method must call the superclass to perform the calculation + * of the animated value.

+ * + * @param fraction The elapsed fraction of the animation. + */ + void animateValue(float fraction) { + fraction = mInterpolator.getInterpolation(fraction); + int numValues = mValues.length; + for (int i = 0; i < numValues; ++i) { + mValues[i].calculateValue(fraction); + } + if (mUpdateListeners != null) { + int numListeners = mUpdateListeners.size(); + for (int i = 0; i < numListeners; ++i) { + mUpdateListeners.get(i).onAnimationUpdate(this); + } + } + } + + @Override + public ValueAnimator clone() { + final ValueAnimator anim = (ValueAnimator) super.clone(); + if (mUpdateListeners != null) { + ArrayList oldListeners = mUpdateListeners; + anim.mUpdateListeners = new ArrayList(); + int numListeners = oldListeners.size(); + for (int i = 0; i < numListeners; ++i) { + anim.mUpdateListeners.add(oldListeners.get(i)); + } + } + anim.mSeekTime = -1; + anim.mPlayingBackwards = false; + anim.mCurrentIteration = 0; + anim.mInitialized = false; + anim.mPlayingState = STOPPED; + anim.mStartedDelay = false; + PropertyValuesHolder[] oldValues = mValues; + if (oldValues != null) { + int numValues = oldValues.length; + anim.mValues = new PropertyValuesHolder[numValues]; + for (int i = 0; i < numValues; ++i) { + anim.mValues[i] = oldValues[i].clone(); + } + anim.mValuesMap = new HashMap(numValues); + for (int i = 0; i < numValues; ++i) { + PropertyValuesHolder valuesHolder = mValues[i]; + anim.mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder); + } + } + return anim; + } + + /** + * Implementors of this interface can add themselves as update listeners + * to an ValueAnimator instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * ValueAnimator. + */ + public static interface AnimatorUpdateListener { + /** + *

Notifies the occurrence of another frame of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationUpdate(ValueAnimator animation); + + } +} \ No newline at end of file diff --git a/core/java/android/animation/package.html b/core/java/android/animation/package.html new file mode 100644 index 0000000000000000000000000000000000000000..b66669b490187dbb17934d49721c778ca46cd0e2 --- /dev/null +++ b/core/java/android/animation/package.html @@ -0,0 +1,6 @@ + + +Provides classes for animating values over time, and setting those values on target +objects. + + diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java new file mode 100644 index 0000000000000000000000000000000000000000..29f2e3088c0ade7a7637118633bfde6a692acdea --- /dev/null +++ b/core/java/android/app/ActionBar.java @@ -0,0 +1,474 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.Window; +import android.widget.SpinnerAdapter; + +/** + * This is the public interface to the contextual ActionBar. + * The ActionBar acts as a replacement for the title bar in Activities. + * It provides facilities for creating toolbar actions as well as + * methods of navigating around an application. + */ +public abstract class ActionBar { + /** + * Standard navigation mode. Consists of either a logo or icon + * and title text with an optional subtitle. Clicking any of these elements + * will dispatch onActionItemSelected to the registered Callback with + * a MenuItem with item ID android.R.id.home. + */ + public static final int NAVIGATION_MODE_STANDARD = 0; + + /** + * Dropdown list navigation mode. Instead of static title text this mode + * presents a dropdown menu for navigation within the activity. + */ + public static final int NAVIGATION_MODE_DROPDOWN_LIST = 1; + + /** + * Tab navigation mode. Instead of static title text this mode + * presents a series of tabs for navigation within the activity. + */ + public static final int NAVIGATION_MODE_TABS = 2; + + /** + * Custom navigation mode. This navigation mode is set implicitly whenever + * a custom navigation view is set. See {@link #setCustomNavigationMode(View)}. + */ + public static final int NAVIGATION_MODE_CUSTOM = 3; + + /** + * Use logo instead of icon if available. This flag will cause appropriate + * navigation modes to use a wider logo in place of the standard icon. + */ + public static final int DISPLAY_USE_LOGO = 0x1; + + /** + * Hide 'home' elements in this action bar, leaving more space for other + * navigation elements. This includes logo and icon. + */ + public static final int DISPLAY_HIDE_HOME = 0x2; + + /** + * Set the action bar into custom navigation mode, supplying a view + * for custom navigation. + * + * Custom navigation views appear between the application icon and + * any action buttons and may use any space available there. Common + * use cases for custom navigation views might include an auto-suggesting + * address bar for a browser or other navigation mechanisms that do not + * translate well to provided navigation modes. + * + * @param view Custom navigation view to place in the ActionBar. + */ + public abstract void setCustomNavigationMode(View view); + + /** + * Set the action bar into dropdown navigation mode and supply an adapter + * that will provide views for navigation choices. + * + * @param adapter An adapter that will provide views both to display + * the current navigation selection and populate views + * within the dropdown navigation menu. + * @param callback A NavigationCallback that will receive events when the user + * selects a navigation item. + */ + public abstract void setDropdownNavigationMode(SpinnerAdapter adapter, + NavigationCallback callback); + + /** + * Set the action bar into dropdown navigation mode and supply an adapter that will + * provide views for navigation choices. + * + * @param adapter An adapter that will provide views both to display the current + * navigation selection and populate views within the dropdown + * navigation menu. + * @param callback A NavigationCallback that will receive events when the user + * selects a navigation item. + * @param defaultSelectedPosition Position within the provided adapter that should be + * selected from the outset. + */ + public abstract void setDropdownNavigationMode(SpinnerAdapter adapter, + NavigationCallback callback, int defaultSelectedPosition); + + /** + * Set the selected navigation item in dropdown or tabbed navigation modes. + * + * @param position Position of the item to select. + */ + public abstract void setSelectedNavigationItem(int position); + + /** + * Get the position of the selected navigation item in dropdown or tabbed navigation modes. + * + * @return Position of the selected item. + */ + public abstract int getSelectedNavigationItem(); + + /** + * Set the action bar into standard navigation mode, using the currently set title + * and/or subtitle. + * + * Standard navigation mode is default. The title is automatically set to the name of + * your Activity on startup if an action bar is present. + */ + public abstract void setStandardNavigationMode(); + + /** + * Set the action bar's title. This will only be displayed in standard navigation mode. + * + * @param title Title to set + * + * @see #setTitle(int) + */ + public abstract void setTitle(CharSequence title); + + /** + * Set the action bar's title. This will only be displayed in standard navigation mode. + * + * @param resId Resource ID of title string to set + * + * @see #setTitle(CharSequence) + */ + public abstract void setTitle(int resId); + + /** + * Set the action bar's subtitle. This will only be displayed in standard navigation mode. + * Set to null to disable the subtitle entirely. + * + * @param subtitle Subtitle to set + * + * @see #setSubtitle(int) + */ + public abstract void setSubtitle(CharSequence subtitle); + + /** + * Set the action bar's subtitle. This will only be displayed in standard navigation mode. + * + * @param resId Resource ID of subtitle string to set + * + * @see #setSubtitle(CharSequence) + */ + public abstract void setSubtitle(int resId); + + /** + * Set display options. This changes all display option bits at once. To change + * a limited subset of display options, see {@link #setDisplayOptions(int, int)}. + * + * @param options A combination of the bits defined by the DISPLAY_ constants + * defined in ActionBar. + */ + public abstract void setDisplayOptions(int options); + + /** + * Set selected display options. Only the options specified by mask will be changed. + * To change all display option bits at once, see {@link #setDisplayOptions(int)}. + * + *

Example: setDisplayOptions(0, DISPLAY_HIDE_HOME) will disable the + * {@link #DISPLAY_HIDE_HOME} option. + * setDisplayOptions(DISPLAY_HIDE_HOME, DISPLAY_HIDE_HOME | DISPLAY_USE_LOGO) + * will enable {@link #DISPLAY_HIDE_HOME} and disable {@link #DISPLAY_USE_LOGO}. + * + * @param options A combination of the bits defined by the DISPLAY_ constants + * defined in ActionBar. + * @param mask A bit mask declaring which display options should be changed. + */ + public abstract void setDisplayOptions(int options, int mask); + + /** + * Set the ActionBar's background. + * + * @param d Background drawable + */ + public abstract void setBackgroundDrawable(Drawable d); + + /** + * @return The current custom navigation view. + */ + public abstract View getCustomNavigationView(); + + /** + * Returns the current ActionBar title in standard mode. + * Returns null if {@link #getNavigationMode()} would not return + * {@link #NAVIGATION_MODE_STANDARD}. + * + * @return The current ActionBar title or null. + */ + public abstract CharSequence getTitle(); + + /** + * Returns the current ActionBar subtitle in standard mode. + * Returns null if {@link #getNavigationMode()} would not return + * {@link #NAVIGATION_MODE_STANDARD}. + * + * @return The current ActionBar subtitle or null. + */ + public abstract CharSequence getSubtitle(); + + /** + * Returns the current navigation mode. The result will be one of: + *

    + *
  • {@link #NAVIGATION_MODE_STANDARD}
  • + *
  • {@link #NAVIGATION_MODE_DROPDOWN_LIST}
  • + *
  • {@link #NAVIGATION_MODE_TABS}
  • + *
  • {@link #NAVIGATION_MODE_CUSTOM}
  • + *
+ * + * @return The current navigation mode. + * + * @see #setStandardNavigationMode() + * @see #setStandardNavigationMode(CharSequence) + * @see #setStandardNavigationMode(CharSequence, CharSequence) + * @see #setDropdownNavigationMode(SpinnerAdapter) + * @see #setTabNavigationMode() + * @see #setCustomNavigationMode(View) + */ + public abstract int getNavigationMode(); + + /** + * @return The current set of display options. + */ + public abstract int getDisplayOptions(); + + /** + * Set the action bar into tabbed navigation mode. + * + * @see #addTab(Tab) + * @see #insertTab(Tab, int) + * @see #removeTab(Tab) + * @see #removeTabAt(int) + */ + public abstract void setTabNavigationMode(); + + /** + * Create and return a new {@link Tab}. + * This tab will not be included in the action bar until it is added. + * + * @return A new Tab + * + * @see #addTab(Tab) + * @see #insertTab(Tab, int) + */ + public abstract Tab newTab(); + + /** + * Add a tab for use in tabbed navigation mode. The tab will be added at the end of the list. + * + * @param tab Tab to add + */ + public abstract void addTab(Tab tab); + + /** + * Add a tab for use in tabbed navigation mode. The tab will be inserted at + * position. + * + * @param tab The tab to add + * @param position The new position of the tab + */ + public abstract void addTab(Tab tab, int position); + + /** + * Remove a tab from the action bar. + * + * @param tab The tab to remove + */ + public abstract void removeTab(Tab tab); + + /** + * Remove a tab from the action bar. + * + * @param position Position of the tab to remove + */ + public abstract void removeTabAt(int position); + + /** + * Select the specified tab. If it is not a child of this action bar it will be added. + * + * @param tab Tab to select + */ + public abstract void selectTab(Tab tab); + + /** + * Returns the currently selected tab if in tabbed navigation mode and there is at least + * one tab present. + * + * @return The currently selected tab or null + */ + public abstract Tab getSelectedTab(); + + /** + * Retrieve the current height of the ActionBar. + * + * @return The ActionBar's height + */ + public abstract int getHeight(); + + /** + * Show the ActionBar if it is not currently showing. + * If the window hosting the ActionBar does not have the feature + * {@link Window#FEATURE_ACTION_BAR_OVERLAY} it will resize application + * content to fit the new space available. + */ + public abstract void show(); + + /** + * Hide the ActionBar if it is not currently showing. + * If the window hosting the ActionBar does not have the feature + * {@link Window#FEATURE_ACTION_BAR_OVERLAY} it will resize application + * content to fit the new space available. + */ + public abstract void hide(); + + /** + * @return true if the ActionBar is showing, false otherwise. + */ + public abstract boolean isShowing(); + + /** + * Callback interface for ActionBar navigation events. + */ + public interface NavigationCallback { + /** + * This method is called whenever a navigation item in your action bar + * is selected. + * + * @param itemPosition Position of the item clicked. + * @param itemId ID of the item clicked. + * @return True if the event was handled, false otherwise. + */ + public boolean onNavigationItemSelected(int itemPosition, long itemId); + } + + /** + * A tab in the action bar. + * + *

Tabs manage the hiding and showing of {@link Fragment}s. + */ + public static abstract class Tab { + /** + * An invalid position for a tab. + * + * @see #getPosition() + */ + public static final int INVALID_POSITION = -1; + + /** + * Return the current position of this tab in the action bar. + * + * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in + * the action bar. + */ + public abstract int getPosition(); + + /** + * Return the icon associated with this tab. + * + * @return The tab's icon + */ + public abstract Drawable getIcon(); + + /** + * Return the text of this tab. + * + * @return The tab's text + */ + public abstract CharSequence getText(); + + /** + * Set the icon displayed on this tab. + * + * @param icon The drawable to use as an icon + */ + public abstract void setIcon(Drawable icon); + + /** + * Set the text displayed on this tab. Text may be truncated if there is not + * room to display the entire string. + * + * @param text The text to display + */ + public abstract void setText(CharSequence text); + + /** + * Set a custom view to be used for this tab. This overrides values set by + * {@link #setText(CharSequence)} and {@link #setIcon(Drawable)}. + * + * @param view Custom view to be used as a tab. + */ + public abstract void setCustomView(View view); + + /** + * Retrieve a previously set custom view for this tab. + * + * @return The custom view set by {@link #setCustomView(View)}. + */ + public abstract View getCustomView(); + + /** + * Give this Tab an arbitrary object to hold for later use. + * + * @param obj Object to store + */ + public abstract void setTag(Object obj); + + /** + * @return This Tab's tag object. + */ + public abstract Object getTag(); + + /** + * Set the {@link TabListener} that will handle switching to and from this tab. + * All tabs must have a TabListener set before being added to the ActionBar. + * + * @param listener Listener to handle tab selection events + */ + public abstract void setTabListener(TabListener listener); + + /** + * Select this tab. Only valid if the tab has been added to the action bar. + */ + public abstract void select(); + } + + /** + * Callback interface invoked when a tab is focused, unfocused, added, or removed. + */ + public interface TabListener { + /** + * Called when a tab enters the selected state. + * + * @param tab The tab that was selected + * @param ft A {@link FragmentTransaction} for queuing fragment operations to execute + * during a tab switch. The previous tab's unselect and this tab's select will be + * executed in a single transaction. + */ + public void onTabSelected(Tab tab, FragmentTransaction ft); + + /** + * Called when a tab exits the selected state. + * + * @param tab The tab that was unselected + * @param ft A {@link FragmentTransaction} for queuing fragment operations to execute + * during a tab switch. This tab's unselect and the newly selected tab's select + * will be executed in a single transaction. + */ + public void onTabUnselected(Tab tab, FragmentTransaction ft); + } +} diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 72bf825ea2c3659c07439a9692532482e939a048..ee49d97bea836cb4dbe8366992f20d75b25039de 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -16,19 +16,22 @@ package android.app; +import com.android.internal.app.ActionBarImpl; import com.android.internal.policy.PolicyManager; import android.content.ComponentCallbacks; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; +import android.content.CursorLoader; import android.content.IIntentSender; +import android.content.Intent; import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -39,6 +42,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.Parcelable; import android.os.RemoteException; import android.text.Selection; import android.text.SpannableStringBuilder; @@ -49,7 +53,9 @@ import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.SparseArray; +import android.view.ActionMode; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextThemeWrapper; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -58,18 +64,18 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; import android.view.ViewManager; import android.view.Window; import android.view.WindowManager; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View.OnCreateContextMenuListener; -import android.view.ViewGroup.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.widget.AdapterView; import android.widget.FrameLayout; -import android.widget.LinearLayout; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; @@ -110,6 +116,7 @@ import java.util.HashMap; * *

Topics covered here: *

    + *
  1. Fragments *
  2. Activity Lifecycle *
  3. Configuration Changes *
  4. Starting Activities and Getting Results @@ -118,6 +125,14 @@ import java.util.HashMap; *
  5. Process Lifecycle *
* + * + *

Fragments

+ * + *

Starting with {@link android.os.Build.VERSION_CODES#HONEYCOMB}, Activity + * implementations can make use of the {@link Fragment} class to better + * modularize their code, build more sophisticated user interfaces for larger + * screens, and help scale their application between small and large screens. + * * *

Activity Lifecycle

* @@ -592,7 +607,7 @@ import java.util.HashMap; * or finished. */ public class Activity extends ContextThemeWrapper - implements LayoutInflater.Factory, + implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks { private static final String TAG = "Activity"; @@ -604,9 +619,8 @@ public class Activity extends ContextThemeWrapper /** Start of user-defined activity results. */ public static final int RESULT_FIRST_USER = 1; - private static long sInstanceCount = 0; - private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState"; + private static final String FRAGMENTS_TAG = "android:fragments"; private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds"; private static final String SAVED_DIALOGS_TAG = "android:savedDialogs"; private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_"; @@ -628,18 +642,28 @@ public class Activity extends ContextThemeWrapper private ComponentName mComponent; /*package*/ ActivityInfo mActivityInfo; /*package*/ ActivityThread mMainThread; - /*package*/ Object mLastNonConfigurationInstance; - /*package*/ HashMap mLastNonConfigurationChildInstances; Activity mParent; boolean mCalled; + boolean mCheckedForLoaderManager; + boolean mStarted; private boolean mResumed; private boolean mStopped; boolean mFinished; boolean mStartedActivity; + /** true if the activity is being destroyed in order to recreate it with a new configuration */ + /*package*/ boolean mChangingConfigurations = false; /*package*/ int mConfigChangeFlags; /*package*/ Configuration mCurrentConfig; private SearchManager mSearchManager; + static final class NonConfigurationInstances { + Object activity; + HashMap children; + ArrayList fragments; + SparseArray loaders; + } + /* package */ NonConfigurationInstances mLastNonConfigurationInstances; + private Window mWindow; private WindowManager mWindowManager; @@ -647,10 +671,16 @@ public class Activity extends ContextThemeWrapper /*package*/ boolean mWindowAdded = false; /*package*/ boolean mVisibleFromServer = false; /*package*/ boolean mVisibleFromClient = true; + /*package*/ ActionBarImpl mActionBar = null; private CharSequence mTitle; private int mTitleColor = 0; + final FragmentManagerImpl mFragments = new FragmentManagerImpl(); + + SparseArray mAllLoaderManagers; + LoaderManagerImpl mLoaderManager; + private static final class ManagedCursor { ManagedCursor(Cursor cursor) { mCursor = cursor; @@ -677,24 +707,7 @@ public class Activity extends ContextThemeWrapper protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused}; private Thread mUiThread; - private final Handler mHandler = new Handler(); - - // Used for debug only - /* - public Activity() { - ++sInstanceCount; - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - --sInstanceCount; - } - */ - - public static long getInstanceCount() { - return sInstanceCount; - } + final Handler mHandler = new Handler(); /** Return the intent that started this activity. */ public Intent getIntent() { @@ -747,6 +760,30 @@ public class Activity extends ContextThemeWrapper return mWindow; } + /** + * Return the LoaderManager for this fragment, creating it if needed. + */ + public LoaderManager getLoaderManager() { + if (mLoaderManager != null) { + return mLoaderManager; + } + mCheckedForLoaderManager = true; + mLoaderManager = getLoaderManager(-1, mStarted, true); + return mLoaderManager; + } + + LoaderManagerImpl getLoaderManager(int index, boolean started, boolean create) { + if (mAllLoaderManagers == null) { + mAllLoaderManagers = new SparseArray(); + } + LoaderManagerImpl lm = mAllLoaderManagers.get(index); + if (lm == null && create) { + lm = new LoaderManagerImpl(started); + mAllLoaderManagers.put(index, lm); + } + return lm; + } + /** * Calls {@link android.view.Window#getCurrentFocus} on the * Window of this Activity to return the currently focused view. @@ -801,6 +838,15 @@ public class Activity extends ContextThemeWrapper protected void onCreate(Bundle savedInstanceState) { mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, false); + if (mLastNonConfigurationInstances != null) { + mAllLoaderManagers = mLastNonConfigurationInstances.loaders; + } + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); + mFragments.restoreAllState(p, mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.fragments : null); + } + mFragments.dispatchCreate(); mCalled = true; } @@ -933,6 +979,13 @@ public class Activity extends ContextThemeWrapper */ protected void onStart() { mCalled = true; + mStarted = true; + if (mLoaderManager != null) { + mLoaderManager.doStart(); + } else if (!mCheckedForLoaderManager) { + mLoaderManager = getLoaderManager(-1, mStarted, false); + } + mCheckedForLoaderManager = true; } /** @@ -1085,6 +1138,10 @@ public class Activity extends ContextThemeWrapper */ protected void onSaveInstanceState(Bundle outState) { outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState()); + Parcelable p = mFragments.saveAllState(); + if (p != null) { + outState.putParcelable(FRAGMENTS_TAG, p); + } } /** @@ -1408,7 +1465,8 @@ public class Activity extends ContextThemeWrapper * {@link #onRetainNonConfigurationInstance()}. */ public Object getLastNonConfigurationInstance() { - return mLastNonConfigurationInstance; + return mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.activity : null; } /** @@ -1420,6 +1478,11 @@ public class Activity extends ContextThemeWrapper * {@link #getLastNonConfigurationInstance()} in the new activity * instance. * + * If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * or later, consider instead using a {@link Fragment} with + * {@link Fragment#setRetainInstance(boolean) + * Fragment.setRetainInstance(boolean}. + * *

This function is called purely as an optimization, and you must * not rely on it being called. When it is called, a number of guarantees * will be made to help optimize configuration switching: @@ -1475,8 +1538,9 @@ public class Activity extends ContextThemeWrapper * @return Returns the object previously returned by * {@link #onRetainNonConfigurationChildInstances()} */ - HashMap getLastNonConfigurationChildInstances() { - return mLastNonConfigurationChildInstances; + HashMap getLastNonConfigurationChildInstances() { + return mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.children : null; } /** @@ -1490,10 +1554,77 @@ public class Activity extends ContextThemeWrapper return null; } + NonConfigurationInstances retainNonConfigurationInstances() { + Object activity = onRetainNonConfigurationInstance(); + HashMap children = onRetainNonConfigurationChildInstances(); + ArrayList fragments = mFragments.retainNonConfig(); + boolean retainLoaders = false; + if (mAllLoaderManagers != null) { + // prune out any loader managers that were already stopped and so + // have nothing useful to retain. + for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { + LoaderManagerImpl lm = mAllLoaderManagers.valueAt(i); + if (lm.mRetaining) { + retainLoaders = true; + } else { + lm.doDestroy(); + mAllLoaderManagers.removeAt(i); + } + } + } + if (activity == null && children == null && fragments == null && !retainLoaders) { + return null; + } + + NonConfigurationInstances nci = new NonConfigurationInstances(); + nci.activity = activity; + nci.children = children; + nci.fragments = fragments; + nci.loaders = mAllLoaderManagers; + return nci; + } + public void onLowMemory() { mCalled = true; } + /** + * Return the FragmentManager for interacting with fragments associated + * with this activity. + */ + public FragmentManager getFragmentManager() { + return mFragments; + } + + /** + * Start a series of edit operations on the Fragments associated with + * this activity. + * @deprecated use {@link #getFragmentManager}. + */ + @Deprecated + public FragmentTransaction openFragmentTransaction() { + return mFragments.openTransaction(); + } + + void invalidateFragmentIndex(int index) { + //Log.v(TAG, "invalidateFragmentIndex: index=" + index); + if (mAllLoaderManagers != null) { + LoaderManagerImpl lm = mAllLoaderManagers.get(index); + if (lm != null) { + lm.doDestroy(); + } + mAllLoaderManagers.remove(index); + } + } + + /** + * Called when a Fragment is being attached to this activity, immediately + * after the call to its {@link Fragment#onAttach Fragment.onAttach()} + * method and before {@link Fragment#onCreate Fragment.onCreate()}. + */ + public void onAttachFragment(Fragment fragment) { + } + /** * Wrapper around * {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)} @@ -1501,6 +1632,10 @@ public class Activity extends ContextThemeWrapper * {@link #startManagingCursor} so that the activity will manage its * lifecycle for you. * + * If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * or later, consider instead using {@link LoaderManager} instead, available + * via {@link #getLoaderManager()}. + * * @param uri The URI of the content provider to query. * @param projection List of columns to return. * @param selection SQL WHERE clause. @@ -1511,12 +1646,12 @@ public class Activity extends ContextThemeWrapper * @see ContentResolver#query(android.net.Uri , String[], String, String[], String) * @see #startManagingCursor * @hide + * + * @deprecated Use {@link CursorLoader} instead. */ - public final Cursor managedQuery(Uri uri, - String[] projection, - String selection, - String sortOrder) - { + @Deprecated + public final Cursor managedQuery(Uri uri, String[] projection, String selection, + String sortOrder) { Cursor c = getContentResolver().query(uri, projection, selection, null, sortOrder); if (c != null) { startManagingCursor(c); @@ -1531,6 +1666,10 @@ public class Activity extends ContextThemeWrapper * {@link #startManagingCursor} so that the activity will manage its * lifecycle for you. * + * If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * or later, consider instead using {@link LoaderManager} instead, available + * via {@link #getLoaderManager()}. + * * @param uri The URI of the content provider to query. * @param projection List of columns to return. * @param selection SQL WHERE clause. @@ -1541,13 +1680,12 @@ public class Activity extends ContextThemeWrapper * * @see ContentResolver#query(android.net.Uri , String[], String, String[], String) * @see #startManagingCursor + * + * @deprecated Use {@link CursorLoader} instead. */ - public final Cursor managedQuery(Uri uri, - String[] projection, - String selection, - String[] selectionArgs, - String sortOrder) - { + @Deprecated + public final Cursor managedQuery(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { Cursor c = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder); if (c != null) { startManagingCursor(c); @@ -1555,40 +1693,6 @@ public class Activity extends ContextThemeWrapper return c; } - /** - * Wrapper around {@link Cursor#commitUpdates()} that takes care of noting - * that the Cursor needs to be requeried. You can call this method in - * {@link #onPause} or {@link #onStop} to have the system call - * {@link Cursor#requery} for you if the activity is later resumed. This - * allows you to avoid determing when to do the requery yourself (which is - * required for the Cursor to see any data changes that were committed with - * it). - * - * @param c The Cursor whose changes are to be committed. - * - * @see #managedQuery(android.net.Uri , String[], String, String[], String) - * @see #startManagingCursor - * @see Cursor#commitUpdates() - * @see Cursor#requery - * @hide - */ - @Deprecated - public void managedCommitUpdates(Cursor c) { - synchronized (mManagedCursors) { - final int N = mManagedCursors.size(); - for (int i=0; iIf you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * or later, consider instead using {@link LoaderManager} instead, available + * via {@link #getLoaderManager()}. + * * @param c The Cursor to be managed. * * @see #managedQuery(android.net.Uri , String[], String, String[], String) * @see #stopManagingCursor + * + * @deprecated Use {@link CursorLoader} instead. */ + @Deprecated public void startManagingCursor(Cursor c) { synchronized (mManagedCursors) { mManagedCursors.add(new ManagedCursor(c)); @@ -1616,7 +1727,10 @@ public class Activity extends ContextThemeWrapper * @param c The Cursor that was being managed. * * @see #startManagingCursor + * + * @deprecated Use {@link CursorLoader} instead. */ + @Deprecated public void stopManagingCursor(Cursor c) { synchronized (mManagedCursors) { final int N = mManagedCursors.size(); @@ -1671,7 +1785,54 @@ public class Activity extends ContextThemeWrapper public View findViewById(int id) { return getWindow().findViewById(id); } - + + /** + * Retrieve a reference to this activity's ActionBar. + * + * @return The Activity's ActionBar, or null if it does not have one. + */ + public ActionBar getActionBar() { + initActionBar(); + return mActionBar; + } + + /** + * Creates a new ActionBar, locates the inflated ActionBarView, + * initializes the ActionBar with the view, and sets mActionBar. + */ + private void initActionBar() { + Window window = getWindow(); + if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) { + return; + } + + mActionBar = new ActionBarImpl(this); + } + + /** + * Finds a fragment that was identified by the given id either when inflated + * from XML or as the container ID when added in a transaction. This only + * returns fragments that are currently added to the activity's content. + * @return The fragment if found or null otherwise. + * @deprecated use {@link #getFragmentManager}. + */ + @Deprecated + public Fragment findFragmentById(int id) { + return mFragments.findFragmentById(id); + } + + /** + * Finds a fragment that was identified by the given tag either when inflated + * from XML or as supplied when added in a transaction. This only + * returns fragments that are currently added to the activity's content. + * @return The fragment if found or null otherwise. + * @deprecated use {@link #getFragmentManager}. + */ + @Deprecated + public Fragment findFragmentByTag(String tag) { + return mFragments.findFragmentByTag(tag); + } + /** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. @@ -1680,6 +1841,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); + initActionBar(); } /** @@ -1691,6 +1853,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view) { getWindow().setContentView(view); + initActionBar(); } /** @@ -1703,6 +1866,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); + initActionBar(); } /** @@ -1714,6 +1878,7 @@ public class Activity extends ContextThemeWrapper */ public void addContentView(View view, ViewGroup.LayoutParams params) { getWindow().addContentView(view, params); + initActionBar(); } /** @@ -1936,13 +2101,66 @@ public class Activity extends ContextThemeWrapper return false; } + /** + * Flag for {@link #popBackStack(String, int)} + * and {@link #popBackStack(int, int)}: If set, and the name or ID of + * a back stack entry has been supplied, then all matching entries will + * be consumed until one that doesn't match is found or the bottom of + * the stack is reached. Otherwise, all entries up to but not including that entry + * will be removed. + */ + public static final int POP_BACK_STACK_INCLUSIVE = 1<<0; + + /** + * Pop the top state off the back stack. Returns true if there was one + * to pop, else false. + * @deprecated use {@link #getFragmentManager}. + */ + @Deprecated + public boolean popBackStack() { + return mFragments.popBackStack(); + } + + /** + * Pop the last fragment transition from the local activity's fragment + * back stack. If there is nothing to pop, false is returned. + * @param name If non-null, this is the name of a previous back state + * to look for; if found, all states up to that state will be popped. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. If null, only the top state is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + * @deprecated use {@link #getFragmentManager}. + */ + @Deprecated + public boolean popBackStack(String name, int flags) { + return mFragments.popBackStack(name, flags); + } + + /** + * Pop all back stack states up to the one with the given identifier. + * @param id Identifier of the stated to be popped. If no identifier exists, + * false is returned. + * The identifier is the number returned by + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + * @deprecated use {@link #getFragmentManager}. + */ + @Deprecated + public boolean popBackStack(int id, int flags) { + return mFragments.popBackStack(id, flags); + } + /** * 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(); + if (!mFragments.popBackStack()) { + finish(); + } } /** @@ -2180,7 +2398,9 @@ public class Activity extends ContextThemeWrapper */ public boolean onCreatePanelMenu(int featureId, Menu menu) { if (featureId == Window.FEATURE_OPTIONS_PANEL) { - return onCreateOptionsMenu(menu); + boolean show = onCreateOptionsMenu(menu); + show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater()); + return show; } return false; } @@ -2197,6 +2417,7 @@ public class Activity extends ContextThemeWrapper public boolean onPreparePanel(int featureId, View view, Menu menu) { if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) { boolean goforit = onPrepareOptionsMenu(menu); + goforit |= mFragments.dispatchPrepareOptionsMenu(menu); return goforit && menu.hasVisibleItems(); } return true; @@ -2227,11 +2448,17 @@ public class Activity extends ContextThemeWrapper // doesn't call through to superclass's implmeentation of each // of these methods below EventLog.writeEvent(50000, 0, item.getTitleCondensed()); - return onOptionsItemSelected(item); + if (onOptionsItemSelected(item)) { + return true; + } + return mFragments.dispatchOptionsItemSelected(item); case Window.FEATURE_CONTEXT_MENU: EventLog.writeEvent(50000, 1, item.getTitleCondensed()); - return onContextItemSelected(item); + if (onContextItemSelected(item)) { + return true; + } + return mFragments.dispatchContextItemSelected(item); default: return false; @@ -2250,6 +2477,7 @@ public class Activity extends ContextThemeWrapper public void onPanelClosed(int featureId, Menu menu) { switch (featureId) { case Window.FEATURE_OPTIONS_PANEL: + mFragments.dispatchOptionsMenuClosed(menu); onOptionsMenuClosed(menu); break; @@ -2259,6 +2487,15 @@ public class Activity extends ContextThemeWrapper } } + /** + * Declare that the options menu has changed, so should be recreated. + * The {@link #onCreateOptionsMenu(Menu)} method will be called the next + * time it needs to be displayed. + */ + public void invalidateOptionsMenu() { + mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL); + } + /** * Initialize the contents of the Activity's standard options menu. You * should place your menu items in to menu. @@ -2482,6 +2719,9 @@ public class Activity extends ContextThemeWrapper * by the activity. The default implementation calls through to * {@link #onCreateDialog(int)} for compatibility. * + * If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * or later, consider instead using a {@link DialogFragment} instead. + * *

If you use {@link #showDialog(int)}, the activity will call through to * this method the first time, and hang onto it thereafter. Any dialog * that is created by this method will automatically be saved and restored @@ -2554,6 +2794,9 @@ public class Activity extends ContextThemeWrapper * will be made with the same id the first time this is called for a given * id. From thereafter, the dialog will be automatically saved and restored. * + * If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * or later, consider instead using a {@link DialogFragment} instead. + * *

Each time a dialog is shown, {@link #onPrepareDialog(int, Dialog, Bundle)} will * be made to provide an opportunity to do any timely preparation. * @@ -3100,6 +3343,36 @@ public class Activity extends ContextThemeWrapper } } + /** + * This is called when a Fragment in this activity calls its + * {@link Fragment#startActivity} or {@link Fragment#startActivityForResult} + * method. + * + *

This method throws {@link android.content.ActivityNotFoundException} + * if there was no Activity found to run the given Intent. + * + * @param fragment The fragment making the call. + * @param intent The intent to start. + * @param requestCode Reply request code. < 0 if reply is not requested. + * + * @throws android.content.ActivityNotFoundException + * + * @see Fragment#startActivity + * @see Fragment#startActivityForResult + */ + public void startActivityFromFragment(Fragment fragment, Intent intent, + int requestCode) { + Instrumentation.ActivityResult ar = + mInstrumentation.execStartActivity( + this, mMainThread.getApplicationThread(), mToken, fragment, + intent, requestCode); + if (ar != null) { + mMainThread.sendActivityResult( + mToken, fragment.mWho, requestCode, + ar.getResultCode(), ar.getResultData()); + } + } + /** * Like {@link #startActivityFromChild(Activity, Intent, int)}, but * taking a IntentSender; see @@ -3258,6 +3531,19 @@ public class Activity extends ContextThemeWrapper return mFinished; } + /** + * Check to see whether this activity is in the process of being destroyed in order to be + * recreated with a new configuration. This is often used in + * {@link #onStop} to determine whether the state needs to be cleaned up or will be passed + * on to the next instance of the activity via {@link #onRetainNonConfigurationInstance()}. + * + * @return If the activity is being torn down in order to be recreated with a new configuration, + * returns true; else returns false. + */ + public boolean isChangingConfigurations() { + return mChangingConfigurations; + } + /** * Call this when your activity is done and should be closed. The * ActivityResult is propagated back to whoever launched you via @@ -3359,8 +3645,7 @@ public class Activity extends ContextThemeWrapper * @see #createPendingResult * @see #setResult(int) */ - protected void onActivityResult(int requestCode, int resultCode, - Intent data) { + protected void onActivityResult(int requestCode, int resultCode, Intent data) { } /** @@ -3744,9 +4029,12 @@ public class Activity extends ContextThemeWrapper } /** - * Stub implementation of {@link android.view.LayoutInflater.Factory#onCreateView} used when - * inflating with the LayoutInflater returned by {@link #getSystemService}. This - * implementation simply returns null for all view names. + * Standard implementation of + * {@link android.view.LayoutInflater.Factory#onCreateView} used when + * inflating with the LayoutInflater returned by {@link #getSystemService}. + * This implementation does nothing and is for + * pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} apps. Newer apps + * should use {@link #onCreateView(View, String, Context, AttributeSet)}. * * @see android.view.LayoutInflater#createView * @see android.view.Window#getLayoutInflater @@ -3755,6 +4043,172 @@ public class Activity extends ContextThemeWrapper return null; } + /** + * Standard implementation of + * {@link android.view.LayoutInflater.Factory2#onCreateView(View, String, Context, AttributeSet)} + * used when inflating with the LayoutInflater returned by {@link #getSystemService}. + * This implementation handles tags to embed fragments inside + * of the activity. + * + * @see android.view.LayoutInflater#createView + * @see android.view.Window#getLayoutInflater + */ + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + if (!"fragment".equals(name)) { + return onCreateView(name, context, attrs); + } + + String fname = attrs.getAttributeValue(null, "class"); + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Fragment); + if (fname == null) { + fname = a.getString(com.android.internal.R.styleable.Fragment_name); + } + int id = a.getResourceId(com.android.internal.R.styleable.Fragment_id, View.NO_ID); + String tag = a.getString(com.android.internal.R.styleable.Fragment_tag); + a.recycle(); + + int containerId = parent != null ? parent.getId() : 0; + if (containerId == View.NO_ID && id == View.NO_ID && tag == null) { + throw new IllegalArgumentException(attrs.getPositionDescription() + + ": Must specify unique android:id, android:tag, or have a parent with an id for " + fname); + } + + // If we restored from a previous state, we may already have + // instantiated this fragment from the state and should use + // that instance instead of making a new one. + Fragment fragment = id != View.NO_ID ? mFragments.findFragmentById(id) : null; + if (fragment == null && tag != null) { + fragment = mFragments.findFragmentByTag(tag); + } + if (fragment == null && containerId != View.NO_ID) { + fragment = mFragments.findFragmentById(containerId); + } + + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "onCreateView: id=0x" + + Integer.toHexString(id) + " fname=" + fname + + " existing=" + fragment); + if (fragment == null) { + fragment = Fragment.instantiate(this, fname); + fragment.mFromLayout = true; + fragment.mFragmentId = id != 0 ? id : containerId; + fragment.mContainerId = containerId; + fragment.mTag = tag; + fragment.mInLayout = true; + fragment.mImmediateActivity = this; + fragment.mFragmentManager = mFragments; + fragment.onInflate(attrs, fragment.mSavedFragmentState); + mFragments.addFragment(fragment, true); + + } else if (fragment.mInLayout) { + // A fragment already exists and it is not one we restored from + // previous state. + throw new IllegalArgumentException(attrs.getPositionDescription() + + ": Duplicate id 0x" + Integer.toHexString(id) + + ", tag " + tag + ", or parent id 0x" + Integer.toHexString(containerId) + + " with another fragment for " + fname); + } else { + // This fragment was retained from a previous instance; get it + // going now. + fragment.mInLayout = true; + fragment.mImmediateActivity = this; + // If this fragment is newly instantiated (either right now, or + // from last saved state), then give it the attributes to + // initialize itself. + if (!fragment.mRetaining) { + fragment.onInflate(attrs, fragment.mSavedFragmentState); + } + mFragments.moveToState(fragment); + } + + if (fragment.mView == null) { + throw new IllegalStateException("Fragment " + fname + + " did not create a view."); + } + if (id != 0) { + fragment.mView.setId(id); + } + if (fragment.mView.getTag() == null) { + fragment.mView.setTag(tag); + } + return fragment.mView; + } + + /** + * Print the Activity's state into the given stream. This gets invoked if + * you run "adb shell dumpsys activity ". + * + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mFragments.dump("", fd, writer, args); + } + + /** + * Bit indicating that this activity is "immersive" and should not be + * interrupted by notifications if possible. + * + * This value is initially set by the manifest property + * android:immersive but may be changed at runtime by + * {@link #setImmersive}. + * + * @see android.content.pm.ActivityInfo#FLAG_IMMERSIVE + * @hide + */ + public boolean isImmersive() { + try { + return ActivityManagerNative.getDefault().isImmersive(mToken); + } catch (RemoteException e) { + return false; + } + } + + /** + * Adjust the current immersive mode setting. + * + * Note that changing this value will have no effect on the activity's + * {@link android.content.pm.ActivityInfo} structure; that is, if + * android:immersive is set to true + * in the application's manifest entry for this activity, the {@link + * android.content.pm.ActivityInfo#flags ActivityInfo.flags} member will + * always have its {@link android.content.pm.ActivityInfo#FLAG_IMMERSIVE + * FLAG_IMMERSIVE} bit set. + * + * @see #isImmersive + * @see android.content.pm.ActivityInfo#FLAG_IMMERSIVE + * @hide + */ + public void setImmersive(boolean i) { + try { + ActivityManagerNative.getDefault().setImmersive(mToken, i); + } catch (RemoteException e) { + // pass + } + } + + /** + * Start a context mode. + * + * @param callback Callback that will manage lifecycle events for this context mode + * @return The ContextMode that was started, or null if it was canceled + * + * @see ActionMode + */ + public ActionMode startActionMode(ActionMode.Callback callback) { + return mWindow.getDecorView().startActionMode(callback); + } + + public ActionMode onStartActionMode(ActionMode.Callback callback) { + initActionBar(); + if (mActionBar != null) { + return mActionBar.startActionMode(callback); + } + return null; + } + // ------------------ Internal API ------------------ final void setParent(Activity parent) { @@ -3763,28 +4217,30 @@ public class Activity extends ContextThemeWrapper final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, Application application, Intent intent, ActivityInfo info, CharSequence title, - Activity parent, String id, Object lastNonConfigurationInstance, + Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config) { attach(context, aThread, instr, token, 0, application, intent, info, title, parent, id, - lastNonConfigurationInstance, null, config); + lastNonConfigurationInstances, config); } final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, - Object lastNonConfigurationInstance, - HashMap lastNonConfigurationChildInstances, + NonConfigurationInstances lastNonConfigurationInstances, Configuration config) { attachBaseContext(context); + mFragments.attachActivity(this); + mWindow = PolicyManager.makeNewWindow(this); mWindow.setCallback(this); + mWindow.getLayoutInflater().setFactory2(this); if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } mUiThread = Thread.currentThread(); - + mMainThread = aThread; mInstrumentation = instr; mToken = token; @@ -3796,10 +4252,10 @@ public class Activity extends ContextThemeWrapper mTitle = title; mParent = parent; mEmbeddedID = id; - mLastNonConfigurationInstance = lastNonConfigurationInstance; - mLastNonConfigurationChildInstances = lastNonConfigurationChildInstances; + mLastNonConfigurationInstances = lastNonConfigurationInstances; - mWindow.setWindowManager(null, mToken, mComponent.flattenToString()); + mWindow.setWindowManager(null, mToken, mComponent.flattenToString(), + (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); if (mParent != null) { mWindow.setContainer(mParent.getWindow()); } @@ -3811,23 +4267,41 @@ public class Activity extends ContextThemeWrapper return mParent != null ? mParent.getActivityToken() : mToken; } + final void performCreate(Bundle icicle) { + onCreate(icicle); + mFragments.dispatchActivityCreated(); + } + final void performStart() { + mFragments.mStateSaved = false; mCalled = false; + mFragments.execPendingActions(); mInstrumentation.callActivityOnStart(this); if (!mCalled) { throw new SuperNotCalledException( "Activity " + mComponent.toShortString() + " did not call through to super.onStart()"); } + mFragments.dispatchStart(); + if (mAllLoaderManagers != null) { + for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { + mAllLoaderManagers.valueAt(i).finishRetain(); + } + } } final void performRestart() { + mFragments.mStateSaved = false; + synchronized (mManagedCursors) { final int N = mManagedCursors.size(); for (int i=0; i 0) { pw.println(" DATABASES"); - printRow(pw, " %8s %8s %14s %s", "pgsz", "dbsz", "Lookaside(b)", "Dbname"); + printRow(pw, " %8s %8s %14s %14s %s", "pgsz", "dbsz", "Lookaside(b)", "cache", + "Dbname"); for (int i = 0; i < N; i++) { DbStats dbStats = stats.dbStats.get(i); - printRow(pw, DB_INFO_FORMAT, dbStats.pageSize, dbStats.dbSize, - dbStats.lookaside, dbStats.dbName); + printRow(pw, DB_INFO_FORMAT, + (dbStats.pageSize > 0) ? String.valueOf(dbStats.pageSize) : " ", + (dbStats.dbSize > 0) ? String.valueOf(dbStats.dbSize) : " ", + (dbStats.lookaside > 0) ? String.valueOf(dbStats.lookaside) : " ", + dbStats.cache, dbStats.dbName); } } @@ -873,6 +910,8 @@ public final class ActivityThread { public static final int ENABLE_JIT = 132; public static final int DISPATCH_PACKAGE_BROADCAST = 133; public static final int SCHEDULE_CRASH = 134; + public static final int DUMP_HEAP = 135; + public static final int DUMP_ACTIVITY = 136; String codeToString(int code) { if (localLOGV) { switch (code) { @@ -911,6 +950,8 @@ public final class ActivityThread { case ENABLE_JIT: return "ENABLE_JIT"; case DISPATCH_PACKAGE_BROADCAST: return "DISPATCH_PACKAGE_BROADCAST"; case SCHEDULE_CRASH: return "SCHEDULE_CRASH"; + case DUMP_HEAP: return "DUMP_HEAP"; + case DUMP_ACTIVITY: return "DUMP_ACTIVITY"; } } return "(unknown)"; @@ -1005,7 +1046,7 @@ public final class ActivityThread { scheduleGcIdler(); break; case DUMP_SERVICE: - handleDumpService((DumpServiceInfo)msg.obj); + handleDumpService((DumpComponentInfo)msg.obj); break; case LOW_MEMORY: handleLowMemory(); @@ -1036,13 +1077,38 @@ public final class ActivityThread { break; case SCHEDULE_CRASH: throw new RemoteServiceException((String)msg.obj); + case DUMP_HEAP: + handleDumpHeap(msg.arg1 != 0, (DumpHeapData)msg.obj); + break; + case DUMP_ACTIVITY: + handleDumpActivity((DumpComponentInfo)msg.obj); + break; } } void maybeSnapshot() { if (mBoundApplication != null) { - SamplingProfilerIntegration.writeSnapshot( - mBoundApplication.processName); + // convert the *private* ActivityThread.PackageInfo to *public* known + // android.content.pm.PackageInfo + String packageName = mBoundApplication.info.mPackageName; + android.content.pm.PackageInfo packageInfo = null; + try { + Context context = getSystemContext(); + if(context == null) { + Log.e(TAG, "cannot get a valid context"); + return; + } + PackageManager pm = context.getPackageManager(); + if(pm == null) { + Log.e(TAG, "cannot get a valid PackageManager"); + return; + } + packageInfo = pm.getPackageInfo( + packageName, PackageManager.GET_ACTIVITIES); + } catch (NameNotFoundException e) { + Log.e(TAG, "cannot get package info for " + packageName, e); + } + SamplingProfilerIntegration.writeSnapshot(mBoundApplication.processName, packageInfo); } } } @@ -1433,7 +1499,7 @@ public final class ActivityThread { public final Activity startActivityNow(Activity parent, String id, Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state, - Object lastNonConfigurationInstance) { + Activity.NonConfigurationInstances lastNonConfigurationInstances) { ActivityClientRecord r = new ActivityClientRecord(); r.token = token; r.ident = 0; @@ -1442,7 +1508,7 @@ public final class ActivityThread { r.parent = parent; r.embeddedID = id; r.activityInfo = activityInfo; - r.lastNonConfigurationInstance = lastNonConfigurationInstance; + r.lastNonConfigurationInstances = lastNonConfigurationInstances; if (localLOGV) { ComponentName compname = intent.getComponent(); String name; @@ -1564,14 +1630,12 @@ public final class ActivityThread { + 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); + r.embeddedID, r.lastNonConfigurationInstances, config); if (customIntent != null) { activity.mIntent = customIntent; } - r.lastNonConfigurationInstance = null; - r.lastNonConfigurationChildInstances = null; + r.lastNonConfigurationInstances = null; activity.mStartedActivity = false; int theme = r.activityInfo.getThemeResource(); if (theme != 0) { @@ -1984,9 +2048,9 @@ public final class ActivityThread { } } - private void handleDumpService(DumpServiceInfo info) { + private void handleDumpService(DumpComponentInfo info) { try { - Service s = mServices.get(info.service); + Service s = mServices.get(info.token); if (s != null) { PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd)); s.dump(info.fd, pw, info.args); @@ -2000,6 +2064,22 @@ public final class ActivityThread { } } + private void handleDumpActivity(DumpComponentInfo info) { + try { + ActivityClientRecord r = mActivities.get(info.token); + if (r != null && r.activity != null) { + PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd)); + r.activity.dump(info.fd, pw, info.args); + pw.close(); + } + } finally { + synchronized (info) { + info.dumped = true; + info.notifyAll(); + } + } + } + private final void handleServiceArgs(ServiceArgsData data) { Service s = mServices.get(data.token); if (s != null) { @@ -2548,6 +2628,9 @@ public final class ActivityThread { if (finishing) { r.activity.mFinished = true; } + if (getNonConfigInstance) { + r.activity.mChangingConfigurations = true; + } if (!r.paused) { try { r.activity.mCalled = false; @@ -2588,8 +2671,8 @@ public final class ActivityThread { } if (getNonConfigInstance) { try { - r.lastNonConfigurationInstance - = r.activity.onRetainNonConfigurationInstance(); + r.lastNonConfigurationInstances + = r.activity.retainNonConfigurationInstances(); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -2598,22 +2681,10 @@ public final class ActivityThread { + ": " + e.toString(), e); } } - try { - r.lastNonConfigurationChildInstances - = r.activity.onRetainNonConfigurationChildInstances(); - } catch (Exception e) { - if (!mInstrumentation.onException(r.activity, e)) { - throw new RuntimeException( - "Unable to retain child activities " - + safeToComponentShortString(r.intent) - + ": " + e.toString(), e); - } - } - } try { r.activity.mCalled = false; - r.activity.onDestroy(); + mInstrumentation.callActivityOnDestroy(r.activity); if (!r.activity.mCalled) { throw new SuperNotCalledException( "Activity " + safeToComponentShortString(r.intent) + @@ -3025,6 +3096,25 @@ public final class ActivityThread { } } + final void handleDumpHeap(boolean managed, DumpHeapData dhd) { + if (managed) { + try { + Debug.dumpHprofData(dhd.path, dhd.fd.getFileDescriptor()); + } catch (IOException e) { + Slog.w(TAG, "Managed heap dump failed on path " + dhd.path + + " -- can the process access this path?"); + } finally { + try { + dhd.fd.close(); + } catch (IOException e) { + Slog.w(TAG, "Failure closing profile fd", e); + } + } + } else { + Debug.dumpNativeHeap(dhd.fd.getFileDescriptor()); + } + } + final void handleDispatchPackageBroadcast(int cmd, String[] packages) { boolean hasPkgInfo = false; if (packages != null) { @@ -3583,6 +3673,7 @@ public final class ActivityThread { } public static final ActivityThread systemMain() { + HardwareRenderer.disable(); ActivityThread thread = new ActivityThread(); thread.attach(true); return thread; diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index 2714de58b6fe485ac925958fa13611d2055804bf..61a8fc3312f9a92e6db9ac1411f454003820d8a9 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -265,12 +265,22 @@ public class AlertDialog extends Dialog implements DialogInterface { public static class Builder { private final AlertController.AlertParams P; + private int mTheme; /** * Constructor using a context for this builder and the {@link AlertDialog} it creates. */ public Builder(Context context) { + this(context, com.android.internal.R.style.Theme_Dialog_Alert); + } + + /** + * Constructor using a context and theme for this builder and + * the {@link AlertDialog} it creates. + */ + public Builder(Context context, int theme) { P = new AlertController.AlertParams(context); + mTheme = theme; } /** @@ -783,7 +793,7 @@ public class AlertDialog extends Dialog implements DialogInterface { * to do and want this to be created and displayed. */ public AlertDialog create() { - final AlertDialog dialog = new AlertDialog(P.mContext); + final AlertDialog dialog = new AlertDialog(P.mContext, mTheme); P.apply(dialog.mAlert); dialog.setCancelable(P.mCancelable); dialog.setOnCancelListener(P.mOnCancelListener); diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java index 8f940d5d12b98475d300d909092aa2a5e2e9c1cc..1e012ebd1e6cbf745e29f00faded43c4570e23ea 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -184,11 +184,11 @@ public class ApplicationErrorReport implements Parcelable { candidate = SystemProperties.get(DEFAULT_ERROR_RECEIVER_PROPERTY); return getErrorReportReceiver(pm, packageName, candidate); } - + /** * Return activity in receiverPackage that handles ACTION_APP_ERROR. * - * @param pm PackageManager isntance + * @param pm PackageManager instance * @param errorPackage package which caused the error * @param receiverPackage candidate package to receive the error * @return activity component within receiverPackage which handles @@ -581,6 +581,9 @@ public class ApplicationErrorReport implements Parcelable { case TYPE_BATTERY: batteryInfo.dump(pw, prefix); break; + case TYPE_RUNNING_SERVICE: + runningServiceInfo.dump(pw, prefix); + break; } } } diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 1c20062f3f84b026dbc6964203d0a565311bea68..95689fc3df42ca2e11beb5a4c4af549ccedf7b06 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -403,6 +403,32 @@ public abstract class ApplicationThreadNative extends Binder scheduleCrash(msg); return true; } + + case DUMP_HEAP_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + boolean managed = data.readInt() != 0; + String path = data.readString(); + ParcelFileDescriptor fd = data.readInt() != 0 + ? data.readFileDescriptor() : null; + dumpHeap(managed, path, fd); + return true; + } + + case DUMP_ACTIVITY_TRANSACTION: { + data.enforceInterface(IApplicationThread.descriptor); + ParcelFileDescriptor fd = data.readFileDescriptor(); + final IBinder activity = data.readStrongBinder(); + final String[] args = data.readStringArray(); + if (fd != null) { + dumpActivity(fd.getFileDescriptor(), activity, args); + try { + fd.close(); + } catch (IOException e) { + } + } + return true; + } } return super.onTransact(code, data, reply, flags); @@ -829,5 +855,33 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } + + public void dumpHeap(boolean managed, String path, + ParcelFileDescriptor fd) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeInt(managed ? 1 : 0); + data.writeString(path); + if (fd != null) { + data.writeInt(1); + fd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } + mRemote.transact(DUMP_HEAP_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); + data.recycle(); + } + + public void dumpActivity(FileDescriptor fd, IBinder token, String[] args) + throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeFileDescriptor(fd); + data.writeStrongBinder(token); + data.writeStringArray(args); + mRemote.transact(DUMP_ACTIVITY_TRANSACTION, data, null, 0); + data.recycle(); + } } diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java new file mode 100644 index 0000000000000000000000000000000000000000..e6cc0f964c602adb4038449234c8844d934c16d9 --- /dev/null +++ b/core/java/android/app/BackStackRecord.java @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; + +final class BackStackState implements Parcelable { + final int[] mOps; + final int mTransition; + final int mTransitionStyle; + final String mName; + final int mIndex; + final int mBreadCrumbTitleRes; + final CharSequence mBreadCrumbTitleText; + final int mBreadCrumbShortTitleRes; + final CharSequence mBreadCrumbShortTitleText; + + public BackStackState(FragmentManagerImpl fm, BackStackRecord bse) { + int numRemoved = 0; + BackStackRecord.Op op = bse.mHead; + while (op != null) { + if (op.removed != null) numRemoved += op.removed.size(); + op = op.next; + } + mOps = new int[bse.mNumOp*5 + numRemoved]; + + if (!bse.mAddToBackStack) { + throw new IllegalStateException("Not on back stack"); + } + + op = bse.mHead; + int pos = 0; + while (op != null) { + mOps[pos++] = op.cmd; + mOps[pos++] = op.fragment.mIndex; + mOps[pos++] = op.enterAnim; + mOps[pos++] = op.exitAnim; + if (op.removed != null) { + final int N = op.removed.size(); + mOps[pos++] = N; + for (int i=0; i 0) { + op.removed = new ArrayList(N); + for (int i=0; i CREATOR + = new Parcelable.Creator() { + public BackStackState createFromParcel(Parcel in) { + return new BackStackState(in); + } + + public BackStackState[] newArray(int size) { + return new BackStackState[size]; + } + }; +} + +/** + * @hide Entry of an operation on the fragment back stack. + */ +final class BackStackRecord implements FragmentTransaction, + FragmentManager.BackStackEntry, Runnable { + static final String TAG = "BackStackEntry"; + + final FragmentManagerImpl mManager; + + static final int OP_NULL = 0; + static final int OP_ADD = 1; + static final int OP_REPLACE = 2; + static final int OP_REMOVE = 3; + static final int OP_HIDE = 4; + static final int OP_SHOW = 5; + + static final class Op { + Op next; + Op prev; + int cmd; + Fragment fragment; + int enterAnim; + int exitAnim; + ArrayList removed; + } + + Op mHead; + Op mTail; + int mNumOp; + int mEnterAnim; + int mExitAnim; + int mTransition; + int mTransitionStyle; + boolean mAddToBackStack; + String mName; + boolean mCommitted; + int mIndex; + + int mBreadCrumbTitleRes; + CharSequence mBreadCrumbTitleText; + int mBreadCrumbShortTitleRes; + CharSequence mBreadCrumbShortTitleText; + + public BackStackRecord(FragmentManagerImpl manager) { + mManager = manager; + } + + public int getId() { + return mIndex; + } + + public CharSequence getBreadCrumbTitle() { + if (mBreadCrumbTitleRes != 0) { + return mManager.mActivity.getText(mBreadCrumbTitleRes); + } + return mBreadCrumbTitleText; + } + + public CharSequence getBreadCrumbShortTitle() { + if (mBreadCrumbShortTitleRes != 0) { + return mManager.mActivity.getText(mBreadCrumbShortTitleRes); + } + return mBreadCrumbShortTitleText; + } + + void addOp(Op op) { + if (mHead == null) { + mHead = mTail = op; + } else { + op.prev = mTail; + mTail.next = op; + mTail = op; + } + op.enterAnim = mEnterAnim; + op.exitAnim = mExitAnim; + mNumOp++; + } + + public FragmentTransaction add(Fragment fragment, String tag) { + doAddOp(0, fragment, tag, OP_ADD); + return this; + } + + public FragmentTransaction add(int containerViewId, Fragment fragment) { + doAddOp(containerViewId, fragment, null, OP_ADD); + return this; + } + + public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) { + doAddOp(containerViewId, fragment, tag, OP_ADD); + return this; + } + + private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) { + if (fragment.mImmediateActivity != null) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + fragment.mImmediateActivity = mManager.mActivity; + fragment.mFragmentManager = mManager; + + if (tag != null) { + if (fragment.mTag != null && !tag.equals(fragment.mTag)) { + throw new IllegalStateException("Can't change tag of fragment " + + fragment + ": was " + fragment.mTag + + " now " + tag); + } + fragment.mTag = tag; + } + + if (containerViewId != 0) { + if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) { + throw new IllegalStateException("Can't change container ID of fragment " + + fragment + ": was " + fragment.mFragmentId + + " now " + containerViewId); + } + fragment.mContainerId = fragment.mFragmentId = containerViewId; + } + + Op op = new Op(); + op.cmd = opcmd; + op.fragment = fragment; + addOp(op); + } + + public FragmentTransaction replace(int containerViewId, Fragment fragment) { + return replace(containerViewId, fragment, null); + } + + public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) { + if (containerViewId == 0) { + throw new IllegalArgumentException("Must use non-zero containerViewId"); + } + + doAddOp(containerViewId, fragment, tag, OP_REPLACE); + return this; + } + + public FragmentTransaction remove(Fragment fragment) { + if (fragment.mImmediateActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + fragment.mImmediateActivity = null; + + Op op = new Op(); + op.cmd = OP_REMOVE; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction hide(Fragment fragment) { + if (fragment.mImmediateActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + + Op op = new Op(); + op.cmd = OP_HIDE; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction show(Fragment fragment) { + if (fragment.mImmediateActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + + Op op = new Op(); + op.cmd = OP_SHOW; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction setCustomAnimations(int enter, int exit) { + mEnterAnim = enter; + mExitAnim = exit; + return this; + } + + public FragmentTransaction setTransition(int transition) { + mTransition = transition; + return this; + } + + public FragmentTransaction setTransitionStyle(int styleRes) { + mTransitionStyle = styleRes; + return this; + } + + public FragmentTransaction addToBackStack(String name) { + mAddToBackStack = true; + mName = name; + return this; + } + + public FragmentTransaction setBreadCrumbTitle(int res) { + mBreadCrumbTitleRes = res; + mBreadCrumbTitleText = null; + return this; + } + + public FragmentTransaction setBreadCrumbTitle(CharSequence text) { + mBreadCrumbTitleRes = 0; + mBreadCrumbTitleText = text; + return this; + } + + public FragmentTransaction setBreadCrumbShortTitle(int res) { + mBreadCrumbShortTitleRes = res; + mBreadCrumbShortTitleText = null; + return this; + } + + public FragmentTransaction setBreadCrumbShortTitle(CharSequence text) { + mBreadCrumbShortTitleRes = 0; + mBreadCrumbShortTitleText = text; + return this; + } + + void bumpBackStackNesting(int amt) { + if (!mAddToBackStack) { + return; + } + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting in " + this + + " by " + amt); + Op op = mHead; + while (op != null) { + op.fragment.mBackStackNesting += amt; + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of " + + op.fragment + " to " + op.fragment.mBackStackNesting); + if (op.removed != null) { + for (int i=op.removed.size()-1; i>=0; i--) { + Fragment r = op.removed.get(i); + r.mBackStackNesting += amt; + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of " + + r + " to " + r.mBackStackNesting); + } + } + op = op.next; + } + } + + public int commit() { + if (mCommitted) throw new IllegalStateException("commit already called"); + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Commit: " + this); + mCommitted = true; + if (mAddToBackStack) { + mIndex = mManager.allocBackStackIndex(this); + } else { + mIndex = -1; + } + mManager.enqueueAction(this); + return mIndex; + } + + public void run() { + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Run: " + this); + + if (mAddToBackStack) { + if (mIndex < 0) { + throw new IllegalStateException("addToBackStack() called after commit()"); + } + } + + bumpBackStackNesting(1); + + Op op = mHead; + while (op != null) { + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + f.mNextAnim = op.enterAnim; + mManager.addFragment(f, false); + } break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (mManager.mAdded != null) { + for (int i=0; i= 0) { + mManager.freeBackStackIndex(mIndex); + mIndex = -1; + } + } + + public String getName() { + return mName; + } + + public int getTransition() { + return mTransition; + } + + public int getTransitionStyle() { + return mTransitionStyle; + } + + public boolean isEmpty() { + return mNumOp == 0; + } +} diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 09ef71024ad4e6e88f9cb7c1cbd5e8623a639bcb..74971364aafdc9c0203dce65bbf50e0303ffd962 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -53,15 +53,17 @@ import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.content.pm.PackageParser.Package; import android.content.res.AssetManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; +import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.hardware.SensorManager; +import android.location.CountryDetector; +import android.location.ICountryDetector; import android.location.ILocationManager; import android.location.LocationManager; import android.media.AudioManager; @@ -86,13 +88,11 @@ import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.StatFs; import android.os.Vibrator; import android.os.FileUtils.FileStatus; import android.os.storage.StorageManager; -import android.provider.Settings; import android.telephony.TelephonyManager; -import android.text.ClipboardManager; +import android.content.ClipboardManager; import android.util.AndroidRuntimeException; import android.util.Log; import android.view.ContextThemeWrapper; @@ -172,6 +172,7 @@ class ContextImpl extends Context { private static ThrottleManager sThrottleManager; private static WifiManager sWifiManager; private static LocationManager sLocationManager; + private static CountryDetector sCountryDetector; private static final HashMap sSharedPrefs = new HashMap(); @@ -213,23 +214,8 @@ class ContextImpl extends Context { private File mExternalFilesDir; private File mExternalCacheDir; - private static long sInstanceCount = 0; - private static final String[] EMPTY_FILE_LIST = {}; - // For debug only - /* - @Override - protected void finalize() throws Throwable { - super.finalize(); - --sInstanceCount; - } - */ - - public static long getInstanceCount() { - return sInstanceCount; - } - @Override public AssetManager getAssets() { return mResources.getAssets(); @@ -264,18 +250,18 @@ class ContextImpl extends Context { public Looper getMainLooper() { return mMainThread.getLooper(); } - + @Override public Context getApplicationContext() { return (mPackageInfo != null) ? mPackageInfo.getApplication() : mMainThread.getApplication(); } - + @Override public void setTheme(int resid) { mThemeResource = resid; } - + @Override public Resources.Theme getTheme() { if (mTheme == null) { @@ -440,7 +426,7 @@ class ContextImpl extends Context { } if (!mFilesDir.exists()) { if(!mFilesDir.mkdirs()) { - Log.w(TAG, "Unable to create files directory"); + Log.w(TAG, "Unable to create files directory " + mFilesDir.getPath()); return null; } FileUtils.setPermissions( @@ -451,7 +437,7 @@ class ContextImpl extends Context { return mFilesDir; } } - + @Override public File getExternalFilesDir(String type) { synchronized (mSync) { @@ -483,7 +469,7 @@ class ContextImpl extends Context { return dir; } } - + @Override public File getCacheDir() { synchronized (mSync) { @@ -503,7 +489,7 @@ class ContextImpl extends Context { } return mCacheDir; } - + @Override public File getExternalCacheDir() { synchronized (mSync) { @@ -525,7 +511,7 @@ class ContextImpl extends Context { return mExternalCacheDir; } } - + @Override public File getFileStreamPath(String name) { return makeFilename(getFilesDir(), name); @@ -545,6 +531,15 @@ class ContextImpl extends Context { return db; } + @Override + public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + File f = validateFilePath(name, true); + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f.getPath(), factory, errorHandler); + setFilePermissionsFromMode(f.getPath(), mode, 0); + return db; + } + @Override public boolean deleteDatabase(String name) { try { @@ -566,7 +561,7 @@ class ContextImpl extends Context { return (list != null) ? list : EMPTY_FILE_LIST; } - + private File getDatabasesDir() { synchronized (mSync) { if (mDatabasesDir == null) { @@ -578,7 +573,7 @@ class ContextImpl extends Context { return mDatabasesDir; } } - + @Override public Drawable getWallpaper() { return getWallpaperManager().getDrawable(); @@ -623,7 +618,8 @@ class ContextImpl extends Context { + " Is this really what you want?"); } mMainThread.getInstrumentation().execStartActivity( - getOuterContext(), mMainThread.getApplicationThread(), null, null, intent, -1); + getOuterContext(), mMainThread.getApplicationThread(), null, + (Activity)null, intent, -1); } @Override @@ -646,7 +642,7 @@ class ContextImpl extends Context { } catch (RemoteException e) { } } - + @Override public void sendBroadcast(Intent intent) { String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); @@ -947,6 +943,8 @@ class ContextImpl extends Context { return AccessibilityManager.getInstance(this); } else if (LOCATION_SERVICE.equals(name)) { return getLocationManager(); + } else if (COUNTRY_DETECTOR.equals(name)) { + return getCountryDetector(); } else if (SEARCH_SERVICE.equals(name)) { return getSearchManager(); } else if (SENSOR_SERVICE.equals(name)) { @@ -1113,6 +1111,17 @@ class ContextImpl extends Context { return sLocationManager; } + private CountryDetector getCountryDetector() { + synchronized (sSync) { + if (sCountryDetector == null) { + IBinder b = ServiceManager.getService(COUNTRY_DETECTOR); + ICountryDetector service = ICountryDetector.Stub.asInterface(b); + sCountryDetector = new CountryDetector(service); + } + } + return sCountryDetector; + } + private SearchManager getSearchManager() { synchronized (mSync) { if (mSearchManager == null) { @@ -1486,8 +1495,6 @@ class ContextImpl extends Context { } ContextImpl() { - // For debug only - //++sInstanceCount; mOuterContext = this; } @@ -1498,7 +1505,6 @@ class ContextImpl extends Context { * @param context Existing application context. */ public ContextImpl(ContextImpl context) { - ++sInstanceCount; mPackageInfo = context.mPackageInfo; mResources = context.mResources; mMainThread = context.mMainThread; @@ -1559,15 +1565,15 @@ class ContextImpl extends Context { final void setActivityToken(IBinder token) { mActivityToken = token; } - + final void setOuterContext(Context context) { mOuterContext = context; } - + final Context getOuterContext() { return mOuterContext; } - + final IBinder getActivityToken() { return mActivityToken; } @@ -1644,7 +1650,7 @@ class ContextImpl extends Context { { return mMainThread.releaseProvider(provider); } - + private final ActivityThread mMainThread; } @@ -1677,7 +1683,7 @@ class ContextImpl extends Context { throw new RuntimeException("Package manager has died", e); } } - + @Override public String[] canonicalToCurrentPackageNames(String[] names) { try { @@ -1686,7 +1692,7 @@ class ContextImpl extends Context { throw new RuntimeException("Package manager has died", e); } } - + @Override public Intent getLaunchIntentForPackage(String packageName) { // First see if the package has an INFO activity; the existence of @@ -1708,8 +1714,9 @@ class ContextImpl extends Context { if (resolveInfo == null) { return null; } - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClassName(packageName, resolveInfo.activityInfo.name); + Intent intent = new Intent(intentToResolve); + intent.setClassName(resolveInfo.activityInfo.applicationInfo.packageName, + resolveInfo.activityInfo.name); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); return intent; } @@ -1875,7 +1882,7 @@ class ContextImpl extends Context { throw new RuntimeException("Package manager has died", e); } } - + @Override public boolean hasSystemFeature(String name) { try { @@ -1884,7 +1891,7 @@ class ContextImpl extends Context { throw new RuntimeException("Package manager has died", e); } } - + @Override public int checkPermission(String permName, String pkgName) { try { @@ -1956,9 +1963,9 @@ class ContextImpl extends Context { throw new RuntimeException("Package manager has died", e); } } - + @Override - public int getUidForSharedUser(String sharedUserName) + public int getUidForSharedUser(String sharedUserName) throws NameNotFoundException { try { int uid = mPM.getUidForSharedUser(sharedUserName); @@ -2362,7 +2369,7 @@ class ContextImpl extends Context { } } } - + private static final class ResourceName { final String packageName; final int iconId; @@ -2534,7 +2541,7 @@ class ContextImpl extends Context { } } @Override - public void clearApplicationUserData(String packageName, + public void clearApplicationUserData(String packageName, IPackageDataObserver observer) { try { mPM.clearApplicationUserData(packageName, observer); @@ -2543,7 +2550,7 @@ class ContextImpl extends Context { } } @Override - public void deleteApplicationCacheFiles(String packageName, + public void deleteApplicationCacheFiles(String packageName, IPackageDataObserver observer) { try { mPM.deleteApplicationCacheFiles(packageName, observer); @@ -2568,9 +2575,9 @@ class ContextImpl extends Context { // Should never happen! } } - + @Override - public void getPackageSizeInfo(String packageName, + public void getPackageSizeInfo(String packageName, IPackageStatsObserver observer) { try { mPM.getPackageSizeInfo(packageName, observer); @@ -2615,7 +2622,7 @@ class ContextImpl extends Context { // Should never happen! } } - + @Override public void replacePreferredActivity(IntentFilter filter, int match, ComponentName[] set, ComponentName activity) { @@ -2634,7 +2641,7 @@ class ContextImpl extends Context { // Should never happen! } } - + @Override public int getPreferredActivities(List outFilters, List outActivities, String packageName) { @@ -2645,7 +2652,7 @@ class ContextImpl extends Context { } return 0; } - + @Override public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags) { @@ -2675,7 +2682,7 @@ class ContextImpl extends Context { // Should never happen! } } - + @Override public int getApplicationEnabledSetting(String packageName) { try { @@ -2802,6 +2809,13 @@ class ContextImpl extends Context { return v != null ? v : defValue; } } + + public Set getStringSet(String key, Set defValues) { + synchronized (this) { + Set v = (Set) mMap.get(key); + return v != null ? v : defValues; + } + } public int getInt(String key, int defValue) { synchronized (this) { @@ -2863,6 +2877,12 @@ class ContextImpl extends Context { return this; } } + public Editor putStringSet(String key, Set values) { + synchronized (this) { + mModified.put(key, values); + return this; + } + } public Editor putInt(String key, int value) { synchronized (this) { mModified.put(key, value); diff --git a/core/java/android/app/DatePickerDialog.java b/core/java/android/app/DatePickerDialog.java index 009671f3e4e9a0bbd84dc71e9682f40edadd2041..8ba480d23bace87d0bfe10cb300acf1141196f12 100644 --- a/core/java/android/app/DatePickerDialog.java +++ b/core/java/android/app/DatePickerDialog.java @@ -21,7 +21,6 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import android.text.TextUtils.TruncateAt; -import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; import android.widget.DatePicker; @@ -39,13 +38,13 @@ import java.util.Calendar; *

See the Date Picker * tutorial.

*/ -public class DatePickerDialog extends AlertDialog implements OnClickListener, +public class DatePickerDialog extends AlertDialog implements OnClickListener, OnDateChangedListener { private static final String YEAR = "year"; private static final String MONTH = "month"; private static final String DAY = "day"; - + private final DatePicker mDatePicker; private final OnDateSetListener mCallBack; private final Calendar mCalendar; @@ -83,7 +82,7 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, int year, int monthOfYear, int dayOfMonth) { - this(context, com.android.internal.R.style.Theme_Dialog_Alert, + this(context, com.android.internal.R.style.Theme_Dialog_Alert, callBack, year, monthOfYear, dayOfMonth); } @@ -109,17 +108,17 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, mInitialDay = dayOfMonth; DateFormatSymbols symbols = new DateFormatSymbols(); mWeekDays = symbols.getShortWeekdays(); - + mTitleDateFormat = java.text.DateFormat. getDateInstance(java.text.DateFormat.FULL); mCalendar = Calendar.getInstance(); updateTitle(mInitialYear, mInitialMonth, mInitialDay); - - setButton(context.getText(R.string.date_time_set), this); - setButton2(context.getText(R.string.cancel), (OnClickListener) null); + + setButton(BUTTON_POSITIVE, context.getText(R.string.date_time_set), this); + setButton(BUTTON_NEGATIVE, context.getText(R.string.cancel), (OnClickListener) null); setIcon(R.drawable.ic_dialog_time); - - LayoutInflater inflater = + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.date_picker_dialog, null); setView(view); @@ -139,20 +138,20 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, title.setSingleLine(); title.setEllipsize(TruncateAt.END); } - + public void onClick(DialogInterface dialog, int which) { if (mCallBack != null) { mDatePicker.clearFocus(); - mCallBack.onDateSet(mDatePicker, mDatePicker.getYear(), + mCallBack.onDateSet(mDatePicker, mDatePicker.getYear(), mDatePicker.getMonth(), mDatePicker.getDayOfMonth()); } } - + public void onDateChanged(DatePicker view, int year, int month, int day) { updateTitle(year, month, day); } - + public void updateDate(int year, int monthOfYear, int dayOfMonth) { mInitialYear = year; mInitialMonth = monthOfYear; @@ -166,7 +165,7 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, mCalendar.set(Calendar.DAY_OF_MONTH, day); setTitle(mTitleDateFormat.format(mCalendar.getTime())); } - + @Override public Bundle onSaveInstanceState() { Bundle state = super.onSaveInstanceState(); @@ -175,7 +174,7 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, state.putInt(DAY, mDatePicker.getDayOfMonth()); return state; } - + @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index da8c9e566977245e177c0d78237c7fbfc41d671a..a0be0cde1f5d09a6cda2b9c2ddeb5c49b6a0a1b0 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -16,18 +16,21 @@ package android.app; +import com.android.internal.app.ActionBarImpl; import com.android.internal.policy.PolicyManager; -import android.content.Context; -import android.content.DialogInterface; import android.content.ComponentName; +import android.content.Context; import android.content.ContextWrapper; +import android.content.DialogInterface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.view.ActionMode; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.KeyEvent; @@ -36,13 +39,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.View.OnCreateContextMenuListener; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View.OnCreateContextMenuListener; -import android.view.ViewGroup.LayoutParams; import android.view.accessibility.AccessibilityEvent; import java.lang.ref.WeakReference; @@ -76,6 +78,7 @@ public class Dialog implements DialogInterface, Window.Callback, final WindowManager mWindowManager; Window mWindow; View mDecor; + private ActionBarImpl mActionBar; /** * This field should be made private, so it is hidden from the SDK. * {@hide} @@ -176,6 +179,15 @@ public class Dialog implements DialogInterface, Window.Callback, return mContext; } + /** + * Retrieve the {@link ActionBar} attached to this dialog, if present. + * + * @return The ActionBar attached to the dialog or null if no ActionBar is present. + */ + public ActionBar getActionBar() { + return mActionBar; + } + /** * Sets the Activity that owns this dialog. An example use: This Dialog will * use the suggested volume control stream of the Activity. @@ -216,6 +228,9 @@ public class Dialog implements DialogInterface, Window.Callback, public void show() { if (mShowing) { if (mDecor != null) { + if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) { + mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR); + } mDecor.setVisibility(View.VISIBLE); } return; @@ -227,6 +242,11 @@ public class Dialog implements DialogInterface, Window.Callback, onStart(); mDecor = mWindow.getDecorView(); + + if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) { + mActionBar = new ActionBarImpl(this); + } + WindowManager.LayoutParams l = mWindow.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) { @@ -310,7 +330,7 @@ public class Dialog implements DialogInterface, Window.Callback, } /** - * Similar to {@link Activity#onCreate}, you should initialized your dialog + * Similar to {@link Activity#onCreate}, you should initialize your dialog * in this method, including calling {@link #setContentView}. * @param savedInstanceState If this dialog is being reinitalized after a * the hosting activity was previously shut down, holds the result from @@ -774,6 +794,13 @@ public class Dialog implements DialogInterface, Window.Callback, mWindow.closePanel(Window.FEATURE_OPTIONS_PANEL); } + /** + * @see Activity#invalidateOptionsMenu() + */ + public void invalidateOptionsMenu() { + mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL); + } + /** * @see Activity#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) */ @@ -832,8 +859,15 @@ public class Dialog implements DialogInterface, Window.Callback, } } + public ActionMode onStartActionMode(ActionMode.Callback callback) { + if (mActionBar != null) { + return mActionBar.startActionMode(callback); + } + return null; + } + /** - * @return The activity associated with this dialog, or null if there is no assocaited activity. + * @return The activity associated with this dialog, or null if there is no associated activity. */ private ComponentName getAssociatedActivity() { Activity activity = mOwnerActivity; diff --git a/core/java/android/app/DialogFragment.java b/core/java/android/app/DialogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..e8dfac99c4bc83e4c5757ccf1542c452cb602877 --- /dev/null +++ b/core/java/android/app/DialogFragment.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +/** + * A fragment that displays a dialog window, floating on top of its + * activity's window. This fragment contains a Dialog object, which it + * displays as appropriate based on the fragment's state. Control of + * the dialog (deciding when to show, hide, dismiss it) should be done through + * the API here, not with direct calls on the dialog. + * + *

Implementations should override this class and implement + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} to supply the + * content of the dialog. Alternatively, they can override + * {@link #onCreateDialog(Bundle)} to create an entirely custom dialog, such + * as an AlertDialog, with its own content. + * + *

Topics covered here: + *

    + *
  1. Lifecycle + *
  2. Basic Dialog + *
  3. Alert Dialog + *
  4. Selecting Between Dialog or Embedding + *
+ * + * + *

Lifecycle

+ * + *

DialogFragment does various things to keep the fragment's lifecycle + * driving it, instead of the Dialog. Note that dialogs are generally + * autonomous entities -- they are their own window, receiving their own + * input events, and often deciding on their own when to disappear (by + * receiving a back key event or the user clicking on a button). + * + *

DialogFragment needs to ensure that what is happening with the Fragment + * and Dialog states remains consistent. To do this, it watches for dismiss + * events from the dialog and takes are of removing its own state when they + * happen. This means you should use {@link #show(FragmentManager, String)} + * or {@link #show(FragmentTransaction, String)} to add an instance of + * DialogFragment to your UI, as these keep track of how DialogFragment should + * remove itself when the dialog is dismissed. + * + * + *

Basic Dialog

+ * + *

The simplest use of DialogFragment is as a floating container for the + * fragment's view hierarchy. A simple implementation may look like this: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialog.java + * dialog} + * + *

An example showDialog() method on the Activity could be: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialog.java + * add_dialog} + * + *

This removes any currently shown dialog, creates a new DialogFragment + * with an argument, and shows it as a new state on the back stack. When the + * transaction is popped, the current DialogFragment and its Dialog will be + * destroyed, and the previous one (if any) re-shown. Note that in this case + * DialogFragment will take care of popping the transaction of the Dialog + * is dismissed separately from it. + * + * + *

Alert Dialog

+ * + *

Instead of (or in addition to) implementing {@link #onCreateView} to + * generate the view hierarchy inside of a dialog, you may implement + * {@link #onCreateDialog(Bundle)} to create your own custom Dialog object. + * + *

This is most useful for creating an {@link AlertDialog}, allowing you + * to display standard alerts to the user that are managed by a fragment. + * A simple example implementation of this is: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentAlertDialog.java + * dialog} + * + *

The activity creating this fragment may have the following methods to + * show the dialog and receive results from it: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentAlertDialog.java + * activity} + * + *

Note that in this case the fragment is not placed on the back stack, it + * is just added as an indefinitely running fragment. Because dialogs normally + * are modal, this will still operate as a back stack, since the dialog will + * capture user input until it is dismissed. When it is dismissed, DialogFragment + * will take care of removing itself from its fragment manager. + * + * + *

Selecting Between Dialog or Embedding

+ * + *

A DialogFragment can still optionally be used as a normal fragment, if + * desired. This is useful if you have a fragment that in some cases should + * be shown as a dialog and others embedded in a larger UI. This behavior + * will normally be automatically selected for you based on how you are using + * the fragment, but can be customized with {@link #setShowsDialog(boolean)}. + * + *

For example, here is a simple dialog fragment: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java + * dialog} + * + *

An instance of this fragment can be created and shown as a dialog: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java + * show_dialog} + * + *

It can also be added as content in a view hierarchy: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java + * embed} + */ +public class DialogFragment extends Fragment + implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { + + /** + * Style for {@link #setStyle(int, int)}: a basic, + * normal dialog. + */ + public static final int STYLE_NORMAL = 0; + + /** + * Style for {@link #setStyle(int, int)}: don't include + * a title area. + */ + public static final int STYLE_NO_TITLE = 1; + + /** + * Style for {@link #setStyle(int, int)}: don't draw + * any frame at all; the view hierarchy returned by {@link #onCreateView} + * is entirely responsible for drawing the dialog. + */ + public static final int STYLE_NO_FRAME = 2; + + /** + * Style for {@link #setStyle(int, int)}: like + * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog. + * The user can not touch it, and its window will not receive input focus. + */ + public static final int STYLE_NO_INPUT = 3; + + private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; + private static final String SAVED_STYLE = "android:style"; + private static final String SAVED_THEME = "android:theme"; + private static final String SAVED_CANCELABLE = "android:cancelable"; + private static final String SAVED_SHOWS_DIALOG = "android:showsDialog"; + private static final String SAVED_BACK_STACK_ID = "android:backStackId"; + + int mStyle = STYLE_NORMAL; + int mTheme = 0; + boolean mCancelable = true; + boolean mShowsDialog = true; + int mBackStackId = -1; + + Dialog mDialog; + boolean mDestroyed; + boolean mRemoved; + + public DialogFragment() { + } + + /** + * Call to customize the basic appearance and behavior of the + * fragment's dialog. This can be used for some common dialog behaviors, + * taking care of selecting flags, theme, and other options for you. The + * same effect can be achieve by manually setting Dialog and Window + * attributes yourself. Calling this after the fragment's Dialog is + * created will have no effect. + * + * @param style Selects a standard style: may be {@link #STYLE_NORMAL}, + * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or + * {@link #STYLE_NO_INPUT}. + * @param theme Optional custom theme. If 0, an appropriate theme (based + * on the style) will be selected for you. + */ + public void setStyle(int style, int theme) { + mStyle = style; + if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) { + mTheme = android.R.style.Theme_Dialog_NoFrame; + } + if (theme != 0) { + mTheme = theme; + } + } + + /** + * @deprecated Please use {@link #show(FragmentManager, String)}. + */ + @Deprecated + public void show(Activity activity, String tag) { + FragmentTransaction ft = activity.openFragmentTransaction(); + ft.add(this, tag); + ft.commit(); + } + + /** + * Display the dialog, adding the fragment to the given FragmentManager. This + * is a convenience for explicitly creating a transaction, adding the + * fragment to it with the given tag, and committing it. This does + * not add the transaction to the back stack. When the fragment + * is dismissed, a new transaction will be executed to remove it from + * the activity. + * @param manager The FragmentManager this fragment will be added to. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + */ + public void show(FragmentManager manager, String tag) { + FragmentTransaction ft = manager.openTransaction(); + ft.add(this, tag); + ft.commit(); + } + + /** + * Display the dialog, adding the fragment using an existing transaction + * and then committing the transaction. + * @param transaction An existing transaction in which to add the fragment. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + * @return Returns the identifier of the committed transaction, as per + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. + */ + public int show(FragmentTransaction transaction, String tag) { + transaction.add(this, tag); + mRemoved = false; + mBackStackId = transaction.commit(); + return mBackStackId; + } + + /** + * Dismiss the fragment and its dialog. If the fragment was added to the + * back stack, all back stack state up to and including this entry will + * be popped. Otherwise, a new transaction will be committed to remove + * the fragment. + */ + public void dismiss() { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + } + mRemoved = true; + if (mBackStackId >= 0) { + getFragmentManager().popBackStack(mBackStackId, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + mBackStackId = -1; + } else { + FragmentTransaction ft = getFragmentManager().openTransaction(); + ft.remove(this); + ft.commit(); + } + } + + public Dialog getDialog() { + return mDialog; + } + + public int getTheme() { + return mTheme; + } + + /** + * Control whether the shown Dialog is cancelable. Use this instead of + * directly calling {@link Dialog#setCancelable(boolean) + * Dialog.setCancelable(boolean)}, because DialogFragment needs to change + * its behavior based on this. + * + * @param cancelable If true, the dialog is cancelable. The default + * is true. + */ + public void setCancelable(boolean cancelable) { + mCancelable = cancelable; + if (mDialog != null) mDialog.setCancelable(cancelable); + } + + /** + * Return the current value of {@link #setCancelable(boolean)}. + */ + public boolean getCancelable() { + return mCancelable; + } + + /** + * Controls whether this fragment should be shown in a dialog. If not + * set, no Dialog will be created in {@link #onActivityCreated(Bundle)}, + * and the fragment's view hierarchy will thus not be added to it. This + * allows you to instead use it as a normal fragment (embedded inside of + * its activity). + * + *

This is normally set for you based on whether the fragment is + * associated with a container view ID passed to + * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}. + * If the fragment was added with a container, setShowsDialog will be + * initialized to false; otherwise, it will be true. + * + * @param showsDialog If true, the fragment will be displayed in a Dialog. + * If false, no Dialog will be created and the fragment's view hierarchly + * left undisturbed. + */ + public void setShowsDialog(boolean showsDialog) { + mShowsDialog = showsDialog; + } + + /** + * Return the current value of {@link #setShowsDialog(boolean)}. + */ + public boolean getShowsDialog() { + return mShowsDialog; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mShowsDialog = mContainerId == 0; + + if (savedInstanceState != null) { + mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL); + mTheme = savedInstanceState.getInt(SAVED_THEME, 0); + mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true); + mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1); + } + } + + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new Dialog(getActivity(), getTheme()); + } + + public void onCancel(DialogInterface dialog) { + } + + public void onDismiss(DialogInterface dialog) { + if (!mRemoved) { + dismiss(); + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (!mShowsDialog) { + return; + } + + mDialog = onCreateDialog(savedInstanceState); + mDestroyed = false; + switch (mStyle) { + case STYLE_NO_INPUT: + mDialog.getWindow().addFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + // fall through... + case STYLE_NO_FRAME: + case STYLE_NO_TITLE: + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + } + View view = getView(); + if (view != null) { + if (view.getParent() != null) { + throw new IllegalStateException("DialogFragment can not be attached to a container view"); + } + mDialog.setContentView(view); + } + mDialog.setOwnerActivity(getActivity()); + mDialog.setCancelable(mCancelable); + mDialog.setOnCancelListener(this); + mDialog.setOnDismissListener(this); + if (savedInstanceState != null) { + Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); + if (dialogState != null) { + mDialog.onRestoreInstanceState(dialogState); + } + } + } + + @Override + public void onStart() { + super.onStart(); + if (mDialog != null) { + mRemoved = false; + mDialog.show(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mDialog != null) { + Bundle dialogState = mDialog.onSaveInstanceState(); + if (dialogState != null) { + outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); + } + } + if (mStyle != STYLE_NORMAL) { + outState.putInt(SAVED_STYLE, mStyle); + } + if (mTheme != 0) { + outState.putInt(SAVED_THEME, mTheme); + } + if (!mCancelable) { + outState.putBoolean(SAVED_CANCELABLE, mCancelable); + } + if (!mShowsDialog) { + outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + } + if (mBackStackId != -1) { + outState.putInt(SAVED_BACK_STACK_ID, mBackStackId); + } + } + + @Override + public void onStop() { + super.onStop(); + if (mDialog != null) { + mDialog.hide(); + } + } + + /** + * Remove dialog. + */ + @Override + public void onDestroyView() { + super.onDestroyView(); + mDestroyed = true; + if (mDialog != null) { + // Set removed here because this dismissal is just to hide + // the dialog -- we don't want this to cause the fragment to + // actually be removed. + mRemoved = true; + mDialog.dismiss(); + mDialog = null; + } + } +} diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java new file mode 100644 index 0000000000000000000000000000000000000000..12bf7e5c5a0d30e0aebdbed709729d46aa13fa61 --- /dev/null +++ b/core/java/android/app/Fragment.java @@ -0,0 +1,1208 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.animation.Animator; +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AndroidRuntimeException; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnCreateContextMenuListener; +import android.widget.AdapterView; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashMap; + +final class FragmentState implements Parcelable { + final String mClassName; + final int mIndex; + final boolean mFromLayout; + final int mFragmentId; + final int mContainerId; + final String mTag; + final boolean mRetainInstance; + final Bundle mArguments; + + Bundle mSavedFragmentState; + + Fragment mInstance; + + public FragmentState(Fragment frag) { + mClassName = frag.getClass().getName(); + mIndex = frag.mIndex; + mFromLayout = frag.mFromLayout; + mFragmentId = frag.mFragmentId; + mContainerId = frag.mContainerId; + mTag = frag.mTag; + mRetainInstance = frag.mRetainInstance; + mArguments = frag.mArguments; + } + + public FragmentState(Parcel in) { + mClassName = in.readString(); + mIndex = in.readInt(); + mFromLayout = in.readInt() != 0; + mFragmentId = in.readInt(); + mContainerId = in.readInt(); + mTag = in.readString(); + mRetainInstance = in.readInt() != 0; + mArguments = in.readBundle(); + mSavedFragmentState = in.readBundle(); + } + + public Fragment instantiate(Activity activity) { + if (mInstance != null) { + return mInstance; + } + + mInstance = Fragment.instantiate(activity, mClassName, mArguments); + + if (mSavedFragmentState != null) { + mSavedFragmentState.setClassLoader(activity.getClassLoader()); + mInstance.mSavedFragmentState = mSavedFragmentState; + } + mInstance.setIndex(mIndex); + mInstance.mFromLayout = mFromLayout; + mInstance.mFragmentId = mFragmentId; + mInstance.mContainerId = mContainerId; + mInstance.mTag = mTag; + mInstance.mRetainInstance = mRetainInstance; + mInstance.mFragmentManager = activity.mFragments; + + return mInstance; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassName); + dest.writeInt(mIndex); + dest.writeInt(mFromLayout ? 1 : 0); + dest.writeInt(mFragmentId); + dest.writeInt(mContainerId); + dest.writeString(mTag); + dest.writeInt(mRetainInstance ? 1 : 0); + dest.writeBundle(mArguments); + dest.writeBundle(mSavedFragmentState); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public FragmentState createFromParcel(Parcel in) { + return new FragmentState(in); + } + + public FragmentState[] newArray(int size) { + return new FragmentState[size]; + } + }; +} + +/** + * A Fragment is a piece of an application's user interface or behavior + * that can be placed in an {@link Activity}. Interaction with fragments + * is done through {@link FragmentManager}, which can be obtained via + * {@link Activity#getFragmentManager() Activity.getFragmentManager()} and + * {@link Fragment#getFragmentManager() Fragment.getFragmentManager()}. + * + *

The Fragment class can be used many ways to achieve a wide variety of + * results. It is core, it represents a particular operation or interface + * that is running within a larger {@link Activity}. A Fragment is closely + * tied to the Activity it is in, and can not be used apart from one. Though + * Fragment defines its own lifecycle, that lifecycle is dependent on its + * activity: if the activity is stopped, no fragments inside of it can be + * started; when the activity is destroyed, all fragments will be destroyed. + * + *

All subclasses of Fragment must include a public empty constructor. + * The framework will often re-instantiate a fragment class when needed, + * in particular during state restore, and needs to be able to find this + * constructor to instantiate it. If the empty constructor is not available, + * a runtime exception will occur in some cases during state restore. + * + *

Topics covered here: + *

    + *
  1. Lifecycle + *
  2. Layout + *
  3. Back Stack + *
+ * + * + *

Lifecycle

+ * + *

Though a Fragment's lifecycle is tied to its owning activity, it has + * its own wrinkle on the standard activity lifecycle. It includes basic + * activity lifecycle methods such as {@link #onResume}, but also important + * are methods related to interactions with the activity and UI generation. + * + *

The core series of lifecycle methods that are called to bring a fragment + * up to resumed state (interacting with the user) are: + * + *

    + *
  1. {@link #onAttach} called once the fragment is associated with its activity. + *
  2. {@link #onCreate} called to do initial creation of the fragment. + *
  3. {@link #onCreateView} creates and returns the view hierarchy associated + * with the fragment. + *
  4. {@link #onActivityCreated} tells the fragment that its activity has + * completed its own {@link Activity#onCreate Activity.onCreaate}. + *
  5. {@link #onStart} makes the fragment visible to the user (based on its + * containing activity being started). + *
  6. {@link #onResume} makes the fragment interacting with the user (based on its + * containing activity being resumed). + *
+ * + *

As a fragment is no longer being used, it goes through a reverse + * series of callbacks: + * + *

    + *
  1. {@link #onPause} fragment is no longer interacting with the user either + * because its activity is being paused or a fragment operation is modifying it + * in the activity. + *
  2. {@link #onStop} fragment is no longer visible to the user either + * because its activity is being stopped or a fragment operation is modifying it + * in the activity. + *
  3. {@link #onDestroyView} allows the fragment to clean up resources + * associated with its View. + *
  4. {@link #onDestroy} called to do final cleanup of the fragment's state. + *
  5. {@link #onDetach} called immediately prior to the fragment no longer + * being associated with its activity. + *
+ * + * + *

Layout

+ * + *

Fragments can be used as part of your application's layout, allowing + * you to better modularize your code and more easily adjust your user + * interface to the screen it is running on. As an example, we can look + * at a simple program consisting of a list of items, and display of the + * details of each item.

+ * + *

An activity's layout XML can include <fragment> tags + * to embed fragment instances inside of the layout. For example, here is + * a simple layout that embeds one fragment:

+ * + * {@sample development/samples/ApiDemos/res/layout/fragment_layout.xml layout} + * + *

The layout is installed in the activity in the normal way:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java + * main} + * + *

The titles fragment, showing a list of titles, is very simple, relying + * on {@link ListFragment} for most of its work. Note the implementation of + * clicking an item, which can either update + * the content of the details fragment or start a new activity show the + * details depending on whether the current activity's layout can show the + * details.

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java + * titles} + * + *

The details fragment showing the contents of selected item here just + * displays a string of text based on an index of a string array built in to + * the app:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java + * details} + * + *

In this case when the user clicks on a title, there is no details + * fragment in the current activity, so the title title fragment's click code will + * launch a new activity to display the details fragment:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java + * details_activity} + * + *

However the screen may be large enough to show both the list of titles + * and details about the currently selected title. To use such a layout on + * a landscape screen, this alternative layout can be placed under layout-land:

+ * + * {@sample development/samples/ApiDemos/res/layout-land/fragment_layout.xml layout} + * + *

Note how the prior code will adjust to this alternative UI flow: the + * titles fragment will now show its text inside of its activity, and the + * details activity will finish of it finds itself running in a configuration + * where the details can be shown inline. + * + *

When a configuration change causes the activity hosting these fragments + * to restart, its new instance may use a different layout that doesn't + * include the same fragments as the previous layout. In this case all of + * the previous fragments will still be instantiated and running in the new + * instance; however, any that are no longer associated with a <fragment> + * tag in the view hierarchy will not have their content view created and will + * return false from {@link #isInLayout}. + * + *

The attributes of the <fragment> tag are used to control the + * LayoutParams provider when attaching the fragment's view to the parent + * container. They can alse be parsed by the fragment in {@link #onInflate} + * as parameters. + * + *

The fragment being instantiated must have some kind of unique identifier + * so that it can be re-associated with a previous instance if the parent + * activity needs to be destroyed and recreated. This can be provided these + * ways: + * + *

    + *
  • If nothing is explicitly supplied, the view ID of the container will + * be used. + *
  • android:tag can be used in <fragment> to provide + * a specific tag name for the fragment. + *
  • android:id can be used in <fragment> to provide + * a specific identifier for the fragment. + *
+ * + * + *

Back Stack

+ * + *

The transaction in which fragments are modified can be placed on an + * internal back-stack of the owning activity. When the user presses back + * in the activity, any transactions on the back stack are popped off before + * the activity itself is finished. + * + *

For example, consider this simple fragment that is instantiated with + * an integer argument and displays that in a TextView in its UI:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentStack.java + * fragment} + * + *

A function that creates a new instance of the fragment, replacing + * whatever current fragment instance is being shown and pushing that change + * on to the back stack could be written as: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentStack.java + * add_stack} + * + *

After each call to this function, a new entry is on the stack, and + * pressing back will pop it to return the user to whatever previous state + * the activity UI was in. + */ +public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener { + private static final HashMap> sClassMap = + new HashMap>(); + + static final int INITIALIZING = 0; // Not yet created. + static final int CREATED = 1; // Created. + static final int ACTIVITY_CREATED = 2; // The activity has finished its creation. + static final int STARTED = 3; // Created and started, not resumed. + static final int RESUMED = 4; // Created started and resumed. + + int mState = INITIALIZING; + + // When instantiated from saved state, this is the saved state. + Bundle mSavedFragmentState; + SparseArray mSavedViewState; + + // Index into active fragment array. + int mIndex = -1; + + // Internal unique name for this fragment; + String mWho; + + // Construction arguments; + Bundle mArguments; + + // Target fragment. + Fragment mTarget; + + // Target request code. + int mTargetRequestCode; + + // True if the fragment is in the list of added fragments. + boolean mAdded; + + // True if the fragment is in the resumed state. + boolean mResumed; + + // Set to true if this fragment was instantiated from a layout file. + boolean mFromLayout; + + // Set to true when the view has actually been inflated in its layout. + boolean mInLayout; + + // Number of active back stack entries this fragment is in. + int mBackStackNesting; + + // The fragment manager we are associated with. Set as soon as the + // fragment is used in a transaction; cleared after it has been removed + // from all transactions. + FragmentManager mFragmentManager; + + // Set as soon as a fragment is added to a transaction (or removed), + // to be able to do validation. + Activity mImmediateActivity; + + // Activity this fragment is attached to. + Activity mActivity; + + // The optional identifier for this fragment -- either the container ID if it + // was dynamically added to the view hierarchy, or the ID supplied in + // layout. + int mFragmentId; + + // When a fragment is being dynamically added to the view hierarchy, this + // is the identifier of the parent container it is being added to. + int mContainerId; + + // The optional named tag for this fragment -- usually used to find + // fragments that are not part of the layout. + String mTag; + + // Set to true when the app has requested that this fragment be hidden + // from the user. + boolean mHidden; + + // If set this fragment would like its instance retained across + // configuration changes. + boolean mRetainInstance; + + // If set this fragment is being retained across the current config change. + boolean mRetaining; + + // If set this fragment has menu items to contribute. + boolean mHasMenu; + + // Used to verify that subclasses call through to super class. + boolean mCalled; + + // If app has requested a specific animation, this is the one to use. + int mNextAnim; + + // The parent container of the fragment after dynamically added to UI. + ViewGroup mContainer; + + // The View generated for this fragment. + View mView; + + LoaderManagerImpl mLoaderManager; + boolean mStarted; + boolean mCheckedForLoaderManager; + + /** + * Thrown by {@link Fragment#instantiate(Context, String, Bundle)} when + * there is an instantiation failure. + */ + static public class InstantiationException extends AndroidRuntimeException { + public InstantiationException(String msg, Exception cause) { + super(msg, cause); + } + } + + /** + * Default constructor. Every fragment must have an + * empty constructor, so it can be instantiated when restoring its + * activity's state. It is strongly recommended that subclasses do not + * have other constructors with parameters, since these constructors + * will not be called when the fragment is re-instantiated; instead, + * arguments can be supplied by the caller with {@link #setArguments} + * and later retrieved by the Fragment with {@link #getArguments}. + * + *

Applications should generally not implement a constructor. The + * first place application code an run where the fragment is ready to + * be used is in {@link #onAttach(Activity)}, the point where the fragment + * is actually associated with its activity. Some applications may also + * want to implement {@link #onInflate} to retrieve attributes from a + * layout resource, though should take care here because this happens for + * the fragment is attached to its activity. + */ + public Fragment() { + } + + /** + * Like {@link #instantiate(Context, String, Bundle)} but with a null + * argument Bundle. + */ + public static Fragment instantiate(Context context, String fname) { + return instantiate(context, fname, null); + } + + /** + * Create a new instance of a Fragment with the given class name. This is + * the same as calling its empty constructor. + * + * @param context The calling context being used to instantiate the fragment. + * This is currently just used to get its ClassLoader. + * @param fname The class name of the fragment to instantiate. + * @param args Bundle of arguments to supply to the fragment, which it + * can retrieve with {@link #getArguments()}. May be null. + * @return Returns a new fragment instance. + * @throws InstantiationException If there is a failure in instantiating + * the given fragment class. This is a runtime exception; it is not + * normally expected to happen. + */ + public static Fragment instantiate(Context context, String fname, Bundle args) { + try { + Class clazz = sClassMap.get(fname); + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = context.getClassLoader().loadClass(fname); + sClassMap.put(fname, clazz); + } + Fragment f = (Fragment)clazz.newInstance(); + if (args != null) { + args.setClassLoader(f.getClass().getClassLoader()); + f.mArguments = args; + } + return f; + } catch (ClassNotFoundException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (java.lang.InstantiationException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (IllegalAccessException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } + } + + void restoreViewState() { + if (mSavedViewState != null) { + mView.restoreHierarchyState(mSavedViewState); + mSavedViewState = null; + } + } + + void setIndex(int index) { + mIndex = index; + mWho = "android:fragment:" + mIndex; + } + + void clearIndex() { + mIndex = -1; + mWho = null; + } + + /** + * Subclasses can not override equals(). + */ + @Override final public boolean equals(Object o) { + return super.equals(o); + } + + /** + * Subclasses can not override hashCode(). + */ + @Override final public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("Fragment{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mFragmentId != 0) { + sb.append(" id=0x"); + sb.append(Integer.toHexString(mFragmentId)); + } + if (mTag != null) { + sb.append(" "); + sb.append(mTag); + } + sb.append('}'); + return sb.toString(); + } + + /** + * Return the identifier this fragment is known by. This is either + * the android:id value supplied in a layout or the container view ID + * supplied when adding the fragment. + */ + final public int getId() { + return mFragmentId; + } + + /** + * Get the tag name of the fragment, if specified. + */ + final public String getTag() { + return mTag; + } + + /** + * Supply the construction arguments for this fragment. This can only + * be called before the fragment has been attached to its activity; that + * is, you should call it immediately after constructing the fragment. The + * arguments supplied here will be retained across fragment destroy and + * creation. + */ + public void setArguments(Bundle args) { + if (mIndex >= 0) { + throw new IllegalStateException("Fragment already active"); + } + mArguments = args; + } + + /** + * Return the arguments supplied when the fragment was instantiated, + * if any. + */ + final public Bundle getArguments() { + return mArguments; + } + + /** + * Optional target for this fragment. This may be used, for example, + * if this fragment is being started by another, and when done wants to + * give a result back to the first. The target set here is retained + * across instances via {@link FragmentManager#putFragment + * FragmentManager.putFragment()}. + * + * @param fragment The fragment that is the target of this one. + * @param requestCode Optional request code, for convenience if you + * are going to call back with {@link #onActivityResult(int, int, Intent)}. + */ + public void setTargetFragment(Fragment fragment, int requestCode) { + mTarget = fragment; + mTargetRequestCode = requestCode; + } + + /** + * Return the target fragment set by {@link #setTargetFragment}. + */ + final public Fragment getTargetFragment() { + return mTarget; + } + + /** + * Return the target request code set by {@link #setTargetFragment}. + */ + final public int getTargetRequestCode() { + return mTargetRequestCode; + } + + /** + * Return the Activity this fragment is currently associated with. + */ + final public Activity getActivity() { + return mActivity; + } + + /** + * Return the FragmentManager for interacting with fragments associated + * with this fragment's activity. Note that this will be non-null slightly + * before {@link #getActivity()}, during the time from when the fragment is + * placed in a {@link FragmentTransaction} until it is committed and + * attached to its activity. + */ + final public FragmentManager getFragmentManager() { + return mFragmentManager; + } + + /** + * Return true if the fragment is currently added to its activity. + */ + final public boolean isAdded() { + return mActivity != null && mActivity.mFragments.mAdded.contains(this); + } + + /** + * Return true if the layout is included as part of an activity view + * hierarchy via the <fragment> tag. This will always be true when + * fragments are created through the <fragment> tag, except + * in the case where an old fragment is restored from a previous state and + * it does not appear in the layout of the current state. + */ + final public boolean isInLayout() { + return mInLayout; + } + + /** + * Return true if the fragment is in the resumed state. This is true + * for the duration of {@link #onResume()} and {@link #onPause()} as well. + */ + final public boolean isResumed() { + return mResumed; + } + + /** + * Return true if the fragment is currently visible to the user. This means + * it: (1) has been added, (2) has its view attached to the window, and + * (3) is not hidden. + */ + final public boolean isVisible() { + return isAdded() && !isHidden() && mView != null + && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE; + } + + /** + * Return true if the fragment has been hidden. By default fragments + * are shown. You can find out about changes to this state with + * {@link #onHiddenChanged}. Note that the hidden state is orthogonal + * to other states -- that is, to be visible to the user, a fragment + * must be both started and not hidden. + */ + final public boolean isHidden() { + return mHidden; + } + + /** + * Called when the hidden state (as returned by {@link #isHidden()} of + * the fragment has changed. Fragments start out not hidden; this will + * be called whenever the fragment changes state from that. + * @param hidden True if the fragment is now hidden, false if it is not + * visible. + */ + public void onHiddenChanged(boolean hidden) { + } + + /** + * Control whether a fragment instance is retained across Activity + * re-creation (such as from a configuration change). This can only + * be used with fragments not in the back stack. If set, the fragment + * lifecycle will be slightly different when an activity is recreated: + *

    + *
  • {@link #onDestroy()} will not be called (but {@link #onDetach()} still + * will be, because the fragment is being detached from its current activity). + *
  • {@link #onCreate(Bundle)} will not be called since the fragment + * is not being re-created. + *
  • {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} will + * still be called. + *
+ */ + public void setRetainInstance(boolean retain) { + mRetainInstance = retain; + } + + final public boolean getRetainInstance() { + return mRetainInstance; + } + + /** + * Report that this fragment would like to participate in populating + * the options menu by receiving a call to {@link #onCreateOptionsMenu} + * and related methods. + * + * @param hasMenu If true, the fragment has menu items to contribute. + */ + public void setHasOptionsMenu(boolean hasMenu) { + if (mHasMenu != hasMenu) { + mHasMenu = hasMenu; + if (isAdded() && !isHidden()) { + mActivity.invalidateOptionsMenu(); + } + } + } + + /** + * Return the LoaderManager for this fragment, creating it if needed. + */ + public LoaderManager getLoaderManager() { + if (mLoaderManager != null) { + return mLoaderManager; + } + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mIndex, mStarted, true); + return mLoaderManager; + } + + /** + * Call {@link Activity#startActivity(Intent)} on the fragment's + * containing Activity. + */ + public void startActivity(Intent intent) { + mActivity.startActivityFromFragment(this, intent, -1); + } + + /** + * Call {@link Activity#startActivityForResult(Intent, int)} on the fragment's + * containing Activity. + */ + public void startActivityForResult(Intent intent, int requestCode) { + mActivity.startActivityFromFragment(this, intent, requestCode); + } + + /** + * Receive the result from a previous call to + * {@link #startActivityForResult(Intent, int)}. This follows the + * related Activity API as described there in + * {@link Activity#onActivityResult(int, int, Intent)}. + * + * @param requestCode The integer request code originally supplied to + * startActivityForResult(), allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + /** + * Called when a fragment is being created as part of a view layout + * inflation, typically from setting the content view of an activity. This + * will be called immediately after the fragment is created from a + * tag in a layout file. Note this is before the fragment's + * {@link #onAttach(Activity)} has been called; all you should do here is + * parse the attributes and save them away. A convenient thing to do is + * simply copy them into a Bundle that is given to {@link #setArguments(Bundle)}. + * + *

This is called every time the fragment is inflated, even if it is + * being inflated into a new instance with saved state. Because a fragment's + * arguments are retained across instances, it may make no sense to re-parse + * the attributes into new arguments. You may want to first check + * {@link #getArguments()} and only parse the attributes if it returns null, + * the assumption being that if it is non-null those are the same arguments + * from the first time the fragment was inflated. (That said, you may want + * to have layouts change for different configurations such as landscape + * and portrait, which can have different attributes. If so, you will need + * to re-parse the attributes each time this is called to generate new + * arguments.)

+ * + * @param attrs The attributes at the tag where the fragment is + * being created. + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onInflate(AttributeSet attrs, Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when a fragment is first attached to its activity. + * {@link #onCreate(Bundle)} will be called after this. + */ + public void onAttach(Activity activity) { + mCalled = true; + } + + /** + * Called when a fragment loads an animation. + */ + public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { + return null; + } + + /** + * Called to do initial creation of a fragment. This is called after + * {@link #onAttach(Activity)} and before + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * + *

Note that this can be called while the fragment's activity is + * still in the process of being created. As such, you can not rely + * on things like the activity's content view hierarchy being initialized + * at this point. If you want to do work once the activity itself is + * created, see {@link #onActivityCreated(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onCreate(Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null (which + * is the default implementation). This will be called between + * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}. + * + *

If you return a View from here, you will later be called in + * {@link #onDestroyView} when the view is being released. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the View for the fragment's UI, or null. + */ + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return null; + } + + public View getView() { + return mView; + } + + /** + * Called when the fragment's activity has been created and this + * fragment's view hierarchy instantiated. It can be used to do final + * initialization once these pieces are in place, such as retrieving + * views or restoring state. It is also useful for fragments that use + * {@link #setRetainInstance(boolean)} to retain their instance, + * as this callback tells the fragment when it is fully associated with + * the new activity instance. This is called after {@link #onCreateView} + * and before {@link #onStart()}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onActivityCreated(Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when the Fragment is visible to the user. This is generally + * tied to {@link Activity#onStart() Activity.onStart} of the containing + * Activity's lifecycle. + */ + public void onStart() { + mCalled = true; + mStarted = true; + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mIndex, mStarted, false); + } + if (mLoaderManager != null) { + mLoaderManager.doStart(); + } + } + + /** + * Called when the fragment is visible to the user and actively running. + * This is generally + * tied to {@link Activity#onResume() Activity.onResume} of the containing + * Activity's lifecycle. + */ + public void onResume() { + mCalled = true; + } + + /** + * Called to ask the fragment to save its current dynamic state, so it + * can later be reconstructed in a new instance of its process is + * restarted. If a new instance of the fragment later needs to be + * created, the data you place in the Bundle here will be available + * in the Bundle given to {@link #onCreate(Bundle)}, + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, and + * {@link #onActivityCreated(Bundle)}. + * + *

This corresponds to {@link Activity#onSaveInstanceState(Bundle) + * Activity.onSaveInstanceState(Bundle)} and most of the discussion there + * applies here as well. Note however: this method may be called + * at any time before {@link #onDestroy()}. There are many situations + * where a fragment may be mostly torn down (such as when placed on the + * back stack with no UI showing), but its state will not be saved until + * its owning activity actually needs to save its state. + * + * @param outState Bundle in which to place your saved state. + */ + public void onSaveInstanceState(Bundle outState) { + } + + public void onConfigurationChanged(Configuration newConfig) { + mCalled = true; + } + + /** + * Called when the Fragment is no longer resumed. This is generally + * tied to {@link Activity#onPause() Activity.onPause} of the containing + * Activity's lifecycle. + */ + public void onPause() { + mCalled = true; + } + + /** + * Called when the Fragment is no longer started. This is generally + * tied to {@link Activity#onStop() Activity.onStop} of the containing + * Activity's lifecycle. + */ + public void onStop() { + mCalled = true; + } + + public void onLowMemory() { + mCalled = true; + } + + /** + * Called when the view previously created by {@link #onCreateView} has + * been detached from the fragment. The next time the fragment needs + * to be displayed, a new view will be created. This is called + * after {@link #onStop()} and before {@link #onDestroy()}. It is called + * regardless of whether {@link #onCreateView} returned a + * non-null view. Internally it is called after the view's state has + * been saved but before it has been removed from its parent. + */ + public void onDestroyView() { + mCalled = true; + } + + /** + * Called when the fragment is no longer in use. This is called + * after {@link #onStop()} and before {@link #onDetach()}. + */ + public void onDestroy() { + mCalled = true; + //Log.v("foo", "onDestroy: mCheckedForLoaderManager=" + mCheckedForLoaderManager + // + " mLoaderManager=" + mLoaderManager); + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mIndex, mStarted, false); + } + if (mLoaderManager != null) { + mLoaderManager.doDestroy(); + } + } + + /** + * Called when the fragment is no longer attached to its activity. This + * is called after {@link #onDestroy()}. + */ + public void onDetach() { + mCalled = true; + } + + /** + * Initialize the contents of the Activity's standard options menu. You + * should place your menu items in to menu. For this method + * to be called, you must have first called {@link #setHasOptionsMenu}. See + * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu} + * for more information. + * + * @param menu The options menu in which you place your items. + * + * @see #setHasOptionsMenu + * @see #onPrepareOptionsMenu + * @see #onOptionsItemSelected + */ + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + } + + /** + * Prepare the Screen's standard options menu to be displayed. This is + * called right before the menu is shown, every time it is shown. You can + * use this method to efficiently enable/disable items or otherwise + * dynamically modify the contents. See + * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu} + * for more information. + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + * + * @see #setHasOptionsMenu + * @see #onCreateOptionsMenu + */ + public void onPrepareOptionsMenu(Menu menu) { + } + + /** + * This hook is called whenever an item in your options menu is selected. + * The default implementation simply returns false to have the normal + * processing happen (calling the item's Runnable or sending a message to + * its Handler as appropriate). You can use this method for any items + * for which you would like to do processing without those other + * facilities. + * + *

Derived classes should call through to the base class for it to + * perform the default menu handling. + * + * @param item The menu item that was selected. + * + * @return boolean Return false to allow normal menu processing to + * proceed, true to consume it here. + * + * @see #onCreateOptionsMenu + */ + public boolean onOptionsItemSelected(MenuItem item) { + return false; + } + + /** + * This hook is called whenever the options menu is being closed (either by the user canceling + * the menu with the back/menu button, or when an item is selected). + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + */ + public void onOptionsMenuClosed(Menu menu) { + } + + /** + * Called when a context menu for the {@code view} is about to be shown. + * Unlike {@link #onCreateOptionsMenu}, this will be called every + * time the context menu is about to be shown and should be populated for + * the view (or item inside the view for {@link AdapterView} subclasses, + * this can be found in the {@code menuInfo})). + *

+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an + * item has been selected. + *

+ * The default implementation calls up to + * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though + * you can not call this implementation if you don't want that behavior. + *

+ * It is not safe to hold onto the context menu after this method returns. + * {@inheritDoc} + */ + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + getActivity().onCreateContextMenu(menu, v, menuInfo); + } + + /** + * Registers a context menu to be shown for the given view (multiple views + * can show the context menu). This method will set the + * {@link OnCreateContextMenuListener} on the view to this fragment, so + * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be + * called when it is time to show the context menu. + * + * @see #unregisterForContextMenu(View) + * @param view The view that should show a context menu. + */ + public void registerForContextMenu(View view) { + view.setOnCreateContextMenuListener(this); + } + + /** + * Prevents a context menu to be shown for the given view. This method will + * remove the {@link OnCreateContextMenuListener} on the view. + * + * @see #registerForContextMenu(View) + * @param view The view that should stop showing a context menu. + */ + public void unregisterForContextMenu(View view) { + view.setOnCreateContextMenuListener(null); + } + + /** + * This hook is called whenever an item in a context menu is selected. The + * default implementation simply returns false to have the normal processing + * happen (calling the item's Runnable or sending a message to its Handler + * as appropriate). You can use this method for any items for which you + * would like to do processing without those other facilities. + *

+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the + * View that added this menu item. + *

+ * Derived classes should call through to the base class for it to perform + * the default menu handling. + * + * @param item The context menu item that was selected. + * @return boolean Return false to allow normal context menu processing to + * proceed, true to consume it here. + */ + public boolean onContextItemSelected(MenuItem item) { + return false; + } + + /** + * Print the Fragments's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + writer.print(prefix); writer.print("mFragmentId="); writer.print(mFragmentId); + writer.print(" mContainerId="); writer.print(mContainerId); + writer.print(" mTag="); writer.println(mTag); + writer.print(prefix); writer.print("mState="); writer.print(mState); + writer.print(" mIndex="); writer.print(mIndex); + writer.print(" mWho="); writer.print(mWho); + writer.print(" mBackStackNesting="); writer.println(mBackStackNesting); + writer.print(prefix); writer.print("mAdded="); writer.print(mAdded); + writer.print(" mResumed="); writer.print(mResumed); + writer.print(" mFromLayout="); writer.print(mFromLayout); + writer.print(" mInLayout="); writer.println(mInLayout); + writer.print(prefix); writer.print("mHidden="); writer.print(mHidden); + writer.print(" mRetainInstance="); writer.print(mRetainInstance); + writer.print(" mRetaining="); writer.print(mRetaining); + writer.print(" mHasMenu="); writer.println(mHasMenu); + if (mFragmentManager != null) { + writer.print(prefix); writer.print("mFragmentManager="); + writer.println(mFragmentManager); + } + if (mImmediateActivity != null) { + writer.print(prefix); writer.print("mImmediateActivity="); + writer.println(mImmediateActivity); + } + if (mActivity != null) { + writer.print(prefix); writer.print("mActivity="); + writer.println(mActivity); + } + if (mArguments != null) { + writer.print(prefix); writer.print("mArguments="); writer.println(mArguments); + } + if (mSavedFragmentState != null) { + writer.print(prefix); writer.print("mSavedFragmentState="); + writer.println(mSavedFragmentState); + } + if (mSavedViewState != null) { + writer.print(prefix); writer.print("mSavedViewState="); + writer.println(mSavedViewState); + } + if (mTarget != null) { + writer.print(prefix); writer.print("mTarget="); writer.print(mTarget); + writer.print(" mTargetRequestCode="); + writer.println(mTargetRequestCode); + } + if (mNextAnim != 0) { + writer.print(prefix); writer.print("mNextAnim="); writer.println(mNextAnim); + } + if (mContainer != null) { + writer.print(prefix); writer.print("mContainer="); writer.println(mContainer); + } + if (mView != null) { + writer.print(prefix); writer.print("mView="); writer.println(mView); + } + if (mLoaderManager != null) { + writer.print(prefix); writer.print("mLoaderManager="); writer.print(mLoaderManager); + writer.print(" mStarted="); writer.print(mStarted); + writer.print(" mCheckedForLoaderManager="); + writer.println(mCheckedForLoaderManager); + } + } + + void performStop() { + onStop(); + if (mStarted) { + mStarted = false; + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mIndex, mStarted, false); + } + if (mLoaderManager != null) { + if (mActivity == null || !mActivity.mChangingConfigurations) { + mLoaderManager.doStop(); + } else { + mLoaderManager.doRetain(); + } + } + } + } +} diff --git a/core/java/android/app/FragmentBreadCrumbs.java b/core/java/android/app/FragmentBreadCrumbs.java new file mode 100644 index 0000000000000000000000000000000000000000..22e074756291a222632fa072c225a9b461658a79 --- /dev/null +++ b/core/java/android/app/FragmentBreadCrumbs.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.app.FragmentManager.BackStackEntry; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * Helper class for showing "bread crumbs" representing the fragment + * stack in an activity. This is intended to be used with + * {@link ActionBar#setCustomNavigationMode(View) + * ActionBar.setCustomNavigationMode(View)} to place the bread crumbs in + * the navigation area of the action bar. + * + *

The default style for this view is + * {@link android.R.style#Widget_FragmentBreadCrumbs}. + */ +public class FragmentBreadCrumbs extends ViewGroup + implements FragmentManager.OnBackStackChangedListener { + Activity mActivity; + LayoutInflater mInflater; + LinearLayout mContainer; + + // Hahah + BackStackRecord mTopEntry; + + public FragmentBreadCrumbs(Context context) { + this(context, null); + } + + public FragmentBreadCrumbs(Context context, AttributeSet attrs) { + this(context, attrs, android.R.style.Widget_FragmentBreadCrumbs); + } + + public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Attach the bread crumbs to their activity. This must be called once + * when creating the bread crumbs. + */ + public void setActivity(Activity a) { + mActivity = a; + mInflater = (LayoutInflater)a.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mContainer = (LinearLayout)mInflater.inflate( + com.android.internal.R.layout.fragment_bread_crumbs, + this, false); + addView(mContainer); + a.getFragmentManager().addOnBackStackChangedListener(this); + updateCrumbs(); + } + + /** + * Set a custom title for the bread crumbs. This will be the first entry + * shown at the left, representing the root of the bread crumbs. If the + * title is null, it will not be shown. + */ + public void setTitle(CharSequence title, CharSequence shortTitle) { + if (title == null) { + mTopEntry = null; + } else { + mTopEntry = new BackStackRecord((FragmentManagerImpl) + mActivity.getFragmentManager()); + mTopEntry.setBreadCrumbTitle(title); + mTopEntry.setBreadCrumbShortTitle(shortTitle); + } + updateCrumbs(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // Eventually we should implement our own layout of the views, + // rather than relying on a linear layout. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + int childRight = mPaddingLeft + child.getMeasuredWidth() - mPaddingRight; + int childBottom = mPaddingTop + child.getMeasuredHeight() - mPaddingBottom; + child.layout(mPaddingLeft, mPaddingTop, childRight, childBottom); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int count = getChildCount(); + + int maxHeight = 0; + int maxWidth = 0; + + // Find rightmost and bottom-most child + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + measureChild(child, widthMeasureSpec, heightMeasureSpec); + maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); + maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); + } + } + + // Account for padding too + maxWidth += mPaddingLeft + mPaddingRight; + maxHeight += mPaddingTop + mPaddingBottom; + + // Check against our minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), + resolveSize(maxHeight, heightMeasureSpec)); + } + + @Override + public void onBackStackChanged() { + updateCrumbs(); + } + + void updateCrumbs() { + FragmentManager fm = mActivity.getFragmentManager(); + int numEntries = fm.countBackStackEntries(); + int numViews = mContainer.getChildCount(); + for (int i = mTopEntry != null ? -1 : 0; i < numEntries; i++) { + BackStackEntry bse = i == -1 ? mTopEntry : fm.getBackStackEntry(i); + int viewI = mTopEntry != null ? i + 1 : i; + if (viewI < numViews) { + View v = mContainer.getChildAt(viewI); + Object tag = v.getTag(); + if (tag != bse) { + for (int j = viewI; j < numViews; j++) { + mContainer.removeViewAt(viewI); + } + numViews = viewI; + } + } + if (viewI >= numViews) { + View item = mInflater.inflate( + com.android.internal.R.layout.fragment_bread_crumb_item, + this, false); + TextView text = (TextView)item.findViewById(com.android.internal.R.id.title); + text.setText(bse.getBreadCrumbTitle()); + item.setTag(bse); + if (viewI == 0) { + text.setCompoundDrawables(null, null, null, null); + } + mContainer.addView(item); + item.setOnClickListener(mOnClickListener); + } + } + int viewI = mTopEntry != null ? numEntries + 1 : numEntries; + numViews = mContainer.getChildCount(); + while (numViews > viewI) { + mContainer.removeViewAt(numViews-1); + numViews--; + } + } + + private OnClickListener mOnClickListener = new OnClickListener() { + public void onClick(View v) { + if (v.getTag() instanceof BackStackEntry) { + BackStackEntry bse = (BackStackEntry) v.getTag(); + mActivity.getFragmentManager().popBackStack(bse.getId(), + bse == mTopEntry? FragmentManager.POP_BACK_STACK_INCLUSIVE : 0); + } + } + }; +} diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java new file mode 100644 index 0000000000000000000000000000000000000000..da7ba6f547b473b47618c15fbb97a52499a8a3a8 --- /dev/null +++ b/core/java/android/app/FragmentManager.java @@ -0,0 +1,1404 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Interface for interacting with {@link Fragment} objects inside of an + * {@link Activity} + */ +public interface FragmentManager { + /** + * Representation of an entry on the fragment back stack, as created + * with {@link FragmentTransaction#addToBackStack(String) + * FragmentTransaction.addToBackStack()}. Entries can later be + * retrieved with {@link FragmentManager#getBackStackEntry(int) + * FragmentManager.getBackStackEntry()}. + * + *

Note that you should never hold on to a BackStackEntry object; + * the identifier as returned by {@link #getId} is the only thing that + * will be persisted across activity instances. + */ + public interface BackStackEntry { + /** + * Return the unique identifier for the entry. This is the only + * representation of the entry that will persist across activity + * instances. + */ + public int getId(); + + /** + * Return the full bread crumb title for the entry, or null if it + * does not have one. + */ + public CharSequence getBreadCrumbTitle(); + + /** + * Return the short bread crumb title for the entry, or null if it + * does not have one. + */ + public CharSequence getBreadCrumbShortTitle(); + } + + /** + * Interface to watch for changes to the back stack. + */ + public interface OnBackStackChangedListener { + /** + * Called whenever the contents of the back stack change. + */ + public void onBackStackChanged(); + } + + /** + * Start a series of edit operations on the Fragments associated with + * this FragmentManager. + */ + public FragmentTransaction openTransaction(); + + /** + * Finds a fragment that was identified by the given id either when inflated + * from XML or as the container ID when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack associated with this ID are searched. + * @return The fragment if found or null otherwise. + */ + public Fragment findFragmentById(int id); + + /** + * Finds a fragment that was identified by the given tag either when inflated + * from XML or as supplied when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack are searched. + * @return The fragment if found or null otherwise. + */ + public Fragment findFragmentByTag(String tag); + + /** + * Flag for {@link #popBackStack(String, int)} + * and {@link #popBackStack(int, int)}: If set, and the name or ID of + * a back stack entry has been supplied, then all matching entries will + * be consumed until one that doesn't match is found or the bottom of + * the stack is reached. Otherwise, all entries up to but not including that entry + * will be removed. + */ + public static final int POP_BACK_STACK_INCLUSIVE = 1<<0; + + /** + * Pop the top state off the back stack. Returns true if there was one + * to pop, else false. + */ + public boolean popBackStack(); + + /** + * Pop the last fragment transition from the manager's fragment + * back stack. If there is nothing to pop, false is returned. + * @param name If non-null, this is the name of a previous back state + * to look for; if found, all states up to that state will be popped. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. If null, only the top state is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public boolean popBackStack(String name, int flags); + + /** + * Pop all back stack states up to the one with the given identifier. + * @param id Identifier of the stated to be popped. If no identifier exists, + * false is returned. + * The identifier is the number returned by + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public boolean popBackStack(int id, int flags); + + /** + * Return the number of entries currently in the back stack. + */ + public int countBackStackEntries(); + + /** + * Return the BackStackEntry at index index in the back stack; + * entries start index 0 being the bottom of the stack. + */ + public BackStackEntry getBackStackEntry(int index); + + /** + * Add a new listener for changes to the fragment back stack. + */ + public void addOnBackStackChangedListener(OnBackStackChangedListener listener); + + /** + * Remove a listener that was previously added with + * {@link #addOnBackStackChangedListener(OnBackStackChangedListener)}. + */ + public void removeOnBackStackChangedListener(OnBackStackChangedListener listener); + + /** + * Put a reference to a fragment in a Bundle. This Bundle can be + * persisted as saved state, and when later restoring + * {@link #getFragment(Bundle, String)} will return the current + * instance of the same fragment. + * + * @param bundle The bundle in which to put the fragment reference. + * @param key The name of the entry in the bundle. + * @param fragment The Fragment whose reference is to be stored. + */ + public void putFragment(Bundle bundle, String key, Fragment fragment); + + /** + * Retrieve the current Fragment instance for a reference previously + * placed with {@link #putFragment(Bundle, String, Fragment)}. + * + * @param bundle The bundle from which to retrieve the fragment reference. + * @param key The name of the entry in the bundle. + * @return Returns the current Fragment instance that is associated with + * the given reference. + */ + public Fragment getFragment(Bundle bundle, String key); + + /** + * Print the FragmentManager's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args); +} + +final class FragmentManagerState implements Parcelable { + FragmentState[] mActive; + int[] mAdded; + BackStackState[] mBackStack; + + public FragmentManagerState() { + } + + public FragmentManagerState(Parcel in) { + mActive = in.createTypedArray(FragmentState.CREATOR); + mAdded = in.createIntArray(); + mBackStack = in.createTypedArray(BackStackState.CREATOR); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedArray(mActive, flags); + dest.writeIntArray(mAdded); + dest.writeTypedArray(mBackStack, flags); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public FragmentManagerState createFromParcel(Parcel in) { + return new FragmentManagerState(in); + } + + public FragmentManagerState[] newArray(int size) { + return new FragmentManagerState[size]; + } + }; +} + +/** + * Container for fragments associated with an activity. + */ +final class FragmentManagerImpl implements FragmentManager { + static final boolean DEBUG = true; + static final String TAG = "FragmentManager"; + + static final String TARGET_REQUEST_CODE_STATE_TAG = "android:target_req_state"; + static final String TARGET_STATE_TAG = "android:target_state"; + static final String VIEW_STATE_TAG = "android:view_state"; + + ArrayList mPendingActions; + Runnable[] mTmpActions; + boolean mExecutingActions; + + ArrayList mActive; + ArrayList mAdded; + ArrayList mAvailIndices; + ArrayList mBackStack; + + // Must be accessed while locked. + ArrayList mBackStackIndices; + ArrayList mAvailBackStackIndices; + + ArrayList mBackStackChangeListeners; + + int mCurState = Fragment.INITIALIZING; + Activity mActivity; + + boolean mNeedMenuInvalidate; + boolean mStateSaved; + + // Temporary vars for state save and restore. + Bundle mStateBundle = null; + SparseArray mStateArray = null; + + Runnable mExecCommit = new Runnable() { + @Override + public void run() { + execPendingActions(); + } + }; + + @Override + public FragmentTransaction openTransaction() { + return new BackStackRecord(this); + } + + @Override + public boolean popBackStack() { + return popBackStackState(mActivity.mHandler, null, -1, 0); + } + + @Override + public boolean popBackStack(String name, int flags) { + return popBackStackState(mActivity.mHandler, name, -1, flags); + } + + @Override + public boolean popBackStack(int id, int flags) { + if (id < 0) { + throw new IllegalArgumentException("Bad id: " + id); + } + return popBackStackState(mActivity.mHandler, null, id, flags); + } + + @Override + public int countBackStackEntries() { + return mBackStack != null ? mBackStack.size() : 0; + } + + @Override + public BackStackEntry getBackStackEntry(int index) { + return mBackStack.get(index); + } + + @Override + public void addOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners == null) { + mBackStackChangeListeners = new ArrayList(); + } + mBackStackChangeListeners.add(listener); + } + + @Override + public void removeOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners != null) { + mBackStackChangeListeners.remove(listener); + } + } + + @Override + public void putFragment(Bundle bundle, String key, Fragment fragment) { + if (fragment.mIndex < 0) { + throw new IllegalStateException("Fragment " + fragment + + " is not currently in the FragmentManager"); + } + bundle.putInt(key, fragment.mIndex); + } + + @Override + public Fragment getFragment(Bundle bundle, String key) { + int index = bundle.getInt(key, -1); + if (index == -1) { + return null; + } + if (index >= mActive.size()) { + throw new IllegalStateException("Fragement no longer exists for key " + + key + ": index " + index); + } + Fragment f = mActive.get(index); + if (f == null) { + throw new IllegalStateException("Fragement no longer exists for key " + + key + ": index " + index); + } + return f; + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (mActive == null || mActive.size() <= 0) { + return; + } + + writer.print(prefix); writer.println("Active Fragments:"); + + String innerPrefix = prefix + " "; + + int N = mActive.size(); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Added Fragments:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Back Stack:"); + for (int i=0; i Fragment.CREATED) { + newState = Fragment.CREATED; + } + + if (f.mState < newState) { + switch (f.mState) { + case Fragment.INITIALIZING: + if (DEBUG) Log.v(TAG, "moveto CREATED: " + f); + if (f.mSavedFragmentState != null) { + f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG); + f.mTarget = getFragment(f.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG); + if (f.mTarget != null) { + f.mTargetRequestCode = f.mSavedFragmentState.getInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0); + } + } + f.mActivity = mActivity; + f.mCalled = false; + f.onAttach(mActivity); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onAttach()"); + } + mActivity.onAttachFragment(f); + + if (!f.mRetaining) { + f.mCalled = false; + f.onCreate(f.mSavedFragmentState); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onCreate()"); + } + } + f.mRetaining = false; + if (f.mFromLayout) { + // For fragments that are part of the content view + // layout, we need to instantiate the view immediately + // and the inflater will take care of adding it. + f.mView = f.onCreateView(mActivity.getLayoutInflater(), + null, f.mSavedFragmentState); + if (f.mView != null) { + f.mView.setSaveFromParentEnabled(false); + f.restoreViewState(); + if (f.mHidden) f.mView.setVisibility(View.GONE); + } + } + case Fragment.CREATED: + if (newState > Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "moveto CONTENT: " + f); + if (!f.mFromLayout) { + ViewGroup container = null; + if (f.mContainerId != 0) { + container = (ViewGroup)mActivity.findViewById(f.mContainerId); + if (container == null) { + throw new IllegalArgumentException("No view found for id 0x" + + Integer.toHexString(f.mContainerId) + + " for fragment " + f); + } + } + f.mContainer = container; + f.mView = f.onCreateView(mActivity.getLayoutInflater(), + container, f.mSavedFragmentState); + if (f.mView != null) { + f.mView.setSaveFromParentEnabled(false); + if (container != null) { + Animator anim = loadAnimator(f, transit, true, + transitionStyle); + if (anim != null) { + anim.setTarget(f.mView); + anim.start(); + } + container.addView(f.mView); + f.restoreViewState(); + } + if (f.mHidden) f.mView.setVisibility(View.GONE); + } + } + + f.mCalled = false; + f.onActivityCreated(f.mSavedFragmentState); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onReady()"); + } + f.mSavedFragmentState = null; + } + case Fragment.ACTIVITY_CREATED: + if (newState > Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); + f.mCalled = false; + f.onStart(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onStart()"); + } + } + case Fragment.STARTED: + if (newState > Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f); + f.mCalled = false; + f.mResumed = true; + f.onResume(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onResume()"); + } + } + } + } else if (f.mState > newState) { + switch (f.mState) { + case Fragment.RESUMED: + if (newState < Fragment.RESUMED) { + if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f); + f.mCalled = false; + f.onPause(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onPause()"); + } + f.mResumed = false; + } + case Fragment.STARTED: + if (newState < Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f); + f.mCalled = false; + f.performStop(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onStop()"); + } + } + case Fragment.ACTIVITY_CREATED: + if (newState < Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "movefrom CONTENT: " + f); + if (f.mView != null) { + // Need to save the current view state if not + // done already. + if (!mActivity.isFinishing() && f.mSavedViewState == null) { + saveFragmentViewState(f); + } + } + f.mCalled = false; + f.onDestroyView(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDestroyedView()"); + } + if (f.mView != null && f.mContainer != null) { + Animator anim = null; + if (mCurState > Fragment.INITIALIZING) { + anim = loadAnimator(f, transit, false, + transitionStyle); + } + if (anim != null) { + final ViewGroup container = f.mContainer; + final View view = f.mView; + container.startViewTransition(view); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator anim) { + container.endViewTransition(view); + } + }); + anim.setTarget(f.mView); + anim.start(); + + } + f.mContainer.removeView(f.mView); + } + f.mContainer = null; + f.mView = null; + } + case Fragment.CREATED: + if (newState < Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f); + if (!f.mRetaining) { + f.mCalled = false; + f.onDestroy(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDestroy()"); + } + } + + f.mCalled = false; + f.onDetach(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDetach()"); + } + f.mImmediateActivity = null; + f.mActivity = null; + } + } + } + + f.mState = newState; + } + + void moveToState(Fragment f) { + moveToState(f, mCurState, 0, 0); + } + + void moveToState(int newState, boolean always) { + moveToState(newState, 0, 0, always); + } + + void moveToState(int newState, int transit, int transitStyle, boolean always) { + if (mActivity == null && newState != Fragment.INITIALIZING) { + throw new IllegalStateException("No activity"); + } + + if (!always && mCurState == newState) { + return; + } + + mCurState = newState; + if (mActive != null) { + for (int i=0; i= 0) { + return; + } + + if (mAvailIndices == null || mAvailIndices.size() <= 0) { + if (mActive == null) { + mActive = new ArrayList(); + } + f.setIndex(mActive.size()); + mActive.add(f); + + } else { + f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1)); + mActive.set(f.mIndex, f); + } + } + + void makeInactive(Fragment f) { + if (f.mIndex < 0) { + return; + } + + if (DEBUG) Log.v(TAG, "Freeing fragment index " + f.mIndex); + mActive.set(f.mIndex, null); + if (mAvailIndices == null) { + mAvailIndices = new ArrayList(); + } + mAvailIndices.add(f.mIndex); + mActivity.invalidateFragmentIndex(f.mIndex); + f.clearIndex(); + } + + public void addFragment(Fragment fragment, boolean moveToStateNow) { + if (mAdded == null) { + mAdded = new ArrayList(); + } + mAdded.add(fragment); + makeActive(fragment); + if (DEBUG) Log.v(TAG, "add: " + fragment); + fragment.mAdded = true; + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + if (moveToStateNow) { + moveToState(fragment); + } + } + + public void removeFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting); + mAdded.remove(fragment); + final boolean inactive = fragment.mBackStackNesting <= 0; + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED, + transition, transitionStyle); + if (inactive) { + makeInactive(fragment); + } + } + + public void hideFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "hide: " + fragment); + if (!fragment.mHidden) { + fragment.mHidden = true; + if (fragment.mView != null) { + Animator anim = loadAnimator(fragment, transition, true, + transitionStyle); + if (anim != null) { + anim.setTarget(fragment.mView); + anim.start(); + } + fragment.mView.setVisibility(View.GONE); + } + if (fragment.mAdded && fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(true); + } + } + + public void showFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "show: " + fragment); + if (fragment.mHidden) { + fragment.mHidden = false; + if (fragment.mView != null) { + Animator anim = loadAnimator(fragment, transition, true, + transitionStyle); + if (anim != null) { + anim.setTarget(fragment.mView); + anim.start(); + } + fragment.mView.setVisibility(View.VISIBLE); + } + if (fragment.mAdded && fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(false); + } + } + + public Fragment findFragmentById(int id) { + if (mActive != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByTag(String tag) { + if (mActive != null && tag != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByWho(String who) { + if (mActive != null && who != null) { + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && who.equals(f.mWho)) { + return f; + } + } + } + return null; + } + + public void enqueueAction(Runnable action) { + if (mStateSaved) { + throw new IllegalStateException( + "Can not perform this action after onSaveInstanceState"); + } + synchronized (this) { + if (mPendingActions == null) { + mPendingActions = new ArrayList(); + } + mPendingActions.add(action); + if (mPendingActions.size() == 1) { + mActivity.mHandler.removeCallbacks(mExecCommit); + mActivity.mHandler.post(mExecCommit); + } + } + } + + public int allocBackStackIndex(BackStackRecord bse) { + synchronized (this) { + if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int index = mBackStackIndices.size(); + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.add(bse); + return index; + + } else { + int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1); + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.set(index, bse); + return index; + } + } + } + + public void setBackStackIndex(int index, BackStackRecord bse) { + synchronized (this) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int N = mBackStackIndices.size(); + if (index < N) { + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.set(index, bse); + } else { + while (N < index) { + mBackStackIndices.add(null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Adding available back stack index " + N); + mAvailBackStackIndices.add(N); + N++; + } + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.add(bse); + } + } + } + + public void freeBackStackIndex(int index) { + synchronized (this) { + mBackStackIndices.set(index, null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Freeing back stack index " + index); + mAvailBackStackIndices.add(index); + } + } + + /** + * Only call from main thread! + */ + public void execPendingActions() { + if (mExecutingActions) { + throw new IllegalStateException("Recursive entry to execPendingActions"); + } + + while (true) { + int numActions; + + synchronized (this) { + if (mPendingActions == null || mPendingActions.size() == 0) { + return; + } + + numActions = mPendingActions.size(); + if (mTmpActions == null || mTmpActions.length < numActions) { + mTmpActions = new Runnable[numActions]; + } + mPendingActions.toArray(mTmpActions); + mPendingActions.clear(); + mActivity.mHandler.removeCallbacks(mExecCommit); + } + + mExecutingActions = true; + for (int i=0; i(); + } + mBackStack.add(state); + reportBackStackChanged(); + } + + boolean popBackStackState(Handler handler, String name, int id, int flags) { + if (mBackStack == null) { + return false; + } + if (name == null && id < 0 && (flags&Activity.POP_BACK_STACK_INCLUSIVE) == 0) { + int last = mBackStack.size()-1; + if (last < 0) { + return false; + } + final BackStackRecord bss = mBackStack.remove(last); + enqueueAction(new Runnable() { + public void run() { + if (DEBUG) Log.v(TAG, "Popping back stack state: " + bss); + bss.popFromBackStack(true); + reportBackStackChanged(); + } + }); + } else { + int index = -1; + if (name != null || id >= 0) { + // If a name or ID is specified, look for that place in + // the stack. + index = mBackStack.size()-1; + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if (name != null && name.equals(bss.getName())) { + break; + } + if (id >= 0 && id == bss.mIndex) { + break; + } + index--; + } + if (index < 0) { + return false; + } + if ((flags&Activity.POP_BACK_STACK_INCLUSIVE) != 0) { + index--; + // Consume all following entries that match. + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if ((name != null && name.equals(bss.getName())) + || (id >= 0 && id == bss.mIndex)) { + index--; + continue; + } + break; + } + } + } + if (index == mBackStack.size()-1) { + return false; + } + final ArrayList states + = new ArrayList(); + for (int i=mBackStack.size()-1; i>index; i--) { + states.add(mBackStack.remove(i)); + } + enqueueAction(new Runnable() { + public void run() { + final int LAST = states.size()-1; + for (int i=0; i<=LAST; i++) { + if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i)); + states.get(i).popFromBackStack(i == LAST); + } + reportBackStackChanged(); + } + }); + } + return true; + } + + ArrayList retainNonConfig() { + ArrayList fragments = null; + if (mActive != null) { + for (int i=0; i(); + } + fragments.add(f); + f.mRetaining = true; + } + } + } + return fragments; + } + + void saveFragmentViewState(Fragment f) { + if (f.mView == null) { + return; + } + if (mStateArray == null) { + mStateArray = new SparseArray(); + } + f.mView.saveHierarchyState(mStateArray); + if (mStateArray.size() > 0) { + f.mSavedViewState = mStateArray; + mStateArray = null; + } + } + + Parcelable saveAllState() { + mStateSaved = true; + + if (mActive == null || mActive.size() <= 0) { + return null; + } + + // First collect all active fragments. + int N = mActive.size(); + FragmentState[] active = new FragmentState[N]; + boolean haveFragments = false; + for (int i=0; i Fragment.INITIALIZING && fs.mSavedFragmentState == null) { + if (mStateBundle == null) { + mStateBundle = new Bundle(); + } + f.onSaveInstanceState(mStateBundle); + if (!mStateBundle.isEmpty()) { + fs.mSavedFragmentState = mStateBundle; + mStateBundle = null; + } + + if (f.mView != null) { + saveFragmentViewState(f); + if (f.mSavedViewState != null) { + if (fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = new Bundle(); + } + fs.mSavedFragmentState.putSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState); + } + } + + if (f.mTarget != null) { + if (fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = new Bundle(); + } + putFragment(fs.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG, f.mTarget); + if (f.mTargetRequestCode != 0) { + fs.mSavedFragmentState.putInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, + f.mTargetRequestCode); + } + } + + } else { + fs.mSavedFragmentState = f.mSavedFragmentState; + } + + if (DEBUG) Log.v(TAG, "Saved state of " + f + ": " + + fs.mSavedFragmentState); + } + } + + if (!haveFragments) { + if (DEBUG) Log.v(TAG, "saveAllState: no fragments!"); + return null; + } + + int[] added = null; + BackStackState[] backStack = null; + + // Build list of currently added fragments. + if (mAdded != null) { + N = mAdded.size(); + if (N > 0) { + added = new int[N]; + for (int i=0; i 0) { + backStack = new BackStackState[N]; + for (int i=0; i nonConfig) { + // If there is no saved state at all, then there can not be + // any nonConfig fragments either, so that is that. + if (state == null) return; + FragmentManagerState fms = (FragmentManagerState)state; + if (fms.mActive == null) return; + + // First re-attach any non-config instances we are retaining back + // to their saved state, so we don't try to instantiate them again. + if (nonConfig != null) { + for (int i=0; i(fms.mActive.length); + if (mAvailIndices != null) { + mAvailIndices.clear(); + } + for (int i=0; i(); + } + if (DEBUG) Log.v(TAG, "restoreAllState: adding avail #" + i); + mAvailIndices.add(i); + } + } + + // Update the target of all retained fragments. + if (nonConfig != null) { + for (int i=0; i(fms.mAdded.length); + for (int i=0; i(fms.mBackStack.length); + for (int i=0; i= 0) { + setBackStackIndex(bse.mIndex, bse); + } + } + } else { + mBackStack = null; + } + } + + public void attachActivity(Activity activity) { + if (mActivity != null) throw new IllegalStateException(); + mActivity = activity; + } + + public void dispatchCreate() { + mStateSaved = false; + moveToState(Fragment.CREATED, false); + } + + public void dispatchActivityCreated() { + mStateSaved = false; + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchStart() { + mStateSaved = false; + moveToState(Fragment.STARTED, false); + } + + public void dispatchResume() { + mStateSaved = false; + moveToState(Fragment.RESUMED, false); + } + + public void dispatchPause() { + moveToState(Fragment.STARTED, false); + } + + public void dispatchStop() { + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchDestroy() { + moveToState(Fragment.INITIALIZING, false); + mActivity = null; + } + + public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) { + boolean show = false; + if (mActive != null) { + for (int i=0; i

+ * ListFragment hosts a {@link android.widget.ListView ListView} object that can + * be bound to different data sources, typically either an array or a Cursor + * holding query results. Binding, screen layout, and row layout are discussed + * in the following sections. + *

+ * Screen Layout + *

+ *

+ * ListFragment has a default layout that consists of a single list view. + * However, if you desire, you can customize the fragment layout by returning + * your own view hierarchy from {@link #onCreateView}. + * To do this, your view hierarchy must contain a ListView object with the + * id "@android:id/list" (or {@link android.R.id#list} if it's in code) + *

+ * Optionally, your view hierarchy can contain another view object of any type to + * display when the list view is empty. This "empty list" notifier must have an + * id "android:empty". Note that when an empty view is present, the list view + * will be hidden when there is no data to display. + *

+ * The following code demonstrates an (ugly) custom list layout. It has a list + * with a green background, and an alternate red "no data" message. + *

+ * + *
+ * <?xml version="1.0" encoding="utf-8"?>
+ * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ *         android:orientation="vertical"
+ *         android:layout_width="match_parent"
+ *         android:layout_height="match_parent"
+ *         android:paddingLeft="8dp"
+ *         android:paddingRight="8dp">
+ *
+ *     <ListView android:id="@id/android:list"
+ *               android:layout_width="match_parent"
+ *               android:layout_height="match_parent"
+ *               android:background="#00FF00"
+ *               android:layout_weight="1"
+ *               android:drawSelectorOnTop="false"/>
+ *
+ *     <TextView android:id="@id/android:empty"
+ *               android:layout_width="match_parent"
+ *               android:layout_height="match_parent"
+ *               android:background="#FF0000"
+ *               android:text="No data"/>
+ * </LinearLayout>
+ * 
+ * + *

+ * Row Layout + *

+ *

+ * You can specify the layout of individual rows in the list. You do this by + * specifying a layout resource in the ListAdapter object hosted by the fragment + * (the ListAdapter binds the ListView to the data; more on this later). + *

+ * A ListAdapter constructor takes a parameter that specifies a layout resource + * for each row. It also has two additional parameters that let you specify + * which data field to associate with which object in the row layout resource. + * These two parameters are typically parallel arrays. + *

+ *

+ * Android provides some standard row layout resources. These are in the + * {@link android.R.layout} class, and have names such as simple_list_item_1, + * simple_list_item_2, and two_line_list_item. The following layout XML is the + * source for the resource two_line_list_item, which displays two data + * fields,one above the other, for each list row. + *

+ * + *
+ * <?xml version="1.0" encoding="utf-8"?>
+ * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ *     android:layout_width="match_parent"
+ *     android:layout_height="wrap_content"
+ *     android:orientation="vertical">
+ *
+ *     <TextView android:id="@+id/text1"
+ *         android:textSize="16sp"
+ *         android:textStyle="bold"
+ *         android:layout_width="match_parent"
+ *         android:layout_height="wrap_content"/>
+ *
+ *     <TextView android:id="@+id/text2"
+ *         android:textSize="16sp"
+ *         android:layout_width="match_parent"
+ *         android:layout_height="wrap_content"/>
+ * </LinearLayout>
+ * 
+ * + *

+ * You must identify the data bound to each TextView object in this layout. The + * syntax for this is discussed in the next section. + *

+ *

+ * Binding to Data + *

+ *

+ * You bind the ListFragment's ListView object to data using a class that + * implements the {@link android.widget.ListAdapter ListAdapter} interface. + * Android provides two standard list adapters: + * {@link android.widget.SimpleAdapter SimpleAdapter} for static data (Maps), + * and {@link android.widget.SimpleCursorAdapter SimpleCursorAdapter} for Cursor + * query results. + *

+ *

+ * You must use + * {@link #setListAdapter(ListAdapter) ListFragment.setListAdapter()} to + * associate the list with an adapter. Do not directly call + * {@link ListView#setAdapter(ListAdapter) ListView.setAdapter()} or else + * important initialization will be skipped. + *

+ * + * @see #setListAdapter + * @see android.widget.ListView + */ +public class ListFragment extends Fragment { + final private Handler mHandler = new Handler(); + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + final private AdapterView.OnItemClickListener mOnClickListener + = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, int position, long id) { + onListItemClick((ListView)parent, v, position, id); + } + }; + + ListAdapter mAdapter; + ListView mList; + View mEmptyView; + TextView mStandardEmptyView; + View mProgressContainer; + View mListContainer; + boolean mSetEmptyText; + boolean mListShown; + + public ListFragment() { + } + + /** + * Provide default implementation to return a simple list view. Subclasses + * can override to replace with their own layout. If doing so, the + * returned view hierarchy must have a ListView whose id + * is {@link android.R.id#list android.R.id.list} and can optionally + * have a sibling view id {@link android.R.id#empty android.R.id.empty} + * that is to be shown when the list is empty. + * + *

If you are overriding this method with your own custom content, + * consider including the standard layout {@link android.R.layout#list_content} + * in your layout file, so that you continue to retain all of the standard + * behavior of ListFragment. In particular, this is currently the only + * way to have the built-in indeterminant progress state be shown. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(com.android.internal.R.layout.list_content, + container, false); + } + + /** + * Attach to list view once Fragment is ready to run. + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ensureList(); + } + + /** + * Detach from list view. + */ + @Override + public void onDestroyView() { + mHandler.removeCallbacks(mRequestFocus); + mList = null; + super.onDestroyView(); + } + + /** + * This method will be called when an item in the list is selected. + * Subclasses should override. Subclasses can call + * getListView().getItemAtPosition(position) if they need to access the + * data associated with the selected item. + * + * @param l The ListView where the click happened + * @param v The view that was clicked within the ListView + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + */ + public void onListItemClick(ListView l, View v, int position, long id) { + } + + /** + * Provide the cursor for the list view. + */ + public void setListAdapter(ListAdapter adapter) { + boolean hadAdapter = mAdapter != null; + mAdapter = adapter; + if (mList != null) { + mList.setAdapter(adapter); + if (!mListShown && !hadAdapter) { + // The list was hidden, and previously didn't have an + // adapter. It is now time to show it. + setListShown(true, getView().getWindowToken() != null); + } + } + } + + /** + * Set the currently selected list item to the specified + * position with the adapter's data + * + * @param position + */ + public void setSelection(int position) { + ensureList(); + mList.setSelection(position); + } + + /** + * Get the position of the currently selected list item. + */ + public int getSelectedItemPosition() { + ensureList(); + return mList.getSelectedItemPosition(); + } + + /** + * Get the cursor row ID of the currently selected list item. + */ + public long getSelectedItemId() { + ensureList(); + return mList.getSelectedItemId(); + } + + /** + * Get the activity's list view widget. + */ + public ListView getListView() { + ensureList(); + return mList; + } + + /** + * The default content for a ListFragment has a TextView that can + * be shown when the list is empty. If you would like to have it + * shown, call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ensureList(); + if (mStandardEmptyView == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + mStandardEmptyView.setText(text); + if (!mSetEmptyText) { + mList.setEmptyView(mStandardEmptyView); + mSetEmptyText = true; + } + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + *

Applications do not normally need to use this themselves. The default + * behavior of ListFragment is to start with the list not being shown, only + * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}. + * If the list at that point had not been shown, when it does get shown + * it will be do without the user ever seeing the hidden state. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + */ + public void setListShown(boolean shown) { + setListShown(shown, true); + } + + /** + * Like {@link #setListShown(boolean)}, but no animation is used when + * transitioning from the previous state. + */ + public void setListShownNoAnimation(boolean shown) { + setListShown(shown, false); + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + * @param animate If true, an animation will be used to transition to the + * new state. + */ + private void setListShown(boolean shown, boolean animate) { + ensureList(); + if (mProgressContainer == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (mListShown == shown) { + return; + } + mListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.GONE); + } + } + + /** + * Get the ListAdapter associated with this activity's ListView. + */ + public ListAdapter getListAdapter() { + return mAdapter; + } + + private void ensureList() { + if (mList != null) { + return; + } + View root = getView(); + if (root == null) { + throw new IllegalStateException("Content view not yet created"); + } + if (root instanceof ListView) { + mList = (ListView)root; + } else { + mStandardEmptyView = (TextView)root.findViewById( + com.android.internal.R.id.internalEmpty); + if (mStandardEmptyView == null) { + mEmptyView = root.findViewById(android.R.id.empty); + } + mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer); + mListContainer = root.findViewById(com.android.internal.R.id.listContainer); + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mList = (ListView)rawListView; + if (mList == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + if (mEmptyView != null) { + mList.setEmptyView(mEmptyView); + } + } + mListShown = true; + mList.setOnItemClickListener(mOnClickListener); + if (mAdapter != null) { + setListAdapter(mAdapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (mProgressContainer != null) { + setListShown(false, false); + } + } + mHandler.post(mRequestFocus); + } +} diff --git a/core/java/android/app/LoaderManager.java b/core/java/android/app/LoaderManager.java new file mode 100644 index 0000000000000000000000000000000000000000..28abcaa7735f14242c1fde6afd06e1057c7be21a --- /dev/null +++ b/core/java/android/app/LoaderManager.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.content.Loader; +import android.os.Bundle; +import android.util.Log; +import android.util.SparseArray; + +/** + * Interface associated with an {@link Activity} or {@link Fragment} for managing + * one or more {@link android.content.Loader} instances associated with it. + */ +public interface LoaderManager { + /** + * Callback interface for a client to interact with the manager. + */ + public interface LoaderCallbacks { + /** + * Instantiate and return a new Loader for the given ID. + * + * @param id The ID whose loader is to be created. + * @param args Any arguments supplied by the caller. + * @return Return a new Loader instance that is ready to start loading. + */ + public Loader onCreateLoader(int id, Bundle args); + + /** + * Called when a previously created loader has finished its load. + * @param loader The Loader that has finished. + * @param data The data generated by the Loader. + */ + public void onLoadFinished(Loader loader, D data); + } + + /** + * Ensures a loader is initialized and active. If the loader doesn't + * already exist, one is created and (if the activity/fragment is currently + * started) starts the loader. Otherwise the last created + * loader is re-used. + * + *

In either case, the given callback is associated with the loader, and + * will be called as the loader state changes. If at the point of call + * the caller is in its started state, and the requested loader + * already exists and has generated its data, then + * callback. {@link LoaderCallbacks#onLoadFinished} will + * be called immediately (inside of this function), so you must be prepared + * for this to happen. + */ + public Loader initLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback); + + /** + * Creates a new loader in this manager, registers the callbacks to it, + * and (if the activity/fragment is currently started) starts loading it. + * If a loader with the same id has previously been + * started it will automatically be destroyed when the new loader completes + * its work. The callback will be delivered before the old loader + * is destroyed. + */ + public Loader restartLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback); + + /** + * Stops and removes the loader with the given ID. + */ + public void stopLoader(int id); + + /** + * Return the Loader with the given id or null if no matching Loader + * is found. + */ + public Loader getLoader(int id); +} + +class LoaderManagerImpl implements LoaderManager { + static final String TAG = "LoaderManagerImpl"; + static final boolean DEBUG = true; + + // These are the currently active loaders. A loader is here + // from the time its load is started until it has been explicitly + // stopped or restarted by the application. + final SparseArray mLoaders = new SparseArray(); + + // These are previously run loaders. This list is maintained internally + // to avoid destroying a loader while an application is still using it. + // It allows an application to restart a loader, but continue using its + // previously run loader until the new loader's data is available. + final SparseArray mInactiveLoaders = new SparseArray(); + + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + + final class LoaderInfo implements Loader.OnLoadCompleteListener { + final int mId; + final Bundle mArgs; + LoaderManager.LoaderCallbacks mCallbacks; + Loader mLoader; + Object mData; + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + boolean mDestroyed; + boolean mListenerRegistered; + + public LoaderInfo(int id, Bundle args, LoaderManager.LoaderCallbacks callbacks) { + mId = id; + mArgs = args; + mCallbacks = callbacks; + } + + void start() { + if (mRetaining && mRetainingStarted) { + // Our owner is started, but we were being retained from a + // previous instance in the started state... so there is really + // nothing to do here, since the loaders are still started. + mStarted = true; + return; + } + + if (mStarted) { + // If loader already started, don't restart. + return; + } + + if (DEBUG) Log.v(TAG, " Starting: " + this); + if (mLoader == null && mCallbacks != null) { + mLoader = mCallbacks.onCreateLoader(mId, mArgs); + } + if (mLoader != null) { + if (!mListenerRegistered) { + mLoader.registerListener(mId, this); + mListenerRegistered = true; + } + mLoader.startLoading(); + mStarted = true; + } + } + + void retain() { + if (DEBUG) Log.v(TAG, " Retaining: " + this); + mRetaining = true; + mRetainingStarted = mStarted; + mStarted = false; + mCallbacks = null; + } + + void finishRetain() { + if (mRetaining) { + if (DEBUG) Log.v(TAG, " Finished Retaining: " + this); + mRetaining = false; + if (mStarted != mRetainingStarted) { + if (!mStarted) { + // This loader was retained in a started state, but + // at the end of retaining everything our owner is + // no longer started... so make it stop. + stop(); + } + } + if (mStarted && mData != null && mCallbacks != null) { + // This loader was retained, and now at the point of + // finishing the retain we find we remain started, have + // our data, and the owner has a new callback... so + // let's deliver the data now. + mCallbacks.onLoadFinished(mLoader, mData); + } + } + } + + void stop() { + if (DEBUG) Log.v(TAG, " Stopping: " + this); + mStarted = false; + if (!mRetaining) { + if (mLoader != null && mListenerRegistered) { + // Let the loader know we're done with it + mListenerRegistered = false; + mLoader.unregisterListener(this); + mLoader.stopLoading(); + } + mData = null; + } + } + + void destroy() { + if (DEBUG) Log.v(TAG, " Destroying: " + this); + mDestroyed = true; + mCallbacks = null; + if (mLoader != null) { + if (mListenerRegistered) { + mListenerRegistered = false; + mLoader.unregisterListener(this); + } + mLoader.destroy(); + } + } + + @Override public void onLoadComplete(Loader loader, Object data) { + if (DEBUG) Log.v(TAG, "onLoadComplete: " + this + " mDestroyed=" + mDestroyed); + + if (mDestroyed) { + return; + } + + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + mData = data; + if (mCallbacks != null) { + mCallbacks.onLoadFinished(loader, data); + } + + if (DEBUG) Log.v(TAG, "onLoadFinished returned: " + this); + + // We have now given the application the new loader with its + // loaded data, so it should have stopped using the previous + // loader. If there is a previous loader on the inactive list, + // clean it up. + LoaderInfo info = mInactiveLoaders.get(mId); + if (info != null && info != this) { + info.destroy(); + mInactiveLoaders.remove(mId); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("LoaderInfo{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" #"); + sb.append(mId); + if (mArgs != null) { + sb.append(" "); + sb.append(mArgs.toString()); + } + sb.append("}"); + return sb.toString(); + } + } + + LoaderManagerImpl(boolean started) { + mStarted = started; + } + + private LoaderInfo createLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback) { + LoaderInfo info = new LoaderInfo(id, args, (LoaderManager.LoaderCallbacks)callback); + mLoaders.put(id, info); + Loader loader = callback.onCreateLoader(id, args); + info.mLoader = (Loader)loader; + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), + // so only start them here if we're past that point of the activitiy's + // life cycle + info.start(); + } + return info; + } + + @SuppressWarnings("unchecked") + public Loader initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) { + LoaderInfo info = mLoaders.get(id); + + if (DEBUG) Log.v(TAG, "initLoader in " + this + ": cur=" + info); + + if (info == null) { + // Loader doesn't already exist; create. + info = createLoader(id, args, (LoaderManager.LoaderCallbacks)callback); + } else { + info.mCallbacks = (LoaderManager.LoaderCallbacks)callback; + } + + if (info.mData != null && mStarted) { + // If the loader has already generated its data, report it now. + info.mCallbacks.onLoadFinished(info.mLoader, info.mData); + } + + return (Loader)info.mLoader; + } + + @SuppressWarnings("unchecked") + public Loader restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) { + LoaderInfo info = mLoaders.get(id); + if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": cur=" + info); + if (info != null) { + LoaderInfo inactive = mInactiveLoaders.get(id); + if (inactive != null) { + if (info.mData != null) { + // This loader now has data... we are probably being + // called from within onLoadComplete, where we haven't + // yet destroyed the last inactive loader. So just do + // that now. + if (DEBUG) Log.v(TAG, " Removing last inactive loader in " + this); + inactive.destroy(); + mInactiveLoaders.put(id, info); + } else { + // We already have an inactive loader for this ID that we are + // waiting for! Now we have three active loaders... let's just + // drop the one in the middle, since we are still waiting for + // its result but that result is already out of date. + if (DEBUG) Log.v(TAG, " Removing intermediate loader in " + this); + info.destroy(); + } + } else { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + if (DEBUG) Log.v(TAG, " Making inactive: " + info); + mInactiveLoaders.put(id, info); + } + } + + info = createLoader(id, args, (LoaderManager.LoaderCallbacks)callback); + return (Loader)info.mLoader; + } + + public void stopLoader(int id) { + if (DEBUG) Log.v(TAG, "stopLoader in " + this + " of " + id); + int idx = mLoaders.indexOfKey(id); + if (idx >= 0) { + LoaderInfo info = mLoaders.valueAt(idx); + mLoaders.removeAt(idx); + info.destroy(); + } + } + + @SuppressWarnings("unchecked") + public Loader getLoader(int id) { + LoaderInfo loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + return (Loader)mLoaders.get(id).mLoader; + } + return null; + } + + void doStart() { + if (DEBUG) Log.v(TAG, "Starting: " + this); + + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).start(); + } + mStarted = true; + } + + void doStop() { + if (DEBUG) Log.v(TAG, "Stopping: " + this); + + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).stop(); + } + mStarted = false; + } + + void doRetain() { + if (DEBUG) Log.v(TAG, "Retaining: " + this); + + mRetaining = true; + mStarted = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).retain(); + } + } + + void finishRetain() { + if (DEBUG) Log.v(TAG, "Finished Retaining: " + this); + + mRetaining = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).finishRetain(); + } + } + + void doDestroy() { + if (!mRetaining) { + if (DEBUG) Log.v(TAG, "Destroying Active: " + this); + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).destroy(); + } + } + + if (DEBUG) Log.v(TAG, "Destroying Inactive: " + this); + for (int i = mInactiveLoaders.size()-1; i >= 0; i--) { + mInactiveLoaders.valueAt(i).destroy(); + } + mInactiveLoaders.clear(); + } +} diff --git a/core/java/android/app/LoaderManagingFragment.java b/core/java/android/app/LoaderManagingFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..af7117009450b64d5f37ca0a1130a23efbf5c786 --- /dev/null +++ b/core/java/android/app/LoaderManagingFragment.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.content.Loader; +import android.os.Bundle; + +import java.util.HashMap; + +/** + * A Fragment that has utility methods for managing {@link Loader}s. + * + * @param The type of data returned by the Loader. If you're using multiple Loaders with + * different return types use Object and case the results. + */ +public abstract class LoaderManagingFragment extends Fragment + implements Loader.OnLoadCompleteListener { + private boolean mStarted = false; + + static final class LoaderInfo { + public Bundle args; + public Loader loader; + } + private HashMap> mLoaders; + private HashMap> mInactiveLoaders; + + /** + * Registers a loader with this activity, registers the callbacks on it, and starts it loading. + * If a loader with the same id has previously been started it will automatically be destroyed + * when the new loader completes it's work. The callback will be delivered before the old loader + * is destroyed. + */ + public Loader startLoading(int id, Bundle args) { + LoaderInfo info = mLoaders.get(id); + if (info != null) { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + mInactiveLoaders.put(id, info); + } + info = new LoaderInfo(); + info.args = args; + mLoaders.put(id, info); + Loader loader = onCreateLoader(id, args); + info.loader = loader; + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), so only start them + // here if we're past that point of the activitiy's life cycle + loader.registerListener(id, this); + loader.startLoading(); + } + return loader; + } + + protected abstract Loader onCreateLoader(int id, Bundle args); + protected abstract void onInitializeLoaders(); + protected abstract void onLoadFinished(Loader loader, D data); + + public final void onLoadComplete(Loader loader, D data) { + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + onLoadFinished(loader, data); + + // Look for an inactive loader and destroy it if found + int id = loader.getId(); + LoaderInfo info = mInactiveLoaders.get(id); + if (info != null) { + Loader oldLoader = info.loader; + if (oldLoader != null) { + oldLoader.destroy(); + } + mInactiveLoaders.remove(id); + } + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + + if (mLoaders == null) { + // Look for a passed along loader and create a new one if it's not there +// TODO: uncomment once getLastNonConfigurationInstance method is available +// mLoaders = (HashMap) getLastNonConfigurationInstance(); + if (mLoaders == null) { + mLoaders = new HashMap>(); + onInitializeLoaders(); + } + } + if (mInactiveLoaders == null) { + mInactiveLoaders = new HashMap>(); + } + } + + @Override + public void onStart() { + super.onStart(); + + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (HashMap.Entry> entry : mLoaders.entrySet()) { + LoaderInfo info = entry.getValue(); + Loader loader = info.loader; + int id = entry.getKey(); + if (loader == null) { + loader = onCreateLoader(id, info.args); + info.loader = loader; + } + loader.registerListener(id, this); + loader.startLoading(); + } + + mStarted = true; + } + + @Override + public void onStop() { + super.onStop(); + + for (HashMap.Entry> entry : mLoaders.entrySet()) { + LoaderInfo info = entry.getValue(); + Loader loader = info.loader; + if (loader == null) { + continue; + } + + // Let the loader know we're done with it + loader.unregisterListener(this); + + // The loader isn't getting passed along to the next instance so ask it to stop loading + if (!getActivity().isChangingConfigurations()) { + loader.stopLoading(); + } + } + + mStarted = false; + } + + /* TO DO: This needs to be turned into a retained fragment. + @Override + public Object onRetainNonConfigurationInstance() { + // Pass the loader along to the next guy + Object result = mLoaders; + mLoaders = null; + return result; + } + **/ + + @Override + public void onDestroy() { + super.onDestroy(); + + if (mLoaders != null) { + for (HashMap.Entry> entry : mLoaders.entrySet()) { + LoaderInfo info = entry.getValue(); + Loader loader = info.loader; + if (loader == null) { + continue; + } + loader.destroy(); + } + } + } + + /** + * Stops and removes the loader with the given ID. + */ + public void stopLoading(int id) { + if (mLoaders != null) { + LoaderInfo info = mLoaders.remove(id); + if (info != null) { + Loader loader = info.loader; + if (loader != null) { + loader.unregisterListener(this); + loader.destroy(); + } + } + } + } + + /** + * @return the Loader with the given id or null if no matching Loader + * is found. + */ + public Loader getLoader(int id) { + LoaderInfo loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + return mLoaders.get(id).loader; + } + return null; + } +} diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java index a24fcae26639cc48854f63b4a76f605821868faa..524de6fc3fa1e81ed427da15f31afb779f0e3eb3 100644 --- a/core/java/android/app/LocalActivityManager.java +++ b/core/java/android/app/LocalActivityManager.java @@ -20,13 +20,11 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Binder; import android.os.Bundle; -import android.util.Config; import android.util.Log; import android.view.Window; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; /** @@ -38,7 +36,7 @@ import java.util.Map; */ public class LocalActivityManager { private static final String TAG = "LocalActivityManager"; - private static final boolean localLOGV = false || Config.LOGV; + private static final boolean localLOGV = false; // Internal token for an Activity being managed by LocalActivityManager. private static class LocalActivityRecord extends Binder { @@ -112,11 +110,16 @@ public class LocalActivityManager { if (r.curState == INITIALIZING) { // Get the lastNonConfigurationInstance for the activity - HashMap lastNonConfigurationInstances = - mParent.getLastNonConfigurationChildInstances(); - Object instance = null; + HashMap lastNonConfigurationInstances = + mParent.getLastNonConfigurationChildInstances(); + Object instanceObj = null; if (lastNonConfigurationInstances != null) { - instance = lastNonConfigurationInstances.get(r.id); + instanceObj = lastNonConfigurationInstances.get(r.id); + } + Activity.NonConfigurationInstances instance = null; + if (instanceObj != null) { + instance = new Activity.NonConfigurationInstances(); + instance.activity = instanceObj; } // We need to have always created the activity. @@ -346,7 +349,7 @@ public class LocalActivityManager { } private Window performDestroy(LocalActivityRecord r, boolean finish) { - Window win = null; + Window win; win = r.window; if (r.curState == RESUMED && !finish) { performPause(r, finish); @@ -380,7 +383,8 @@ public class LocalActivityManager { if (r != null) { win = performDestroy(r, finish); if (finish) { - mActivities.remove(r); + mActivities.remove(id); + mActivityArray.remove(r); } } return win; @@ -441,10 +445,8 @@ public class LocalActivityManager { */ public void dispatchCreate(Bundle state) { if (state != null) { - final Iterator i = state.keySet().iterator(); - while (i.hasNext()) { + for (String id : state.keySet()) { try { - final String id = i.next(); final Bundle astate = state.getBundle(id); LocalActivityRecord r = mActivities.get(id); if (r != null) { @@ -457,9 +459,7 @@ public class LocalActivityManager { } } catch (Exception e) { // Recover from -all- app errors. - Log.e(TAG, - "Exception thrown when restoring LocalActivityManager state", - e); + Log.e(TAG, "Exception thrown when restoring LocalActivityManager state", e); } } } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 856943d5f380623f44abf158f5e5b8dd6410b89f..e602518d3106c8e8e9a184bb364cc4e9e0c9045d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -21,6 +21,7 @@ import java.util.Date; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.media.AudioManager; import android.net.Uri; import android.os.Parcel; @@ -116,15 +117,54 @@ public class Notification implements Parcelable * If this facility is used for something else, please give the user an option * to turn it off and use a normal notification, as this can be extremely * disruptive. + * + *

Use with {@link #FLAG_HIGH_PRIORITY} to ensure that this notification + * will reach the user even when other notifications are suppressed. */ public PendingIntent fullScreenIntent; /** * Text to scroll across the screen when this item is added to - * the status bar. + * the status bar on large and smaller devices. + * + *

This field is provided separately from the other ticker fields + * both for compatibility and to allow an application to choose different + * text for when the text scrolls in and when it is displayed all at once + * in conjunction with one or more icons. + * + * @see #tickerTitle + * @see #tickerSubtitle + * @see #tickerIcons */ public CharSequence tickerText; + /** + * The title line for the ticker over a the fat status bar on xlarge devices. + * + * @see #tickerText + * @see #tickerSubtitle + * @see #tickerIcons + */ + public CharSequence tickerTitle; + + /** + * The subtitle line for the ticker over a the fat status bar on xlarge devices. + * + * @see #tickerText + * @see #tickerTitle + * @see #tickerIcons + */ + public CharSequence tickerSubtitle; + + /** + * The icons to show to the left of the other ticker fields. + * + * @see #tickerText + * @see #tickerTitle + * @see #tickerSubtitle + */ + public Bitmap[] tickerIcons; + /** * The view that will represent this notification in the expanded status bar. */ @@ -275,6 +315,14 @@ public class Notification implements Parcelable */ public static final int FLAG_FOREGROUND_SERVICE = 0x00000040; + /** + * Bit to be bitwise-ored into the {@link #flags} field that should be set if this notification + * represents a high-priority event that may be shown to the user even if notifications are + * otherwise unavailable (that is, when the status bar is hidden). This flag is ideally used + * in conjunction with {@link #fullScreenIntent}. + */ + public static final int FLAG_HIGH_PRIORITY = 0x00000080; + public int flags; /** @@ -335,6 +383,21 @@ public class Notification implements Parcelable if (parcel.readInt() != 0) { tickerText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); } + if (parcel.readInt() != 0) { + tickerTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + } + if (parcel.readInt() != 0) { + tickerSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + } + final int tickerIconCount = parcel.readInt(); + if (tickerIconCount >= 0) { + tickerIcons = new Bitmap[tickerIconCount]; + for (int i=0; inull. * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or null. * @param query Intent query, or null. - * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or null. * @param actionKey The key code of the action key that was pressed, * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. * @param actionMsg The message for the action key that was pressed, @@ -1249,7 +1239,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * @return The intent. */ private Intent createIntent(String action, Uri data, String extraData, String query, - String componentName, int actionKey, String actionMsg) { + int actionKey, String actionMsg) { // Now build the Intent Intent intent = new Intent(action); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 2e9cd96f2695b8b9478fa005dc148cfea61e98b9..671501255e28b70499589f88458da6ea262dccd7 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -16,16 +16,12 @@ package android.app; -import android.Manifest; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; @@ -40,7 +36,7 @@ import java.util.List; /** * This class provides access to the system search services. - * + * *

In practice, you won't interact with this class directly, as search * services are provided through methods in {@link android.app.Activity Activity} * and the {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} @@ -56,7 +52,7 @@ import java.util.List; * href="{@docRoot}guide/topics/search/index.html">Search.

* */ -public class SearchManager +public class SearchManager implements DialogInterface.OnDismissListener, DialogInterface.OnCancelListener { @@ -65,20 +61,20 @@ public class SearchManager /** * This is a shortcut definition for the default menu key to use for invoking search. - * + * * See Menu.Item.setAlphabeticShortcut() for more information. */ public final static char MENU_KEY = 's'; /** * This is a shortcut definition for the default menu key to use for invoking search. - * + * * See Menu.Item.setAlphabeticShortcut() for more information. */ public final static int MENU_KEYCODE = KeyEvent.KEYCODE_S; /** - * Intent extra data key: Use this key with + * Intent extra data key: Use this key with * {@link android.content.Intent#getStringExtra * content.Intent.getStringExtra()} * to obtain the query string from Intent.ACTION_SEARCH. @@ -103,7 +99,7 @@ public class SearchManager * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getBundleExtra * content.Intent.getBundleExtra()} - * to obtain any additional app-specific data that was inserted by the + * to obtain any additional app-specific data that was inserted by the * activity that launched the search. */ public final static String APP_DATA = "app_data"; @@ -127,7 +123,7 @@ public class SearchManager * file. */ public final static String ACTION_KEY = "action_key"; - + /** * Intent extra data key: This key will be used for the extra populated by the * {@link #SUGGEST_COLUMN_INTENT_EXTRA_DATA} column. @@ -153,11 +149,19 @@ public class SearchManager * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()} * to obtain the action message that was defined for a particular search action key and/or - * suggestion. It will be null if the search was launched by typing "enter", touched the the - * "GO" button, or other means not involving any action key. + * suggestion. It will be null if the search was launched by typing "enter", touched the the + * "GO" button, or other means not involving any action key. */ public final static String ACTION_MSG = "action_msg"; - + + /** + * Flag to specify that the entry can be used for query refinement, i.e., the query text + * in the search field can be replaced with the text in this entry, when a query refinement + * icon is clicked. The suggestion list should show such a clickable icon beside the entry. + *

Use this flag as a bit-field for {@link #SUGGEST_COLUMN_FLAGS}. + */ + public final static int FLAG_QUERY_REFINEMENT = 1 << 0; + /** * Uri path for queried suggestions data. This is the path that the search manager * will use when querying your content provider for suggestions data based on user input @@ -182,12 +186,12 @@ public class SearchManager * @see #SUGGEST_COLUMN_SHORTCUT_ID */ public final static String SUGGEST_URI_PATH_SHORTCUT = "search_suggest_shortcut"; - + /** * MIME type for shortcut validation. You'll use this in your suggestions content provider * in the getType() function. */ - public final static String SHORTCUT_MIME_TYPE = + public final static String SHORTCUT_MIME_TYPE = "vnd.android.cursor.item/vnd.android.search.suggest"; /** @@ -195,7 +199,7 @@ public class SearchManager */ public final static String SUGGEST_COLUMN_FORMAT = "suggest_format"; /** - * Column name for suggestions cursor. Required. This is the primary line of text that + * Column name for suggestions cursor. Required. This is the primary line of text that * will be presented to the user as the suggestion. */ public final static String SUGGEST_COLUMN_TEXT_1 = "suggest_text_1"; @@ -227,8 +231,8 @@ public class SearchManager *

  • file ({@link android.content.ContentResolver#SCHEME_FILE})
  • * * - * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} - * for more information on these schemes. + * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} + * for more information on these schemes. */ public final static String SUGGEST_COLUMN_ICON_1 = "suggest_icon_1"; /** @@ -243,8 +247,8 @@ public class SearchManager *
  • file ({@link android.content.ContentResolver#SCHEME_FILE})
  • * * - * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} - * for more information on these schemes. + * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} + * for more information on these schemes. */ public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2"; /** @@ -275,12 +279,6 @@ public class SearchManager * an extra under the key {@link #EXTRA_DATA_KEY}. */ public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data"; - /** - * TODO: Remove - * - * @hide - */ - public final static String SUGGEST_COLUMN_INTENT_COMPONENT_NAME = "suggest_intent_component"; /** * Column name for suggestions cursor. Optional. If this column exists and * this element exists at the given row, then "/" and this value will be appended to the data @@ -289,8 +287,8 @@ public class SearchManager */ public final static String SUGGEST_COLUMN_INTENT_DATA_ID = "suggest_intent_data_id"; /** - * Column name for suggestions cursor. Required if action is - * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}, optional otherwise. If this + * Column name for suggestions cursor. Required if action is + * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}, optional otherwise. If this * column exists and this element exists at the given row, this is the data that will be * used when forming the suggestion's query. */ @@ -306,15 +304,6 @@ public class SearchManager */ public final static String SUGGEST_COLUMN_SHORTCUT_ID = "suggest_shortcut_id"; - /** - * Column name for suggestions cursor. Optional. This column is used to specify the - * cursor item's background color if it needs a non-default background color. A non-zero value - * indicates a valid background color to override the default. - * - * @hide For internal use, not part of the public API. - */ - public final static String SUGGEST_COLUMN_BACKGROUND_COLOR = "suggest_background_color"; - /** * Column name for suggestions cursor. Optional. This column is used to specify * that a spinner should be shown in lieu of an icon2 while the shortcut of this suggestion @@ -323,6 +312,15 @@ public class SearchManager public final static String SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING = "suggest_spinner_while_refreshing"; + /** + * Column name for suggestions cursor. Optional. This column is used to specify + * additional flags per item. Multiple flags can be specified. + *

    + * Must be one of {@link #FLAG_QUERY_REFINEMENT} or 0 to indicate no flags. + *

    + */ + public final static String SUGGEST_COLUMN_FLAGS = "suggest_flags"; + /** * Column value for suggestion column {@link #SUGGEST_COLUMN_SHORTCUT_ID} when a suggestion * should not be stored as a shortcut in global search. @@ -343,16 +341,16 @@ public class SearchManager * {@link #EXTRA_SELECT_QUERY}, * {@link #APP_DATA}. */ - public final static String INTENT_ACTION_GLOBAL_SEARCH + public final static String INTENT_ACTION_GLOBAL_SEARCH = "android.search.action.GLOBAL_SEARCH"; - + /** * Intent action for starting the global search settings activity. * The global search provider should handle this intent. */ - public final static String INTENT_ACTION_SEARCH_SETTINGS + public final static String INTENT_ACTION_SEARCH_SETTINGS = "android.search.action.SEARCH_SETTINGS"; - + /** * Intent action for starting a web search provider's settings activity. * Web search providers should handle this intent if they have provider-specific @@ -368,7 +366,7 @@ public class SearchManager */ public final static String INTENT_ACTION_SEARCHABLES_CHANGED = "android.search.action.SEARCHABLES_CHANGED"; - + /** * Intent action broadcasted to inform that the search settings have changed in some way. * Either searchables have been enabled or disabled, or a different web search provider @@ -377,14 +375,6 @@ public class SearchManager public final static String INTENT_ACTION_SEARCH_SETTINGS_CHANGED = "android.search.action.SETTINGS_CHANGED"; - /** - * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION}, - * the search dialog will take no action. - * - * @hide - */ - public final static String INTENT_ACTION_NONE = "android.search.action.ZILCH"; - /** * This means that context is voice, and therefore the SearchDialog should * continue showing the microphone until the user indicates that he/she does @@ -413,7 +403,7 @@ public class SearchManager * 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; /* package */ OnDismissListener mDismissListener = null; @@ -427,15 +417,15 @@ public class SearchManager mService = ISearchManager.Stub.asInterface( ServiceManager.getService(Context.SEARCH_SERVICE)); } - + /** * Launch search UI. * *

    The search manager will open a search widget in an overlapping - * window, and the underlying activity may be obscured. The search + * window, and the underlying activity may be obscured. The search * entry state will remain in effect until one of the following events: *

      - *
    • The user completes the search. In most cases this will launch + *
    • The user completes the search. In most cases this will launch * a search intent.
    • *
    • The user uses the back, home, or other keys to exit the search.
    • *
    • The application calls the {@link #stopSearch} @@ -443,8 +433,8 @@ public class SearchManager * activity from which it was launched.
    • * *

      Most applications will not use this interface to invoke search. - * The primary method for invoking search is to call - * {@link android.app.Activity#onSearchRequested Activity.onSearchRequested()} or + * The primary method for invoking search is to call + * {@link android.app.Activity#onSearchRequested Activity.onSearchRequested()} or * {@link android.app.Activity#startSearch Activity.startSearch()}. * * @param initialQuery A search string can be pre-entered here, but this @@ -456,19 +446,19 @@ public class SearchManager * and the user would expect to be able to keep typing. This parameter is only meaningful * if initialQuery is a non-empty string. * @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 + * @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. * @param globalSearch If false, this will only launch the search that has been specifically - * defined by the application (which is usually defined as a local search). If no default + * defined by the application (which is usually defined as a local search). If no default * search is defined in the current application or activity, global search will be launched. * If true, this will always launch a platform-global (e.g. web-based) search instead. - * + * * @see android.app.Activity#onSearchRequested * @see #stopSearch */ - public void startSearch(String initialQuery, + public void startSearch(String initialQuery, boolean selectInitialQuery, ComponentName launchActivity, Bundle appSearchData, @@ -595,7 +585,7 @@ public class SearchManager *

      Typically the user will terminate the search UI by launching a * search or by canceling. This function allows the underlying application * or activity to cancel the search prematurely (for any reason). - * + * *

      This function can be safely called at any time (even if no search is active.) * * @see #startSearch @@ -607,12 +597,12 @@ public class SearchManager } /** - * Determine if the Search UI is currently displayed. - * + * Determine if the Search UI is currently displayed. + * * This is provided primarily for application test purposes. * * @return Returns true if the search UI is currently displayed. - * + * * @hide */ public boolean isVisible() { @@ -631,7 +621,7 @@ public class SearchManager */ public void onDismiss(); } - + /** * See {@link SearchManager#setOnCancelListener} for configuring your activity to monitor * search UI state. @@ -647,7 +637,7 @@ public class SearchManager /** * Set or clear the callback that will be invoked whenever the search UI is dismissed. - * + * * @param listener The {@link OnDismissListener} to use, or null. */ public void setOnDismissListener(final OnDismissListener listener) { @@ -656,7 +646,7 @@ public class SearchManager /** * Set or clear the callback that will be invoked whenever the search UI is canceled. - * + * * @param listener The {@link OnCancelListener} to use, or null. */ public void setOnCancelListener(OnCancelListener listener) { @@ -767,10 +757,10 @@ public class SearchManager // finally, make the query return mContext.getContentResolver().query(uri, null, selection, selArgs, null); } - + /** * Returns a list of the searchable activities that can be included in global search. - * + * * @return a list containing searchable information for all searchable activities * that have the android:includeInGlobalSearch attribute set * in their searchable meta-data. diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 8d8864f34b369b2aea3895abe4dade9d482f2ea6..5705bff03e632bbf59c1cd90806f6b3390ffc25c 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -68,7 +68,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { private SearchableInfo mSearchable; private Context mProviderContext; private WeakHashMap mOutsideDrawablesCache; - private SparseArray mBackgroundsCache; private boolean mClosed = false; // URL color @@ -80,7 +79,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { private int mText2UrlCol; private int mIconName1Col; private int mIconName2Col; - private int mBackgroundColorCol; static final int NONE = -1; @@ -109,7 +107,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mProviderContext = mSearchable.getProviderContext(mContext, activityContext); mOutsideDrawablesCache = outsideDrawablesCache; - mBackgroundsCache = new SparseArray(); mStartSpinnerRunnable = new Runnable() { public void run() { @@ -243,8 +240,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL); mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); - mBackgroundColorCol = - c.getColumnIndex(SearchManager.SUGGEST_COLUMN_BACKGROUND_COLOR); } } catch (Exception e) { Log.e(LOG_TAG, "error changing cursor and caching columns", e); @@ -283,13 +278,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { public void bindView(View view, Context context, Cursor cursor) { ChildViewCache views = (ChildViewCache) view.getTag(); - int backgroundColor = 0; - if (mBackgroundColorCol != -1) { - backgroundColor = cursor.getInt(mBackgroundColorCol); - } - Drawable background = getItemBackground(backgroundColor); - view.setBackgroundDrawable(background); - if (views.mText1 != null) { String text1 = getStringOrNull(cursor, mText1Col); setViewText(views.mText1, text1); @@ -342,33 +330,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { return text; } - /** - * Gets a drawable with no color when selected or pressed, and the given color when - * neither selected nor pressed. - * - * @return A drawable, or {@code null} if the given color is transparent. - */ - private Drawable getItemBackground(int backgroundColor) { - if (backgroundColor == 0) { - return null; - } else { - Drawable.ConstantState cachedBg = mBackgroundsCache.get(backgroundColor); - if (cachedBg != null) { - if (DBG) Log.d(LOG_TAG, "Background cache hit for color " + backgroundColor); - return cachedBg.newDrawable(mProviderContext.getResources()); - } - if (DBG) Log.d(LOG_TAG, "Creating new background for color " + backgroundColor); - ColorDrawable transparent = new ColorDrawable(0); - ColorDrawable background = new ColorDrawable(backgroundColor); - StateListDrawable newBg = new StateListDrawable(); - newBg.addState(new int[]{android.R.attr.state_selected}, transparent); - newBg.addState(new int[]{android.R.attr.state_pressed}, transparent); - newBg.addState(new int[]{}, background); - mBackgroundsCache.put(backgroundColor, newBg.getConstantState()); - return newBg; - } - } - private void setViewText(TextView v, CharSequence text) { // Set the text even if it's null, since we need to clear any previous text. v.setText(text); @@ -601,21 +562,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { * @return A non-null drawable. */ private Drawable getDefaultIcon1(Cursor cursor) { - // First check the component that the suggestion is originally from - String c = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME); - if (c != null) { - ComponentName component = ComponentName.unflattenFromString(c); - if (component != null) { - Drawable drawable = getActivityIconWithCache(component); - if (drawable != null) { - return drawable; - } - } else { - Log.w(LOG_TAG, "Bad component name: " + c); - } - } - - // Then check the component that gave us the suggestion + // Check the component that gave us the suggestion Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity()); if (drawable != null) { return drawable; diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java index 62d3f7df314fd6a37ae8566f6574d47f585d5da0..521d41c3ec4b98ffd35d3a79af8e85d6b69b21cd 100644 --- a/core/java/android/app/TimePickerDialog.java +++ b/core/java/android/app/TimePickerDialog.java @@ -36,7 +36,7 @@ import java.util.Calendar; *

      See the Time Picker * tutorial.

      */ -public class TimePickerDialog extends AlertDialog implements OnClickListener, +public class TimePickerDialog extends AlertDialog implements OnClickListener, OnTimeChangedListener { /** @@ -56,12 +56,12 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, private static final String HOUR = "hour"; private static final String MINUTE = "minute"; private static final String IS_24_HOUR = "is24hour"; - + private final TimePicker mTimePicker; private final OnTimeSetListener mCallback; private final Calendar mCalendar; private final java.text.DateFormat mDateFormat; - + int mInitialHourOfDay; int mInitialMinute; boolean mIs24HourView; @@ -101,12 +101,13 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, mDateFormat = DateFormat.getTimeFormat(context); mCalendar = Calendar.getInstance(); updateTitle(mInitialHourOfDay, mInitialMinute); - - setButton(context.getText(R.string.date_time_set), this); - setButton2(context.getText(R.string.cancel), (OnClickListener) null); + + setButton(BUTTON_POSITIVE, context.getText(R.string.date_time_set), this); + setButton(BUTTON_NEGATIVE, context.getText(R.string.cancel), + (OnClickListener) null); setIcon(R.drawable.ic_dialog_time); - - LayoutInflater inflater = + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.time_picker_dialog, null); setView(view); @@ -118,11 +119,11 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, mTimePicker.setIs24HourView(mIs24HourView); mTimePicker.setOnTimeChangedListener(this); } - + public void onClick(DialogInterface dialog, int which) { if (mCallback != null) { mTimePicker.clearFocus(); - mCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), + mCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), mTimePicker.getCurrentMinute()); } } @@ -130,7 +131,7 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { updateTitle(hourOfDay, minute); } - + public void updateTime(int hourOfDay, int minutOfHour) { mTimePicker.setCurrentHour(hourOfDay); mTimePicker.setCurrentMinute(minutOfHour); @@ -141,7 +142,7 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, mCalendar.set(Calendar.MINUTE, minute); setTitle(mDateFormat.format(mCalendar.getTime())); } - + @Override public Bundle onSaveInstanceState() { Bundle state = super.onSaveInstanceState(); @@ -150,7 +151,7 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, state.putBoolean(IS_24_HOUR, mTimePicker.is24HourView()); return state; } - + @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index e455a5966c8af23a30c06c1c5af5d1de0c51e6b2..92b7cf5198875afa3d6f7c3a99d3e76ead02d9ef 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -235,8 +235,13 @@ public class WallpaperManager { if (width <= 0 || height <= 0) { // Degenerate case: no size requested, just load // bitmap as-is. - Bitmap bm = BitmapFactory.decodeFileDescriptor( - fd.getFileDescriptor(), null, null); + Bitmap bm = null; + try { + bm = BitmapFactory.decodeFileDescriptor( + fd.getFileDescriptor(), null, null); + } catch (OutOfMemoryError e) { + Log.w(TAG, "Can't decode file", e); + } try { fd.close(); } catch (IOException e) { @@ -277,7 +282,12 @@ public class WallpaperManager { if (width <= 0 || height <= 0) { // Degenerate case: no size requested, just load // bitmap as-is. - Bitmap bm = BitmapFactory.decodeStream(is, null, null); + Bitmap bm = null; + try { + bm = BitmapFactory.decodeStream(is, null, null); + } catch (OutOfMemoryError e) { + Log.w(TAG, "Can't decode stream", e); + } try { is.close(); } catch (IOException e) { diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 0bcd65cd48fe3fe8468504d0ac42a1ad6c1ddd2b..2237c821b234df5896228e11292704f00cc816e8 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -103,6 +103,15 @@ public final class DeviceAdminInfo implements Parcelable { */ public static final int USES_POLICY_WIPE_DATA = 4; + /** + * A type of policy that this device admin can use: able to specify the + * device Global Proxy, via {@link DevicePolicyManager#setGlobalProxy}. + * + *

      To control this policy, the device admin must have a "set-global-proxy" + * tag in the "uses-policies" section of its meta-data. + */ + public static final int USES_POLICY_SETS_GLOBAL_PROXY = 5; + /** @hide */ public static class PolicyInfo { public final int ident; @@ -138,6 +147,9 @@ public final class DeviceAdminInfo implements Parcelable { sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_FORCE_LOCK, "force-lock", com.android.internal.R.string.policylab_forceLock, com.android.internal.R.string.policydesc_forceLock)); + sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_SETS_GLOBAL_PROXY, "set-global-proxy", + com.android.internal.R.string.policylab_setGlobalProxy, + com.android.internal.R.string.policydesc_setGlobalProxy)); for (int i=0; iYou can optionally include the {@link #EXTRA_ADD_EXPLANATION} * field to provide the user with additional explanation (in addition * to your component's description) about what is being added. @@ -76,7 +78,7 @@ public class DevicePolicyManager { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN"; - + /** * Activity action: send when any policy admin changes a policy. * This is generally used to find out when a new policy is in effect. @@ -92,7 +94,7 @@ public class DevicePolicyManager { * @see #ACTION_ADD_DEVICE_ADMIN */ public static final String EXTRA_DEVICE_ADMIN = "android.app.extra.DEVICE_ADMIN"; - + /** * An optional CharSequence providing additional explanation for why the * admin is being added. @@ -100,22 +102,21 @@ public class DevicePolicyManager { * @see #ACTION_ADD_DEVICE_ADMIN */ public static final String EXTRA_ADD_EXPLANATION = "android.app.extra.ADD_EXPLANATION"; - - /** - * Activity action: have the user enter a new password. This activity - * should be launched after using {@link #setPasswordQuality(ComponentName, int)} - * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the - * user enter a new password that meets the current requirements. You can - * use {@link #isActivePasswordSufficient()} to determine whether you need - * to have the user select a new password in order to meet the current - * constraints. Upon being resumed from this activity, - * you can check the new password characteristics to see if they are - * sufficient. + + /** + * Activity action: have the user enter a new password. This activity should + * be launched after using {@link #setPasswordQuality(ComponentName, int)}, + * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the user + * enter a new password that meets the current requirements. You can use + * {@link #isActivePasswordSufficient()} to determine whether you need to + * have the user select a new password in order to meet the current + * constraints. Upon being resumed from this activity, you can check the new + * password characteristics to see if they are sufficient. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD"; - + /** * Return true if the given administrator component is currently * active (enabled) in the system. @@ -130,7 +131,7 @@ public class DevicePolicyManager { } return false; } - + /** * Return a list of all currently active device administrator's component * names. Note that if there are no administrators than null may be @@ -146,7 +147,7 @@ public class DevicePolicyManager { } return null; } - + /** * @hide */ @@ -160,7 +161,7 @@ public class DevicePolicyManager { } return false; } - + /** * Remove a current administration component. This can only be called * by the application that owns the administration component; if you @@ -176,28 +177,28 @@ public class DevicePolicyManager { } } } - + /** * Constant for {@link #setPasswordQuality}: the policy has no requirements * for the password. Note that quality constants are ordered so that higher * values are more restrictive. */ public static final int PASSWORD_QUALITY_UNSPECIFIED = 0; - + /** * Constant for {@link #setPasswordQuality}: the policy requires some kind * of password, but doesn't care what it is. Note that quality constants * are ordered so that higher values are more restrictive. */ public static final int PASSWORD_QUALITY_SOMETHING = 0x10000; - + /** * Constant for {@link #setPasswordQuality}: the user must have entered a * password containing at least numeric characters. Note that quality * constants are ordered so that higher values are more restrictive. */ public static final int PASSWORD_QUALITY_NUMERIC = 0x20000; - + /** * Constant for {@link #setPasswordQuality}: the user must have entered a * password containing at least alphabetic (or other symbol) characters. @@ -205,7 +206,7 @@ public class DevicePolicyManager { * restrictive. */ public static final int PASSWORD_QUALITY_ALPHABETIC = 0x40000; - + /** * Constant for {@link #setPasswordQuality}: the user must have entered a * password containing at least both> numeric and @@ -213,7 +214,19 @@ public class DevicePolicyManager { * ordered so that higher values are more restrictive. */ public static final int PASSWORD_QUALITY_ALPHANUMERIC = 0x50000; - + + /** + * Constant for {@link #setPasswordQuality}: the user must have entered a + * password containing at least a letter, a numerical digit and a special + * symbol, by default. With this password quality, passwords can be + * restricted to contain various sets of characters, like at least an + * uppercase letter, etc. These are specified using various methods, + * like {@link #setPasswordMinimumLowerCase(ComponentName, int)}. Note + * that quality constants are ordered so that higher values are more + * restrictive. + */ + public static final int PASSWORD_QUALITY_COMPLEX = 0x60000; + /** * Called by an application that is administering the device to set the * password restrictions it is imposing. After setting this, the user @@ -222,21 +235,21 @@ public class DevicePolicyManager { * will remain until the user has set a new one, so the change does not * take place immediately. To prompt the user for a new password, use * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. - * + * *

      Quality constants are ordered so that higher values are more restrictive; * thus the highest requested quality constant (between the policy set here, * the user's preference, and any other considerations) is the one that * is in effect. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param quality The new desired quality. One of * {@link #PASSWORD_QUALITY_UNSPECIFIED}, {@link #PASSWORD_QUALITY_SOMETHING}, * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC}, - * or {@link #PASSWORD_QUALITY_ALPHANUMERIC}. + * {@link #PASSWORD_QUALITY_ALPHANUMERIC} or {@link #PASSWORD_QUALITY_COMPLEX}. */ public void setPasswordQuality(ComponentName admin, int quality) { if (mService != null) { @@ -247,7 +260,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current minimum password quality for all admins * or a particular one. @@ -264,7 +277,7 @@ public class DevicePolicyManager { } return PASSWORD_QUALITY_UNSPECIFIED; } - + /** * Called by an application that is administering the device to set the * minimum allowed password length. After setting this, the user @@ -274,14 +287,14 @@ public class DevicePolicyManager { * take place immediately. To prompt the user for a new password, use * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This * constraint is only imposed if the administrator has also requested either - * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC}, - * or {@link #PASSWORD_QUALITY_ALPHANUMERIC} + * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC} + * {@link #PASSWORD_QUALITY_ALPHANUMERIC}, or {@link #PASSWORD_QUALITY_COMPLEX} * with {@link #setPasswordQuality}. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param length The new desired minimum password length. A value of 0 * means there is no restriction. @@ -295,7 +308,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current minimum password length for all admins * or a particular one. @@ -312,7 +325,379 @@ public class DevicePolicyManager { } return 0; } - + + /** + * Called by an application that is administering the device to set the + * minimum number of upper case letters required in the password. After + * setting this, the user will not be able to enter a new password that is + * not at least as restrictive as what has been set. Note that the current + * password will remain until the user has set a new one, so the change does + * not take place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 0. + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of upper case letters + * required in the password. A value of 0 means there is no + * restriction. + */ + public void setPasswordMinimumUpperCase(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumUpperCase(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of upper case letters required in the + * password for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumUpperCase(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of upper case letters required in the + * password. + */ + public int getPasswordMinimumUpperCase(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumUpperCase(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of lower case letters required in the password. After + * setting this, the user will not be able to enter a new password that is + * not at least as restrictive as what has been set. Note that the current + * password will remain until the user has set a new one, so the change does + * not take place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 0. + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of lower case letters + * required in the password. A value of 0 means there is no + * restriction. + */ + public void setPasswordMinimumLowerCase(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumLowerCase(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of lower case letters required in the + * password for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumLowerCase(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of lower case letters required in the + * password. + */ + public int getPasswordMinimumLowerCase(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumLowerCase(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of letters required in the password. After setting this, + * the user will not be able to enter a new password that is not at least as + * restrictive as what has been set. Note that the current password will + * remain until the user has set a new one, so the change does not take + * place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 1. + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of letters required in the + * password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumLetters(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumLetters(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of letters required in the password for all + * admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumLetters(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of letters required in the password. + */ + public int getPasswordMinimumLetters(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumLetters(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of numerical digits required in the password. After + * setting this, the user will not be able to enter a new password that is + * not at least as restrictive as what has been set. Note that the current + * password will remain until the user has set a new one, so the change does + * not take place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 1. + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of numerical digits required + * in the password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumNumeric(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumNumeric(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of numerical digits required in the password + * for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumNumeric(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of numerical digits required in the password. + */ + public int getPasswordMinimumNumeric(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumNumeric(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of symbols required in the password. After setting this, + * the user will not be able to enter a new password that is not at least as + * restrictive as what has been set. Note that the current password will + * remain until the user has set a new one, so the change does not take + * place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 1. + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of symbols required in the + * password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumSymbols(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumSymbols(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of symbols required in the password for all + * admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumSymbols(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of symbols required in the password. + */ + public int getPasswordMinimumSymbols(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumSymbols(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of non-letter characters (numerical digits or symbols) + * required in the password. After setting this, the user will not be able + * to enter a new password that is not at least as restrictive as what has + * been set. Note that the current password will remain until the user has + * set a new one, so the change does not take place immediately. To prompt + * the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} after + * setting this value. This constraint is only imposed if the administrator + * has also requested {@link #PASSWORD_QUALITY_COMPLEX} with + * {@link #setPasswordQuality}. The default value is 0. + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of letters required in the + * password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumNonLetter(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumNonLetter(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of non-letter characters required in the + * password for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumNonLetter(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of letters required in the password. + */ + public int getPasswordMinimumNonLetter(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumNonLetter(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the length + * of the password history. After setting this, the user will not be able to + * enter a new password that is the same as any password in the history. Note + * that the current password will remain until the user has set a new one, so + * the change does not take place immediately. To prompt the user for a new + * password, use {@link #ACTION_SET_NEW_PASSWORD} after setting this value. + * This constraint is only imposed if the administrator has also requested + * either {@link #PASSWORD_QUALITY_NUMERIC}, + * {@link #PASSWORD_QUALITY_ALPHABETIC}, or + * {@link #PASSWORD_QUALITY_ALPHANUMERIC} with {@link #setPasswordQuality}. + * + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this + * method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired length of password history. A value of 0 + * means there is no restriction. + */ + public void setPasswordHistoryLength(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordHistoryLength(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current password history length for all admins + * or a particular one. + * @param admin The name of the admin component to check, or null to aggregate + * all admins. + * @return The length of the password history + */ + public int getPasswordHistoryLength(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordHistoryLength(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + /** * Return the maximum password length that the device supports for a * particular password quality. @@ -323,16 +708,16 @@ public class DevicePolicyManager { // Kind-of arbitrary. return 16; } - + /** * Determine whether the current password the user has set is sufficient * to meet the policy requirements (quality, minimum length) that have been * requested. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @return Returns true if the password meets the current requirements, * else false. */ @@ -346,11 +731,11 @@ public class DevicePolicyManager { } return false; } - + /** * Retrieve the number of times the user has failed at entering a * password since that last successful password entry. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to be able to call * this method; if it has not, a security exception will be thrown. @@ -373,14 +758,14 @@ public class DevicePolicyManager { * watching for failed passwords and wiping the device, and requires * that you request both {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}}. - * + * *

      To implement any other policy (e.g. wiping data for a particular * application only, erasing or revoking credentials, or reporting the * failure to a server), you should implement * {@link DeviceAdminReceiver#onPasswordFailed(Context, android.content.Intent)} * instead. Do not use this API, because if the maximum count is reached, * the device will be wiped immediately, and your callback will not be invoked. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param num The number of failed password attempts at which point the * device will wipe its data. @@ -394,7 +779,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current maximum number of login attempts that are allowed * before the device wipes itself, for all admins @@ -412,13 +797,13 @@ public class DevicePolicyManager { } return 0; } - + /** * Flag for {@link #resetPassword}: don't allow other admins to change * the password again until the user has entered it. */ public static final int RESET_PASSWORD_REQUIRE_ENTRY = 0x0001; - + /** * Force a new device unlock password (the password needed to access the * entire device, not for individual accounts) on the user. This takes @@ -431,11 +816,11 @@ public class DevicePolicyManager { * that the password may be a stronger quality (containing alphanumeric * characters when the requested quality is only numeric), in which case * the currently active quality will be increased to match. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param password The new password for the user. * @param flags May be 0 or {@link #RESET_PASSWORD_REQUIRE_ENTRY}. * @return Returns true if the password was applied, or false if it is @@ -451,16 +836,16 @@ public class DevicePolicyManager { } return false; } - + /** * Called by an application that is administering the device to set the * maximum time for user activity until the device will lock. This limits * the length that the user can set. It takes effect immediately. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param timeMs The new desired maximum time to lock in milliseconds. * A value of 0 means there is no restriction. @@ -474,7 +859,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current maximum time to unlock for all admins * or a particular one. @@ -491,11 +876,11 @@ public class DevicePolicyManager { } return 0; } - + /** * Make the device lock immediately, as if the lock screen timeout has * expired at the point of this call. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} to be able to call * this method; if it has not, a security exception will be thrown. @@ -509,16 +894,16 @@ public class DevicePolicyManager { } } } - + /** * Ask the user date be wiped. This will cause the device to reboot, * erasing all user data while next booting up. External storage such * as SD cards will not be erased. - * + * *

      The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param flags Bit mask of additional options: currently must be 0. */ public void wipeData(int flags) { @@ -530,7 +915,92 @@ public class DevicePolicyManager { } } } - + + /** + * Called by an application that is administering the device to set the + * global proxy and exclusion list. + *

      + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_SETS_GLOBAL_PROXY} to be able to call + * this method; if it has not, a security exception will be thrown. + * Only the first device admin can set the proxy. If a second admin attempts + * to set the proxy, the {@link ComponentName} of the admin originally setting the + * proxy will be returned. If successful in setting the proxy, null will + * be returned. + * The method can be called repeatedly by the device admin alrady setting the + * proxy to update the proxy and exclusion list. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param proxySpec the global proxy desired. Must be an HTTP Proxy. + * Pass Proxy.NO_PROXY to reset the proxy. + * @param exclusionList a list of domains to be excluded from the global proxy. + * @return returns null if the proxy was successfully set, or a {@link ComponentName} + * of the device admin that sets thew proxy otherwise. + */ + public ComponentName setGlobalProxy(ComponentName admin, Proxy proxySpec, + List exclusionList ) { + if (proxySpec == null) { + throw new NullPointerException(); + } + if (mService != null) { + try { + String hostSpec; + String exclSpec; + if (proxySpec.equals(Proxy.NO_PROXY)) { + hostSpec = null; + exclSpec = null; + } else { + if (!proxySpec.type().equals(Proxy.Type.HTTP)) { + throw new IllegalArgumentException(); + } + InetSocketAddress sa = (InetSocketAddress)proxySpec.address(); + String hostName = sa.getHostName(); + int port = sa.getPort(); + StringBuilder hostBuilder = new StringBuilder(); + hostSpec = hostBuilder.append(hostName) + .append(":").append(Integer.toString(port)).toString(); + if (exclusionList == null) { + exclSpec = ""; + } else { + StringBuilder listBuilder = new StringBuilder(); + boolean firstDomain = true; + for (String exclDomain : exclusionList) { + if (!firstDomain) { + listBuilder = listBuilder.append(","); + } else { + firstDomain = false; + } + listBuilder = listBuilder.append(exclDomain.trim()); + } + exclSpec = listBuilder.toString(); + } + android.net.Proxy.validate(hostName, Integer.toString(port), exclSpec); + } + return mService.setGlobalProxy(admin, hostSpec, exclSpec); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return null; + } + + /** + * Returns the component name setting the global proxy. + * @return ComponentName object of the device admin that set the global proxy, or + * null if no admin has set the proxy. + */ + public ComponentName getGlobalProxyAdmin() { + if (mService != null) { + try { + return mService.getGlobalProxyAdmin(); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return null; + } + /** * @hide */ @@ -543,7 +1013,7 @@ public class DevicePolicyManager { } } } - + /** * @hide */ @@ -556,10 +1026,10 @@ public class DevicePolicyManager { Log.w(TAG, "Unable to retrieve device policy " + cn, e); return null; } - + ResolveInfo ri = new ResolveInfo(); ri.activityInfo = ai; - + try { return new DeviceAdminInfo(mContext, ri); } catch (XmlPullParserException e) { @@ -570,7 +1040,7 @@ public class DevicePolicyManager { return null; } } - + /** * @hide */ @@ -587,16 +1057,18 @@ public class DevicePolicyManager { /** * @hide */ - public void setActivePasswordState(int quality, int length) { + public void setActivePasswordState(int quality, int length, int letters, int uppercase, + int lowercase, int numbers, int symbols, int nonletter) { if (mService != null) { try { - mService.setActivePasswordState(quality, length); + mService.setActivePasswordState(quality, length, letters, uppercase, lowercase, + numbers, symbols, nonletter); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } } } - + /** * @hide */ @@ -609,7 +1081,7 @@ public class DevicePolicyManager { } } } - + /** * @hide */ @@ -622,4 +1094,5 @@ public class DevicePolicyManager { } } } + } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 6fc4dc5add605696e81a4fe8fe1e756bc962f1df..3fcd6fce5140b33e4adf898da485a51e420ae8dc 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -27,10 +27,31 @@ import android.os.RemoteCallback; interface IDevicePolicyManager { void setPasswordQuality(in ComponentName who, int quality); int getPasswordQuality(in ComponentName who); - + void setPasswordMinimumLength(in ComponentName who, int length); int getPasswordMinimumLength(in ComponentName who); + + void setPasswordMinimumUpperCase(in ComponentName who, int length); + int getPasswordMinimumUpperCase(in ComponentName who); + + void setPasswordMinimumLowerCase(in ComponentName who, int length); + int getPasswordMinimumLowerCase(in ComponentName who); + + void setPasswordMinimumLetters(in ComponentName who, int length); + int getPasswordMinimumLetters(in ComponentName who); + + void setPasswordMinimumNumeric(in ComponentName who, int length); + int getPasswordMinimumNumeric(in ComponentName who); + + void setPasswordMinimumSymbols(in ComponentName who, int length); + int getPasswordMinimumSymbols(in ComponentName who); + + void setPasswordMinimumNonLetter(in ComponentName who, int length); + int getPasswordMinimumNonLetter(in ComponentName who); + void setPasswordHistoryLength(in ComponentName who, int length); + int getPasswordHistoryLength(in ComponentName who); + boolean isActivePasswordSufficient(); int getCurrentFailedPasswordAttempts(); @@ -45,6 +66,9 @@ interface IDevicePolicyManager { void lockNow(); void wipeData(int flags); + + ComponentName setGlobalProxy(in ComponentName admin, String proxySpec, String exclusionList); + ComponentName getGlobalProxyAdmin(); void setActiveAdmin(in ComponentName policyReceiver); boolean isAdminActive(in ComponentName policyReceiver); @@ -53,7 +77,8 @@ interface IDevicePolicyManager { void getRemoveWarning(in ComponentName policyReceiver, in RemoteCallback result); void removeActiveAdmin(in ComponentName policyReceiver); - void setActivePasswordState(int quality, int length); + void setActivePasswordState(int quality, int length, int letters, int uppercase, int lowercase, + int numbers, int symbols, int nonletter); void reportFailedPasswordAttempt(); void reportSuccessfulPasswordAttempt(); } diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index b2fc13ffba6cf12f379afa2d8cdf5acc487bb34d..7730942e467afaf39039558cc693ece37a09252e 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -39,6 +39,7 @@ public class AppWidgetHost { static final int HANDLE_UPDATE = 1; static final int HANDLE_PROVIDER_CHANGED = 2; + static final int HANDLE_VIEW_DATA_CHANGED = 3; final static Object sServiceLock = new Object(); static IAppWidgetService sService; @@ -60,6 +61,13 @@ public class AppWidgetHost { msg.obj = info; msg.sendToTarget(); } + + public void viewDataChanged(int appWidgetId, int viewId) { + Message msg = mHandler.obtainMessage(HANDLE_VIEW_DATA_CHANGED); + msg.arg1 = appWidgetId; + msg.arg2 = viewId; + msg.sendToTarget(); + } } class UpdateHandler extends Handler { @@ -77,6 +85,10 @@ public class AppWidgetHost { onProviderChanged(msg.arg1, (AppWidgetProviderInfo)msg.obj); break; } + case HANDLE_VIEW_DATA_CHANGED: { + viewDataChanged(msg.arg1, msg.arg2); + break; + } } } } @@ -250,6 +262,16 @@ public class AppWidgetHost { v.updateAppWidget(views); } } + + void viewDataChanged(int appWidgetId, int viewId) { + AppWidgetHostView v; + synchronized (mViews) { + v = mViews.get(appWidgetId); + } + if (v != null) { + v.viewDataChanged(viewId); + } + } } diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index b33b0970d825e79e6b41136df01309063780f581..4f8ee9382d7663f0d41e4dd3e1bf248b899bb653 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -23,15 +23,18 @@ import android.graphics.Bitmap; 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.os.Parcelable; +import android.os.SystemClock; 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.widget.Adapter; +import android.widget.AdapterView; +import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.RemoteViews; import android.widget.TextView; @@ -257,6 +260,22 @@ public class AppWidgetHostView extends FrameLayout { } } + /** + * Process data-changed notifications for the specified view in the specified + * set of {@link RemoteViews} views. + */ + void viewDataChanged(int viewId) { + View v = findViewById(viewId); + if ((v != null) && (v instanceof AdapterView)) { + AdapterView adapterView = (AdapterView) v; + Adapter adapter = adapterView.getAdapter(); + if (adapter instanceof BaseAdapter) { + BaseAdapter baseAdapter = (BaseAdapter) adapter; + baseAdapter.notifyDataSetChanged(); + } + } + } + /** * Build a {@link Context} cloned into another package name, usually for the * purposes of reading remote resources. @@ -275,6 +294,7 @@ public class AppWidgetHostView extends FrameLayout { } } + @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { if (CROSSFADE) { int alpha; diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index d4ce6a13b2ad301552fd7502c75a7e5416d7f8e2..2a583c14f438b1e30e360d6c26355cf8290e7e10 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -22,7 +22,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.DisplayMetrics; -import android.util.Log; import android.util.TypedValue; import android.widget.RemoteViews; @@ -149,7 +148,7 @@ public class AppWidgetManager { * instances as possible. * * - * + * * @see AppWidgetProvider#onUpdate AppWidgetProvider.onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) */ public static final String ACTION_APPWIDGET_UPDATE = "android.appwidget.action.APPWIDGET_UPDATE"; @@ -163,7 +162,7 @@ public class AppWidgetManager { /** * Sent when an instance of an AppWidget is removed from the last host. - * + * * @see AppWidgetProvider#onEnabled AppWidgetProvider.onEnabled(Context context) */ public static final String ACTION_APPWIDGET_DISABLED = "android.appwidget.action.APPWIDGET_DISABLED"; @@ -172,7 +171,7 @@ public class AppWidgetManager { * Sent when an instance of an AppWidget is added to a host for the first time. * This broadcast is sent at boot time if there is a AppWidgetHost installed with * an instance for this provider. - * + * * @see AppWidgetProvider#onEnabled AppWidgetProvider.onEnabled(Context context) */ public static final String ACTION_APPWIDGET_ENABLED = "android.appwidget.action.APPWIDGET_ENABLED"; @@ -183,20 +182,21 @@ public class AppWidgetManager { * @see AppWidgetProviderInfo */ public static final String META_DATA_APPWIDGET_PROVIDER = "android.appwidget.provider"; - + /** * Field for the manifest meta-data tag used to indicate any previous name for the * app widget receiver. * * @see AppWidgetProviderInfo - * + * * @hide Pending API approval */ public static final String META_DATA_APPWIDGET_OLD_NAME = "android.appwidget.oldName"; - static WeakHashMap> sManagerCache = new WeakHashMap(); + static WeakHashMap> sManagerCache = + new WeakHashMap>(); static IAppWidgetService sService; - + Context mContext; private DisplayMetrics mDisplayMetrics; @@ -219,7 +219,7 @@ public class AppWidgetManager { } if (result == null) { result = new AppWidgetManager(context); - sManagerCache.put(context, new WeakReference(result)); + sManagerCache.put(context, new WeakReference(result)); } return result; } @@ -233,6 +233,10 @@ public class AppWidgetManager { /** * Set the RemoteViews to use for the specified appWidgetIds. * + * Note that the RemoteViews parameter will be cached by the AppWidgetService, and hence should + * contain a complete representation of the widget. For performing partial widget updates, see + * {@link #partiallyUpdateAppWidget(int[], RemoteViews)}. + * *

      * It is okay to call this method both inside an {@link #ACTION_APPWIDGET_UPDATE} broadcast, * and outside of the handler. @@ -253,6 +257,10 @@ public class AppWidgetManager { /** * Set the RemoteViews to use for the specified appWidgetId. * + * Note that the RemoteViews parameter will be cached by the AppWidgetService, and hence should + * contain a complete representation of the widget. For performing partial widget updates, see + * {@link #partiallyUpdateAppWidget(int, RemoteViews)}. + * *

      * It is okay to call this method both inside an {@link #ACTION_APPWIDGET_UPDATE} broadcast, * and outside of the handler. @@ -265,6 +273,59 @@ public class AppWidgetManager { updateAppWidget(new int[] { appWidgetId }, views); } + /** + * Perform an incremental update or command on the widget(s) specified by appWidgetIds. + * + * This update differs from {@link #updateAppWidget(int[], RemoteViews)} in that the + * RemoteViews object which is passed is understood to be an incomplete representation of the + * widget, and hence is not cached by the AppWidgetService. Note that because these updates are + * not cached, any state that they modify that is not restored by restoreInstanceState will not + * persist in the case that the widgets are restored using the cached version in + * AppWidgetService. + * + * Use with {@link RemoteViews#showNext(int)}, {@link RemoteViews#showPrevious(int)}, + * {@link RemoteViews#setScrollPosition(int, int)} and similar commands. + * + *

      + * It is okay to call this method both inside an {@link #ACTION_APPWIDGET_UPDATE} broadcast, + * and outside of the handler. + * This method will only work when called from the uid that owns the AppWidget provider. + * + * @param appWidgetIds The AppWidget instances for which to set the RemoteViews. + * @param views The RemoteViews object containing the incremental update / command. + */ + public void partiallyUpdateAppWidget(int[] appWidgetIds, RemoteViews views) { + try { + sService.partiallyUpdateAppWidgetIds(appWidgetIds, views); + } catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + } + + /** + * Perform an incremental update or command on the widget specified by appWidgetId. + * + * This update differs from {@link #updateAppWidget(int, RemoteViews)} in that the RemoteViews + * object which is passed is understood to be an incomplete representation of the widget, and + * hence is not cached by the AppWidgetService. Note that because these updates are not cached, + * any state that they modify that is not restored by restoreInstanceState will not persist in + * the case that the widgets are restored using the cached version in AppWidgetService. + * + * Use with {@link RemoteViews#showNext(int)}, {@link RemoteViews#showPrevious(int)}, + * {@link RemoteViews#setScrollPosition(int, int)} and similar commands. + * + *

      + * It is okay to call this method both inside an {@link #ACTION_APPWIDGET_UPDATE} broadcast, + * and outside of the handler. + * This method will only work when called from the uid that owns the AppWidget provider. + * + * @param appWidgetId The AppWidget instance for which to set the RemoteViews. + * @param views The RemoteViews object containing the incremental update / command. + */ + public void partiallyUpdateAppWidget(int appWidgetId, RemoteViews views) { + partiallyUpdateAppWidget(new int[] { appWidgetId }, views); + } + /** * Set the RemoteViews to use for all AppWidget instances for the supplied AppWidget provider. * @@ -287,12 +348,47 @@ public class AppWidgetManager { } } + /** + * Notifies the specified collection view in all the specified AppWidget instances + * to invalidate their currently data. + * + * @param appWidgetIds The AppWidget instances for which to notify of view data changes. + * @param viewId The collection view id. + */ + public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) { + try { + sService.notifyAppWidgetViewDataChanged(appWidgetIds, viewId); + } + catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + } + + /** + * Notifies the specified collection view in all the specified AppWidget instance + * to invalidate it's currently data. + * + * @param appWidgetId The AppWidget instance for which to notify of view data changes. + * @param viewId The collection view id. + */ + public void notifyAppWidgetViewDataChanged(int appWidgetId, int viewId) { + notifyAppWidgetViewDataChanged(new int[] { appWidgetId }, viewId); + } + /** * Return a list of the AppWidget providers that are currently installed. */ public List getInstalledProviders() { try { - return sService.getInstalledProviders(); + List providers = sService.getInstalledProviders(); + for (AppWidgetProviderInfo info : providers) { + // Converting complex to dp. + info.minWidth = + TypedValue.complexToDimensionPixelSize(info.minWidth, mDisplayMetrics); + info.minHeight = + TypedValue.complexToDimensionPixelSize(info.minHeight, mDisplayMetrics); + } + return providers; } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); @@ -310,7 +406,7 @@ public class AppWidgetManager { AppWidgetProviderInfo info = sService.getAppWidgetInfo(appWidgetId); if (info != null) { // Converting complex to dp. - info.minWidth = + info.minWidth = TypedValue.complexToDimensionPixelSize(info.minWidth, mDisplayMetrics); info.minHeight = TypedValue.complexToDimensionPixelSize(info.minHeight, mDisplayMetrics); @@ -344,7 +440,7 @@ public class AppWidgetManager { /** * Get the list of appWidgetIds that have been bound to the given AppWidget * provider. - * + * * @param provider The {@link android.content.BroadcastReceiver} that is the * AppWidget provider to find appWidgetIds for. */ diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java index cee2865cd0001d5f85e2b11f8de67d0e717c588d..396e92de893d8452acb74214d24a05a1a8b4fbab 100644 --- a/core/java/android/appwidget/AppWidgetProviderInfo.java +++ b/core/java/android/appwidget/AppWidgetProviderInfo.java @@ -110,6 +110,17 @@ public class AppWidgetProviderInfo implements Parcelable { * @hide Pending API approval */ public String oldName; + + /** + * A preview of what the AppWidget will look like after it's configured. + * If not supplied, the AppWidget's icon will be used. + * + *

      This field corresponds to the android:previewImage attribute in + * the <receiver> element in the AndroidManifest.xml file. + * + * @hide Pending API approval + */ + public int previewImage; public AppWidgetProviderInfo() { } @@ -130,6 +141,7 @@ public class AppWidgetProviderInfo implements Parcelable { } this.label = in.readString(); this.icon = in.readInt(); + this.previewImage = in.readInt(); } @@ -152,6 +164,7 @@ public class AppWidgetProviderInfo implements Parcelable { } out.writeString(this.label); out.writeInt(this.icon); + out.writeInt(this.previewImage); } public int describeContents() { diff --git a/core/java/android/bluetooth/AtCommandHandler.java b/core/java/android/bluetooth/AtCommandHandler.java index 8de2133e49440795b7a1e8614c8c0aeef0db498c..6deab34ed483ce72d14c12f3798bd1315ca80013 100644 --- a/core/java/android/bluetooth/AtCommandHandler.java +++ b/core/java/android/bluetooth/AtCommandHandler.java @@ -73,7 +73,7 @@ public abstract class AtCommandHandler { * least one element in this array. * @return The result of this command. */ - // Typically used to set this paramter + // Typically used to set this parameter public AtCommandResult handleSetCommand(Object[] args) { return new AtCommandResult(AtCommandResult.ERROR); } @@ -83,11 +83,12 @@ public abstract class AtCommandHandler { * Test commands are part of the Extended command syntax, and are typically * used to request an indication of the range of legal values that "FOO" * can take.

      - * By defualt we return an OK result, to indicate that this command is at + * By default we return an OK result, to indicate that this command is at * least recognized.

      * @return The result of this command. */ public AtCommandResult handleTestCommand() { return new AtCommandResult(AtCommandResult.OK); } + } diff --git a/core/java/android/bluetooth/BluetoothAssignedNumbers.java b/core/java/android/bluetooth/BluetoothAssignedNumbers.java new file mode 100644 index 0000000000000000000000000000000000000000..55bc814066ce77a8c3b5c3a64547349d32896799 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothAssignedNumbers.java @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +/** + * Bluetooth Assigned Numbers. + *

      + * For now we only include Company ID values. + * @see + * The Official Bluetooth SIG Member Website | Company Identifiers + * + * @hide + */ +public class BluetoothAssignedNumbers { + + //// Bluetooth SIG Company ID values + + /* + * Ericsson Technology Licensing. + */ + public static final int ERICSSON_TECHNOLOGY = 0x0000; + + /* + * Nokia Mobile Phones. + */ + public static final int NOKIA_MOBILE_PHONES = 0x0001; + + /* + * Intel Corp. + */ + public static final int INTEL = 0x0002; + + /* + * IBM Corp. + */ + public static final int IBM = 0x0003; + + /* + * Toshiba Corp. + */ + public static final int TOSHIBA = 0x0004; + + /* + * 3Com. + */ + public static final int THREECOM = 0x0005; + + /* + * Microsoft. + */ + public static final int MICROSOFT = 0x0006; + + /* + * Lucent. + */ + public static final int LUCENT = 0x0007; + + /* + * Motorola. + */ + public static final int MOTOROLA = 0x0008; + + /* + * Infineon Technologies AG. + */ + public static final int INFINEON_TECHNOLOGIES = 0x0009; + + /* + * Cambridge Silicon Radio. + */ + public static final int CAMBRIDGE_SILICON_RADIO = 0x000A; + + /* + * Silicon Wave. + */ + public static final int SILICON_WAVE = 0x000B; + + /* + * Digianswer A/S. + */ + public static final int DIGIANSWER = 0x000C; + + /* + * Texas Instruments Inc. + */ + public static final int TEXAS_INSTRUMENTS = 0x000D; + + /* + * Parthus Technologies Inc. + */ + public static final int PARTHUS_TECHNOLOGIES = 0x000E; + + /* + * Broadcom Corporation. + */ + public static final int BROADCOM = 0x000F; + + /* + * Mitel Semiconductor. + */ + public static final int MITEL_SEMICONDUCTOR = 0x0010; + + /* + * Widcomm, Inc. + */ + public static final int WIDCOMM = 0x0011; + + /* + * Zeevo, Inc. + */ + public static final int ZEEVO = 0x0012; + + /* + * Atmel Corporation. + */ + public static final int ATMEL = 0x0013; + + /* + * Mitsubishi Electric Corporation. + */ + public static final int MITSUBISHI_ELECTRIC = 0x0014; + + /* + * RTX Telecom A/S. + */ + public static final int RTX_TELECOM = 0x0015; + + /* + * KC Technology Inc. + */ + public static final int KC_TECHNOLOGY = 0x0016; + + /* + * Newlogic. + */ + public static final int NEWLOGIC = 0x0017; + + /* + * Transilica, Inc. + */ + public static final int TRANSILICA = 0x0018; + + /* + * Rohde & Schwarz GmbH & Co. KG. + */ + public static final int ROHDE_AND_SCHWARZ = 0x0019; + + /* + * TTPCom Limited. + */ + public static final int TTPCOM = 0x001A; + + /* + * Signia Technologies, Inc. + */ + public static final int SIGNIA_TECHNOLOGIES = 0x001B; + + /* + * Conexant Systems Inc. + */ + public static final int CONEXANT_SYSTEMS = 0x001C; + + /* + * Qualcomm. + */ + public static final int QUALCOMM = 0x001D; + + /* + * Inventel. + */ + public static final int INVENTEL = 0x001E; + + /* + * AVM Berlin. + */ + public static final int AVM_BERLIN = 0x001F; + + /* + * BandSpeed, Inc. + */ + public static final int BANDSPEED = 0x0020; + + /* + * Mansella Ltd. + */ + public static final int MANSELLA = 0x0021; + + /* + * NEC Corporation. + */ + public static final int NEC = 0x0022; + + /* + * WavePlus Technology Co., Ltd. + */ + public static final int WAVEPLUS_TECHNOLOGY = 0x0023; + + /* + * Alcatel. + */ + public static final int ALCATEL = 0x0024; + + /* + * Philips Semiconductors. + */ + public static final int PHILIPS_SEMICONDUCTORS = 0x0025; + + /* + * C Technologies. + */ + public static final int C_TECHNOLOGIES = 0x0026; + + /* + * Open Interface. + */ + public static final int OPEN_INTERFACE = 0x0027; + + /* + * R F Micro Devices. + */ + public static final int RF_MICRO_DEVICES = 0x0028; + + /* + * Hitachi Ltd. + */ + public static final int HITACHI = 0x0029; + + /* + * Symbol Technologies, Inc. + */ + public static final int SYMBOL_TECHNOLOGIES = 0x002A; + + /* + * Tenovis. + */ + public static final int TENOVIS = 0x002B; + + /* + * Macronix International Co. Ltd. + */ + public static final int MACRONIX = 0x002C; + + /* + * GCT Semiconductor. + */ + public static final int GCT_SEMICONDUCTOR = 0x002D; + + /* + * Norwood Systems. + */ + public static final int NORWOOD_SYSTEMS = 0x002E; + + /* + * MewTel Technology Inc. + */ + public static final int MEWTEL_TECHNOLOGY = 0x002F; + + /* + * ST Microelectronics. + */ + public static final int ST_MICROELECTRONICS = 0x0030; + + /* + * Synopsys. + */ + public static final int SYNOPSYS = 0x0031; + + /* + * Red-M (Communications) Ltd. + */ + public static final int RED_M = 0x0032; + + /* + * Commil Ltd. + */ + public static final int COMMIL = 0x0033; + + /* + * Computer Access Technology Corporation (CATC). + */ + public static final int CATC = 0x0034; + + /* + * Eclipse (HQ Espana) S.L. + */ + public static final int ECLIPSE = 0x0035; + + /* + * Renesas Technology Corp. + */ + public static final int RENESAS_TECHNOLOGY = 0x0036; + + /* + * Mobilian Corporation. + */ + public static final int MOBILIAN_CORPORATION = 0x0037; + + /* + * Terax. + */ + public static final int TERAX = 0x0038; + + /* + * Integrated System Solution Corp. + */ + public static final int INTEGRATED_SYSTEM_SOLUTION = 0x0039; + + /* + * Matsushita Electric Industrial Co., Ltd. + */ + public static final int MATSUSHITA_ELECTRIC = 0x003A; + + /* + * Gennum Corporation. + */ + public static final int GENNUM = 0x003B; + + /* + * Research In Motion. + */ + public static final int RESEARCH_IN_MOTION = 0x003C; + + /* + * IPextreme, Inc. + */ + public static final int IPEXTREME = 0x003D; + + /* + * Systems and Chips, Inc. + */ + public static final int SYSTEMS_AND_CHIPS = 0x003E; + + /* + * Bluetooth SIG, Inc. + */ + public static final int BLUETOOTH_SIG = 0x003F; + + /* + * Seiko Epson Corporation. + */ + public static final int SEIKO_EPSON = 0x0040; + + /* + * Integrated Silicon Solution Taiwan, Inc. + */ + public static final int INTEGRATED_SILICON_SOLUTION = 0x0041; + + /* + * CONWISE Technology Corporation Ltd. + */ + public static final int CONWISE_TECHNOLOGY = 0x0042; + + /* + * PARROT SA. + */ + public static final int PARROT = 0x0043; + + /* + * Socket Mobile. + */ + public static final int SOCKET_MOBILE = 0x0044; + + /* + * Atheros Communications, Inc. + */ + public static final int ATHEROS_COMMUNICATIONS = 0x0045; + + /* + * MediaTek, Inc. + */ + public static final int MEDIATEK = 0x0046; + + /* + * Bluegiga. + */ + public static final int BLUEGIGA = 0x0047; + + /* + * Marvell Technology Group Ltd. + */ + public static final int MARVELL = 0x0048; + + /* + * 3DSP Corporation. + */ + public static final int THREE_DSP = 0x0049; + + /* + * Accel Semiconductor Ltd. + */ + public static final int ACCEL_SEMICONDUCTOR = 0x004A; + + /* + * Continental Automotive Systems. + */ + public static final int CONTINENTAL_AUTOMOTIVE = 0x004B; + + /* + * Apple, Inc. + */ + public static final int APPLE = 0x004C; + + /* + * Staccato Communications, Inc. + */ + public static final int STACCATO_COMMUNICATIONS = 0x004D; + + /* + * Avago Technologies. + */ + public static final int AVAGO = 0x004E; + + /* + * APT Licensing Ltd. + */ + public static final int APT_LICENSING = 0x004F; + + /* + * SiRF Technology, Inc. + */ + public static final int SIRF_TECHNOLOGY = 0x0050; + + /* + * Tzero Technologies, Inc. + */ + public static final int TZERO_TECHNOLOGIES = 0x0051; + + /* + * J&M Corporation. + */ + public static final int J_AND_M = 0x0052; + + /* + * Free2move AB. + */ + public static final int FREE2MOVE = 0x0053; + + /* + * 3DiJoy Corporation. + */ + public static final int THREE_DIJOY = 0x0054; + + /* + * Plantronics, Inc. + */ + public static final int PLANTRONICS = 0x0055; + + /* + * Sony Ericsson Mobile Communications. + */ + public static final int SONY_ERICSSON = 0x0056; + + /* + * Harman International Industries, Inc. + */ + public static final int HARMAN_INTERNATIONAL = 0x0057; + + /* + * Vizio, Inc. + */ + public static final int VIZIO = 0x0058; + + /* + * Nordic Semiconductor ASA. + */ + public static final int NORDIC_SEMICONDUCTOR = 0x0059; + + /* + * EM Microelectronic-Marin SA. + */ + public static final int EM_MICROELECTRONIC_MARIN = 0x005A; + + /* + * Ralink Technology Corporation. + */ + public static final int RALINK_TECHNOLOGY = 0x005B; + + /* + * Belkin International, Inc. + */ + public static final int BELKIN_INTERNATIONAL = 0x005C; + + /* + * Realtek Semiconductor Corporation. + */ + public static final int REALTEK_SEMICONDUCTOR = 0x005D; + + /* + * Stonestreet One, LLC. + */ + public static final int STONESTREET_ONE = 0x005E; + + /* + * Wicentric, Inc. + */ + public static final int WICENTRIC = 0x005F; + + /* + * RivieraWaves S.A.S. + */ + public static final int RIVIERAWAVES = 0x0060; + + /* + * You can't instantiate one of these. + */ + private BluetoothAssignedNumbers() { + } + +} diff --git a/core/java/android/bluetooth/BluetoothClass.java b/core/java/android/bluetooth/BluetoothClass.java index c7fea9e1dad026f728b5795be98d2bcb575a1c78..c8381c96a39a7e0c99b45ac7e34dd065f660dea2 100644 --- a/core/java/android/bluetooth/BluetoothClass.java +++ b/core/java/android/bluetooth/BluetoothClass.java @@ -259,6 +259,12 @@ public final class BluetoothClass implements Parcelable { public static final int PROFILE_A2DP = 1; /** @hide */ public static final int PROFILE_OPP = 2; + /** @hide */ + public static final int PROFILE_HID = 3; + /** @hide */ + public static final int PROFILE_PANU = 4; + /** @hide */ + public static final int PROFILE_NAP = 5; /** * Check class bits for possible bluetooth profile support. @@ -324,6 +330,14 @@ public final class BluetoothClass implements Parcelable { default: return false; } + } else if (profile == PROFILE_HID) { + return (getDeviceClass() & Device.Major.PERIPHERAL) == Device.Major.PERIPHERAL; + } else if (profile == PROFILE_PANU || profile == PROFILE_NAP){ + // No good way to distinguish between the two, based on class bits. + if (hasService(Service.NETWORKING)) { + return true; + } + return (getDeviceClass() & Device.Major.NETWORKING) == Device.Major.NETWORKING; } else { return false; } diff --git a/core/java/android/bluetooth/BluetoothDevicePicker.java b/core/java/android/bluetooth/BluetoothDevicePicker.java index 05eed0e6de633c6f5db7c4990a11f206d991cc54..741572151ffa2320d66483f65b4cba427189f70b 100644 --- a/core/java/android/bluetooth/BluetoothDevicePicker.java +++ b/core/java/android/bluetooth/BluetoothDevicePicker.java @@ -63,4 +63,9 @@ public interface BluetoothDevicePicker { 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; + /** Ask device picker to show BT devices that support + * Personal Area Networking User (PANU) profile*/ + public static final int FILTER_TYPE_PANU = 3; + /** Ask device picker to show BT devices that support Network Access Point (NAP) profile */ + public static final int FILTER_TYPE_NAP = 4; } diff --git a/core/java/android/bluetooth/BluetoothDeviceProfileState.java b/core/java/android/bluetooth/BluetoothDeviceProfileState.java index 8e655e213d62450dedf4045e7dccaaea9b4f3b55..1fd71518dee3e99cae2eb755402ba8cff9dfec99 100644 --- a/core/java/android/bluetooth/BluetoothDeviceProfileState.java +++ b/core/java/android/bluetooth/BluetoothDeviceProfileState.java @@ -58,19 +58,24 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine private static final String TAG = "BluetoothDeviceProfileState"; private static final boolean DBG = true; //STOPSHIP - Change to false + // TODO(): Restructure the state machine to make it scalable with regard to profiles. public static final int CONNECT_HFP_OUTGOING = 1; public static final int CONNECT_HFP_INCOMING = 2; public static final int CONNECT_A2DP_OUTGOING = 3; public static final int CONNECT_A2DP_INCOMING = 4; + public static final int CONNECT_HID_OUTGOING = 5; + public static final int CONNECT_HID_INCOMING = 6; - public static final int DISCONNECT_HFP_OUTGOING = 5; - private static final int DISCONNECT_HFP_INCOMING = 6; - public static final int DISCONNECT_A2DP_OUTGOING = 7; - public static final int DISCONNECT_A2DP_INCOMING = 8; + public static final int DISCONNECT_HFP_OUTGOING = 50; + private static final int DISCONNECT_HFP_INCOMING = 51; + public static final int DISCONNECT_A2DP_OUTGOING = 52; + public static final int DISCONNECT_A2DP_INCOMING = 53; + public static final int DISCONNECT_HID_OUTGOING = 54; + public static final int DISCONNECT_HID_INCOMING = 55; - public static final int UNPAIR = 9; - public static final int AUTO_CONNECT_PROFILES = 10; - public static final int TRANSITION_TO_STABLE = 11; + public static final int UNPAIR = 100; + public static final int AUTO_CONNECT_PROFILES = 101; + public static final int TRANSITION_TO_STABLE = 102; private static final int AUTO_CONNECT_DELAY = 6000; // 6 secs @@ -79,6 +84,8 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine private IncomingHandsfree mIncomingHandsfree = new IncomingHandsfree(); private IncomingA2dp mIncomingA2dp = new IncomingA2dp(); private OutgoingA2dp mOutgoingA2dp = new OutgoingA2dp(); + private OutgoingHid mOutgoingHid = new OutgoingHid(); + private IncomingHid mIncomingHid = new IncomingHid(); private Context mContext; private BluetoothService mService; @@ -89,6 +96,7 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine private BluetoothDevice mDevice; private int mHeadsetState; private int mA2dpState; + private int mHidState; private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -125,6 +133,19 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine newState == BluetoothA2dp.STATE_DISCONNECTED) { sendMessage(TRANSITION_TO_STABLE); } + } else if (action.equals(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED)) { + int newState = intent.getIntExtra(BluetoothInputDevice.EXTRA_INPUT_DEVICE_STATE, 0); + int oldState = + intent.getIntExtra(BluetoothInputDevice.EXTRA_PREVIOUS_INPUT_DEVICE_STATE, 0); + mHidState = newState; + if (oldState == BluetoothInputDevice.STATE_CONNECTED && + newState == BluetoothInputDevice.STATE_DISCONNECTED) { + sendMessage(DISCONNECT_HID_INCOMING); + } + if (newState == BluetoothInputDevice.STATE_CONNECTED || + newState == BluetoothInputDevice.STATE_DISCONNECTED) { + sendMessage(TRANSITION_TO_STABLE); + } } else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { if (!getCurrentState().equals(mBondedDevice)) { Log.e(TAG, "State is: " + getCurrentState()); @@ -165,6 +186,8 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine addState(mIncomingHandsfree); addState(mIncomingA2dp); addState(mOutgoingA2dp); + addState(mOutgoingHid); + addState(mIncomingHid); setInitialState(mBondedDevice); IntentFilter filter = new IntentFilter(); @@ -172,6 +195,7 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine filter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); filter.addAction(BluetoothHeadset.ACTION_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + filter.addAction(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED); mContext.registerReceiver(mBroadcastReceiver, filter); @@ -224,6 +248,14 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine case DISCONNECT_A2DP_INCOMING: transitionTo(mIncomingA2dp); break; + case CONNECT_HID_OUTGOING: + case DISCONNECT_HID_OUTGOING: + transitionTo(mOutgoingHid); + break; + case CONNECT_HID_INCOMING: + case DISCONNECT_HID_INCOMING: + transitionTo(mIncomingHid); + break; case UNPAIR: if (mHeadsetState != BluetoothHeadset.STATE_DISCONNECTED) { sendMessage(DISCONNECT_HFP_OUTGOING); @@ -233,6 +265,10 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine sendMessage(DISCONNECT_A2DP_OUTGOING); deferMessage(message); break; + } else if (mHidState != BluetoothInputDevice.STATE_DISCONNECTED) { + sendMessage(DISCONNECT_HID_OUTGOING); + deferMessage(message); + break; } processCommand(UNPAIR); break; @@ -254,6 +290,10 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine mA2dpService.getConnectedSinks().length == 0) { mA2dpService.connectSink(mDevice); } + if (mService.getInputDevicePriority(mDevice) == + BluetoothInputDevice.PRIORITY_AUTO_CONNECT) { + mService.connectInputDevice(mDevice); + } } break; case TRANSITION_TO_STABLE: @@ -342,6 +382,23 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine deferMessage(deferMsg); } break; + case CONNECT_HID_OUTGOING: + case DISCONNECT_HID_OUTGOING: + deferMessage(message); + break; + case CONNECT_HID_INCOMING: + transitionTo(mIncomingHid); + if (mStatus) { + deferMsg.what = mCommand; + deferMessage(deferMsg); + } + break; + case DISCONNECT_HID_INCOMING: + if (mStatus) { + deferMsg.what = mCommand; + deferMessage(deferMsg); + } + break; // ignore case UNPAIR: case AUTO_CONNECT_PROFILES: deferMessage(message); @@ -409,6 +466,13 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine // If this causes incoming HFP to fail, it is more of a headset problem // since both connections are incoming ones. break; + case CONNECT_HID_OUTGOING: + case DISCONNECT_HID_OUTGOING: + deferMessage(message); + break; + case CONNECT_HID_INCOMING: + case DISCONNECT_HID_INCOMING: + break; // ignore case UNPAIR: case AUTO_CONNECT_PROFILES: deferMessage(message); @@ -496,6 +560,23 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine case DISCONNECT_A2DP_INCOMING: // Ignore, will be handled by Bluez break; + case CONNECT_HID_OUTGOING: + case DISCONNECT_HID_OUTGOING: + deferMessage(message); + break; + case CONNECT_HID_INCOMING: + transitionTo(mIncomingHid); + if (mStatus) { + deferMsg.what = mCommand; + deferMessage(deferMsg); + } + break; + case DISCONNECT_HID_INCOMING: + if (mStatus) { + deferMsg.what = mCommand; + deferMessage(deferMsg); + } + break; // ignore case UNPAIR: case AUTO_CONNECT_PROFILES: deferMessage(message); @@ -561,6 +642,13 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine case DISCONNECT_A2DP_INCOMING: // Ignore, will be handled by Bluez break; + case CONNECT_HID_OUTGOING: + case DISCONNECT_HID_OUTGOING: + deferMessage(message); + break; + case CONNECT_HID_INCOMING: + case DISCONNECT_HID_INCOMING: + break; // ignore case UNPAIR: case AUTO_CONNECT_PROFILES: deferMessage(message); @@ -576,6 +664,143 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine } + private class OutgoingHid extends HierarchicalState { + private boolean mStatus = false; + private int mCommand; + + @Override + protected void enter() { + log("Entering OutgoingHid state with: " + getCurrentMessage().what); + mCommand = getCurrentMessage().what; + if (mCommand != CONNECT_HID_OUTGOING && + mCommand != DISCONNECT_HID_OUTGOING) { + Log.e(TAG, "Error: OutgoingHid state with command:" + mCommand); + } + mStatus = processCommand(mCommand); + if (!mStatus) sendMessage(TRANSITION_TO_STABLE); + } + + @Override + protected boolean processMessage(Message message) { + log("OutgoingHid State->Processing Message: " + message.what); + Message deferMsg = new Message(); + switch(message.what) { + // defer all outgoing messages + case CONNECT_HFP_OUTGOING: + case CONNECT_A2DP_OUTGOING: + case CONNECT_HID_OUTGOING: + case DISCONNECT_HFP_OUTGOING: + case DISCONNECT_A2DP_OUTGOING: + case DISCONNECT_HID_OUTGOING: + deferMessage(message); + break; + + case CONNECT_HFP_INCOMING: + transitionTo(mIncomingHandsfree); + case CONNECT_A2DP_INCOMING: + transitionTo(mIncomingA2dp); + + // Don't cancel HID outgoing as there is no guarantee it + // will get canceled. + // It might already be connected but we might not have got the + // INPUT_DEVICE_STATE_CHANGE. Hence, no point disconnecting here. + // The worst case, the connection will fail, retry. + if (mStatus) { + deferMsg.what = mCommand; + deferMessage(deferMsg); + } + break; + case CONNECT_HID_INCOMING: + // Bluez will take care of the conflicts + transitionTo(mIncomingHid); + break; + + case DISCONNECT_HFP_INCOMING: + case DISCONNECT_A2DP_INCOMING: + // At this point, we are already disconnected + // with HFP. Sometimes HID connection can + // fail due to the disconnection of HFP. So add a retry + // for the HID. + if (mStatus) { + deferMsg.what = mCommand; + deferMessage(deferMsg); + } + break; + case DISCONNECT_HID_INCOMING: + // Ignore, will be handled by Bluez + break; + + case UNPAIR: + case AUTO_CONNECT_PROFILES: + deferMessage(message); + break; + case TRANSITION_TO_STABLE: + transitionTo(mBondedDevice); + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + } + + private class IncomingHid extends HierarchicalState { + private boolean mStatus = false; + private int mCommand; + + @Override + protected void enter() { + log("Entering IncomingHid state with: " + getCurrentMessage().what); + mCommand = getCurrentMessage().what; + if (mCommand != CONNECT_HID_INCOMING && + mCommand != DISCONNECT_HID_INCOMING) { + Log.e(TAG, "Error: IncomingHid state with command:" + mCommand); + } + mStatus = processCommand(mCommand); + if (!mStatus) sendMessage(TRANSITION_TO_STABLE); + } + + @Override + protected boolean processMessage(Message message) { + log("IncomingHid State->Processing Message: " + message.what); + Message deferMsg = new Message(); + switch(message.what) { + case CONNECT_HFP_OUTGOING: + case CONNECT_HFP_INCOMING: + case DISCONNECT_HFP_OUTGOING: + case CONNECT_A2DP_INCOMING: + case CONNECT_A2DP_OUTGOING: + case DISCONNECT_A2DP_OUTGOING: + case CONNECT_HID_OUTGOING: + case CONNECT_HID_INCOMING: + case DISCONNECT_HID_OUTGOING: + deferMessage(message); + break; + case DISCONNECT_HFP_INCOMING: + // Shouldn't happen but if does, we can handle it. + // Depends if the headset can handle it. + // Incoming HID will be handled by Bluez, Disconnect HFP + // the socket would have already been closed. + // ignore + break; + case DISCONNECT_HID_INCOMING: + case DISCONNECT_A2DP_INCOMING: + // Ignore, will be handled by Bluez + break; + case UNPAIR: + case AUTO_CONNECT_PROFILES: + deferMessage(message); + break; + case TRANSITION_TO_STABLE: + transitionTo(mBondedDevice); + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + } + synchronized void cancelCommand(int command) { if (command == CONNECT_HFP_OUTGOING ) { @@ -619,6 +844,10 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine case CONNECT_A2DP_INCOMING: // ignore, Bluez takes care return true; + case CONNECT_HID_OUTGOING: + return mService.connectInputDeviceInternal(mDevice); + case CONNECT_HID_INCOMING: + return true; case DISCONNECT_HFP_OUTGOING: if (!mHeadsetServiceConnected) { deferHeadsetMessage(command); @@ -645,6 +874,15 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine return mA2dpService.disconnectSinkInternal(mDevice); } break; + case DISCONNECT_HID_INCOMING: + // ignore + return true; + case DISCONNECT_HID_OUTGOING: + if (mService.getInputDevicePriority(mDevice) == + BluetoothInputDevice.PRIORITY_AUTO_CONNECT) { + mService.setInputDevicePriority(mDevice, BluetoothInputDevice.PRIORITY_ON); + } + return mService.disconnectInputDeviceInternal(mDevice); case UNPAIR: return mService.removeBondInternal(mDevice.getAddress()); default: @@ -653,6 +891,7 @@ public final class BluetoothDeviceProfileState extends HierarchicalStateMachine return false; } + /*package*/ BluetoothDevice getDevice() { return mDevice; } diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 4a91a8c907219ad50baf90b0ca6e8766e87782a3..be21d46430d46e979b0d136289c156ac252acafc 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -45,7 +45,7 @@ import android.util.Log; * This BluetoothHeadset object is not immediately bound to the * BluetoothHeadset 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 BluetootHeadset after construction. + * immediately call methods on BluetoothHeadset after construction. * * Android only supports one connected Bluetooth Headset at a time. * @@ -84,6 +84,47 @@ public final class BluetoothHeadset { public static final String EXTRA_DISCONNECT_INITIATOR = "android.bluetooth.headset.extra.DISCONNECT_INITIATOR"; + /** + * Broadcast Action: Indicates a headset has posted a vendor-specific event. + *

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

      Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_VENDOR_SPECIFIC_HEADSET_EVENT = + "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT"; + + /** + * A String extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} + * intents that contains the name of the vendor-specific command. + * @hide + */ + public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD = + "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD"; + + /** + * An int extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} + * intents that contains the Company ID of the vendor defining the vendor-specific + * command. + * @see + * Bluetooth SIG Assigned Numbers - Company Identifiers + * @hide + */ + public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID = + "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID"; + + /** + * A Parcelable String array extra field in + * {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intents that contains + * the arguments to the vendor-specific command. + * @hide + */ + public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS = + "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_ARGS"; + + /** * TODO(API release): Consider incorporating as new state in * HEADSET_STATE_CHANGED @@ -108,7 +149,7 @@ public final class BluetoothHeadset { public static final int RESULT_FAILURE = 0; public static final int RESULT_SUCCESS = 1; - /** Connection canceled before completetion. */ + /** Connection canceled before completion. */ public static final int RESULT_CANCELED = 2; /** Values for {@link #EXTRA_DISCONNECT_INITIATOR} */ diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java new file mode 100644 index 0000000000000000000000000000000000000000..179383827dcbbc838f1440d0d2276c186dca031a --- /dev/null +++ b/core/java/android/bluetooth/BluetoothInputDevice.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Public API for controlling the Bluetooth HID (Input Device) Profile + * + * BluetoothInputDevice is a proxy object used to make calls to Bluetooth Service + * which handles the HID profile. + * + * Creating a BluetoothInputDevice object will initiate a binding with the + * Bluetooth service. Users of this object should call close() when they + * are finished, so that this proxy object can unbind from the service. + * + * Currently the Bluetooth service runs in the system server and this + * proxy object will be immediately bound to the service on construction. + * + * @hide + */ +public final class BluetoothInputDevice { + private static final String TAG = "BluetoothInputDevice"; + private static final boolean DBG = false; + + /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */ + public static final String EXTRA_INPUT_DEVICE_STATE = + "android.bluetooth.inputdevice.extra.INPUT_DEVICE_STATE"; + /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */ + public static final String EXTRA_PREVIOUS_INPUT_DEVICE_STATE = + "android.bluetooth.inputdevice.extra.PREVIOUS_INPUT_DEVICE_STATE"; + + /** Indicates the state of an input device has changed. + * This intent will always contain EXTRA_INPUT_DEVICE_STATE, + * EXTRA_PREVIOUS_INPUT_DEVICE_STATE and BluetoothDevice.EXTRA_DEVICE + * extras. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_INPUT_DEVICE_STATE_CHANGED = + "android.bluetooth.inputdevice.action.INPUT_DEVICE_STATE_CHANGED"; + + public static final int STATE_DISCONNECTED = 0; + public static final int STATE_CONNECTING = 1; + public static final int STATE_CONNECTED = 2; + public static final int STATE_DISCONNECTING = 3; + + /** + * Auto connection, incoming and outgoing connection are allowed at this + * priority level. + */ + public static final int PRIORITY_AUTO_CONNECT = 1000; + /** + * Incoming and outgoing connection are allowed at this priority level + */ + public static final int PRIORITY_ON = 100; + /** + * Connections to the device are not allowed at this priority level. + */ + public static final int PRIORITY_OFF = 0; + /** + * Default priority level when the device is unpaired. + */ + public static final int PRIORITY_UNDEFINED = -1; + + private final IBluetooth mService; + private final Context mContext; + + /** + * Create a BluetoothInputDevice proxy object for interacting with the local + * Bluetooth Service which handle the HID profile. + * @param c Context + */ + public BluetoothInputDevice(Context c) { + mContext = c; + + IBinder b = ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE); + if (b != null) { + mService = IBluetooth.Stub.asInterface(b); + } else { + Log.w(TAG, "Bluetooth 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; + } + } + + /** Initiate a connection to an Input device. + * + * This function returns false on error and true if the connection + * attempt is being made. + * + * Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when the + * connection is completed. + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean connectInputDevice(BluetoothDevice device) { + if (DBG) log("connectInputDevice(" + device + ")"); + try { + return mService.connectInputDevice(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** Initiate disconnect from an Input Device. + * This function return false on error and true if the disconnection + * attempt is being made. + * + * Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when + * disconnect is completed. + * + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean disconnectInputDevice(BluetoothDevice device) { + if (DBG) log("disconnectInputDevice(" + device + ")"); + try { + return mService.disconnectInputDevice(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** Check if a specified InputDevice is connected. + * + * @param device Remote BT device. + * @return True if connected , false otherwise and on error. + * @hide + */ + public boolean isInputDeviceConnected(BluetoothDevice device) { + if (DBG) log("isInputDeviceConnected(" + device + ")"); + int state = getInputDeviceState(device); + if (state == STATE_CONNECTED) return true; + return false; + } + + /** Check if any Input Device is connected. + * + * @return a unmodifiable set of connected Input Devices, or null on error. + * @hide + */ + public Set getConnectedInputDevices() { + if (DBG) log("getConnectedInputDevices()"); + try { + return Collections.unmodifiableSet( + new HashSet( + Arrays.asList(mService.getConnectedInputDevices()))); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return null; + } + } + + /** Get the state of an Input Device. + * + * @param device Remote BT device. + * @return The current state of the Input Device + * @hide + */ + public int getInputDeviceState(BluetoothDevice device) { + if (DBG) log("getInputDeviceState(" + device + ")"); + try { + return mService.getInputDeviceState(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return STATE_DISCONNECTED; + } + } + + /** + * Set priority of an input device. + * + * Priority is a non-negative integer. Priority can take the following + * values: + * {@link PRIORITY_ON}, {@link PRIORITY_OFF}, {@link PRIORITY_AUTO_CONNECT} + * + * @param device Paired device. + * @param priority Integer priority + * @return true if priority is set, false on error + */ + public boolean setInputDevicePriority(BluetoothDevice device, int priority) { + if (DBG) log("setInputDevicePriority(" + device + ", " + priority + ")"); + try { + return mService.setInputDevicePriority(device, priority); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** + * Get the priority associated with an Input Device. + * + * @param device Input Device + * @return non-negative priority, or negative error code on error. + */ + public int getInputDevicePriority(BluetoothDevice device) { + if (DBG) log("getInputDevicePriority(" + device + ")"); + try { + return mService.getInputDevicePriority(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return PRIORITY_OFF; + } + } + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/BluetoothPan.java b/core/java/android/bluetooth/BluetoothPan.java new file mode 100644 index 0000000000000000000000000000000000000000..9d0b3f20a5dd2f72f3634747610b519139f3ef39 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothPan.java @@ -0,0 +1,192 @@ +/* + * 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; +import android.content.Context; +import android.os.ServiceManager; +import android.os.RemoteException; +import android.os.IBinder; +import android.util.Log; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * @hide + */ +public final class BluetoothPan { + private static final String TAG = "BluetoothPan"; + private static final boolean DBG = false; + + /** int extra for ACTION_PAN_STATE_CHANGED */ + public static final String EXTRA_PAN_STATE = + "android.bluetooth.pan.extra.STATE"; + /** int extra for ACTION_PAN_STATE_CHANGED */ + public static final String EXTRA_PREVIOUS_PAN_STATE = + "android.bluetooth.pan.extra.PREVIOUS_STATE"; + + /** Indicates the state of an PAN device has changed. + * This intent will always contain EXTRA_DEVICE_STATE, + * EXTRA_PREVIOUS_DEVICE_STATE and BluetoothDevice.EXTRA_DEVICE + * extras. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PAN_STATE_CHANGED = + "android.bluetooth.pan.action.STATE_CHANGED"; + + public static final String NAP_ROLE = "nap"; + public static final String NAP_BRIDGE = "pan1"; + + public static final int MAX_CONNECTIONS = 7; + + public static final int STATE_DISCONNECTED = 0; + public static final int STATE_CONNECTING = 1; + public static final int STATE_CONNECTED = 2; + public static final int STATE_DISCONNECTING = 3; + + private final IBluetooth mService; + private final Context mContext; + + /** + * Create a BluetoothPan proxy object for interacting with the local + * Bluetooth Pan service. + * @param c Context + */ + public BluetoothPan(Context c) { + mContext = c; + + IBinder b = ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE); + if (b != null) { + mService = IBluetooth.Stub.asInterface(b); + } else { + Log.w(TAG, "Bluetooth 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; + } + } + + /** + * Initiate a PAN connection. + * + * This function returns false on error and true if the connection + * attempt is being made. + * + * Listen for {@link #ACTION_PAN_STATE_CHANGED} to find out when the + * connection is completed. + * + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean connect(BluetoothDevice device) { + if (DBG) log("connect(" + device + ")"); + try { + return mService.connectPanDevice(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** + * Initiate disconnect from PAN. + * + * This function return false on error and true if the disconnection + * attempt is being made. + * + * Listen for {@link #ACTION_PAN_STATE_CHANGED} to find out when + * disconnect is completed. + * + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean disconnect(BluetoothDevice device) { + if (DBG) log("disconnect(" + device + ")"); + try { + return mService.disconnectPanDevice(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** Get the state of a PAN Device. + * + * This function returns an int representing the state of the PAN connection + * + * @param device Remote BT device. + * @return The current state of the PAN Device + * @hide + */ + public int getPanDeviceState(BluetoothDevice device) { + if (DBG) log("getPanDeviceState(" + device + ")"); + try { + return mService.getPanDeviceState(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return STATE_DISCONNECTED; + } + } + + /** Returns a set of all the connected PAN Devices + * + * Does not include devices that are currently connecting or disconnecting + * + * @return a unmodifiable set of connected PAN Devices, or null on error. + * @hide + */ + public Set getConnectedDevices() { + if (DBG) log("getConnectedDevices"); + try { + return Collections.unmodifiableSet( + new HashSet( + Arrays.asList(mService.getConnectedPanDevices()))); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return null; + } + } + + private static void log(String msg) { + Log.d(TAG, msg); + } + + public void setBluetoothTethering(boolean value) { + try { + mService.setBluetoothTethering(value); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + } + + public boolean isTetheringOn() { + try { + return mService.isTetheringOn(); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } +} diff --git a/core/java/android/bluetooth/BluetoothProfileState.java b/core/java/android/bluetooth/BluetoothProfileState.java index 946dcaa01c867faa2d533ab03a6ce5e9009d9206..ad70d0de0ef4bd3b3996a565a6b55582ccecff71 100644 --- a/core/java/android/bluetooth/BluetoothProfileState.java +++ b/core/java/android/bluetooth/BluetoothProfileState.java @@ -43,8 +43,9 @@ public class BluetoothProfileState extends HierarchicalStateMachine { private static final boolean DBG = true; // STOPSHIP - change to false. private static final String TAG = "BluetoothProfileState"; - public static int HFP = 0; - public static int A2DP = 1; + public static final int HFP = 0; + public static final int A2DP = 1; + public static final int HID = 2; private static int TRANSITION_TO_STABLE = 100; @@ -70,6 +71,12 @@ public class BluetoothProfileState extends HierarchicalStateMachine { newState == BluetoothA2dp.STATE_DISCONNECTED)) { sendMessage(TRANSITION_TO_STABLE); } + } else if (action.equals(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED)) { + int newState = intent.getIntExtra(BluetoothInputDevice.EXTRA_INPUT_DEVICE_STATE, 0); + if (mProfile == HID && (newState == BluetoothInputDevice.STATE_CONNECTED || + newState == BluetoothInputDevice.STATE_DISCONNECTED)) { + sendMessage(TRANSITION_TO_STABLE); + } } } }; @@ -84,6 +91,7 @@ public class BluetoothProfileState extends HierarchicalStateMachine { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); filter.addAction(BluetoothHeadset.ACTION_STATE_CHANGED); + filter.addAction(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED); context.registerReceiver(mBroadcastReceiver, filter); } diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java index 4164a3d6e66587f83c59b1311697383804947811..3fbfc70bdb148a2c0b78006bc9fb207f0040e78c 100644 --- a/core/java/android/bluetooth/BluetoothUuid.java +++ b/core/java/android/bluetooth/BluetoothUuid.java @@ -49,10 +49,18 @@ public final class BluetoothUuid { ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"); public static final ParcelUuid ObexObjectPush = ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb"); + public static final ParcelUuid Hid = + ParcelUuid.fromString("00001124-0000-1000-8000-00805f9b34fb"); + public static final ParcelUuid PANU = + ParcelUuid.fromString("00001115-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid NAP = + ParcelUuid.fromString("00001116-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid BNEP = + ParcelUuid.fromString("0000000f-0000-1000-8000-00805F9B34FB"); public static final ParcelUuid[] RESERVED_UUIDS = { AudioSink, AudioSource, AdvAudioDist, HSP, Handsfree, AvrcpController, AvrcpTarget, - ObexObjectPush}; + ObexObjectPush, PANU, NAP}; public static boolean isAudioSource(ParcelUuid uuid) { return uuid.equals(AudioSource); @@ -82,6 +90,21 @@ public final class BluetoothUuid { return uuid.equals(AvrcpTarget); } + public static boolean isInputDevice(ParcelUuid uuid) { + return uuid.equals(Hid); + } + + public static boolean isPanu(ParcelUuid uuid) { + return uuid.equals(PANU); + } + + public static boolean isNap(ParcelUuid uuid) { + return uuid.equals(NAP); + } + + public static boolean isBnep(ParcelUuid uuid) { + return uuid.equals(BNEP); + } /** * Returns true if ParcelUuid is present in uuidArray * diff --git a/core/java/android/bluetooth/HeadsetBase.java b/core/java/android/bluetooth/HeadsetBase.java index e2935c95d324a423c2550c519b1e53912c4f5880..9ef2eb5f3cec82654b95a1d672ca9945dcf9223f 100644 --- a/core/java/android/bluetooth/HeadsetBase.java +++ b/core/java/android/bluetooth/HeadsetBase.java @@ -74,8 +74,8 @@ public final class HeadsetBase { private native void cleanupNativeDataNative(); - public HeadsetBase(PowerManager pm, BluetoothAdapter adapter, BluetoothDevice device, - int rfcommChannel) { + public HeadsetBase(PowerManager pm, BluetoothAdapter adapter, + BluetoothDevice device, int rfcommChannel) { mDirection = DIRECTION_OUTGOING; mConnectTimestamp = System.currentTimeMillis(); mAdapter = adapter; @@ -89,9 +89,10 @@ public final class HeadsetBase { initializeNativeDataNative(-1); } - /* Create from an already exisiting rfcomm connection */ - public HeadsetBase(PowerManager pm, BluetoothAdapter adapter, BluetoothDevice device, - int socketFd, int rfcommChannel, Handler handler) { + /* Create from an existing rfcomm connection */ + public HeadsetBase(PowerManager pm, BluetoothAdapter adapter, + BluetoothDevice device, + int socketFd, int rfcommChannel, Handler handler) { mDirection = DIRECTION_INCOMING; mConnectTimestamp = System.currentTimeMillis(); mAdapter = adapter; @@ -128,7 +129,7 @@ public final class HeadsetBase { (System.currentTimeMillis() - timestamp) + " ms"); if (result.getResultCode() == AtCommandResult.ERROR) { - Log.i(TAG, "Error pocessing <" + input + ">"); + Log.i(TAG, "Error processing <" + input + ">"); } sendURC(result.toString()); @@ -142,8 +143,9 @@ public final class HeadsetBase { */ protected void initializeAtParser() { mAtParser = new AtParser(); - //TODO(): Get rid of this as there are no parsers registered. But because of dependencies, - //it needs to be done as part of refactoring HeadsetBase and BluetoothHandsfree + + //TODO(): Get rid of this as there are no parsers registered. But because of dependencies + // it needs to be done as part of refactoring HeadsetBase and BluetoothHandsfree } public AtParser getAtParser() { @@ -159,8 +161,7 @@ public final class HeadsetBase { String input = readNative(500); if (input != null) { handleInput(input); - } - else { + } else { last_read_error = getLastReadStatusNative(); if (last_read_error != 0) { Log.i(TAG, "headset read error " + last_read_error); @@ -179,8 +180,6 @@ public final class HeadsetBase { mEventThread.start(); } - - private native String readNative(int timeout_ms); private native int getLastReadStatusNative(); diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl index 6dd8dd64d844a6fac1ee23a73b2b86881d3474eb..cc231466e304769351b7edc0a063a053495b2c0f 100644 --- a/core/java/android/bluetooth/IBluetooth.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -17,6 +17,7 @@ package android.bluetooth; import android.bluetooth.IBluetoothCallback; +import android.bluetooth.BluetoothDevice; import android.os.ParcelUuid; /** @@ -76,4 +77,19 @@ interface IBluetooth boolean connectHeadset(String address); boolean disconnectHeadset(String address); boolean notifyIncomingConnection(String address); + + // HID profile APIs + boolean connectInputDevice(in BluetoothDevice device); + boolean disconnectInputDevice(in BluetoothDevice device); + BluetoothDevice[] getConnectedInputDevices(); // change to Set<> once AIDL supports + int getInputDeviceState(in BluetoothDevice device); + boolean setInputDevicePriority(in BluetoothDevice device, int priority); + int getInputDevicePriority(in BluetoothDevice device); + + boolean isTetheringOn(); + void setBluetoothTethering(boolean value); + int getPanDeviceState(in BluetoothDevice device); + BluetoothDevice[] getConnectedPanDevices(); + boolean connectPanDevice(in BluetoothDevice device); + boolean disconnectPanDevice(in BluetoothDevice device); } diff --git a/core/java/android/bluetooth/ScoSocket.java b/core/java/android/bluetooth/ScoSocket.java deleted file mode 100644 index b65a99a048e35c40df9ca9461971937c3fee0ad3..0000000000000000000000000000000000000000 --- a/core/java/android/bluetooth/ScoSocket.java +++ /dev/null @@ -1,206 +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.os.Handler; -import android.os.Message; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.util.Log; - -/** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * Simple SCO Socket. - * Currently in Android, there is no support for sending data over a SCO - * socket - this is managed by the hardware link to the Bluetooth Chip. This - * class is instead intended for management of the SCO socket lifetime, - * and is tailored for use with the headset / handsfree profiles. - * @hide - */ -public class ScoSocket { - private static final String TAG = "ScoSocket"; - private static final boolean DBG = true; - private static final boolean VDBG = false; // even more logging - - public static final int STATE_READY = 1; // Ready for use. No threads or sockets - public static final int STATE_ACCEPT = 2; // accept() thread running - public static final int STATE_CONNECTING = 3; // connect() thread running - public static final int STATE_CONNECTED = 4; // connected, waiting for close() - public static final int STATE_CLOSED = 5; // was connected, now closed. - - private int mState; - private int mNativeData; - private Handler mHandler; - private int mAcceptedCode; - private int mConnectedCode; - private int mClosedCode; - - private WakeLock mWakeLock; // held while in STATE_CONNECTING - - static { - classInitNative(); - } - private native static void classInitNative(); - - public ScoSocket(PowerManager pm, Handler handler, int acceptedCode, int connectedCode, - int closedCode) { - initNative(); - mState = STATE_READY; - mHandler = handler; - mAcceptedCode = acceptedCode; - mConnectedCode = connectedCode; - mClosedCode = closedCode; - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ScoSocket"); - mWakeLock.setReferenceCounted(false); - if (VDBG) log(this + " SCO OBJECT CTOR"); - } - private native void initNative(); - - protected void finalize() throws Throwable { - try { - if (VDBG) log(this + " SCO OBJECT DTOR"); - destroyNative(); - releaseWakeLockNow(); - } finally { - super.finalize(); - } - } - private native void destroyNative(); - - /** Connect this SCO socket to the given BT address. - * Does not block. - */ - public synchronized boolean connect(String address, String name) { - if (DBG) log("connect() " + this); - if (mState != STATE_READY) { - if (DBG) log("connect(): Bad state"); - return false; - } - acquireWakeLock(); - if (connectNative(address, name)) { - mState = STATE_CONNECTING; - return true; - } else { - mState = STATE_CLOSED; - releaseWakeLockNow(); - return false; - } - } - private native boolean connectNative(String address, String name); - - /** Accept incoming SCO connections. - * Does not block. - */ - public synchronized boolean accept() { - if (VDBG) log("accept() " + this); - if (mState != STATE_READY) { - if (DBG) log("Bad state"); - return false; - } - if (acceptNative()) { - mState = STATE_ACCEPT; - return true; - } else { - mState = STATE_CLOSED; - return false; - } - } - private native boolean acceptNative(); - - public synchronized void close() { - if (DBG) log(this + " SCO OBJECT close() mState = " + mState); - acquireWakeLock(); - mState = STATE_CLOSED; - closeNative(); - releaseWakeLock(); - } - private native void closeNative(); - - public synchronized int getState() { - return mState; - } - - private synchronized void onConnected(int result) { - if (VDBG) log(this + " onConnected() mState = " + mState + " " + this); - if (mState != STATE_CONNECTING) { - if (DBG) log("Strange state, closing " + mState + " " + this); - return; - } - if (result >= 0) { - mState = STATE_CONNECTED; - } else { - mState = STATE_CLOSED; - } - mHandler.obtainMessage(mConnectedCode, mState, -1, this).sendToTarget(); - releaseWakeLockNow(); - } - - private synchronized void onAccepted(int result) { - if (VDBG) log("onAccepted() " + this); - if (mState != STATE_ACCEPT) { - if (DBG) log("Strange state " + this); - return; - } - if (result >= 0) { - mState = STATE_CONNECTED; - } else { - mState = STATE_CLOSED; - } - mHandler.obtainMessage(mAcceptedCode, mState, -1, this).sendToTarget(); - } - - private synchronized void onClosed() { - if (DBG) log("onClosed() " + this); - if (mState != STATE_CLOSED) { - mState = STATE_CLOSED; - mHandler.obtainMessage(mClosedCode, mState, -1, this).sendToTarget(); - releaseWakeLock(); - } - } - - private void acquireWakeLock() { - if (!mWakeLock.isHeld()) { - mWakeLock.acquire(); - if (VDBG) log("mWakeLock.acquire() " + this); - } - } - - private void releaseWakeLock() { - if (mWakeLock.isHeld()) { - // Keep apps processor awake for a further 2 seconds. - // This is a hack to resolve issue http://b/1616263 - in which - // we are left in a 80 mA power state when remotely terminating a - // call while connected to BT headset "HTC BH S100 " with A2DP and - // HFP profiles. - if (VDBG) log("mWakeLock.release() in 2 sec" + this); - mWakeLock.acquire(2000); - } - } - - private void releaseWakeLockNow() { - if (mWakeLock.isHeld()) { - if (VDBG) log("mWakeLock.release() now" + this); - mWakeLock.release(); - } - } - - private void log(String msg) { - Log.d(TAG, msg); - } -} diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..b19c0724b1e510b86acd8a9ca0716e517c85889a --- /dev/null +++ b/core/java/android/content/AsyncTaskLoader.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.AsyncTask; + +/** + * Abstract Loader that provides an {@link AsyncTask} to do the work. + * + * @param the data type to be loaded. + */ +public abstract class AsyncTaskLoader extends Loader { + final class LoadTask extends AsyncTask { + + private D result; + + /* Runs on a worker thread */ + @Override + protected D doInBackground(Void... params) { + result = AsyncTaskLoader.this.loadInBackground(); + return result; + } + + /* Runs on the UI thread */ + @Override + protected void onPostExecute(D data) { + AsyncTaskLoader.this.dispatchOnLoadComplete(data); + } + + @Override + protected void onCancelled() { + AsyncTaskLoader.this.onCancelled(result); + } + } + + LoadTask mTask; + + public AsyncTaskLoader(Context context) { + super(context); + } + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + @Override + public void forceLoad() { + cancelLoad(); + mTask = new LoadTask(); + mTask.execute((Void[]) null); + } + + /** + * Attempt to cancel the current load task. See {@link AsyncTask#cancel(boolean)} + * for more info. + * + * @return false if the task could not be canceled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called, and + * true otherwise + */ + public boolean cancelLoad() { + if (mTask != null) { + boolean cancelled = mTask.cancel(false); + mTask = null; + return cancelled; + } + return false; + } + + /** + * Called if the task was canceled before it was completed. Gives the class a chance + * to properly dispose of the result. + */ + public void onCancelled(D data) { + } + + void dispatchOnLoadComplete(D data) { + mTask = null; + deliverResult(data); + } + + /** + * Called on a worker thread to perform the actual load. Implementations should not deliver the + * results directly, but should return them from this method, which will eventually end up + * calling deliverResult on the UI thread. If implementations need to process + * the results on the UI thread they may override deliverResult and do so + * there. + * + * @return the result of the load + */ + public abstract D loadInBackground(); +} diff --git a/core/java/android/content/ClipData.aidl b/core/java/android/content/ClipData.aidl new file mode 100644 index 0000000000000000000000000000000000000000..5fc12ce1fe7eb97040cdff7935826e4133933a2e --- /dev/null +++ b/core/java/android/content/ClipData.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +parcelable ClipData; diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java new file mode 100644 index 0000000000000000000000000000000000000000..a19b13299ee740efbc0c2bc781898cb93713e0ec --- /dev/null +++ b/core/java/android/content/ClipData.java @@ -0,0 +1,494 @@ +/** + * Copyright (c) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +/** + * Representation of a clipped data on the clipboard. + * + *

      ClippedData is a complex type containing one or Item instances, + * each of which can hold one or more representations of an item of data. + * For display to the user, it also has a label and iconic representation.

      + * + *

      A ClipData is a sub-class of {@link ClipDescription}, which describes + * important meta-data about the clip. In particular, its {@link #getMimeType(int)} + * must return correct MIME type(s) describing the data in the clip. For help + * in correctly constructing a clip with the correct MIME type, use + * {@link #newPlainText(CharSequence, Bitmap, CharSequence)}, + * {@link #newUri(ContentResolver, CharSequence, Bitmap, Uri)}, and + * {@link #newIntent(CharSequence, Bitmap, Intent)}. + * + *

      Each Item instance can be one of three main classes of data: a simple + * CharSequence of text, a single Intent object, or a Uri. See {@link Item} + * for more details. + * + * + *

      Implementing Paste or Drop

      + * + *

      To implement a paste or drop of a ClippedData object into an application, + * the application must correctly interpret the data for its use. If the {@link Item} + * it contains is simple text or an Intent, there is little to be done: text + * can only be interpreted as text, and an Intent will typically be used for + * creating shortcuts (such as placing icons on the home screen) or other + * actions. + * + *

      If all you want is the textual representation of the clipped data, you + * can use the convenience method {@link Item#coerceToText Item.coerceToText}. + * In this case there is generally no need to worry about the MIME types + * reported by {@link #getMimeType(int)}, since any clip item an always be + * converted to a string. + * + *

      More complicated exchanges will be done through URIs, in particular + * "content:" URIs. A content URI allows the recipient of a ClippedData item + * to interact closely with the ContentProvider holding the data in order to + * negotiate the transfer of that data. The clip must also be filled in with + * the available MIME types; {@link #newUri(ContentResolver, CharSequence, Bitmap, Uri)} + * will take care of correctly doing this. + * + *

      For example, here is the paste function of a simple NotePad application. + * When retrieving the data from the clipboard, it can do either two things: + * if the clipboard contains a URI reference to an existing note, it copies + * the entire structure of the note into a new note; otherwise, it simply + * coerces the clip into text and uses that as the new note's contents. + * + * {@sample development/samples/NotePad/src/com/example/android/notepad/NoteEditor.java + * paste} + * + *

      In many cases an application can paste various types of streams of data. For + * example, an e-mail application may want to allow the user to paste an image + * or other binary data as an attachment. This is accomplished through the + * ContentResolver {@link ContentResolver#getStreamTypes(Uri, String)} and + * {@link ContentResolver#openTypedAssetFileDescriptor(Uri, String, android.os.Bundle)} + * methods. These allow a client to discover the type(s) of data that a particular + * content URI can make available as a stream and retrieve the stream of data. + * + *

      For example, the implementation of {@link Item#coerceToText Item.coerceToText} + * itself uses this to try to retrieve a URI clip as a stream of text: + * + * {@sample frameworks/base/core/java/android/content/ClipData.java coerceToText} + * + * + *

      Implementing Copy or Drag

      + * + *

      To be the source of a clip, the application must construct a ClippedData + * object that any recipient can interpret best for their context. If the clip + * is to contain a simple text, Intent, or URI, this is easy: an {@link Item} + * containing the appropriate data type can be constructed and used. + * + *

      More complicated data types require the implementation of support in + * a ContentProvider for describing and generating the data for the recipient. + * A common scenario is one where an application places on the clipboard the + * content: URI of an object that the user has copied, with the data at that + * URI consisting of a complicated structure that only other applications with + * direct knowledge of the structure can use. + * + *

      For applications that do not have intrinsic knowledge of the data structure, + * the content provider holding it can make the data available as an arbitrary + * number of types of data streams. This is done by implementing the + * ContentProvider {@link ContentProvider#getStreamTypes(Uri, String)} and + * {@link ContentProvider#openTypedAssetFile(Uri, String, android.os.Bundle)} + * methods. + * + *

      Going back to our simple NotePad application, this is the implementation + * it may have to convert a single note URI (consisting of a title and the note + * text) into a stream of plain text data. + * + * {@sample development/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java + * stream} + * + *

      The copy operation in our NotePad application is now just a simple matter + * of making a clip containing the URI of the note being copied: + * + * {@sample development/samples/NotePad/src/com/example/android/notepad/NotesList.java + * copy} + * + *

      Note if a paste operation needs this clip as text (for example to paste + * into an editor), then {@link Item#coerceToText(Context)} will ask the content + * provider for the clip URI as text and successfully paste the entire note. + */ +public class ClipData extends ClipDescription { + static final String[] MIMETYPES_TEXT_PLAIN = new String[] { MIMETYPE_TEXT_PLAIN }; + static final String[] MIMETYPES_TEXT_URILIST = new String[] { MIMETYPE_TEXT_URILIST }; + static final String[] MIMETYPES_TEXT_INTENT = new String[] { MIMETYPE_TEXT_INTENT }; + + final Bitmap mIcon; + + final ArrayList mItems = new ArrayList(); + + /** + * Description of a single item in a ClippedData. + * + *

      The types than an individual item can currently contain are:

      + * + *
        + *
      • Text: a basic string of text. This is actually a CharSequence, + * so it can be formatted text supported by corresponding Android built-in + * style spans. (Custom application spans are not supported and will be + * stripped when transporting through the clipboard.) + *
      • Intent: an arbitrary Intent object. A typical use is the shortcut + * to create when pasting a clipped item on to the home screen. + *
      • Uri: a URI reference. This may be any URI (such as an http: URI + * representing a bookmark), however it is often a content: URI. Using + * content provider references as clips like this allows an application to + * share complex or large clips through the standard content provider + * facilities. + *
      + */ + public static class Item { + final CharSequence mText; + final Intent mIntent; + final Uri mUri; + + /** + * Create an Item consisting of a single block of (possibly styled) text. + */ + public Item(CharSequence text) { + mText = text; + mIntent = null; + mUri = null; + } + + /** + * Create an Item consisting of an arbitrary Intent. + */ + public Item(Intent intent) { + mText = null; + mIntent = intent; + mUri = null; + } + + /** + * Create an Item consisting of an arbitrary URI. + */ + public Item(Uri uri) { + mText = null; + mIntent = null; + mUri = uri; + } + + /** + * Create a complex Item, containing multiple representations of + * text, intent, and/or URI. + */ + public Item(CharSequence text, Intent intent, Uri uri) { + mText = text; + mIntent = intent; + mUri = uri; + } + + /** + * Retrieve the raw text contained in this Item. + */ + public CharSequence getText() { + return mText; + } + + /** + * Retrieve the raw Intent contained in this Item. + */ + public Intent getIntent() { + return mIntent; + } + + /** + * Retrieve the raw URI contained in this Item. + */ + public Uri getUri() { + return mUri; + } + + /** + * Turn this item into text, regardless of the type of data it + * actually contains. + * + *

      The algorithm for deciding what text to return is: + *

        + *
      • If {@link #getText} is non-null, return that. + *
      • If {@link #getUri} is non-null, try to retrieve its data + * as a text stream from its content provider. If this succeeds, copy + * the text into a String and return it. If it is not a content: URI or + * the content provider does not supply a text representation, return + * the raw URI as a string. + *
      • If {@link #getIntent} is non-null, convert that to an intent: + * URI and returnit. + *
      • Otherwise, return an empty string. + *
      + * + * @param context The caller's Context, from which its ContentResolver + * and other things can be retrieved. + * @return Returns the item's textual representation. + */ +//BEGIN_INCLUDE(coerceToText) + public CharSequence coerceToText(Context context) { + // If this Item has an explicit textual value, simply return that. + if (mText != null) { + return mText; + } + + // If this Item has a URI value, try using that. + if (mUri != null) { + + // First see if the URI can be opened as a plain text stream + // (of any sub-type). If so, this is the best textual + // representation for it. + FileInputStream stream = null; + try { + // Ask for a stream of the desired type. + AssetFileDescriptor descr = context.getContentResolver() + .openTypedAssetFileDescriptor(mUri, "text/*", null); + stream = descr.createInputStream(); + InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); + + // Got it... copy the stream into a local string and return it. + StringBuilder builder = new StringBuilder(128); + char[] buffer = new char[8192]; + int len; + while ((len=reader.read(buffer)) > 0) { + builder.append(buffer, 0, len); + } + return builder.toString(); + + } catch (FileNotFoundException e) { + // Unable to open content URI as text... not really an + // error, just something to ignore. + + } catch (IOException e) { + // Something bad has happened. + Log.w("ClippedData", "Failure loading text", e); + return e.toString(); + + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + // If we couldn't open the URI as a stream, then the URI itself + // probably serves fairly well as a textual representation. + return mUri.toString(); + } + + // Finally, if all we have is an Intent, then we can just turn that + // into text. Not the most user-friendly thing, but it's something. + if (mIntent != null) { + return mIntent.toUri(Intent.URI_INTENT_SCHEME); + } + + // Shouldn't get here, but just in case... + return ""; + } +//END_INCLUDE(coerceToText) + } + + /** + * Create a new clip. + * + * @param label Label to show to the user describing this clip. + * @param mimeTypes An array of MIME types this data is available as. + * @param icon Bitmap providing the user with an iconing representation of + * the clip. + * @param item The contents of the first item in the clip. + */ + public ClipData(CharSequence label, String[] mimeTypes, Bitmap icon, Item item) { + super(label, mimeTypes); + if (item == null) { + throw new NullPointerException("item is null"); + } + mIcon = icon; + mItems.add(item); + } + + /** + * Create a new ClipData holding data of the type {@link #MIMETYPE_TEXT_PLAIN}. + * + * @param label User-visible label for the clip data. + * @param icon Iconic representation of the clip data. + * @param text The actual text in the clip. + * @return Returns a new ClipData containing the specified data. + */ + static public ClipData newPlainText(CharSequence label, Bitmap icon, CharSequence text) { + Item item = new Item(text); + return new ClipData(label, MIMETYPES_TEXT_PLAIN, icon, item); + } + + /** + * Create a new ClipData holding an Intent with MIME type {@link #MIMETYPE_TEXT_INTENT}. + * + * @param label User-visible label for the clip data. + * @param icon Iconic representation of the clip data. + * @param intent The actual Intent in the clip. + * @return Returns a new ClipData containing the specified data. + */ + static public ClipData newIntent(CharSequence label, Bitmap icon, Intent intent) { + Item item = new Item(intent); + return new ClipData(label, MIMETYPES_TEXT_INTENT, icon, item); + } + + /** + * Create a new ClipData holding a URI. If the URI is a content: URI, + * this will query the content provider for the MIME type of its data and + * use that as the MIME type. Otherwise, it will use the MIME type + * {@link #MIMETYPE_TEXT_URILIST}. + * + * @param resolver ContentResolver used to get information about the URI. + * @param label User-visible label for the clip data. + * @param icon Iconic representation of the clip data. + * @param uri The URI in the clip. + * @return Returns a new ClipData containing the specified data. + */ + static public ClipData newUri(ContentResolver resolver, CharSequence label, + Bitmap icon, Uri uri) { + Item item = new Item(uri); + String[] mimeTypes = null; + if ("content".equals(uri.getScheme())) { + String realType = resolver.getType(uri); + mimeTypes = resolver.getStreamTypes(uri, "*/*"); + if (mimeTypes == null) { + if (realType != null) { + mimeTypes = new String[] { realType, MIMETYPE_TEXT_URILIST }; + } + } else { + String[] tmp = new String[mimeTypes.length + (realType != null ? 2 : 1)]; + int i = 0; + if (realType != null) { + tmp[0] = realType; + i++; + } + System.arraycopy(mimeTypes, 0, tmp, i, mimeTypes.length); + tmp[i + mimeTypes.length] = MIMETYPE_TEXT_URILIST; + mimeTypes = tmp; + } + } + if (mimeTypes == null) { + mimeTypes = MIMETYPES_TEXT_URILIST; + } + return new ClipData(label, mimeTypes, icon, item); + } + + /** + * Create a new ClipData holding an URI with MIME type {@link #MIMETYPE_TEXT_URILIST}. + * Unlike {@link #newUri(ContentResolver, CharSequence, Bitmap, Uri)}, nothing + * is inferred about the URI -- if it is a content: URI holding a bitmap, + * the reported type will still be uri-list. Use this with care! + * + * @param label User-visible label for the clip data. + * @param icon Iconic representation of the clip data. + * @param uri The URI in the clip. + * @return Returns a new ClipData containing the specified data. + */ + static public ClipData newRawUri(CharSequence label, Bitmap icon, Uri uri) { + Item item = new Item(uri); + return new ClipData(label, MIMETYPES_TEXT_URILIST, icon, item); + } + + public void addItem(Item item) { + if (item == null) { + throw new NullPointerException("item is null"); + } + mItems.add(item); + } + + public Bitmap getIcon() { + return mIcon; + } + + public int getItemCount() { + return mItems.size(); + } + + public Item getItem(int index) { + return mItems.get(index); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + if (mIcon != null) { + dest.writeInt(1); + mIcon.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + final int N = mItems.size(); + dest.writeInt(N); + for (int i=0; i CREATOR = + new Parcelable.Creator() { + + public ClipData createFromParcel(Parcel source) { + return new ClipData(source); + } + + public ClipData[] newArray(int size) { + return new ClipData[size]; + } + }; +} diff --git a/core/java/android/content/ClipDescription.aidl b/core/java/android/content/ClipDescription.aidl new file mode 100644 index 0000000000000000000000000000000000000000..391fd5a086d69a46086daaed26d4bed6a14f94c0 --- /dev/null +++ b/core/java/android/content/ClipDescription.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +parcelable ClipDescription; diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java new file mode 100644 index 0000000000000000000000000000000000000000..0d9d8070be099fcc95872cf586c1d4d93cbee482 --- /dev/null +++ b/core/java/android/content/ClipDescription.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; + +/** + * Meta-data describing the contents of a {@link ClipData}. Provides enough + * information to know if you can handle the ClipData, but not the data + * itself. + */ +public class ClipDescription implements Parcelable { + /** + * The MIME type for a clip holding plain text. + */ + public static final String MIMETYPE_TEXT_PLAIN = "text/plain"; + + /** + * The MIME type for a clip holding one or more URIs. This should be + * used for URIs that are meaningful to a user (such as an http: URI). + * It should not be used for a content: URI that references some + * other piece of data; in that case the MIME type should be the type + * of the referenced data. + */ + public static final String MIMETYPE_TEXT_URILIST = "text/uri-list"; + + /** + * The MIME type for a clip holding an Intent. + */ + public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent"; + + final CharSequence mLabel; + final String[] mMimeTypes; + + /** + * Create a new clip. + * + * @param label Label to show to the user describing this clip. + * @param mimeTypes An array of MIME types this data is available as. + */ + public ClipDescription(CharSequence label, String[] mimeTypes) { + if (mimeTypes == null) { + throw new NullPointerException("mimeTypes is null"); + } + mLabel = label; + mMimeTypes = mimeTypes; + } + + /** + * Create a copy of a ClipDescription. + */ + public ClipDescription(ClipDescription o) { + mLabel = o.mLabel; + mMimeTypes = o.mMimeTypes; + } + + /** + * Helper to compare two MIME types, where one may be a pattern. + * @param concreteType A fully-specified MIME type. + * @param desiredType A desired MIME type that may be a pattern such as *\/*. + * @return Returns true if the two MIME types match. + */ + public static boolean compareMimeTypes(String concreteType, String desiredType) { + final int typeLength = desiredType.length(); + if (typeLength == 3 && desiredType.equals("*/*")) { + return true; + } + + final int slashpos = desiredType.indexOf('/'); + if (slashpos > 0) { + if (typeLength == slashpos+2 && desiredType.charAt(slashpos+1) == '*') { + if (desiredType.regionMatches(0, concreteType, 0, slashpos+1)) { + return true; + } + } else if (desiredType.equals(concreteType)) { + return true; + } + } + + return false; + } + + /** + * Return the label for this clip. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * Check whether the clip description contains the given MIME type. + * + * @param mimeType The desired MIME type. May be a pattern. + * @return Returns true if one of the MIME types in the clip description + * matches the desired MIME type, else false. + */ + public boolean hasMimeType(String mimeType) { + for (int i=0; i array = null; + for (int i=0; i(); + } + array.add(mMimeTypes[i]); + } + } + if (array == null) { + return null; + } + String[] rawArray = new String[array.size()]; + array.toArray(rawArray); + return rawArray; + } + + /** + * Return the number of MIME types the clip is available in. + */ + public int getMimeTypeCount() { + return mMimeTypes.length; + } + + /** + * Return one of the possible clip MIME types. + */ + public String getMimeType(int index) { + return mMimeTypes[index]; + } + + /** @hide */ + public void validate() { + if (mMimeTypes == null) { + throw new NullPointerException("null mime types"); + } + if (mMimeTypes.length <= 0) { + throw new IllegalArgumentException("must have at least 1 mime type"); + } + for (int i=0; i CREATOR = + new Parcelable.Creator() { + + public ClipDescription createFromParcel(Parcel source) { + return new ClipDescription(source); + } + + public ClipDescription[] newArray(int size) { + return new ClipDescription[size]; + } + }; +} diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java new file mode 100644 index 0000000000000000000000000000000000000000..0ea06482dbe4a8869afb0389b1e1e5edc4861b9a --- /dev/null +++ b/core/java/android/content/ClipboardManager.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.content.Context; +import android.os.Message; +import android.os.RemoteException; +import android.os.Handler; +import android.os.IBinder; +import android.os.ServiceManager; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Interface to the clipboard service, for placing and retrieving text in + * the global clipboard. + * + *

      + * You do not instantiate this class directly; instead, retrieve it through + * {@link android.content.Context#getSystemService}. + * + *

      + * The ClipboardManager API itself is very simple: it consists of methods + * to atomically get and set the current primary clipboard data. That data + * is expressed as a {@link ClipData} object, which defines the protocol + * for data exchange between applications. + * + * @see android.content.Context#getSystemService + */ +public class ClipboardManager extends android.text.ClipboardManager { + private final static Object sStaticLock = new Object(); + private static IClipboard sService; + + private final Context mContext; + + private final ArrayList mPrimaryClipChangedListeners + = new ArrayList(); + + private final IOnPrimaryClipChangedListener.Stub mPrimaryClipChangedServiceListener + = new IOnPrimaryClipChangedListener.Stub() { + public void dispatchPrimaryClipChanged() { + mHandler.sendEmptyMessage(MSG_REPORT_PRIMARY_CLIP_CHANGED); + } + }; + + static final int MSG_REPORT_PRIMARY_CLIP_CHANGED = 1; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REPORT_PRIMARY_CLIP_CHANGED: + reportPrimaryClipChanged(); + } + } + }; + + public interface OnPrimaryClipChangedListener { + void onPrimaryClipChanged(); + } + + static private IClipboard getService() { + synchronized (sStaticLock) { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService("clipboard"); + sService = IClipboard.Stub.asInterface(b); + return sService; + } + } + + /** {@hide} */ + public ClipboardManager(Context context, Handler handler) { + mContext = context; + } + + /** + * Sets the current primary clip on the clipboard. This is the clip that + * is involved in normal cut and paste operations. + * + * @param clip The clipped data item to set. + */ + public void setPrimaryClip(ClipData clip) { + try { + getService().setPrimaryClip(clip); + } catch (RemoteException e) { + } + } + + /** + * Returns the current primary clip on the clipboard. + */ + public ClipData getPrimaryClip() { + try { + return getService().getPrimaryClip(); + } catch (RemoteException e) { + return null; + } + } + + /** + * Returns a description of the current primary clip on the clipboard + * but not a copy of its data. + */ + public ClipDescription getPrimaryClipDescription() { + try { + return getService().getPrimaryClipDescription(); + } catch (RemoteException e) { + return null; + } + } + + /** + * Returns true if there is currently a primary clip on the clipboard. + */ + public boolean hasPrimaryClip() { + try { + return getService().hasPrimaryClip(); + } catch (RemoteException e) { + return false; + } + } + + public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener what) { + synchronized (mPrimaryClipChangedListeners) { + if (mPrimaryClipChangedListeners.size() == 0) { + try { + getService().addPrimaryClipChangedListener( + mPrimaryClipChangedServiceListener); + } catch (RemoteException e) { + } + } + mPrimaryClipChangedListeners.add(what); + } + } + + public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener what) { + synchronized (mPrimaryClipChangedListeners) { + mPrimaryClipChangedListeners.remove(what); + if (mPrimaryClipChangedListeners.size() == 0) { + try { + getService().removePrimaryClipChangedListener( + mPrimaryClipChangedServiceListener); + } catch (RemoteException e) { + } + } + } + } + + /** + * @deprecated Use {@link #getPrimaryClip()} instead. This retrieves + * the primary clip and tries to coerce it to a string. + */ + public CharSequence getText() { + ClipData clip = getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItem(0).coerceToText(mContext); + } + return null; + } + + /** + * @deprecated Use {@link #setPrimaryClip(ClipData)} instead. This + * creates a ClippedItem holding the given text and sets it as the + * primary clip. It has no label or icon. + */ + public void setText(CharSequence text) { + setPrimaryClip(ClipData.newPlainText(null, null, text)); + } + + /** + * @deprecated Use {@link #hasPrimaryClip()} instead. + */ + public boolean hasText() { + try { + return getService().hasPrimaryClip(); + } catch (RemoteException e) { + return false; + } + } + + void reportPrimaryClipChanged() { + Object[] listeners; + + synchronized (mPrimaryClipChangedListeners) { + final int N = mPrimaryClipChangedListeners.size(); + if (N <= 0) { + return; + } + listeners = mPrimaryClipChangedListeners.toArray(); + } + + for (int i=0; i */ public abstract class ContentProvider implements ComponentCallbacks { + private static final String TAG = "ContentProvider"; + /* * Note: if you add methods to ContentProvider, you must add similar methods to * MockContentProvider. @@ -249,6 +254,18 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.call(method, request, args); } + @Override + public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { + return ContentProvider.this.getStreamTypes(uri, mimeTypeFilter); + } + + @Override + public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeType, Bundle opts) + throws FileNotFoundException { + enforceReadPermission(uri); + return ContentProvider.this.openTypedAssetFile(uri, mimeType, opts); + } + private void enforceReadPermission(Uri uri) { final int uid = Binder.getCallingUid(); if (uid == mMyUid) { @@ -749,6 +766,144 @@ public abstract class ContentProvider implements ComponentCallbacks { return ParcelFileDescriptor.open(new File(path), modeBits); } + /** + * Called by a client to determine the types of data streams that this + * content provider supports for the given URI. The default implementation + * returns null, meaning no types. If your content provider stores data + * of a particular type, return that MIME type if it matches the given + * mimeTypeFilter. If it can perform type conversions, return an array + * of all supported MIME types that match mimeTypeFilter. + * + * @param uri The data in the content provider being queried. + * @param mimeTypeFilter The type of data the client desires. May be + * a pattern, such as *\/* to retrieve all possible data types. + * @return Returns null if there are no possible data streams for the + * given mimeTypeFilter. Otherwise returns an array of all available + * concrete MIME types. + * + * @see #getType(Uri) + * @see #openTypedAssetFile(Uri, String, Bundle) + * @see ClipDescription#compareMimeTypes(String, String) + */ + public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { + return null; + } + + /** + * Called by a client to open a read-only stream containing data of a + * particular MIME type. This is like {@link #openAssetFile(Uri, String)}, + * except the file can only be read-only and the content provider may + * perform data conversions to generate data of the desired type. + * + *

      The default implementation compares the given mimeType against the + * result of {@link #getType(Uri)} and, if the match, simple calls + * {@link #openAssetFile(Uri, String)}. + * + *

      See {@link ClipData} for examples of the use and implementation + * of this method. + * + * @param uri The data in the content provider being queried. + * @param mimeTypeFilter The type of data the client desires. May be + * a pattern, such as *\/*, if the caller does not have specific type + * requirements; in this case the content provider will pick its best + * type matching the pattern. + * @param opts Additional options from the client. The definitions of + * these are specific to the content provider being called. + * + * @return Returns a new AssetFileDescriptor from which the client can + * read data of the desired type. + * + * @throws FileNotFoundException Throws FileNotFoundException if there is + * no file associated with the given URI or the mode is invalid. + * @throws SecurityException Throws SecurityException if the caller does + * not have permission to access the data. + * @throws IllegalArgumentException Throws IllegalArgumentException if the + * content provider does not support the requested MIME type. + * + * @see #getStreamTypes(Uri, String) + * @see #openAssetFile(Uri, String) + * @see ClipDescription#compareMimeTypes(String, String) + */ + public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) + throws FileNotFoundException { + if ("*/*".equals(mimeTypeFilter)) { + // If they can take anything, the untyped open call is good enough. + return openAssetFile(uri, "r"); + } + String baseType = getType(uri); + if (baseType != null && ClipDescription.compareMimeTypes(baseType, mimeTypeFilter)) { + // Use old untyped open call if this provider has a type for this + // URI and it matches the request. + return openAssetFile(uri, "r"); + } + throw new FileNotFoundException("Can't open " + uri + " as type " + mimeTypeFilter); + } + + /** + * Interface to write a stream of data to a pipe. Use with + * {@link ContentProvider#openPipeHelper}. + */ + public interface PipeDataWriter { + /** + * Called from a background thread to stream data out to a pipe. + * Note that the pipe is blocking, so this thread can block on + * writes for an arbitrary amount of time if the client is slow + * at reading. + * + * @param output The pipe where data should be written. This will be + * closed for you upon returning from this function. + * @param uri The URI whose data is to be written. + * @param mimeType The desired type of data to be written. + * @param opts Options supplied by caller. + * @param args Your own custom arguments. + */ + public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, + Bundle opts, T args); + } + + /** + * A helper function for implementing {@link #openTypedAssetFile}, for + * creating a data pipe and background thread allowing you to stream + * generated data back to the client. This function returns a new + * ParcelFileDescriptor that should be returned to the caller (the caller + * is responsible for closing it). + * + * @param uri The URI whose data is to be written. + * @param mimeType The desired type of data to be written. + * @param opts Options supplied by caller. + * @param args Your own custom arguments. + * @param func Interface implementing the function that will actually + * stream the data. + * @return Returns a new ParcelFileDescriptor holding the read side of + * the pipe. This should be returned to the caller for reading; the caller + * is responsible for closing it when done. + */ + public ParcelFileDescriptor openPipeHelper(final Uri uri, final String mimeType, + final Bundle opts, final T args, final PipeDataWriter func) + throws FileNotFoundException { + try { + final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); + + AsyncTask task = new AsyncTask() { + @Override + protected Object doInBackground(Object... params) { + func.writeDataToPipe(fds[1], uri, mimeType, opts, args); + try { + fds[1].close(); + } catch (IOException e) { + Log.w(TAG, "Failure closing pipe", e); + } + return null; + } + }; + task.execute((Object[])null); + + return fds[0]; + } catch (IOException e) { + throw new FileNotFoundException("failure making pipe"); + } + } + /** * Returns true if this instance is a temporary content provider. * @return true if this instance is a temporary content provider @@ -775,6 +930,11 @@ public abstract class ContentProvider implements ComponentCallbacks { * @param info Registered information about this content provider */ public void attachInfo(Context context, ProviderInfo info) { + /* + * We may be using AsyncTask from binder threads. Make it init here + * so its static handler is on the main thread. + */ + AsyncTask.init(); /* * Only allow it to be set once, so after the content service gives @@ -823,7 +983,7 @@ public abstract class ContentProvider implements ComponentCallbacks { /** * @hide -- until interface has proven itself * - * Call an provider-defined method. This can be used to implement + * Call a provider-defined method. This can be used to implement * interfaces that are cheaper than using a Cursor. * * @param method Method name to call. Opaque to framework. @@ -833,4 +993,31 @@ public abstract class ContentProvider implements ComponentCallbacks { public Bundle call(String method, String request, Bundle args) { return null; } + + /** + * Implement this to shut down the ContentProvider instance. You can then + * invoke this method in unit tests. + * + *

      + * Android normally handles ContentProvider startup and shutdown + * automatically. You do not need to start up or shut down a + * ContentProvider. When you invoke a test method on a ContentProvider, + * however, a ContentProvider instance is started and keeps running after + * the test finishes, even if a succeeding test instantiates another + * ContentProvider. A conflict develops because the two instances are + * usually running against the same underlying data source (for example, an + * sqlite database). + *

      + *

      + * Implementing shutDown() avoids this conflict by providing a way to + * terminate the ContentProvider. This method can also prevent memory leaks + * from multiple instantiations of the ContentProvider, and it can ensure + * unit test isolation by allowing you to completely clean up the test + * fixture before moving on to the next test. + *

      + */ + public void shutdown() { + Log.w(TAG, "implement ContentProvider shutdown() to make sure all database " + + "connections are gracefully shutdown"); + } } diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 0858ea51c76cc6783dbe633941ad4f023218183d..0540109b4f912614e555954818d5cb9b022ba1f7 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -18,6 +18,7 @@ package android.content; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; import android.os.RemoteException; import android.os.ParcelFileDescriptor; import android.content.res.AssetFileDescriptor; @@ -43,53 +44,77 @@ public class ContentProviderClient { mContentResolver = contentResolver; } - /** see {@link ContentProvider#query} */ + /** See {@link ContentProvider#query 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} */ + /** See {@link ContentProvider#getType ContentProvider.getType} */ public String getType(Uri url) throws RemoteException { return mContentProvider.getType(url); } - /** see {@link ContentProvider#insert} */ + /** See {@link ContentProvider#getStreamTypes ContentProvider.getStreamTypes} */ + public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException { + return mContentProvider.getStreamTypes(url, mimeTypeFilter); + } + + /** See {@link ContentProvider#insert ContentProvider.insert} */ public Uri insert(Uri url, ContentValues initialValues) throws RemoteException { return mContentProvider.insert(url, initialValues); } - /** see {@link ContentProvider#bulkInsert} */ + /** See {@link ContentProvider#bulkInsert ContentProvider.bulkInsert} */ public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException { return mContentProvider.bulkInsert(url, initialValues); } - /** see {@link ContentProvider#delete} */ + /** See {@link ContentProvider#delete ContentProvider.delete} */ public int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException { return mContentProvider.delete(url, selection, selectionArgs); } - /** see {@link ContentProvider#update} */ + /** See {@link ContentProvider#update 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} */ + /** + * See {@link ContentProvider#openFile ContentProvider.openFile}. Note that + * this does not + * take care of non-content: URIs such as file:. It is strongly recommended + * you use the {@link ContentResolver#openFileDescriptor + * ContentResolver.openFileDescriptor} API instead. + */ public ParcelFileDescriptor openFile(Uri url, String mode) throws RemoteException, FileNotFoundException { return mContentProvider.openFile(url, mode); } - /** see {@link ContentProvider#openAssetFile} */ + /** + * See {@link ContentProvider#openAssetFile ContentProvider.openAssetFile}. + * Note that this does not + * take care of non-content: URIs such as file:. It is strongly recommended + * you use the {@link ContentResolver#openAssetFileDescriptor + * ContentResolver.openAssetFileDescriptor} API instead. + */ public AssetFileDescriptor openAssetFile(Uri url, String mode) throws RemoteException, FileNotFoundException { return mContentProvider.openAssetFile(url, mode); } - /** see {@link ContentProvider#applyBatch} */ + /** See {@link ContentProvider#openTypedAssetFile ContentProvider.openTypedAssetFile} */ + public final AssetFileDescriptor openTypedAssetFileDescriptor(Uri uri, + String mimeType, Bundle opts) + throws RemoteException, FileNotFoundException { + return mContentProvider.openTypedAssetFile(uri, mimeType, opts); + } + + /** See {@link ContentProvider#applyBatch ContentProvider.applyBatch} */ public ContentProviderResult[] applyBatch(ArrayList operations) throws RemoteException, OperationApplicationException { return mContentProvider.applyBatch(operations); diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index fdb3d20c9f99a45e98c955854fca665498fb5449..d1ab8d576412fe136ed85bdee4cc035baa48146c 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -257,6 +257,38 @@ abstract public class ContentProviderNative extends Binder implements IContentPr reply.writeBundle(responseBundle); return true; } + + case GET_STREAM_TYPES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String mimeTypeFilter = data.readString(); + String[] types = getStreamTypes(url, mimeTypeFilter); + reply.writeNoException(); + reply.writeStringArray(types); + + return true; + } + + case OPEN_TYPED_ASSET_FILE_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String mimeType = data.readString(); + Bundle opts = data.readBundle(); + + AssetFileDescriptor fd; + fd = openTypedAssetFile(url, mimeType, opts); + reply.writeNoException(); + if (fd != null) { + reply.writeInt(1); + fd.writeToParcel(reply, + Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + reply.writeInt(0); + } + return true; + } } } catch (Exception e) { DatabaseUtils.writeExceptionToParcel(reply, e); @@ -568,5 +600,50 @@ final class ContentProviderProxy implements IContentProvider return bundle; } + public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(mimeTypeFilter); + + mRemote.transact(IContentProvider.GET_STREAM_TYPES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + String[] out = reply.createStringArray(); + + data.recycle(); + reply.recycle(); + + return out; + } + + public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) + throws RemoteException, FileNotFoundException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(mimeType); + data.writeBundle(opts); + + mRemote.transact(IContentProvider.OPEN_TYPED_ASSET_FILE_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(reply); + int has = reply.readInt(); + AssetFileDescriptor fd = has != 0 + ? AssetFileDescriptor.CREATOR.createFromParcel(reply) : null; + + data.recycle(); + reply.recycle(); + + return fd; + } + private IBinder mRemote; } diff --git a/core/java/android/content/ContentQueryMap.java b/core/java/android/content/ContentQueryMap.java index dbcb4a7078a4f0e3e4b7665ad84d1e19c870e50b..c955094cd33dfbfc2c8c054c76bc97192ac7d600 100644 --- a/core/java/android/content/ContentQueryMap.java +++ b/core/java/android/content/ContentQueryMap.java @@ -129,7 +129,9 @@ public class ContentQueryMap extends Observable { /** Requeries the cursor and reads the contents into the cache */ public void requery() { mDirty = false; - mCursor.requery(); + if (!mCursor.requery()) { + throw new IllegalStateException("trying to requery an already closed cursor"); + } readCursorIntoCache(); setChanged(); notifyObservers(); diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 69f7611a4d59d52a03f07ad16a7cfc3fb8e96d91..22feb9a984a68891eb1e2d7040079b38c9494852 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -44,9 +44,9 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.List; import java.util.Random; -import java.util.ArrayList; /** @@ -186,8 +186,7 @@ public abstract class ContentResolver { * using the content:// scheme. * @return A MIME type for the content, or null if the URL is invalid or the type is unknown */ - public final String getType(Uri url) - { + public final String getType(Uri url) { IContentProvider provider = acquireProvider(url); if (provider == null) { return null; @@ -203,6 +202,39 @@ public abstract class ContentResolver { } } + /** + * Query for the possible MIME types for the representations the given + * content URL can be returned when opened as as stream with + * {@link #openTypedAssetFileDescriptor}. Note that the types here are + * not necessarily a superset of the type returned by {@link #getType} -- + * many content providers can not return a raw stream for the structured + * data that they contain. + * + * @param url A Uri identifying content (either a list or specific type), + * using the content:// scheme. + * @param mimeTypeFilter The desired MIME type. This may be a pattern, + * such as *\/*, to query for all available MIME types that match the + * pattern. + * @return Returns an array of MIME type strings for all availablle + * data streams that match the given mimeTypeFilter. If there are none, + * null is returned. + */ + public String[] getStreamTypes(Uri url, String mimeTypeFilter) { + IContentProvider provider = acquireProvider(url); + if (provider == null) { + return null; + } + try { + return provider.getStreamTypes(url, mimeTypeFilter); + } catch (RemoteException e) { + return null; + } catch (java.lang.Exception e) { + return null; + } finally { + releaseProvider(provider); + } + } + /** *

      * Query the given URI, returning a {@link Cursor} over the result set. @@ -349,7 +381,7 @@ public abstract class ContentResolver { } /** - * Open a raw file descriptor to access data under a "content:" URI. This + * Open a raw file descriptor to access data under a URI. This * is like {@link #openAssetFileDescriptor(Uri, String)}, but uses the * underlying {@link ContentProvider#openFile} * ContentProvider.openFile()} method, so will not work with @@ -399,10 +431,9 @@ public abstract class ContentResolver { } /** - * Open a raw file descriptor to access data under a "content:" URI. This + * Open a raw file descriptor to access data under a URI. This * interacts with the underlying {@link ContentProvider#openAssetFile} - * ContentProvider.openAssetFile()} method of the provider associated with the - * given URI, to retrieve any file stored there. + * method of the provider associated with the given URI, to retrieve any file stored there. * *

      Accepts the following URI schemes:
      *
        @@ -434,6 +465,11 @@ public abstract class ContentResolver { * *
      * + *

      Note that if this function is called for read-only input (mode is "r") + * on a content: URI, it will instead call {@link #openTypedAssetFileDescriptor} + * for you with a MIME type of "*\/*". This allows such callers to benefit + * from any built-in data conversion that a provider implements. + * * @param uri The desired URI to open. * @param mode The file mode to use, as per {@link ContentProvider#openAssetFile * ContentProvider.openAssetFile}. @@ -460,29 +496,97 @@ public abstract class ContentResolver { new File(uri.getPath()), modeToMode(uri, mode)); return new AssetFileDescriptor(pfd, 0, -1); } else { - IContentProvider provider = acquireProvider(uri); - if (provider == null) { - throw new FileNotFoundException("No content provider: " + uri); - } - try { - AssetFileDescriptor fd = provider.openAssetFile(uri, mode); - if(fd == null) { - releaseProvider(provider); - return null; + if ("r".equals(mode)) { + return openTypedAssetFileDescriptor(uri, "*/*", null); + } else { + IContentProvider provider = acquireProvider(uri); + if (provider == null) { + throw new FileNotFoundException("No content provider: " + uri); } - ParcelFileDescriptor pfd = new ParcelFileDescriptorInner( - fd.getParcelFileDescriptor(), provider); - return new AssetFileDescriptor(pfd, fd.getStartOffset(), - fd.getDeclaredLength()); - } catch (RemoteException e) { - releaseProvider(provider); - throw new FileNotFoundException("Dead content provider: " + uri); - } catch (FileNotFoundException e) { + try { + AssetFileDescriptor fd = provider.openAssetFile(uri, mode); + if(fd == null) { + releaseProvider(provider); + return null; + } + ParcelFileDescriptor pfd = new ParcelFileDescriptorInner( + fd.getParcelFileDescriptor(), provider); + + // Success! Don't release the provider when exiting, let + // ParcelFileDescriptorInner do that when it is closed. + provider = null; + + return new AssetFileDescriptor(pfd, fd.getStartOffset(), + fd.getDeclaredLength()); + } catch (RemoteException e) { + throw new FileNotFoundException("Dead content provider: " + uri); + } catch (FileNotFoundException e) { + throw e; + } finally { + if (provider != null) { + releaseProvider(provider); + } + } + } + } + } + + /** + * Open a raw file descriptor to access (potentially type transformed) + * data from a "content:" URI. This interacts with the underlying + * {@link ContentProvider#openTypedAssetFile} method of the provider + * associated with the given URI, to retrieve retrieve any appropriate + * data stream for the data stored there. + * + *

      Unlike {@link #openAssetFileDescriptor}, this function only works + * with "content:" URIs, because content providers are the only facility + * with an associated MIME type to ensure that the returned data stream + * is of the desired type. + * + *

      All text/* streams are encoded in UTF-8. + * + * @param uri The desired URI to open. + * @param mimeType The desired MIME type of the returned data. This can + * be a pattern such as *\/*, which will allow the content provider to + * select a type, though there is no way for you to determine what type + * it is returning. + * @param opts Additional provider-dependent options. + * @return Returns a new ParcelFileDescriptor from which you can read the + * data stream from the provider. Note that this may be a pipe, meaning + * you can't seek in it. The only seek you should do is if the + * AssetFileDescriptor contains an offset, to move to that offset before + * reading. You own this descriptor and are responsible for closing it when done. + * @throws FileNotFoundException Throws FileNotFoundException of no + * data of the desired type exists under the URI. + */ + public final AssetFileDescriptor openTypedAssetFileDescriptor(Uri uri, + String mimeType, Bundle opts) throws FileNotFoundException { + IContentProvider provider = acquireProvider(uri); + if (provider == null) { + throw new FileNotFoundException("No content provider: " + uri); + } + try { + AssetFileDescriptor fd = provider.openTypedAssetFile(uri, mimeType, opts); + if (fd == null) { releaseProvider(provider); - throw e; - } catch (RuntimeException e) { + return null; + } + ParcelFileDescriptor pfd = new ParcelFileDescriptorInner( + fd.getParcelFileDescriptor(), provider); + + // Success! Don't release the provider when exiting, let + // ParcelFileDescriptorInner do that when it is closed. + provider = null; + + return new AssetFileDescriptor(pfd, fd.getStartOffset(), + fd.getDeclaredLength()); + } catch (RemoteException e) { + throw new FileNotFoundException("Dead content provider: " + uri); + } catch (FileNotFoundException e) { + throw e; + } finally { + if (provider != null) { releaseProvider(provider); - throw e; } } } @@ -1342,7 +1446,7 @@ public abstract class ContentResolver { } private final class CursorWrapperInner extends CursorWrapper { - private IContentProvider mContentProvider; + private final IContentProvider mContentProvider; public static final String TAG="CursorWrapperInner"; private boolean mCloseFlag = false; @@ -1371,7 +1475,7 @@ public abstract class ContentResolver { } private final class ParcelFileDescriptorInner extends ParcelFileDescriptor { - private IContentProvider mContentProvider; + private final IContentProvider mContentProvider; public static final String TAG="ParcelFileDescriptorInner"; private boolean mReleaseProviderFlag = false; diff --git a/core/java/android/content/ContentValues.java b/core/java/android/content/ContentValues.java index 75787cd34610e67d9d1c4a233ca54042868c92aa..0d25f8035d8a93f4a487a890ddd6efbb3ce00571 100644 --- a/core/java/android/content/ContentValues.java +++ b/core/java/android/content/ContentValues.java @@ -414,6 +414,8 @@ public final class ContentValues implements Parcelable { } catch (ClassCastException e) { if (value instanceof CharSequence) { return Boolean.valueOf(value.toString()); + } else if (value instanceof Number) { + return ((Number) value).intValue() != 0; } else { Log.e(TAG, "Cannot cast value for " + key + " to a Boolean: " + value, e); return null; @@ -446,6 +448,15 @@ public final class ContentValues implements Parcelable { return mValues.entrySet(); } + /** + * Returns a set of all of the keys + * + * @return a set of all of the keys + */ + public Set keySet() { + return mValues.keySet(); + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @SuppressWarnings({"deprecation", "unchecked"}) @@ -488,6 +499,10 @@ public final class ContentValues implements Parcelable { return (ArrayList) mValues.get(key); } + /** + * Returns a string containing a concise, human-readable description of this object. + * @return a printable representation of this object. + */ @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 0100550e810dfcd71e523b1060120f4a02a61557..3d293798850257629dd9881476b116ffc0a3fcdc 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -21,10 +21,12 @@ import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.content.res.TypedArray; +import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import android.media.MediaScannerConnection.OnScanCompletedListener; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -587,6 +589,32 @@ public abstract class Context { public abstract SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory); + /** + * Open a new private SQLiteDatabase associated with this Context's + * application package. Creates the database file if it doesn't exist. + * + *

      Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.

      + * + * @param name The name (unique in the application package) of the database. + * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the + * default operation, {@link #MODE_WORLD_READABLE} + * and {@link #MODE_WORLD_WRITEABLE} to control permissions. + * @param factory An optional factory class that is called to instantiate a + * cursor when query is called. + * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption. if null, {@link android.database.DefaultDatabaseErrorHandler} is assumed. + * @return The contents of a newly created database with the given name. + * @throws android.database.sqlite.SQLiteException if the database file could not be opened. + * + * @see #MODE_PRIVATE + * @see #MODE_WORLD_READABLE + * @see #MODE_WORLD_WRITEABLE + * @see #deleteDatabase + */ + public abstract SQLiteDatabase openOrCreateDatabase(String name, + int mode, CursorFactory factory, DatabaseErrorHandler errorHandler); + /** * Delete an existing private SQLiteDatabase associated with this Context's * application package. @@ -1356,7 +1384,16 @@ public abstract class Context { * @see android.location.LocationManager */ public static final String LOCATION_SERVICE = "location"; - + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.location.CountryDetector} for detecting the country that + * the user is in. + * + * @hide + */ + public static final String COUNTRY_DETECTOR = "country_detector"; + /** * Use with {@link #getSystemService} to retrieve a {@link * android.app.SearchManager} for handling searches. diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index a447108a99fb79dfcd7b935fa584acf082613a03..3f5d215177aace5bb0e734fb9ba8c7a4e815f73e 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -20,6 +20,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; +import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; @@ -203,6 +204,12 @@ public class ContextWrapper extends Context { return mBase.openOrCreateDatabase(name, mode, factory); } + @Override + public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + return mBase.openOrCreateDatabase(name, mode, factory, errorHandler); + } + @Override public boolean deleteDatabase(String name) { return mBase.deleteDatabase(name); diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..42599edb6ce1b01e828fe4ea37d455d8f6744f9b --- /dev/null +++ b/core/java/android/content/CursorLoader.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.database.Cursor; +import android.net.Uri; + +/** + * A loader that queries the {@link ContentResolver} and returns a {@link Cursor}. + */ +public class CursorLoader extends AsyncTaskLoader { + Cursor mCursor; + ForceLoadContentObserver mObserver; + boolean mStopped; + Uri mUri; + String[] mProjection; + String mSelection; + String[] mSelectionArgs; + String mSortOrder; + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder); + // Ensure the cursor window is filled + if (cursor != null) { + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (mStopped) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + super.deliverResult(cursor); + + if (oldCursor != null && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + + public CursorLoader(Context context, Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + super(context); + mObserver = new ForceLoadContentObserver(); + mUri = uri; + mProjection = projection; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + public void startLoading() { + mStopped = false; + + if (mCursor != null) { + deliverResult(mCursor); + } else { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + public void stopLoading() { + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + } + mCursor = null; + + // Attempt to cancel the current load task if possible. + cancelLoad(); + + // Make sure that any outstanding loads clean themselves up properly + mStopped = true; + } + + @Override + public void onCancelled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + public void destroy() { + // Ensure the loader is stopped + stopLoading(); + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public String[] getProjection() { + return mProjection; + } + + public void setProjection(String[] projection) { + mProjection = projection; + } + + public String getSelection() { + return mSelection; + } + + public void setSelection(String selection) { + mSelection = selection; + } + + public String[] getSelectionArgs() { + return mSelectionArgs; + } + + public void setSelectionArgs(String[] selectionArgs) { + mSelectionArgs = selectionArgs; + } + + public String getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(String sortOrder) { + mSortOrder = sortOrder; + } +} diff --git a/core/java/android/text/IClipboard.aidl b/core/java/android/content/IClipboard.aidl similarity index 65% rename from core/java/android/text/IClipboard.aidl rename to core/java/android/content/IClipboard.aidl index 4deb5c8664d9922fbae4301c068b15e41ff90714..3e1fe55f8b95d691f251bb12e7005af3d3856bda 100644 --- a/core/java/android/text/IClipboard.aidl +++ b/core/java/android/content/IClipboard.aidl @@ -14,7 +14,11 @@ * limitations under the License. */ -package android.text; +package android.content; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.IOnPrimaryClipChangedListener; /** * Programming interface to the clipboard, which allows copying and pasting @@ -22,21 +26,15 @@ package android.text; * {@hide} */ interface IClipboard { - /** - * Returns the text on the clipboard. It will eventually be possible - * to store types other than text too, in which case this will return - * null if the type cannot be coerced to text. - */ - CharSequence getClipboardText(); - - /** - * Sets the contents of the clipboard to the specified text. - */ - void setClipboardText(CharSequence text); + void setPrimaryClip(in ClipData clip); + ClipData getPrimaryClip(); + ClipDescription getPrimaryClipDescription(); + boolean hasPrimaryClip(); + void addPrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener); + void removePrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener); /** * Returns true if the clipboard contains text; false otherwise. */ boolean hasClipboardText(); } - diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index 67e7581e5ffde0b849fc3fc4c558b9f593a841b0..8f122ce8f39db68658f64171382ed1ed7e8ae2a1 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -59,6 +59,7 @@ public interface IContentProvider extends IInterface { throws RemoteException, FileNotFoundException; public ContentProviderResult[] applyBatch(ArrayList operations) throws RemoteException, OperationApplicationException; + /** * @hide -- until interface has proven itself * @@ -71,6 +72,11 @@ public interface IContentProvider extends IInterface { */ public Bundle call(String method, String request, Bundle args) throws RemoteException; + // Data interchange. + public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException; + public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) + throws RemoteException, FileNotFoundException; + /* IPC constants */ static final String descriptor = "android.content.IContentProvider"; @@ -84,4 +90,6 @@ public interface IContentProvider extends IInterface { static final int OPEN_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 14; static final int APPLY_BATCH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 19; static final int CALL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 20; + static final int GET_STREAM_TYPES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 21; + static final int OPEN_TYPED_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 22; } diff --git a/core/java/android/content/IOnPrimaryClipChangedListener.aidl b/core/java/android/content/IOnPrimaryClipChangedListener.aidl new file mode 100644 index 0000000000000000000000000000000000000000..fb42a454a1380d1118da2fe1ee12c547c1f51344 --- /dev/null +++ b/core/java/android/content/IOnPrimaryClipChangedListener.aidl @@ -0,0 +1,21 @@ +/** + * 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.content; + +oneway interface IOnPrimaryClipChangedListener { + void dispatchPrimaryClipChanged(); +} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 7154aeee08d484e40f43fc6f8bf647689a8f588b..4d0b8b06905d5604ed80974238c7d6cd909e9577 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -534,6 +534,7 @@ import java.util.Set; *
    • {@link #CATEGORY_CAR_DOCK} *
    • {@link #CATEGORY_DESK_DOCK} *
    • {@link #CATEGORY_CAR_MODE} + *
    • {@link #CATEGORY_APP_MARKET} *
    * *

    Standard Extra Data

    @@ -984,6 +985,15 @@ public class Intent implements Parcelable, Cloneable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_INSERT = "android.intent.action.INSERT"; + /** + * Activity Action: Create a new item in the given container, initializing it + * from the current contents of the clipboard. + *

    Input: {@link #getData} is URI of the directory (vnd.android.cursor.dir/*) + * in which to place the data. + *

    Output: URI of the new data that was created. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PASTE = "android.intent.action.PASTE"; /** * Activity Action: Delete the given data from its container. *

    Input: {@link #getData} is URI of data to be deleted. @@ -2000,6 +2010,11 @@ public class Intent implements Parcelable, Cloneable { */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_EMBED = "android.intent.category.EMBED"; + /** + * This activity allows the user to browse and download new applications. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_APP_MARKET = "android.intent.category.APP_MARKET"; /** * This activity may be exercised by the monkey or other automated test tools. */ diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java new file mode 100644 index 0000000000000000000000000000000000000000..234096a6709efc9d37a968740914f39f3f50d929 --- /dev/null +++ b/core/java/android/content/Loader.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.database.ContentObserver; +import android.os.Handler; + +/** + * An abstract class that performs asynchronous loading of data. While Loaders are active + * they should monitor the source of their data and deliver new results when the contents + * change. + * + * @param The result returned when the load is complete + */ +public abstract class Loader { + int mId; + OnLoadCompleteListener mListener; + Context mContext; + + public final class ForceLoadContentObserver extends ContentObserver { + public ForceLoadContentObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + } + + public interface OnLoadCompleteListener { + /** + * Called on the thread that created the Loader when the load is complete. + * + * @param loader the loader that completed the load + * @param data the result of the load + */ + public void onLoadComplete(Loader loader, D data); + } + + /** + * Stores away the application context associated with context. Since Loaders can be used + * across multiple activities it's dangerous to store the context directly. + * + * @param context used to retrieve the application context. + */ + public Loader(Context context) { + mContext = context.getApplicationContext(); + } + + /** + * Sends the result of the load to the registered listener. Should only be called by subclasses. + * + * Must be called from the UI thread. + * + * @param data the result of the load + */ + public void deliverResult(D data) { + if (mListener != null) { + mListener.onLoadComplete(this, data); + } + } + + /** + * @return an application context retrieved from the Context passed to the constructor. + */ + public Context getContext() { + return mContext; + } + + /** + * @return the ID of this loader + */ + public int getId() { + return mId; + } + + /** + * Registers a class that will receive callbacks when a load is complete. The callbacks will + * be called on the UI thread so it's safe to pass the results to widgets. + * + * Must be called from the UI thread + */ + public void registerListener(int id, OnLoadCompleteListener listener) { + if (mListener != null) { + throw new IllegalStateException("There is already a listener registered"); + } + mListener = listener; + mId = id; + } + + /** + * Must be called from the UI thread + */ + public void unregisterListener(OnLoadCompleteListener listener) { + if (mListener == null) { + throw new IllegalStateException("No listener register"); + } + if (mListener != listener) { + throw new IllegalArgumentException("Attempting to unregister the wrong listener"); + } + mListener = null; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. The loader will monitor the source of + * the data set and may deliver future callbacks if the source changes. Calling + * {@link #stopLoading} will stop the delivery of callbacks. + * + * Must be called from the UI thread + */ + public abstract void startLoading(); + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + public abstract void forceLoad(); + + /** + * Stops delivery of updates until the next time {@link #startLoading()} is called + * + * Must be called from the UI thread + */ + public abstract void stopLoading(); + + /** + * Destroys the loader and frees its resources, making it unusable. + * + * Must be called from the UI thread + */ + public abstract void destroy(); + + /** + * Called when {@link ForceLoadContentObserver} detects a change. Calls {@link #forceLoad()} + * by default. + * + * Must be called from the UI thread + */ + public void onContentChanged() { + forceLoad(); + } +} \ No newline at end of file diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java index 1484204fb5bd774abefcdf379c2c32443960c041..c0788f53dd2af3b34c16753c01b40a312ed7a1da 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/android/content/SharedPreferences.java @@ -17,6 +17,7 @@ package android.content; import java.util.Map; +import java.util.Set; /** * Interface for accessing and modifying preference data returned by {@link @@ -70,6 +71,17 @@ public interface SharedPreferences { */ Editor putString(String key, String value); + /** + * Set a set of String values in the preferences editor, to be written + * back once {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param values The new values for the preference. + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putStringSet(String key, Set values); + /** * Set an int value in the preferences editor, to be written back once * {@link #commit} or {@link #apply} are called. @@ -217,6 +229,20 @@ public interface SharedPreferences { */ String getString(String key, String defValue); + /** + * Retrieve a set of String values from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValues Values to return if this preference does not exist. + * + * @return Returns the preference values if they exist, or defValues. + * Throws ClassCastException if there is a preference with this name + * that is not a Set. + * + * @throws ClassCastException + */ + Set getStringSet(String key, Set defValues); + /** * Retrieve an int value from the preferences. * diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 26b6ad70b351b5e3af1ce222e6651924bb79fa5b..950d339effa862cbf70b73bb726ff293986b34dd 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -16,6 +16,8 @@ package android.content; +import com.google.android.collect.Maps; + import com.android.internal.R; import com.android.internal.util.ArrayUtils; @@ -56,6 +58,7 @@ import android.util.Pair; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -127,14 +130,13 @@ public class SyncManager implements OnAccountsUpdateListener { private static final int INITIALIZATION_UNBIND_DELAY_MS = 5000; - private static final String SYNC_WAKE_LOCK = "*sync*"; + private static final String SYNC_WAKE_LOCK_PREFIX = "*sync*"; private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm"; private Context mContext; private volatile Account[] mAccounts = INITIAL_ACCOUNTS_ARRAY; - volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; volatile private boolean mDataConnectionIsConnected = false; volatile private boolean mStorageIsLow = false; @@ -196,6 +198,8 @@ public class SyncManager implements OnAccountsUpdateListener { private static final Account[] INITIAL_ACCOUNTS_ARRAY = new Account[0]; + private final PowerManager mPowerManager; + public void onAccountsUpdated(Account[] accounts) { // remember if this was the first time this was called after an update final boolean justBootedUp = mAccounts == INITIAL_ACCOUNTS_ARRAY; @@ -357,15 +361,13 @@ public class SyncManager implements OnAccountsUpdateListener { } else { mNotificationMgr = null; } - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mSyncWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SYNC_WAKE_LOCK); - mSyncWakeLock.setReferenceCounted(false); + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); // This WakeLock is used to ensure that we stay awake between the time that we receive // a sync alarm notification and when we finish processing it. We need to do this // because we don't do the work in the alarm handler, rather we do it in a message // handler. - mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + mHandleAlarmWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, HANDLE_SYNC_ALARM_WAKE_LOCK); mHandleAlarmWakeLock.setReferenceCounted(false); @@ -1303,6 +1305,9 @@ public class SyncManager implements OnAccountsUpdateListener { public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); private Long mAlarmScheduleTime = null; public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker(); + private PowerManager.WakeLock mSyncWakeLock; + private final HashMap, PowerManager.WakeLock> mWakeLocks = + Maps.newHashMap(); // used to track if we have installed the error notification so that we don't reinstall // it if sync is still failing @@ -1316,6 +1321,18 @@ public class SyncManager implements OnAccountsUpdateListener { } } + private PowerManager.WakeLock getSyncWakeLock(String accountType, String authority) { + final Pair wakeLockKey = Pair.create(accountType, authority); + PowerManager.WakeLock wakeLock = mWakeLocks.get(wakeLockKey); + if (wakeLock == null) { + final String name = SYNC_WAKE_LOCK_PREFIX + "_" + authority + "_" + accountType; + wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); + wakeLock.setReferenceCounted(false); + mWakeLocks.put(wakeLockKey, wakeLock); + } + return wakeLock; + } + private void waitUntilReadyToRun() { CountDownLatch latch = mReadyToRunLatch; if (latch != null) { @@ -1478,8 +1495,9 @@ public class SyncManager implements OnAccountsUpdateListener { } } finally { final boolean isSyncInProgress = mActiveSyncContext != null; - if (!isSyncInProgress) { + if (!isSyncInProgress && mSyncWakeLock != null) { mSyncWakeLock.release(); + mSyncWakeLock = null; } manageSyncNotification(); manageErrorNotification(); @@ -1701,13 +1719,31 @@ public class SyncManager implements OnAccountsUpdateListener { mActiveSyncContext.close(); mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); - mSyncWakeLock.setWorkSource(null); runStateIdle(); return; } - mSyncWakeLock.setWorkSource(new WorkSource(syncAdapterInfo.uid)); - mSyncWakeLock.acquire(); + // Find the wakelock for this account and authority and store it in mSyncWakeLock. + // Be sure to release the previous wakelock so that we don't end up with it being + // held until it is used again. + // There are a couple tricky things about this code: + // - make sure that we acquire the new wakelock before releasing the old one, + // otherwise the device might go to sleep as soon as we release it. + // - since we use non-reference counted wakelocks we have to be sure not to do + // the release if the wakelock didn't change. Othewise we would do an + // acquire followed by a release on the same lock, resulting in no lock + // being held. + PowerManager.WakeLock oldWakeLock = mSyncWakeLock; + try { + mSyncWakeLock = getSyncWakeLock(op.account.type, op.authority); + mSyncWakeLock.setWorkSource(new WorkSource(syncAdapterInfo.uid)); + mSyncWakeLock.acquire(); + } finally { + if (oldWakeLock != null && oldWakeLock != mSyncWakeLock) { + oldWakeLock.release(); + } + } + // 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 diff --git a/core/java/android/content/XmlDocumentProvider.java b/core/java/android/content/XmlDocumentProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..153ad38b8bfdb0beba3bf76dcd2e83424244153c --- /dev/null +++ b/core/java/android/content/XmlDocumentProvider.java @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.HttpGet; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.ContentResolver.OpenResourceIdResult; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.net.http.AndroidHttpClient; +import android.util.Log; +import android.widget.CursorAdapter; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.BitSet; +import java.util.Stack; +import java.util.regex.Pattern; + +/** + * A read-only content provider which extracts data out of an XML document. + * + *

    A XPath-like selection pattern is used to select some nodes in the XML document. Each such + * node will create a row in the {@link Cursor} result.

    + * + * Each row is then populated with columns that are also defined as XPath-like projections. These + * projections fetch attributes values or text in the matching row node or its children. + * + *

    To add this provider in your application, you should add its declaration to your application + * manifest: + *

    + * <provider android:name="android.content.XmlDocumentProvider" android:authorities="xmldocument" />
    + * 
    + *

    + * + *

    Node selection syntax

    + * The node selection syntax is made of the concatenation of an arbitrary number (at least one) of + * /node_name node selection patterns. + * + *

    The /root/child1/child2 pattern will for instance match all nodes named + * child2 which are children of a node named child1 which are themselves + * children of a root node named root.

    + * + * Any / separator in the previous expression can be replaced by a // + * separator instead, which indicated a descendant instead of a child. + * + *

    The //node1//node2 pattern will for instance match all nodes named + * node2 which are descendant of a node named node1 located anywhere in + * the document hierarchy.

    + * + * Node names can contain namespaces in the form namespace:node. + * + *

    Projection syntax

    + * For every selected node, the projection will then extract actual data from this node and its + * descendant. + * + *

    Use a syntax similar to the selection syntax described above to select the text associated + * with a child of the selected node. The implicit root of this projection pattern is the selected + * node. / will hence refer to the text of the selected node, while + * /child1 will fetch the text of its child named child1 and + * //child1 will match any descendant named child1. If several + * nodes match the projection pattern, their texts are appended as a result.

    + * + * A projection can also fetch any node attribute by appending a @attribute_name + * pattern to the previously described syntax. //child1@price will for instance match + * the attribute price of any child1 descendant. + * + *

    If a projection does not match any node/attribute, its associated value will be an empty + * string.

    + * + *

    Example

    + * Using the following XML document: + *
    + * <library>
    + *   <book id="EH94">
    + *     <title>The Old Man and the Sea</title>
    + *     <author>Ernest Hemingway</author>
    + *   </book>
    + *   <book id="XX10">
    + *     <title>The Arabian Nights: Tales of 1,001 Nights</title>
    + *   </book>
    + *   <no-id>
    + *     <book>
    + *       <title>Animal Farm</title>
    + *       <author>George Orwell</author>
    + *     </book>
    + *   </no-id>
    + * </library>
    + * 
    + * A selection pattern of /library//book will match the three book entries (while + * /library/book will only match the first two ones). + * + *

    Defining the projections as /title, /author and @id + * will retrieve the associated data. Note that the author of the second book as well as the id of + * the third are empty strings. + */ +public class XmlDocumentProvider extends ContentProvider { + /* + * Ideas for improvement: + * - Expand XPath-like syntax to allow for [nb] child number selector + * - Address the starting . bug in AbstractCursor which prevents a true XPath syntax. + * - Provide an alternative to concatenation when several node match (list-like). + * - Support namespaces in attribute names. + * - Incremental Cursor creation, pagination + */ + private static final String LOG_TAG = "XmlDocumentProvider"; + private AndroidHttpClient mHttpClient; + + @Override + public boolean onCreate() { + return true; + } + + /** + * Query data from the XML document referenced in the URI. + * + *

    The XML document can be a local resource or a file that will be downloaded from the + * Internet. In the latter case, your application needs to request the INTERNET permission in + * its manifest.

    + * + * The URI will be of the form content://xmldocument/?resource=R.xml.myFile for a + * local resource. xmldocument should match the authority declared for this + * provider in your manifest. Internet documents are referenced using + * content://xmldocument/?url= followed by an encoded version of the URL of your + * document (see {@link Uri#encode(String)}). + * + *

    The number of columns of the resulting Cursor is equal to the size of the projection + * array plus one, named _id which will contain a unique row id (allowing the + * Cursor to be used with a {@link CursorAdapter}). The other columns' names are the projection + * patterns.

    + * + * @param uri The URI of your local resource or Internet document. + * @param projection A set of patterns that will be used to extract data from each selected + * node. See class documentation for pattern syntax. + * @param selection A selection pattern which will select the nodes that will create the + * Cursor's rows. See class documentation for pattern syntax. + * @param selectionArgs This parameter is ignored. + * @param sortOrder The row order in the resulting cursor is determined from the node order in + * the XML document. This parameter is ignored. + * @return A Cursor or null in case of error. + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + + XmlPullParser parser = null; + mHttpClient = null; + + final String url = uri.getQueryParameter("url"); + if (url != null) { + parser = getUriXmlPullParser(url); + } else { + final String resource = uri.getQueryParameter("resource"); + if (resource != null) { + Uri resourceUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + + getContext().getPackageName() + "/" + resource); + parser = getResourceXmlPullParser(resourceUri); + } + } + + if (parser != null) { + XMLCursor xmlCursor = new XMLCursor(selection, projection); + try { + xmlCursor.parseWith(parser); + return xmlCursor; + } catch (IOException e) { + Log.w(LOG_TAG, "I/O error while parsing XML " + uri, e); + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Error while parsing XML " + uri, e); + } finally { + if (mHttpClient != null) { + mHttpClient.close(); + } + } + } + + return null; + } + + /** + * Creates an XmlPullParser for the provided URL. Can be overloaded to provide your own parser. + * @param url The URL of the XML document that is to be parsed. + * @return An XmlPullParser on this document. + */ + protected XmlPullParser getUriXmlPullParser(String url) { + XmlPullParser parser = null; + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + parser = factory.newPullParser(); + } catch (XmlPullParserException e) { + Log.e(LOG_TAG, "Unable to create XmlPullParser", e); + return null; + } + + InputStream inputStream = null; + try { + final HttpGet get = new HttpGet(url); + mHttpClient = AndroidHttpClient.newInstance("Android"); + HttpResponse response = mHttpClient.execute(get); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final HttpEntity entity = response.getEntity(); + if (entity != null) { + inputStream = entity.getContent(); + } + } + } catch (IOException e) { + Log.w(LOG_TAG, "Error while retrieving XML file " + url, e); + return null; + } + + try { + parser.setInput(inputStream, null); + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Error while reading XML file from " + url, e); + return null; + } + + return parser; + } + + /** + * Creates an XmlPullParser for the provided local resource. Can be overloaded to provide your + * own parser. + * @param resourceUri A fully qualified resource name referencing a local XML resource. + * @return An XmlPullParser on this resource. + */ + protected XmlPullParser getResourceXmlPullParser(Uri resourceUri) { + OpenResourceIdResult resourceId; + try { + resourceId = getContext().getContentResolver().getResourceId(resourceUri); + return resourceId.r.getXml(resourceId.id); + } catch (FileNotFoundException e) { + Log.w(LOG_TAG, "XML resource not found: " + resourceUri.toString(), e); + return null; + } + } + + /** + * Returns "vnd.android.cursor.dir/xmldoc". + */ + @Override + public String getType(Uri uri) { + return "vnd.android.cursor.dir/xmldoc"; + } + + /** + * This ContentProvider is read-only. This method throws an UnsupportedOperationException. + **/ + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + /** + * This ContentProvider is read-only. This method throws an UnsupportedOperationException. + **/ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + /** + * This ContentProvider is read-only. This method throws an UnsupportedOperationException. + **/ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + private static class XMLCursor extends MatrixCursor { + private final Pattern mSelectionPattern; + private Pattern[] mProjectionPatterns; + private String[] mAttributeNames; + private String[] mCurrentValues; + private BitSet[] mActiveTextDepthMask; + private final int mNumberOfProjections; + + public XMLCursor(String selection, String[] projections) { + super(projections); + // The first column in projections is used for the _ID + mNumberOfProjections = projections.length - 1; + mSelectionPattern = createPattern(selection); + createProjectionPattern(projections); + } + + private Pattern createPattern(String input) { + String pattern = input.replaceAll("//", "/(.*/|)").replaceAll("^/", "^/") + "$"; + return Pattern.compile(pattern); + } + + private void createProjectionPattern(String[] projections) { + mProjectionPatterns = new Pattern[mNumberOfProjections]; + mAttributeNames = new String[mNumberOfProjections]; + mActiveTextDepthMask = new BitSet[mNumberOfProjections]; + // Add a column to store _ID + mCurrentValues = new String[mNumberOfProjections + 1]; + + for (int i=0; i= 0) { + mAttributeNames[i] = projection.substring(atIndex+1); + projection = projection.substring(0, atIndex); + } else { + mAttributeNames[i] = null; + } + + // Conforms to XPath standard: reference to local context starts with a . + if (projection.charAt(0) == '.') { + projection = projection.substring(1); + } + mProjectionPatterns[i] = createPattern(projection); + } + } + + public void parseWith(XmlPullParser parser) throws IOException, XmlPullParserException { + StringBuilder path = new StringBuilder(); + Stack pathLengthStack = new Stack(); + + // There are two parsing mode: in root mode, rootPath is updated and nodes matching + // selectionPattern are searched for and currentNodeDepth is negative. + // When a node matching selectionPattern is found, currentNodeDepth is set to 0 and + // updated as children are parsed and projectionPatterns are searched in nodePath. + int currentNodeDepth = -1; + + // Index where local selected node path starts from in path + int currentNodePathStartIndex = 0; + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + + if (eventType == XmlPullParser.START_TAG) { + // Update path + pathLengthStack.push(path.length()); + path.append('/'); + String prefix = null; + try { + // getPrefix is not supported by local Xml resource parser + prefix = parser.getPrefix(); + } catch (RuntimeException e) { + prefix = null; + } + if (prefix != null) { + path.append(prefix); + path.append(':'); + } + path.append(parser.getName()); + + if (currentNodeDepth >= 0) { + currentNodeDepth++; + } else { + // A node matching selection is found: initialize child parsing mode + if (mSelectionPattern.matcher(path.toString()).matches()) { + currentNodeDepth = 0; + currentNodePathStartIndex = path.length(); + mCurrentValues[0] = Integer.toString(getCount()); // _ID + for (int i = 0; i < mNumberOfProjections; i++) { + // Reset values to default (empty string) + mCurrentValues[i + 1] = ""; + mActiveTextDepthMask[i].clear(); + } + } + } + + // This test has to be separated from the previous one as currentNodeDepth can + // be modified above (when a node matching selection is found). + if (currentNodeDepth >= 0) { + final String localNodePath = path.substring(currentNodePathStartIndex); + for (int i = 0; i < mNumberOfProjections; i++) { + if (mProjectionPatterns[i].matcher(localNodePath).matches()) { + String attribute = mAttributeNames[i]; + if (attribute != null) { + mCurrentValues[i + 1] = + parser.getAttributeValue(null, attribute); + } else { + mActiveTextDepthMask[i].set(currentNodeDepth, true); + } + } + } + } + + } else if (eventType == XmlPullParser.END_TAG) { + // Pop last node from path + final int length = pathLengthStack.pop(); + path.setLength(length); + + if (currentNodeDepth >= 0) { + if (currentNodeDepth == 0) { + // Leaving a selection matching node: add a new row with results + addRow(mCurrentValues); + } else { + for (int i = 0; i < mNumberOfProjections; i++) { + mActiveTextDepthMask[i].set(currentNodeDepth, false); + } + } + currentNodeDepth--; + } + + } else if ((eventType == XmlPullParser.TEXT) && (!parser.isWhitespace())) { + for (int i = 0; i < mNumberOfProjections; i++) { + if ((currentNodeDepth >= 0) && + (mActiveTextDepthMask[i].get(currentNodeDepth))) { + mCurrentValues[i + 1] += parser.getText(); + } + } + } + + eventType = parser.next(); + } + } + } +} diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 395c392b12b27aaf753d9b38e6a68ea4a473eb79..e21cb975d928cc5ea51cf48eded31b0e407163fd 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -149,6 +149,27 @@ public class ActivityInfo extends ComponentInfo * {@link android.R.attr#finishOnCloseSystemDialogs} attribute. */ public static final int FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS = 0x0100; + /** + * Bit in {@link #flags} corresponding to an immersive activity + * that wishes not to be interrupted by notifications. + * Applications that hide the system notification bar with + * {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN} + * may still be interrupted by high-priority notifications; for example, an + * incoming phone call may use + * {@link android.app.Notification#fullScreenIntent fullScreenIntent} + * to present a full-screen in-call activity to the user, pausing the + * current activity as a side-effect. An activity with + * {@link #FLAG_IMMERSIVE} set, however, will not be interrupted; the + * notification may be shown in some other way (such as a small floating + * "toast" window). + * {@see android.app.Notification#FLAG_HIGH_PRIORITY} + */ + public static final int FLAG_IMMERSIVE = 0x0200; + /** + * Value for {@link #flags}: true when the application's rendering should + * be hardware accelerated. + */ + public static final int FLAG_HARDWARE_ACCELERATED = 0x0400; /** * Options that have been set in the activity declaration in the * manifest. @@ -159,6 +180,7 @@ public class ActivityInfo extends ComponentInfo * {@link #FLAG_STATE_NOT_NEEDED}, {@link #FLAG_EXCLUDE_FROM_RECENTS}, * {@link #FLAG_ALLOW_TASK_REPARENTING}, {@link #FLAG_NO_HISTORY}, * {@link #FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS}, + * {@link #FLAG_IMMERSIVE}, {@link #FLAG_HARDWARE_ACCELERATED} */ public int flags; diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 0db954db35f0f92700aa2811e43448d9e2c16b52..38d897e440efe975d00c528f097e3ffbc44af361 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -269,7 +269,6 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * increased in size for extra large screens. Corresponds to * {@link android.R.styleable#AndroidManifestSupportsScreens_xlargeScreens * android:xlargeScreens}. - * @hide */ public static final int FLAG_SUPPORTS_XLARGE_SCREENS = 1<<19; @@ -312,7 +311,7 @@ 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_SUPPORTS_LARGE_SCREENS}, {@link #FLAG_SUPPORTS_XLARGE_SCREENS}, * {@link #FLAG_RESIZEABLE_FOR_SCREENS}, * {@link #FLAG_SUPPORTS_SCREEN_DENSITIES}, {@link #FLAG_VM_SAFE_MODE} */ diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index e20cb5ee4134e8323d183a0aee505da5579d54d6..92a8a9977a14b72c3ad06c776756c09c0c5b46f1 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -16,9 +16,6 @@ package android.content.pm; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -30,14 +27,14 @@ import android.content.res.XmlResourceParser; import android.os.Build; import android.os.Bundle; import android.os.PatternMatcher; -import android.provider.Settings; import android.util.AttributeSet; import android.util.Config; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; - import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; @@ -48,7 +45,6 @@ import java.security.cert.CertificateEncodingException; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; -import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -1498,12 +1494,18 @@ public class PackageParser { ai.nonLocalizedLabel = v.coerceToString(); } + int defaultTheme = 0; + if (owner.applicationInfo.targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) { + // As of honeycomb, the default application theme is holographic. + defaultTheme = android.R.style.Theme_Holo; + } + ai.icon = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestApplication_icon, 0); ai.logo = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestApplication_logo, 0); ai.theme = sa.getResourceId( - com.android.internal.R.styleable.AndroidManifestApplication_theme, 0); + com.android.internal.R.styleable.AndroidManifestApplication_theme, defaultTheme); ai.descriptionRes = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestApplication_description, 0); @@ -1535,6 +1537,10 @@ public class PackageParser { ai.flags |= ApplicationInfo.FLAG_VM_SAFE_MODE; } + boolean hardwareAccelerated = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_hardwareAccelerated, + false); + if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_hasCode, true)) { @@ -1634,7 +1640,8 @@ public class PackageParser { String tagName = parser.getName(); if (tagName.equals("activity")) { - Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false); + Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false, + hardwareAccelerated); if (a == null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; @@ -1643,7 +1650,7 @@ public class PackageParser { owner.activities.add(a); } else if (tagName.equals("receiver")) { - Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true); + Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true, false); if (a == null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; @@ -1778,7 +1785,8 @@ public class PackageParser { private Activity parseActivity(Package owner, Resources res, XmlPullParser parser, AttributeSet attrs, int flags, String[] outError, - boolean receiver) throws XmlPullParserException, IOException { + boolean receiver, boolean hardwareAccelerated) + throws XmlPullParserException, IOException { TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestActivity); @@ -1883,7 +1891,19 @@ public class PackageParser { a.info.flags |= ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS; } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_immersive, + false)) { + a.info.flags |= ActivityInfo.FLAG_IMMERSIVE; + } + if (!receiver) { + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_hardwareAccelerated, + hardwareAccelerated)) { + a.info.flags |= ActivityInfo.FLAG_HARDWARE_ACCELERATED; + } + a.info.launchMode = sa.getInt( com.android.internal.R.styleable.AndroidManifestActivity_launchMode, ActivityInfo.LAUNCH_MULTIPLE); diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java index a37e4e8cc3bf31286675c97b02d6d62b47c1e4b3..01ae1dab30f36399f2d5483be48e93a8e358e248 100644 --- a/core/java/android/content/res/AssetFileDescriptor.java +++ b/core/java/android/content/res/AssetFileDescriptor.java @@ -25,8 +25,6 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.channels.FileChannel; /** * File descriptor of an entry in the AssetManager. This provides your own @@ -51,7 +49,7 @@ public class AssetFileDescriptor implements Parcelable { * @param startOffset The location within the file that the asset starts. * This must be 0 if length is UNKNOWN_LENGTH. * @param length The number of bytes of the asset, or - * {@link #UNKNOWN_LENGTH if it extends to the end of the file. + * {@link #UNKNOWN_LENGTH} if it extends to the end of the file. */ public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset, long length) { @@ -125,13 +123,6 @@ public class AssetFileDescriptor implements Parcelable { public void close() throws IOException { mFd.close(); } - - /** - * Checks whether this file descriptor is for a memory file. - */ - private boolean isMemoryFile() throws IOException { - return MemoryFile.isMemoryFile(mFd.getFileDescriptor()); - } /** * Create and return a new auto-close input stream for this asset. This @@ -142,12 +133,6 @@ public class AssetFileDescriptor implements Parcelable { * should only call this once for a particular asset. */ public FileInputStream createInputStream() throws IOException { - if (isMemoryFile()) { - if (mLength > Integer.MAX_VALUE) { - throw new IOException("File length too large for a memory file: " + mLength); - } - return new AutoCloseMemoryFileInputStream(mFd, (int)mLength); - } if (mLength < 0) { return new ParcelFileDescriptor.AutoCloseInputStream(mFd); } @@ -276,66 +261,6 @@ public class AssetFileDescriptor implements Parcelable { super.reset(); } } - - /** - * An input stream that reads from a MemoryFile and closes it when the stream is closed. - * This extends FileInputStream just because {@link #createInputStream} returns - * a FileInputStream. All the FileInputStream methods are - * overridden to use the MemoryFile instead. - */ - private static class AutoCloseMemoryFileInputStream extends FileInputStream { - private ParcelFileDescriptor mParcelFd; - private MemoryFile mFile; - private InputStream mStream; - - public AutoCloseMemoryFileInputStream(ParcelFileDescriptor fd, int length) - throws IOException { - super(fd.getFileDescriptor()); - mParcelFd = fd; - mFile = new MemoryFile(fd.getFileDescriptor(), length, "r"); - mStream = mFile.getInputStream(); - } - - @Override - public int available() throws IOException { - return mStream.available(); - } - - @Override - public void close() throws IOException { - mParcelFd.close(); // must close ParcelFileDescriptor, not just the file descriptor, - // since it could be a subclass of ParcelFileDescriptor. - // E.g. ContentResolver.ParcelFileDescriptorInner.close() releases - // a content provider - mFile.close(); // to unmap the memory file from the address space. - mStream.close(); // doesn't actually do anything - } - - @Override - public FileChannel getChannel() { - return null; - } - - @Override - public int read() throws IOException { - return mStream.read(); - } - - @Override - public int read(byte[] buffer, int offset, int count) throws IOException { - return mStream.read(buffer, offset, count); - } - - @Override - public int read(byte[] buffer) throws IOException { - return mStream.read(buffer); - } - - @Override - public long skip(long count) throws IOException { - return mStream.skip(count); - } - } /** * An OutputStream you can create on a ParcelFileDescriptor, which will @@ -422,15 +347,4 @@ public class AssetFileDescriptor implements Parcelable { } }; - /** - * Creates an AssetFileDescriptor from a memory file. - * - * @hide - */ - public static AssetFileDescriptor fromMemoryFile(MemoryFile memoryFile) - throws IOException { - ParcelFileDescriptor fd = memoryFile.getParcelFileDescriptor(); - return new AssetFileDescriptor(fd, 0, memoryFile.length()); - } - } diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 5a3dd415b9fc41318263b3836c19e3d12cf39508..2f110f020e7a4ee34ad3c5d57a0a757a803b58fd 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -62,7 +62,6 @@ public final class Configuration implements Parcelable, ComparableThe {@link #SCREENLAYOUT_SIZE_MASK} bits define the overall size * of the screen. They may be one of * {@link #SCREENLAYOUT_SIZE_SMALL}, {@link #SCREENLAYOUT_SIZE_NORMAL}, - * {@link #SCREENLAYOUT_SIZE_LARGE}. + * {@link #SCREENLAYOUT_SIZE_LARGE}, or {@link #SCREENLAYOUT_SIZE_XLARGE}. * *

    The {@link #SCREENLAYOUT_LONG_MASK} defines whether the screen * is wider/taller than normal. They may be one of @@ -267,11 +266,18 @@ public final class Configuration implements Parcelable, Comparable= 2 && n <= 4) { - return QUANTITY_FEW; - } - else { - return QUANTITY_OTHER; - } - } - } - - private static PluralRules en; - private static class en extends PluralRules { - int quantityForNumber(int n) { - if (n == 1) { - return QUANTITY_ONE; - } - else { - return QUANTITY_OTHER; - } - } - } -} - diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java old mode 100644 new mode 100755 index 9bb3b75124f3ddd8ce423469d5f9c7d231ca7efb..9b23c1ea89659ca5a8438dfc55209fdddd098f64 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -16,7 +16,6 @@ package android.content.res; - import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -25,6 +24,7 @@ import org.xmlpull.v1.XmlPullParserException; import android.graphics.Movie; import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable.ConstantState; import android.os.Build; import android.os.Bundle; import android.os.SystemProperties; @@ -41,6 +41,8 @@ import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.Locale; +import libcore.icu.NativePluralRules; + /** * Class for accessing an application's resources. This sits on top of the * asset manager of the application (accessible through getAssets()) and @@ -52,6 +54,8 @@ public class Resources { private static final boolean DEBUG_CONFIG = false; private static final boolean TRACE_FOR_PRELOAD = false; + private static final int ID_OTHER = 0x01000004; + // Use the current SDK version code. If we are a development build, // also allow the previous SDK version + 1. private static final int sSdkVersion = Build.VERSION.SDK_INT @@ -66,6 +70,8 @@ public class Resources { = new LongSparseArray(); private static final SparseArray mPreloadedColorStateLists = new SparseArray(); + private static final LongSparseArray sPreloadedColorDrawables + = new LongSparseArray(); private static boolean mPreloaded; /*package*/ final TypedValue mTmpValue = new TypedValue(); @@ -75,6 +81,8 @@ public class Resources { = new LongSparseArray >(); private final SparseArray > mColorStateListCache = new SparseArray >(); + private final LongSparseArray > mColorDrawableCache + = new LongSparseArray >(); private boolean mPreloading; /*package*/ TypedArray mCachedStyledAttributes = null; @@ -86,7 +94,7 @@ public class Resources { /*package*/ final AssetManager mAssets; private final Configuration mConfiguration = new Configuration(); /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics(); - PluralRules mPluralRule; + private NativePluralRules mPluralRule; private CompatibilityInfo mCompatibilityInfo; private Display mDefaultDisplay; @@ -203,9 +211,17 @@ public class Resources { } /** + * Return the character sequence associated with a particular resource ID for a particular + * numerical quantity. + * + *

    See String + * Resources for more on quantity strings. + * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. + * @param quantity The number used to get the correct string for the current language's + * plural rules. * * @throws NotFoundException Throws NotFoundException if the given ID does not exist. * @@ -213,29 +229,52 @@ public class Resources { * possibly styled text information. */ public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { - PluralRules rule = getPluralRule(); - CharSequence res = mAssets.getResourceBagText(id, rule.attrForNumber(quantity)); + NativePluralRules rule = getPluralRule(); + CharSequence res = mAssets.getResourceBagText(id, + attrForQuantityCode(rule.quantityForInt(quantity))); if (res != null) { return res; } - res = mAssets.getResourceBagText(id, PluralRules.ID_OTHER); + res = mAssets.getResourceBagText(id, ID_OTHER); if (res != null) { return res; } throw new NotFoundException("Plural resource ID #0x" + Integer.toHexString(id) + " quantity=" + quantity - + " item=" + PluralRules.stringForQuantity(rule.quantityForNumber(quantity))); + + " item=" + stringForQuantityCode(rule.quantityForInt(quantity))); } - private PluralRules getPluralRule() { + private NativePluralRules getPluralRule() { synchronized (mSync) { if (mPluralRule == null) { - mPluralRule = PluralRules.ruleForLocale(mConfiguration.locale); + mPluralRule = NativePluralRules.forLocale(mConfiguration.locale); } return mPluralRule; } } + private static int attrForQuantityCode(int quantityCode) { + switch (quantityCode) { + case NativePluralRules.ZERO: return 0x01000005; + case NativePluralRules.ONE: return 0x01000006; + case NativePluralRules.TWO: return 0x01000007; + case NativePluralRules.FEW: return 0x01000008; + case NativePluralRules.MANY: return 0x01000009; + default: return ID_OTHER; + } + } + + private static String stringForQuantityCode(int quantityCode) { + switch (quantityCode) { + case NativePluralRules.ZERO: return "zero"; + case NativePluralRules.ONE: return "one"; + case NativePluralRules.TWO: return "two"; + case NativePluralRules.FEW: return "few"; + case NativePluralRules.MANY: return "many"; + default: return "other"; + } + } + /** * Return the string value associated with a particular resource ID. It * will be stripped of any styled text information. @@ -290,6 +329,9 @@ public class Resources { * stripped of any styled text information. * {@more} * + *

    See String + * Resources for more on quantity strings. + * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. @@ -312,6 +354,9 @@ public class Resources { * Return the string value associated with a particular resource ID for a particular * numerical quantity. * + *

    See String + * Resources for more on quantity strings. + * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. @@ -1299,44 +1344,55 @@ public class Resources { (int)(mMetrics.density*160), mConfiguration.keyboard, keyboardHidden, mConfiguration.navigation, width, height, mConfiguration.screenLayout, mConfiguration.uiMode, sSdkVersion); - int N = mDrawableCache.size(); - if (DEBUG_CONFIG) { - Log.d(TAG, "Cleaning up drawables config changes: 0x" - + Integer.toHexString(configChanges)); + + clearDrawableCache(mDrawableCache, configChanges); + clearDrawableCache(mColorDrawableCache, configChanges); + + mColorStateListCache.clear(); + + + flushLayoutCache(); + } + synchronized (mSync) { + if (mPluralRule != null) { + mPluralRule = NativePluralRules.forLocale(config.locale); } - for (int i=0; i ref = mDrawableCache.valueAt(i); - if (ref != null) { - Drawable.ConstantState cs = ref.get(); - if (cs != null) { - if (Configuration.needNewResources( - configChanges, cs.getChangingConfigurations())) { - if (DEBUG_CONFIG) { - Log.d(TAG, "FLUSHING #0x" - + Long.toHexString(mDrawableCache.keyAt(i)) - + " / " + cs + " with changes: 0x" - + Integer.toHexString(cs.getChangingConfigurations())); - } - mDrawableCache.setValueAt(i, null); - } else if (DEBUG_CONFIG) { - Log.d(TAG, "(Keeping #0x" + } + } + + private void clearDrawableCache( + LongSparseArray> cache, + int configChanges) { + int N = cache.size(); + if (DEBUG_CONFIG) { + Log.d(TAG, "Cleaning up drawables config changes: 0x" + + Integer.toHexString(configChanges)); + } + for (int i=0; i ref = cache.valueAt(i); + if (ref != null) { + Drawable.ConstantState cs = ref.get(); + if (cs != null) { + if (Configuration.needNewResources( + configChanges, cs.getChangingConfigurations())) { + if (DEBUG_CONFIG) { + Log.d(TAG, "FLUSHING #0x" + Long.toHexString(mDrawableCache.keyAt(i)) + " / " + cs + " with changes: 0x" - + Integer.toHexString(cs.getChangingConfigurations()) - + ")"); + + Integer.toHexString(cs.getChangingConfigurations())); } + cache.setValueAt(i, null); + } else if (DEBUG_CONFIG) { + Log.d(TAG, "(Keeping #0x" + + Long.toHexString(cache.keyAt(i)) + + " / " + cs + " with changes: 0x" + + Integer.toHexString(cs.getChangingConfigurations()) + + ")"); } } } - mDrawableCache.clear(); - mColorStateListCache.clear(); - flushLayoutCache(); - } - synchronized (mSync) { - if (mPluralRule != null) { - mPluralRule = PluralRules.ruleForLocale(config.locale); - } } + cache.clear(); } /** @@ -1503,7 +1559,7 @@ public class Resources { /** * Parse a series of {@link android.R.styleable#Extra <extra>} tags from * an XML file. You call this when you are at the parent tag of the - * extra tags, and it return once all of the child tags have been parsed. + * extra tags, and it will return once all of the child tags have been parsed. * This will call {@link #parseBundleExtra} for each extra tag encountered. * * @param parser The parser from which to retrieve the extras. @@ -1661,13 +1717,18 @@ public class Resources { } final long key = (((long) value.assetCookie) << 32) | value.data; - Drawable dr = getCachedDrawable(key); + boolean isColorDrawable = false; + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && + value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + isColorDrawable = true; + } + Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key); if (dr != null) { return dr; } - Drawable.ConstantState cs = sPreloadedDrawables.get(key); + Drawable.ConstantState cs = isColorDrawable ? sPreloadedColorDrawables.get(key) : sPreloadedDrawables.get(key); if (cs != null) { dr = cs.newDrawable(this); } else { @@ -1726,13 +1787,21 @@ public class Resources { cs = dr.getConstantState(); if (cs != null) { if (mPreloading) { - sPreloadedDrawables.put(key, cs); + if (isColorDrawable) { + sPreloadedColorDrawables.put(key, cs); + } else { + sPreloadedDrawables.put(key, cs); + } } else { synchronized (mTmpValue) { //Log.i(TAG, "Saving cached drawable @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + cs); - mDrawableCache.put(key, new WeakReference(cs)); + if (isColorDrawable) { + mColorDrawableCache.put(key, new WeakReference(cs)); + } else { + mDrawableCache.put(key, new WeakReference(cs)); + } } } } @@ -1741,9 +1810,11 @@ public class Resources { return dr; } - private Drawable getCachedDrawable(long key) { + private Drawable getCachedDrawable( + LongSparseArray> drawableCache, + long key) { synchronized (mTmpValue) { - WeakReference wr = mDrawableCache.get(key); + WeakReference wr = drawableCache.get(key); if (wr != null) { // we have the key Drawable.ConstantState entry = wr.get(); if (entry != null) { @@ -1753,7 +1824,7 @@ public class Resources { return entry.newDrawable(this); } else { // our entry has been purged - mDrawableCache.delete(key); + drawableCache.delete(key); } } } diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index 038eedfa443db534dd73d809f5beb5cfa5f6a3c5..bfaeb828c9f80f7e4328e6e2a8950fcb60de51a1 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -18,16 +18,11 @@ package android.database; import android.content.ContentResolver; import android.net.Uri; +import android.os.Bundle; import android.util.Config; import android.util.Log; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; import java.lang.ref.WeakReference; -import java.lang.UnsupportedOperationException; import java.util.HashMap; import java.util.Map; @@ -56,6 +51,10 @@ public abstract class AbstractCursor implements CrossProcessCursor { abstract public double getDouble(int column); abstract public boolean isNull(int column); + public int getType(int column) { + throw new UnsupportedOperationException(); + } + // TODO implement getBlob in all cursor types public byte[] getBlob(int column) { throw new UnsupportedOperationException("getBlob is not supported"); @@ -88,7 +87,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { } mDataSetObservable.notifyInvalidated(); } - + public boolean requery() { if (mSelfObserver != null && mSelfObserverRegistered == false) { mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); @@ -108,22 +107,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { deactivateInternal(); } - /** - * @hide - * @deprecated - */ - public boolean commitUpdates(Map> values) { - return false; - } - - /** - * @hide - * @deprecated - */ - public boolean deleteRow() { - return false; - } - /** * This function is called every time the cursor is successfully scrolled * to a new position, giving the subclass a chance to update any state it @@ -204,7 +187,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { * @param window */ public void fillWindow(int position, CursorWindow window) { - if (position < 0 || position > getCount()) { + if (position < 0 || position >= getCount()) { return; } window.acquireReference(); @@ -320,137 +303,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { return getColumnNames()[columnIndex]; } - /** - * @hide - * @deprecated - */ - public boolean updateBlob(int columnIndex, byte[] value) { - return update(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateString(int columnIndex, String value) { - return update(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateShort(int columnIndex, short value) { - return update(columnIndex, Short.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateInt(int columnIndex, int value) { - return update(columnIndex, Integer.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateLong(int columnIndex, long value) { - return update(columnIndex, Long.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateFloat(int columnIndex, float value) { - return update(columnIndex, Float.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateDouble(int columnIndex, double value) { - return update(columnIndex, Double.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateToNull(int columnIndex) { - return update(columnIndex, null); - } - - /** - * @hide - * @deprecated - */ - public boolean update(int columnIndex, Object obj) { - if (!supportsUpdates()) { - return false; - } - - // Long.valueOf() returns null sometimes! -// Long rowid = Long.valueOf(getLong(mRowIdColumnIndex)); - Long rowid = new Long(getLong(mRowIdColumnIndex)); - if (rowid == null) { - throw new IllegalStateException("null rowid. mRowIdColumnIndex = " + mRowIdColumnIndex); - } - - synchronized(mUpdatedRows) { - Map row = mUpdatedRows.get(rowid); - if (row == null) { - row = new HashMap(); - mUpdatedRows.put(rowid, row); - } - row.put(getColumnNames()[columnIndex], obj); - } - - return true; - } - - /** - * Returns true if there are pending updates that have not yet been committed. - * - * @return true if there are pending updates that have not yet been committed. - * @hide - * @deprecated - */ - public boolean hasUpdates() { - synchronized(mUpdatedRows) { - return mUpdatedRows.size() > 0; - } - } - - /** - * @hide - * @deprecated - */ - public void abortUpdates() { - synchronized(mUpdatedRows) { - mUpdatedRows.clear(); - } - } - - /** - * @hide - * @deprecated - */ - public boolean commitUpdates() { - return commitUpdates(null); - } - - /** - * @hide - * @deprecated - */ - public boolean supportsUpdates() { - return mRowIdColumnIndex != -1; - } - public void registerContentObserver(ContentObserver observer) { mContentObservable.registerObserver(observer); } @@ -478,9 +330,9 @@ public abstract class AbstractCursor implements CrossProcessCursor { return mDataSetObservable; } + public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); - } public void unregisterDataSetObserver(DataSetObserver observer) { @@ -522,6 +374,10 @@ public abstract class AbstractCursor implements CrossProcessCursor { } } + public Uri getNotificationUri() { + return mNotifyUri; + } + public boolean getWantsAllOnMoveCalls() { return false; } @@ -535,36 +391,19 @@ public abstract class AbstractCursor implements CrossProcessCursor { } /** - * This function returns true if the field has been updated and is - * used in conjunction with {@link #getUpdatedField} to allow subclasses to - * support reading uncommitted updates. NOTE: This function and - * {@link #getUpdatedField} should be called together inside of a - * block synchronized on mUpdatedRows. - * - * @param columnIndex the column index of the field to check - * @return true if the field has been updated, false otherwise + * @deprecated Always returns false since Cursors do not support updating rows */ + @Deprecated protected boolean isFieldUpdated(int columnIndex) { - if (mRowIdColumnIndex != -1 && mUpdatedRows.size() > 0) { - Map updates = mUpdatedRows.get(mCurrentRowID); - if (updates != null && updates.containsKey(getColumnNames()[columnIndex])) { - return true; - } - } return false; } /** - * This function returns the uncommitted updated value for the field - * at columnIndex. NOTE: This function and {@link #isFieldUpdated} should - * be called together inside of a block synchronized on mUpdatedRows. - * - * @param columnIndex the column index of the field to retrieve - * @return the updated value + * @deprecated Always returns null since Cursors do not support updating rows */ + @Deprecated protected Object getUpdatedField(int columnIndex) { - Map updates = mUpdatedRows.get(mCurrentRowID); - return updates.get(getColumnNames()[columnIndex]); + return null; } /** @@ -614,11 +453,9 @@ public abstract class AbstractCursor implements CrossProcessCursor { } /** - * This HashMap contains a mapping from Long rowIDs to another Map - * that maps from String column names to new values. A NULL value means to - * remove an existing value, and all numeric values are in their class - * forms, i.e. Integer, Long, Float, etc. + * @deprecated This is never updated by this class and should not be used */ + @Deprecated protected HashMap> mUpdatedRows; /** @@ -628,6 +465,11 @@ public abstract class AbstractCursor implements CrossProcessCursor { protected int mRowIdColumnIndex; protected int mPos; + /** + * If {@link #mRowIdColumnIndex} is not -1 this contains contains the value of + * the column at {@link #mRowIdColumnIndex} for the current row this cursor is + * pointing at. + */ protected Long mCurrentRowID; protected ContentResolver mContentResolver; protected boolean mClosed = false; diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java index 27a02e260f52a0a7b01523c4e955f596529b4b52..8addaa83d09040c9e2a07c5f3b4feb3c15bdc17c 100644 --- a/core/java/android/database/AbstractWindowedCursor.java +++ b/core/java/android/database/AbstractWindowedCursor.java @@ -19,202 +19,105 @@ package android.database; /** * A base class for Cursors that store their data in {@link CursorWindow}s. */ -public abstract class AbstractWindowedCursor extends AbstractCursor -{ +public abstract class AbstractWindowedCursor extends AbstractCursor { @Override - public byte[] getBlob(int columnIndex) - { + public byte[] getBlob(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - return (byte[])getUpdatedField(columnIndex); - } - } - return mWindow.getBlob(mPos, columnIndex); } @Override - public String getString(int columnIndex) - { + public String getString(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - return (String)getUpdatedField(columnIndex); - } - } - return mWindow.getString(mPos, columnIndex); } - + @Override - public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) - { + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - super.copyStringToBuffer(columnIndex, buffer); - } - } - mWindow.copyStringToBuffer(mPos, columnIndex, buffer); } @Override - public short getShort(int columnIndex) - { + public short getShort(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.shortValue(); - } - } - return mWindow.getShort(mPos, columnIndex); } @Override - public int getInt(int columnIndex) - { + public int getInt(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.intValue(); - } - } - return mWindow.getInt(mPos, columnIndex); } @Override - public long getLong(int columnIndex) - { + public long getLong(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.longValue(); - } - } - return mWindow.getLong(mPos, columnIndex); } @Override - public float getFloat(int columnIndex) - { + public float getFloat(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.floatValue(); - } - } - return mWindow.getFloat(mPos, columnIndex); } @Override - public double getDouble(int columnIndex) - { + public double getDouble(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.doubleValue(); - } - } - return mWindow.getDouble(mPos, columnIndex); } @Override - public boolean isNull(int columnIndex) - { + public boolean isNull(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - return getUpdatedField(columnIndex) == null; - } - } - - return mWindow.isNull(mPos, columnIndex); + return mWindow.getType(mPos, columnIndex) == Cursor.FIELD_TYPE_NULL; } - public boolean isBlob(int columnIndex) - { - checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Object object = getUpdatedField(columnIndex); - return object == null || object instanceof byte[]; - } - } - - return mWindow.isBlob(mPos, columnIndex); + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isBlob(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_BLOB; } - 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); + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isString(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_STRING; } - public boolean isLong(int columnIndex) - { - checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Object object = getUpdatedField(columnIndex); - return object != null && (object instanceof Integer || object instanceof Long); - } - } + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isLong(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_INTEGER; + } - return mWindow.isLong(mPos, columnIndex); + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isFloat(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_FLOAT; } - public boolean isFloat(int columnIndex) - { + @Override + public int getType(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); + return mWindow.getType(mPos, columnIndex); } @Override - protected void checkPosition() - { + protected void checkPosition() { super.checkPosition(); if (mWindow == null) { - throw new StaleDataException("Access closed cursor"); + throw new StaleDataException("Attempting to access a closed cursor"); } } diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java index baa94d8ad5c366b671618d4d80416f992328103b..fa62d69217783a6a639141a93cfcbab4318e4d04 100644 --- a/core/java/android/database/BulkCursorNative.java +++ b/core/java/android/database/BulkCursorNative.java @@ -17,13 +17,10 @@ package android.database; import android.os.Binder; -import android.os.RemoteException; +import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; -import android.os.Bundle; - -import java.util.HashMap; -import java.util.Map; +import android.os.RemoteException; /** * Native implementation of the bulk cursor. This is only for use in implementing @@ -120,26 +117,6 @@ public abstract class BulkCursorNative extends Binder implements IBulkCursor return true; } - case UPDATE_ROWS_TRANSACTION: { - data.enforceInterface(IBulkCursor.descriptor); - // TODO - what ClassLoader should be passed to readHashMap? - // TODO - switch to Bundle - HashMap> values = data.readHashMap(null); - boolean result = updateRows(values); - reply.writeNoException(); - reply.writeInt((result == true ? 1 : 0)); - return true; - } - - case DELETE_ROW_TRANSACTION: { - data.enforceInterface(IBulkCursor.descriptor); - int position = data.readInt(); - boolean result = deleteRow(position); - reply.writeNoException(); - reply.writeInt((result == true ? 1 : 0)); - return true; - } - case ON_MOVE_TRANSACTION: { data.enforceInterface(IBulkCursor.descriptor); int position = data.readInt(); @@ -343,48 +320,6 @@ final class BulkCursorProxy implements IBulkCursor { return count; } - public boolean updateRows(Map values) throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IBulkCursor.descriptor); - - data.writeMap(values); - - mRemote.transact(UPDATE_ROWS_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - - boolean result = (reply.readInt() == 1 ? true : false); - - data.recycle(); - reply.recycle(); - - return result; - } - - public boolean deleteRow(int position) throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IBulkCursor.descriptor); - - data.writeInt(position); - - mRemote.transact(DELETE_ROW_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - - boolean result = (reply.readInt() == 1 ? true : false); - - data.recycle(); - reply.recycle(); - - return result; - } - public boolean getWantsAllOnMoveCalls() throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java index 1469ea24a6f0bb68053372b1b2af2b9307634bf7..2cb2aec685b0e8d7452006a3e30a675219022c29 100644 --- a/core/java/android/database/BulkCursorToCursorAdaptor.java +++ b/core/java/android/database/BulkCursorToCursorAdaptor.java @@ -16,12 +16,10 @@ package android.database; -import android.os.RemoteException; import android.os.Bundle; +import android.os.RemoteException; import android.util.Log; -import java.util.Map; - /** * Adapts an {@link IBulkCursor} to a {@link Cursor} for use in the local * process. @@ -174,38 +172,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { } } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() { - try { - boolean result = mBulkCursor.deleteRow(mPos); - if (result != false) { - // The window contains the old value, discard it - mWindow = null; - - // Fix up the position - mCount = mBulkCursor.count(); - if (mPos < mCount) { - int oldPos = mPos; - mPos = -1; - moveToPosition(oldPos); - } else { - mPos = mCount; - } - - // Send the change notification - onChange(true); - } - return result; - } catch (RemoteException ex) { - Log.e(TAG, "Unable to delete row because the remote process is dead"); - return false; - } - } - @Override public String[] getColumnNames() { if (mColumns == null) { @@ -219,44 +185,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { return mColumns; } - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates(Map> additionalValues) { - if (!supportsUpdates()) { - Log.e(TAG, "commitUpdates not supported on this cursor, did you include the _id column?"); - return false; - } - - synchronized(mUpdatedRows) { - if (additionalValues != null) { - mUpdatedRows.putAll(additionalValues); - } - - if (mUpdatedRows.size() <= 0) { - return false; - } - - try { - boolean result = mBulkCursor.updateRows(mUpdatedRows); - - if (result == true) { - mUpdatedRows.clear(); - - // Send the change notification - onChange(true); - } - return result; - } catch (RemoteException ex) { - Log.e(TAG, "Unable to commit updates because the remote process is dead"); - return false; - } - } - } - @Override public Bundle getExtras() { try { diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java index 6539156ee41d1e69bcdc588cc5ee3362bbe3cc2e..c03c58677f179f6d9ed2991b01320a1e4304ebfb 100644 --- a/core/java/android/database/Cursor.java +++ b/core/java/android/database/Cursor.java @@ -30,6 +30,25 @@ import java.util.Map; * threads should perform its own synchronization when using the Cursor. */ public interface Cursor { + /* + * Values returned by {@link #getType(int)}. + * These should be consistent with the corresponding types defined in CursorWindow.h + */ + /** Value returned by {@link #getType(int)} if the specified column is null */ + static final int FIELD_TYPE_NULL = 0; + + /** Value returned by {@link #getType(int)} if the specified column type is integer */ + static final int FIELD_TYPE_INTEGER = 1; + + /** Value returned by {@link #getType(int)} if the specified column type is float */ + static final int FIELD_TYPE_FLOAT = 2; + + /** Value returned by {@link #getType(int)} if the specified column type is string */ + static final int FIELD_TYPE_STRING = 3; + + /** Value returned by {@link #getType(int)} if the specified column type is blob */ + static final int FIELD_TYPE_BLOB = 4; + /** * Returns the numbers of rows in the cursor. * @@ -145,22 +164,6 @@ public interface Cursor { */ boolean isAfterLast(); - /** - * Removes the row at the current cursor position from the underlying data - * store. After this method returns the cursor will be pointing to the row - * after the row that is deleted. This has the side effect of decrementing - * the result of count() by one. - *

    - * The query must have the row ID column in its selection, otherwise this - * call will fail. - * - * @hide - * @return whether the record was successfully deleted. - * @deprecated use {@link ContentResolver#delete(Uri, String, String[])} - */ - @Deprecated - boolean deleteRow(); - /** * Returns the zero-based index for the given column name, or -1 if the column doesn't exist. * If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which @@ -295,194 +298,33 @@ public interface Cursor { double getDouble(int columnIndex); /** - * Returns true if the value in the indicated column is null. - * - * @param columnIndex the zero-based index of the target column. - * @return whether the column value is null. - */ - boolean isNull(int columnIndex); - - /** - * Returns true if the cursor supports updates. - * - * @return whether the cursor supports updates. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean supportsUpdates(); - - /** - * Returns true if there are pending updates that have not yet been committed. - * - * @return true if there are pending updates that have not yet been committed. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean hasUpdates(); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateBlob(int columnIndex, byte[] value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateString(int columnIndex, String value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. + * Returns data type of the given column's value. + * The preferred type of the column is returned but the data may be converted to other types + * as documented in the get-type methods such as {@link #getInt(int)}, {@link #getFloat(int)} + * etc. + *

    + * Returned column types are + *

      + *
    • {@link #FIELD_TYPE_NULL}
    • + *
    • {@link #FIELD_TYPE_INTEGER}
    • + *
    • {@link #FIELD_TYPE_FLOAT}
    • + *
    • {@link #FIELD_TYPE_STRING}
    • + *
    • {@link #FIELD_TYPE_BLOB}
    • + *
    + *

    * * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods + * @return column value type */ - @Deprecated - boolean updateShort(int columnIndex, short value); + int getType(int columnIndex); /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateInt(int columnIndex, int value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateLong(int columnIndex, long value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateFloat(int columnIndex, float value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateDouble(int columnIndex, double value); - - /** - * Removes the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. + * Returns true if the value in the indicated column is null. * * @param columnIndex the zero-based index of the target column. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateToNull(int columnIndex); - - /** - * Atomically commits all updates to the backing store. After completion, - * this method leaves the data in an inconsistent state and you should call - * {@link #requery} before reading data from the cursor again. - * - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean commitUpdates(); - - /** - * Atomically commits all updates to the backing store, as well as the - * updates included in values. After completion, - * this method leaves the data in an inconsistent state and you should call - * {@link #requery} before reading data from the cursor again. - * - * @param values A map from row IDs to Maps associating column names with - * updated values. A null value indicates the field should be - removed. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean commitUpdates(Map> values); - - /** - * Reverts all updates made to the cursor since the last call to - * commitUpdates. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods + * @return whether the column value is null. */ - @Deprecated - void abortUpdates(); + boolean isNull(int columnIndex); /** * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called. @@ -496,6 +338,10 @@ public interface Cursor { * contents. This may be done at any time, including after a call to {@link * #deactivate}. * + * Since this method could execute a query on the database and potentially take + * a while, it could cause ANR if it is called on Main (UI) thread. + * A warning is printed if this method is being executed on Main thread. + * * @return true if the requery succeeded, false if not, in which case the * cursor becomes invalid. */ diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 748eb99e7561651ee47da984d2ff59e4cc788073..8bc7de27ff9875d79e116ce9d2e5cd55d022ad40 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -16,16 +16,12 @@ package android.database; -import android.database.sqlite.SQLiteMisuseException; -import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.util.Config; import android.util.Log; -import java.util.Map; - /** * Wraps a BulkCursor around an existing Cursor making it remotable. @@ -38,7 +34,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative private final CrossProcessCursor mCursor; private CursorWindow mWindow; private final String mProviderName; - private final boolean mReadOnly; private ContentObserverProxy mObserver; private static final class ContentObserverProxy extends ContentObserver @@ -98,7 +93,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative "Only CrossProcessCursor cursors are supported across process for now", e); } mProviderName = providerName; - mReadOnly = !allowWrite; createAndRegisterObserverProxy(observer); } @@ -197,31 +191,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } } - public boolean updateRows(Map> values) { - if (mReadOnly) { - Log.w("ContentProvider", "Permission Denial: modifying " - + mProviderName - + " from pid=" + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return false; - } - return mCursor.commitUpdates(values); - } - - public boolean deleteRow(int position) { - if (mReadOnly) { - Log.w("ContentProvider", "Permission Denial: modifying " - + mProviderName - + " from pid=" + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return false; - } - if (mCursor.moveToPosition(position) == false) { - return false; - } - return mCursor.deleteRow(); - } - public Bundle getExtras() { return mCursor.getExtras(); } diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index c756825a7985581730975bffa90a24799cae9c51..599431fd90462b74e435f11d7155571aa1493096 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -217,18 +217,13 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * @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} + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isNull(int row, int col) { - acquireReference(); - try { - return isNull_native(row - mStartPos, col); - } finally { - releaseReference(); - } + return getType(row, col) == Cursor.FIELD_TYPE_NULL; } - private native boolean isNull_native(int row, int col); - /** * Returns a byte array for the given field. * @@ -248,35 +243,56 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { private native byte[] getBlob_native(int row, int col); /** - * Checks if a field contains either a blob or is null. + * Returns data type of the given column's value. + *

    + * Returned column types are + *

      + *
    • {@link Cursor#FIELD_TYPE_NULL}
    • + *
    • {@link Cursor#FIELD_TYPE_INTEGER}
    • + *
    • {@link Cursor#FIELD_TYPE_FLOAT}
    • + *
    • {@link Cursor#FIELD_TYPE_STRING}
    • + *
    • {@link Cursor#FIELD_TYPE_BLOB}
    • + *
    + *

    * * @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 blob + * @return the value type */ - public boolean isBlob(int row, int col) { + public int getType(int row, int col) { acquireReference(); try { - return isBlob_native(row - mStartPos, col); + return getType_native(row - mStartPos, col); } finally { releaseReference(); } } + /** + * Checks if a field contains either a blob 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 blob + * @deprecated use {@link #getType(int, int)} instead + */ + @Deprecated + public boolean isBlob(int row, int col) { + int type = getType(row, col); + return type == Cursor.FIELD_TYPE_BLOB || type == Cursor.FIELD_TYPE_NULL; + } + /** * 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 + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isLong(int row, int col) { - acquireReference(); - try { - return isInteger_native(row - mStartPos, col); - } finally { - releaseReference(); - } + return getType(row, col) == Cursor.FIELD_TYPE_INTEGER; } /** @@ -285,14 +301,11 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * @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 + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isFloat(int row, int col) { - acquireReference(); - try { - return isFloat_native(row - mStartPos, col); - } finally { - releaseReference(); - } + return getType(row, col) == Cursor.FIELD_TYPE_FLOAT; } /** @@ -301,20 +314,15 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * @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 + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isString(int row, int col) { - acquireReference(); - try { - return isString_native(row - mStartPos, col); - } finally { - releaseReference(); - } + int type = getType(row, col); + return type == Cursor.FIELD_TYPE_STRING || type == Cursor.FIELD_TYPE_NULL; } - 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); + private native int getType_native(int row, int col); /** * Returns a String for the given field. diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java index f0aa7d736e53c63b840bf7483f39b15efc31f720..3c3bd430a150f44674d962db9c095390601716e7 100644 --- a/core/java/android/database/CursorWrapper.java +++ b/core/java/android/database/CursorWrapper.java @@ -17,28 +17,26 @@ package android.database; import android.content.ContentResolver; -import android.database.CharArrayBuffer; import android.net.Uri; import android.os.Bundle; -import java.util.Map; - /** - * Wrapper class for Cursor that delegates all calls to the actual cursor object + * Wrapper class for Cursor that delegates all calls to the actual cursor object. The primary + * use for this class is to extend a cursor while overriding only a subset of its methods. */ - public class CursorWrapper implements Cursor { + private final Cursor mCursor; + public CursorWrapper(Cursor cursor) { mCursor = cursor; } - + /** - * @hide - * @deprecated + * @return the wrapped cursor */ - public void abortUpdates() { - mCursor.abortUpdates(); + public Cursor getWrappedCursor() { + return mCursor; } public void close() { @@ -49,23 +47,6 @@ public class CursorWrapper implements Cursor { return mCursor.isClosed(); } - /** - * @hide - * @deprecated - */ - public boolean commitUpdates() { - return mCursor.commitUpdates(); - } - - /** - * @hide - * @deprecated - */ - public boolean commitUpdates( - Map> values) { - return mCursor.commitUpdates(values); - } - public int getCount() { return mCursor.getCount(); } @@ -74,14 +55,6 @@ public class CursorWrapper implements Cursor { mCursor.deactivate(); } - /** - * @hide - * @deprecated - */ - public boolean deleteRow() { - return mCursor.deleteRow(); - } - public boolean moveToFirst() { return mCursor.moveToFirst(); } @@ -147,14 +120,6 @@ public class CursorWrapper implements Cursor { return mCursor.getWantsAllOnMoveCalls(); } - /** - * @hide - * @deprecated - */ - public boolean hasUpdates() { - return mCursor.hasUpdates(); - } - public boolean isAfterLast() { return mCursor.isAfterLast(); } @@ -171,6 +136,10 @@ public class CursorWrapper implements Cursor { return mCursor.isLast(); } + public int getType(int columnIndex) { + return mCursor.getType(columnIndex); + } + public boolean isNull(int columnIndex) { return mCursor.isNull(columnIndex); } @@ -219,14 +188,6 @@ public class CursorWrapper implements Cursor { mCursor.setNotificationUri(cr, uri); } - /** - * @hide - * @deprecated - */ - public boolean supportsUpdates() { - return mCursor.supportsUpdates(); - } - public void unregisterContentObserver(ContentObserver observer) { mCursor.unregisterContentObserver(observer); } @@ -234,72 +195,5 @@ public class CursorWrapper implements Cursor { public void unregisterDataSetObserver(DataSetObserver observer) { mCursor.unregisterDataSetObserver(observer); } - - /** - * @hide - * @deprecated - */ - public boolean updateDouble(int columnIndex, double value) { - return mCursor.updateDouble(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateFloat(int columnIndex, float value) { - return mCursor.updateFloat(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateInt(int columnIndex, int value) { - return mCursor.updateInt(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateLong(int columnIndex, long value) { - return mCursor.updateLong(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateShort(int columnIndex, short value) { - return mCursor.updateShort(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateString(int columnIndex, String value) { - return mCursor.updateString(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateBlob(int columnIndex, byte[] value) { - return mCursor.updateBlob(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateToNull(int columnIndex) { - return mCursor.updateToNull(columnIndex); - } - - private Cursor mCursor; - } diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java index 9200e813531631b7b283c3976fe7ed43c2d81d34..51c72c1877e8befa9a7e5626df86866cd757d877 100644 --- a/core/java/android/database/DataSetObservable.java +++ b/core/java/android/database/DataSetObservable.java @@ -27,8 +27,12 @@ public class DataSetObservable extends Observable { */ public void notifyChanged() { synchronized(mObservers) { - for (DataSetObserver observer : mObservers) { - observer.onChanged(); + // since onChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onChanged(); } } } @@ -39,8 +43,8 @@ public class DataSetObservable extends Observable { */ public void notifyInvalidated() { synchronized (mObservers) { - for (DataSetObserver observer : mObservers) { - observer.onInvalidated(); + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onInvalidated(); } } } diff --git a/core/java/android/pim/vcard/exception/VCardException.java b/core/java/android/database/DatabaseErrorHandler.java similarity index 51% rename from core/java/android/pim/vcard/exception/VCardException.java rename to core/java/android/database/DatabaseErrorHandler.java index e557219f235a411549bf38b0c7cbb716ac90cb3b..f0c5452becdbb7e38ea46b6ca5c2cc85279432b6 100644 --- a/core/java/android/pim/vcard/exception/VCardException.java +++ b/core/java/android/database/DatabaseErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,23 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.pim.vcard.exception; -public class VCardException extends java.lang.Exception { - /** - * Constructs a VCardException object - */ - public VCardException() { - super(); - } +package android.database; + +import android.database.sqlite.SQLiteDatabase; + +/** + * An interface to let the apps define the actions to take when the following errors are detected + * database corruption + */ +public interface DatabaseErrorHandler { /** - * Constructs a VCardException object - * - * @param message the error message + * defines the method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. */ - public VCardException(String message) { - super(message); - } - + void onCorruption(SQLiteDatabase dbObj); } diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 66406cac439a0bd25030604d044796e5aa428b7f..70a7fb66df265e43b847949383e907b4ac371b31 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -31,6 +31,7 @@ import android.database.sqlite.SQLiteFullException; import android.database.sqlite.SQLiteProgram; import android.database.sqlite.SQLiteStatement; import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.text.TextUtils; import android.util.Config; import android.util.Log; @@ -52,6 +53,27 @@ public class DatabaseUtils { private static final String[] countProjection = new String[]{"count(*)"}; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_SELECT = 1; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_UPDATE = 2; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_ATTACH = 3; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_BEGIN = 4; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_COMMIT = 5; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_ABORT = 6; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_PRAGMA = 7; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_DDL = 8; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_UNPREPARED = 9; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_OTHER = 99; + /** * Special function for writing an exception result at the header of * a parcel, to be used when returning an exception from a transaction. @@ -192,6 +214,37 @@ public class DatabaseUtils { } } + /** + * Returns data type of the given object's value. + *

    + * Returned values are + *

      + *
    • {@link Cursor#FIELD_TYPE_NULL}
    • + *
    • {@link Cursor#FIELD_TYPE_INTEGER}
    • + *
    • {@link Cursor#FIELD_TYPE_FLOAT}
    • + *
    • {@link Cursor#FIELD_TYPE_STRING}
    • + *
    • {@link Cursor#FIELD_TYPE_BLOB}
    • + *
    + *

    + * + * @param obj the object whose value type is to be returned + * @return object value type + * @hide + */ + public static int getTypeOfObject(Object obj) { + if (obj == null) { + return Cursor.FIELD_TYPE_NULL; + } else if (obj instanceof byte[]) { + return Cursor.FIELD_TYPE_BLOB; + } else if (obj instanceof Float || obj instanceof Double) { + return Cursor.FIELD_TYPE_FLOAT; + } else if (obj instanceof Long || obj instanceof Integer) { + return Cursor.FIELD_TYPE_INTEGER; + } else { + return Cursor.FIELD_TYPE_STRING; + } + } + /** * Appends an SQL string to the given StringBuilder, including the opening * and closing single quotes. Any single quotes internal to sqlString will @@ -604,14 +657,40 @@ public class DatabaseUtils { * @return the number of rows in the table */ public static long queryNumEntries(SQLiteDatabase db, String table) { - Cursor cursor = db.query(table, countProjection, - null, null, null, null, null); - try { - cursor.moveToFirst(); - return cursor.getLong(0); - } finally { - cursor.close(); - } + return queryNumEntries(db, table, null, null); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE itself). + * Passing null will count all rows for the given table + * @return the number of rows in the table filtered by the selection + */ + public static long queryNumEntries(SQLiteDatabase db, String table, String selection) { + return queryNumEntries(db, table, selection, null); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE itself). + * Passing null will count all rows for the given table + * @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. + * @return the number of rows in the table filtered by the selection + */ + public static long queryNumEntries(SQLiteDatabase db, String table, String selection, + String[] selectionArgs) { + String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : ""; + return longForQuery(db, "select count(*) from " + table + s, + selectionArgs); } /** @@ -632,14 +711,8 @@ public class DatabaseUtils { * first column of the first row. */ public static long longForQuery(SQLiteStatement prog, String[] selectionArgs) { - if (selectionArgs != null) { - int size = selectionArgs.length; - for (int i = 0; i < size; i++) { - bindObjectToProgram(prog, i + 1, selectionArgs[i]); - } - } - long value = prog.simpleQueryForLong(); - return value; + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForLong(); } /** @@ -660,14 +733,36 @@ public class DatabaseUtils { * first column of the first row. */ public static String stringForQuery(SQLiteStatement prog, String[] selectionArgs) { - if (selectionArgs != null) { - int size = selectionArgs.length; - for (int i = 0; i < size; i++) { - bindObjectToProgram(prog, i + 1, selectionArgs[i]); - } + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForString(); + } + + /** + * Utility method to run the query on the db and return the blob value in the + * first column of the first row. + * + * @return A read-only file descriptor for a copy of the blob value. + */ + public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteDatabase db, + String query, String[] selectionArgs) { + SQLiteStatement prog = db.compileStatement(query); + try { + return blobFileDescriptorForQuery(prog, selectionArgs); + } finally { + prog.close(); } - String value = prog.simpleQueryForString(); - return value; + } + + /** + * Utility method to run the pre-compiled query and return the blob value in the + * first column of the first row. + * + * @return A read-only file descriptor for a copy of the blob value. + */ + public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteStatement prog, + String[] selectionArgs) { + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForBlobFileDescriptor(); } /** @@ -680,8 +775,8 @@ public class DatabaseUtils { */ public static void cursorStringToContentValuesIfPresent(Cursor cursor, ContentValues values, String column) { - final int index = cursor.getColumnIndexOrThrow(column); - if (!cursor.isNull(index)) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { values.put(column, cursor.getString(index)); } } @@ -696,8 +791,8 @@ public class DatabaseUtils { */ public static void cursorLongToContentValuesIfPresent(Cursor cursor, ContentValues values, String column) { - final int index = cursor.getColumnIndexOrThrow(column); - if (!cursor.isNull(index)) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { values.put(column, cursor.getLong(index)); } } @@ -712,8 +807,8 @@ public class DatabaseUtils { */ public static void cursorShortToContentValuesIfPresent(Cursor cursor, ContentValues values, String column) { - final int index = cursor.getColumnIndexOrThrow(column); - if (!cursor.isNull(index)) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { values.put(column, cursor.getShort(index)); } } @@ -728,8 +823,8 @@ public class DatabaseUtils { */ public static void cursorIntToContentValuesIfPresent(Cursor cursor, ContentValues values, String column) { - final int index = cursor.getColumnIndexOrThrow(column); - if (!cursor.isNull(index)) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { values.put(column, cursor.getInt(index)); } } @@ -744,8 +839,8 @@ public class DatabaseUtils { */ public static void cursorFloatToContentValuesIfPresent(Cursor cursor, ContentValues values, String column) { - final int index = cursor.getColumnIndexOrThrow(column); - if (!cursor.isNull(index)) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { values.put(column, cursor.getFloat(index)); } } @@ -760,8 +855,8 @@ public class DatabaseUtils { */ public static void cursorDoubleToContentValuesIfPresent(Cursor cursor, ContentValues values, String column) { - final int index = cursor.getColumnIndexOrThrow(column); - if (!cursor.isNull(index)) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { values.put(column, cursor.getDouble(index)); } } @@ -1128,4 +1223,66 @@ public class DatabaseUtils { db.setVersion(dbVersion); db.close(); } + + /** + * Returns one of the following which represent the type of the given SQL statement. + *
      + *
    1. {@link #STATEMENT_SELECT}
    2. + *
    3. {@link #STATEMENT_UPDATE}
    4. + *
    5. {@link #STATEMENT_ATTACH}
    6. + *
    7. {@link #STATEMENT_BEGIN}
    8. + *
    9. {@link #STATEMENT_COMMIT}
    10. + *
    11. {@link #STATEMENT_ABORT}
    12. + *
    13. {@link #STATEMENT_OTHER}
    14. + *
    + * @param sql the SQL statement whose type is returned by this method + * @return one of the values listed above + */ + public static int getSqlStatementType(String sql) { + sql = sql.trim(); + if (sql.length() < 3) { + return STATEMENT_OTHER; + } + String prefixSql = sql.substring(0, 3).toUpperCase(); + if (prefixSql.equals("SEL")) { + return STATEMENT_SELECT; + } else if (prefixSql.equals("INS") || + prefixSql.equals("UPD") || + prefixSql.equals("REP") || + prefixSql.equals("DEL")) { + return STATEMENT_UPDATE; + } else if (prefixSql.equals("ATT")) { + return STATEMENT_ATTACH; + } else if (prefixSql.equals("COM")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("END")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("ROL")) { + return STATEMENT_ABORT; + } else if (prefixSql.equals("BEG")) { + return STATEMENT_BEGIN; + } else if (prefixSql.equals("PRA")) { + return STATEMENT_PRAGMA; + } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") || + prefixSql.equals("ALT")) { + return STATEMENT_DDL; + } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) { + return STATEMENT_UNPREPARED; + } + return STATEMENT_OTHER; + } + + /** + * Appends one set of selection args to another. This is useful when adding a selection + * argument to a user provided set. + */ + public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { + if (originalValues == null || originalValues.length == 0) { + return newValues; + } + String[] result = new String[originalValues.length + newValues.length ]; + System.arraycopy(originalValues, 0, result, 0, originalValues.length); + System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); + return result; + } } diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..3619e487256fd33914ae9cff2a3c83c5ae3c044d --- /dev/null +++ b/core/java/android/database/DefaultDatabaseErrorHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.database; + +import java.io.File; +import java.util.ArrayList; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; +import android.util.Pair; + +/** + * Default class used defining the actions to take when the following errors are detected + * database corruption + */ +public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { + + private static final String TAG = "DefaultDatabaseErrorHandler"; + + /** + * defines the default method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + public void onCorruption(SQLiteDatabase dbObj) { + Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); + + // is the corruption detected even before database could be 'opened'? + if (!dbObj.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + deleteDatabaseFile(dbObj.getPath()); + return; + } + + ArrayList> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + try { + attachedDbs = dbObj.getAttachedDbs(); + } catch (SQLiteException e) { + /* ignore */ + } + try { + dbObj.close(); + } catch (SQLiteException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair p : attachedDbs) { + deleteDatabaseFile(p.second); + } + } else { + // attachedDbs = null is possible when the database is so corrupt that even + // "PRAGMA database_list;" also fails. delete the main database file + deleteDatabaseFile(dbObj.getPath()); + } + } + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + Log.e(TAG, "deleting the database file: " + fileName); + try { + new File(fileName).delete(); + } catch (Exception e) { + /* print warning and ignore exception */ + Log.w(TAG, "delete failed: " + e.getMessage()); + } + } +} diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java index 46790a300ea7cbdcd65303f9b8f9c1e62d4a070c..244c88fad38cdd242e80598493ec586e91e05cb9 100644 --- a/core/java/android/database/IBulkCursor.java +++ b/core/java/android/database/IBulkCursor.java @@ -16,16 +16,14 @@ package android.database; -import android.os.RemoteException; +import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; -import android.os.Bundle; - -import java.util.Map; +import android.os.RemoteException; /** * This interface provides a low-level way to pass bulk cursor data across - * both process and language boundries. Application code should use the Cursor + * both process and language boundaries. Application code should use the Cursor * interface directly. * * {@hide} @@ -54,10 +52,6 @@ public interface IBulkCursor extends IInterface { */ public String[] getColumnNames() throws RemoteException; - public boolean updateRows(Map> values) throws RemoteException; - - public boolean deleteRow(int position) throws RemoteException; - public void deactivate() throws RemoteException; public void close() throws RemoteException; @@ -76,8 +70,6 @@ public interface IBulkCursor extends IInterface { static final int GET_CURSOR_WINDOW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; static final int COUNT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1; static final int GET_COLUMN_NAMES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; - static final int UPDATE_ROWS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; - static final int DELETE_ROW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4; static final int DEACTIVATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5; static final int REQUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 6; static final int ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 7; diff --git a/core/java/android/database/MatrixCursor.java b/core/java/android/database/MatrixCursor.java index d5c3a32d884d2c73fe3c5c80773e4ee2026fe6ff..5c1b9689370adc4ff207d898c408f1a0fbbe8ee1 100644 --- a/core/java/android/database/MatrixCursor.java +++ b/core/java/android/database/MatrixCursor.java @@ -271,6 +271,11 @@ public class MatrixCursor extends AbstractCursor { return Double.parseDouble(value.toString()); } + @Override + public int getType(int column) { + return DatabaseUtils.getTypeOfObject(get(column)); + } + @Override public boolean isNull(int column) { return get(column) == null; diff --git a/core/java/android/database/MergeCursor.java b/core/java/android/database/MergeCursor.java index 722d707e5d1f2d92ac51da4c72a8d4d7b7d53cb4..2c25db765a2ba2f45f51a2c76827b328411ce04a 100644 --- a/core/java/android/database/MergeCursor.java +++ b/core/java/android/database/MergeCursor.java @@ -92,32 +92,6 @@ public class MergeCursor extends AbstractCursor return false; } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() - { - return mCursor.deleteRow(); - } - - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates() { - int length = mCursors.length; - for (int i = 0 ; i < length ; i++) { - if (mCursors[i] != null) { - mCursors[i].commitUpdates(); - } - } - onChange(true); - return true; - } - @Override public String getString(int column) { @@ -154,6 +128,11 @@ public class MergeCursor extends AbstractCursor return mCursor.getDouble(column); } + @Override + public int getType(int column) { + return mCursor.getType(column); + } + @Override public boolean isNull(int column) { diff --git a/core/java/android/database/RequeryOnUiThreadException.java b/core/java/android/database/RequeryOnUiThreadException.java new file mode 100644 index 0000000000000000000000000000000000000000..97a50d8bd8170b69d75264c53bdabaa56e0e1c69 --- /dev/null +++ b/core/java/android/database/RequeryOnUiThreadException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database; + +/** + * An exception that indicates invoking {@link Cursor#requery()} on Main thread could cause ANR. + * This exception should encourage apps to invoke {@link Cursor#requery()} in a background thread. + * @hide + */ +public class RequeryOnUiThreadException extends RuntimeException { + public RequeryOnUiThreadException(String packageName) { + super("In " + packageName + " Requery is executing on main (UI) thread. could cause ANR. " + + "do it in background thread."); + } +} diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java new file mode 100644 index 0000000000000000000000000000000000000000..4f5c4e63770b6446716c5526a857a6f206ecc150 --- /dev/null +++ b/core/java/android/database/sqlite/DatabaseConnectionPool.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 20010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +import android.content.res.Resources; +import android.os.SystemClock; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Random; + +/** + * A connection pool to be used by readers. + * Note that each connection can be used by only one reader at a time. + */ +/* package */ class DatabaseConnectionPool { + + private static final String TAG = "DatabaseConnectionPool"; + + /** The default connection pool size. */ + private volatile int mMaxPoolSize = + Resources.getSystem().getInteger(com.android.internal.R.integer.db_connection_pool_size); + + /** The connection pool objects are stored in this member. + * TODO: revisit this data struct as the number of pooled connections increase beyond + * single-digit values. + */ + private final ArrayList mPool = new ArrayList(mMaxPoolSize); + + /** the main database connection to which this connection pool is attached */ + private final SQLiteDatabase mParentDbObj; + + /** Random number generator used to pick a free connection out of the pool */ + private Random rand; // lazily initialized + + /* package */ DatabaseConnectionPool(SQLiteDatabase db) { + this.mParentDbObj = db; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Max Pool Size: " + mMaxPoolSize); + } + } + + /** + * close all database connections in the pool - even if they are in use! + */ + /* package */ void close() { + synchronized(mParentDbObj) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Closing the connection pool on " + mParentDbObj.getPath() + toString()); + } + for (int i = mPool.size() - 1; i >= 0; i--) { + mPool.get(i).mDb.close(); + } + mPool.clear(); + } + } + + /** + * get a free connection from the pool + * + * @param sql if not null, try to find a connection inthe pool which already has cached + * the compiled statement for this sql. + * @return the Database connection that the caller can use + */ + /* package */ SQLiteDatabase get(String sql) { + SQLiteDatabase db = null; + PoolObj poolObj = null; + synchronized(mParentDbObj) { + int poolSize = mPool.size(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert sql != null; + doAsserts(); + } + if (getFreePoolSize() == 0) { + // no free ( = available) connections + if (mMaxPoolSize == poolSize) { + // maxed out. can't open any more connections. + // let the caller wait on one of the pooled connections + // preferably a connection caching the pre-compiled statement of the given SQL + if (mMaxPoolSize == 1) { + poolObj = mPool.get(0); + } else { + for (int i = 0; i < mMaxPoolSize; i++) { + if (mPool.get(i).mDb.isSqlInStatementCache(sql)) { + poolObj = mPool.get(i); + break; + } + } + if (poolObj == null) { + // there are no database connections with the given SQL pre-compiled. + // ok to return any of the connections. + if (rand == null) { + rand = new Random(SystemClock.elapsedRealtime()); + } + poolObj = mPool.get(rand.nextInt(mMaxPoolSize)); + } + } + db = poolObj.mDb; + } else { + // create a new connection and add it to the pool, since we haven't reached + // max pool size allowed + db = mParentDbObj.createPoolConnection((short)(poolSize + 1)); + poolObj = new PoolObj(db); + mPool.add(poolSize, poolObj); + } + } else { + // there are free connections available. pick one + // preferably a connection caching the pre-compiled statement of the given SQL + for (int i = 0; i < poolSize; i++) { + if (mPool.get(i).isFree() && mPool.get(i).mDb.isSqlInStatementCache(sql)) { + poolObj = mPool.get(i); + break; + } + } + if (poolObj == null) { + // didn't find a free database connection with the given SQL already + // pre-compiled. return a free connection (this means, the same SQL could be + // pre-compiled on more than one database connection. potential wasted memory.) + for (int i = 0; i < poolSize; i++) { + if (mPool.get(i).isFree()) { + poolObj = mPool.get(i); + break; + } + } + } + db = poolObj.mDb; + } + + assert poolObj != null; + assert poolObj.mDb == db; + + poolObj.acquire(); + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "END get-connection: " + toString() + poolObj.toString()); + } + return db; + // TODO if a thread acquires a connection and dies without releasing the connection, then + // there could be a connection leak. + } + + /** + * release the given database connection back to the pool. + * @param db the connection to be released + */ + /* package */ void release(SQLiteDatabase db) { + PoolObj poolObj; + synchronized(mParentDbObj) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert db.mConnectionNum > 0; + doAsserts(); + assert mPool.get(db.mConnectionNum - 1).mDb == db; + } + + poolObj = mPool.get(db.mConnectionNum - 1); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "BEGIN release-conn: " + toString() + poolObj.toString()); + } + + if (poolObj.isFree()) { + throw new IllegalStateException("Releasing object already freed: " + + db.mConnectionNum); + } + + poolObj.release(); + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "END release-conn: " + toString() + poolObj.toString()); + } + } + + /** + * Returns a list of all database connections in the pool (both free and busy connections). + * This method is used when "adb bugreport" is done. + */ + /* package */ ArrayList getConnectionList() { + ArrayList list = new ArrayList(); + synchronized(mParentDbObj) { + for (int i = mPool.size() - 1; i >= 0; i--) { + list.add(mPool.get(i).mDb); + } + } + return list; + } + + /** + * package level access for testing purposes only. otherwise, private should be sufficient. + */ + /* package */ int getFreePoolSize() { + int count = 0; + for (int i = mPool.size() - 1; i >= 0; i--) { + if (mPool.get(i).isFree()) { + count++; + } + } + return count++; + } + + /** + * only for testing purposes + */ + /* package */ ArrayList getPool() { + return mPool; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + buff.append("db: "); + buff.append(mParentDbObj.getPath()); + buff.append(", totalsize = "); + buff.append(mPool.size()); + buff.append(", #free = "); + buff.append(getFreePoolSize()); + buff.append(", maxpoolsize = "); + buff.append(mMaxPoolSize); + for (PoolObj p : mPool) { + buff.append("\n"); + buff.append(p.toString()); + } + return buff.toString(); + } + + private void doAsserts() { + for (int i = 0; i < mPool.size(); i++) { + mPool.get(i).verify(); + assert mPool.get(i).mDb.mConnectionNum == (i + 1); + } + } + + /* package */ void setMaxPoolSize(int size) { + synchronized(mParentDbObj) { + mMaxPoolSize = size; + } + } + + /* package */ int getMaxPoolSize() { + synchronized(mParentDbObj) { + return mMaxPoolSize; + } + } + + /** only used for testing purposes. */ + /* package */ boolean isDatabaseObjFree(SQLiteDatabase db) { + return mPool.get(db.mConnectionNum - 1).isFree(); + } + + /** only used for testing purposes. */ + /* package */ int getSize() { + return mPool.size(); + } + + /** + * represents objects in the connection pool. + * package-level access for testing purposes only. + */ + /* package */ static class PoolObj { + + private final SQLiteDatabase mDb; + private boolean mFreeBusyFlag = FREE; + private static final boolean FREE = true; + private static final boolean BUSY = false; + + /** the number of threads holding this connection */ + // @GuardedBy("this") + private int mNumHolders = 0; + + /** contains the threadIds of the threads holding this connection. + * used for debugging purposes only. + */ + // @GuardedBy("this") + private HashSet mHolderIds = new HashSet(); + + public PoolObj(SQLiteDatabase db) { + mDb = db; + } + + private synchronized void acquire() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert isFree(); + long id = Thread.currentThread().getId(); + assert !mHolderIds.contains(id); + mHolderIds.add(id); + } + + mNumHolders++; + mFreeBusyFlag = BUSY; + } + + private synchronized void release() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + long id = Thread.currentThread().getId(); + assert mHolderIds.size() == mNumHolders; + assert mHolderIds.contains(id); + mHolderIds.remove(id); + } + + mNumHolders--; + if (mNumHolders == 0) { + mFreeBusyFlag = FREE; + } + } + + private synchronized boolean isFree() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + verify(); + } + return (mFreeBusyFlag == FREE); + } + + private synchronized void verify() { + if (mFreeBusyFlag == FREE) { + assert mNumHolders == 0; + } else { + assert mNumHolders > 0; + } + } + + /** + * only for testing purposes + */ + /* package */ synchronized int getNumHolders() { + return mNumHolders; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder(); + buff.append(", conn # "); + buff.append(mDb.mConnectionNum); + buff.append(", mCountHolders = "); + synchronized(this) { + buff.append(mNumHolders); + buff.append(", freeBusyFlag = "); + buff.append(mFreeBusyFlag); + for (Long l : mHolderIds) { + buff.append(", id = " + l); + } + } + return buff.toString(); + } + } +} diff --git a/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java index 8ac4c0f99ccaf9c68fdcf5679f7f3ac1c2900263..f28c70fe1efe5164e1c986520fa08e7991348be3 100644 --- a/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java +++ b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java @@ -21,13 +21,11 @@ package android.database.sqlite; * that is not explicitly closed * @hide */ -public class DatabaseObjectNotClosedException extends RuntimeException -{ +public class DatabaseObjectNotClosedException extends RuntimeException { private static final String s = "Application did not close the cursor or database object " + "that was opened here"; - public DatabaseObjectNotClosedException() - { + public DatabaseObjectNotClosedException() { super(s); } } diff --git a/core/java/android/pim/vcard/exception/VCardVersionException.java b/core/java/android/database/sqlite/SQLiteAccessPermException.java similarity index 59% rename from core/java/android/pim/vcard/exception/VCardVersionException.java rename to core/java/android/database/sqlite/SQLiteAccessPermException.java index 0709fe4113ee79ac4f9286c6fc0f5166c00a92c8..238da4bdb78ad05afc158e590c3c1caeb7ce63e1 100644 --- a/core/java/android/pim/vcard/exception/VCardVersionException.java +++ b/core/java/android/database/sqlite/SQLiteAccessPermException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.pim.vcard.exception; + +package android.database.sqlite; /** - * VCardException used only when the version of the vCard is different. + * This exception class is used when sqlite can't access the database file + * due to lack of permissions on the file. */ -public class VCardVersionException extends VCardException { - public VCardVersionException() { - super(); - } - public VCardVersionException(String message) { - super(message); +public class SQLiteAccessPermException extends SQLiteException { + public SQLiteAccessPermException() {} + + public SQLiteAccessPermException(String error) { + super(error); } } diff --git a/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java new file mode 100644 index 0000000000000000000000000000000000000000..41f2f9c28349690a735cb0a2c57d94793b5605d6 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +/** + * Thrown if the the bind or column parameter index is out of range + */ +public class SQLiteBindOrColumnIndexOutOfRangeException extends SQLiteException { + public SQLiteBindOrColumnIndexOutOfRangeException() {} + + public SQLiteBindOrColumnIndexOutOfRangeException(String error) { + super(error); + } +} diff --git a/core/java/android/database/sqlite/SQLiteBlobTooBigException.java b/core/java/android/database/sqlite/SQLiteBlobTooBigException.java new file mode 100644 index 0000000000000000000000000000000000000000..a82676b8a53f1484f0ffa41c0ac1351522fd9fa9 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteBlobTooBigException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +public class SQLiteBlobTooBigException extends SQLiteException { + public SQLiteBlobTooBigException() {} + + public SQLiteBlobTooBigException(String error) { + super(error); + } +} diff --git a/core/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java b/core/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java new file mode 100644 index 0000000000000000000000000000000000000000..6f01796c93c23f91471600b69a5e9091189b3aab --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +public class SQLiteCantOpenDatabaseException extends SQLiteException { + public SQLiteCantOpenDatabaseException() {} + + public SQLiteCantOpenDatabaseException(String error) { + super(error); + } +} diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java index 1830f6c844988b40a7dd616b144616be31034c13..96e6f227163f2756caedb1262f1a5e9bee74a70c 100644 --- a/core/java/android/database/sqlite/SQLiteClosable.java +++ b/core/java/android/database/sqlite/SQLiteClosable.java @@ -23,13 +23,14 @@ import android.database.CursorWindow; */ public abstract class SQLiteClosable { private int mReferenceCount = 1; - private Object mLock = new Object(); + private Object mLock = new Object(); // STOPSHIP remove this line protected abstract void onAllReferencesReleased(); protected void onAllReferencesReleasedFromContainer() {} public void acquireReference() { - synchronized(mLock) { + synchronized(mLock) { // STOPSHIP change 'mLock' to 'this' + checkRefCount(); if (mReferenceCount <= 0) { throw new IllegalStateException( "attempt to re-open an already-closed object: " + getObjInfo()); @@ -39,7 +40,8 @@ public abstract class SQLiteClosable { } public void releaseReference() { - synchronized(mLock) { + synchronized(mLock) { // STOPSHIP change 'mLock' to 'this' + checkRefCount(); mReferenceCount--; if (mReferenceCount == 0) { onAllReferencesReleased(); @@ -48,7 +50,8 @@ public abstract class SQLiteClosable { } public void releaseReferenceFromContainer() { - synchronized(mLock) { + synchronized(mLock) { // STOPSHIP change 'mLock' to 'this' + checkRefCount(); mReferenceCount--; if (mReferenceCount == 0) { onAllReferencesReleasedFromContainer(); @@ -63,8 +66,7 @@ public abstract class SQLiteClosable { if (this instanceof SQLiteDatabase) { buff.append("database = "); buff.append(((SQLiteDatabase)this).getPath()); - } else if (this instanceof SQLiteProgram || this instanceof SQLiteStatement || - this instanceof SQLiteQuery) { + } else if (this instanceof SQLiteProgram) { buff.append("mSql = "); buff.append(((SQLiteProgram)this).mSql); } else if (this instanceof CursorWindow) { @@ -74,4 +76,12 @@ public abstract class SQLiteClosable { buff.append(") "); return buff.toString(); } + + // STOPSHIP remove this method before shipping + private void checkRefCount() { + if (mReferenceCount > 1000) { + throw new IllegalStateException("bad refcount: " + mReferenceCount + + ". file bug against frameworks->database" + getObjInfo()); + } + } } diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java index 25aa9b378554a839eb9a1c9759220d196f246aa0..588384b7f9b807e1cad5ca0cb2f751662fdb5d6f 100644 --- a/core/java/android/database/sqlite/SQLiteCompiledSql.java +++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java @@ -31,12 +31,12 @@ import android.util.Log; private static final String TAG = "SQLiteCompiledSql"; /** The database this program is compiled against. */ - /* package */ SQLiteDatabase mDatabase; + /* package */ final SQLiteDatabase mDatabase; /** * Native linkage, do not modify. This comes from the database. */ - /* package */ int nHandle = 0; + /* package */ final int nHandle; /** * Native linkage, do not modify. When non-0 this holds a reference to a valid @@ -54,61 +54,21 @@ import android.util.Log; private boolean mInUse = false; /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) { - if (!db.isOpen()) { - throw new IllegalStateException("database " + db.getPath() + " already closed"); - } + db.verifyDbIsOpen(); + db.verifyLockOwner(); mDatabase = db; mSqlStmt = sql; mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - this.nHandle = db.mNativeHandle; - compile(sql, true); - } - - /** - * Compiles the given SQL into a SQLite byte code program using sqlite3_prepare_v2(). If - * this method has been called previously without a call to close and forCompilation is set - * to false the previous compilation will be used. Setting forceCompilation to true will - * always re-compile the program and should be done if you pass differing SQL strings to this - * method. - * - *

    Note: this method acquires the database lock.

    - * - * @param sql the SQL string to compile - * @param forceCompilation forces the SQL to be recompiled in the event that there is an - * existing compiled SQL program already around - */ - private void compile(String sql, boolean forceCompilation) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - // Only compile if we don't have a valid statement already or the caller has - // explicitly requested a recompile. - if (forceCompilation) { - mDatabase.lock(); - try { - // Note that the native_compile() takes care of destroying any previously - // existing programs before it compiles. - native_compile(sql); - } finally { - mDatabase.unlock(); - } - } + nHandle = db.mNativeHandle; + native_compile(sql); } /* package */ void releaseSqlStatement() { // Note that native_finalize() checks to make sure that nStatement is // non-null before destroying it. if (nStatement != 0) { - if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.v(TAG, "closed and deallocated DbObj (id#" + nStatement +")"); - } - try { - mDatabase.lock(); - native_finalize(); - nStatement = 0; - } finally { - mDatabase.unlock(); - } + mDatabase.finalizeStatementLater(nStatement); + nStatement = 0; } } @@ -117,23 +77,28 @@ import android.util.Log; */ /* package */ synchronized boolean acquire() { if (mInUse) { - // someone already has acquired it. + // it is already in use. return false; } mInUse = true; - if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.v(TAG, "Acquired DbObj (id#" + nStatement + ") from DB cache"); - } return true; } /* package */ synchronized void release() { - if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.v(TAG, "Released DbObj (id#" + nStatement + ") back to DB cache"); - } mInUse = false; } + /* package */ synchronized boolean isInUse() { + return mInUse; + } + + /* package */ synchronized void releaseIfNotInUse() { + // if it is not in use, release its memory from the database + if (!isInUse()) { + releaseSqlStatement(); + } + } + /** * Make sure that the native resource is cleaned up. */ @@ -142,19 +107,39 @@ import android.util.Log; try { if (nStatement == 0) return; // finalizer should NEVER get called - if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.v(TAG, "** warning ** Finalized DbObj (id#" + nStatement + ")"); + // but if the database itself is not closed and is GC'ed, then + // all sub-objects attached to the database could end up getting GC'ed too. + // in that case, don't print any warning. + if (!mInUse) { + int len = mSqlStmt.length(); + Log.w(TAG, "Releasing statement in a finalizer. Please ensure " + + "that you explicitly call close() on your cursor: " + + mSqlStmt.substring(0, (len > 100) ? 100 : len), mStackTrace); } - int len = mSqlStmt.length(); - Log.w(TAG, "Releasing statement in a finalizer. Please ensure " + - "that you explicitly call close() on your cursor: " + - mSqlStmt.substring(0, (len > 100) ? 100 : len), mStackTrace); releaseSqlStatement(); } finally { super.finalize(); } } + @Override public String toString() { + synchronized(this) { + StringBuilder buff = new StringBuilder(); + buff.append(" nStatement="); + buff.append(nStatement); + buff.append(", mInUse="); + buff.append(mInUse); + buff.append(", db="); + buff.append(mDatabase.getPath()); + buff.append(", db_connectionNum="); + buff.append(mDatabase.mConnectionNum); + buff.append(", sql="); + int len = mSqlStmt.length(); + buff.append(mSqlStmt.substring(0, (len > 100) ? 100 : len)); + return buff.toString(); + } + } + /** * Compiles SQL into a SQLite program. * @@ -162,5 +147,4 @@ import android.util.Log; * @param sql The SQL to compile. */ private final native void native_compile(String sql); - private final native void native_finalize(); } diff --git a/core/java/android/database/sqlite/SQLiteContentHelper.java b/core/java/android/database/sqlite/SQLiteContentHelper.java deleted file mode 100644 index 2800d86279b1ef352961c2c16b43844ee595dd70..0000000000000000000000000000000000000000 --- a/core/java/android/database/sqlite/SQLiteContentHelper.java +++ /dev/null @@ -1,92 +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.database.sqlite; - -import android.content.res.AssetFileDescriptor; -import android.database.Cursor; -import android.os.MemoryFile; - -import java.io.FileNotFoundException; -import java.io.IOException; - -/** - * Some helper functions for using SQLite database to implement content providers. - * - * @hide - */ -public class SQLiteContentHelper { - - /** - * Runs an SQLite query and returns an AssetFileDescriptor for the - * blob in column 0 of the first row. If the first column does - * not contain a blob, an unspecified exception is thrown. - * - * @param db Handle to a readable database. - * @param sql SQL query, possibly with query arguments. - * @param selectionArgs Query argument values, or {@code null} for no argument. - * @return If no exception is thrown, a non-null AssetFileDescriptor is returned. - * @throws FileNotFoundException If the query returns no results or the - * value of column 0 is NULL, or if there is an error creating the - * asset file descriptor. - */ - public static AssetFileDescriptor getBlobColumnAsAssetFile(SQLiteDatabase db, String sql, - String[] selectionArgs) throws FileNotFoundException { - try { - MemoryFile file = simpleQueryForBlobMemoryFile(db, sql, selectionArgs); - if (file == null) { - throw new FileNotFoundException("No results."); - } - return AssetFileDescriptor.fromMemoryFile(file); - } catch (IOException ex) { - throw new FileNotFoundException(ex.toString()); - } - } - - /** - * Runs an SQLite query and returns a MemoryFile for the - * blob in column 0 of the first row. If the first column does - * not contain a blob, an unspecified exception is thrown. - * - * @return A memory file, or {@code null} if the query returns no results - * or the value column 0 is NULL. - * @throws IOException If there is an error creating the memory file. - */ - // TODO: make this native and use the SQLite blob API to reduce copying - private static MemoryFile simpleQueryForBlobMemoryFile(SQLiteDatabase db, String sql, - String[] selectionArgs) throws IOException { - Cursor cursor = db.rawQuery(sql, selectionArgs); - if (cursor == null) { - return null; - } - try { - if (!cursor.moveToFirst()) { - return null; - } - byte[] bytes = cursor.getBlob(0); - if (bytes == null) { - return null; - } - MemoryFile file = new MemoryFile(null, bytes.length); - file.writeBytes(bytes, 0, 0, bytes.length); - file.deactivate(); - return file; - } finally { - cursor.close(); - } - } - -} diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index c7e58faf26b24c62a379eee8c0bc2ed9db65e281..fa7763d50ad187de8fea1d475ccc71d72434b077 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -16,20 +16,19 @@ package android.database.sqlite; +import android.app.ActivityThread; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; import android.database.DataSetObserver; -import android.database.SQLException; - +import android.database.RequeryOnUiThreadException; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.Process; -import android.text.TextUtils; import android.util.Config; import android.util.Log; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; @@ -41,32 +40,29 @@ import java.util.concurrent.locks.ReentrantLock; * threads should perform its own synchronization when using the SQLiteCursor. */ public class SQLiteCursor extends AbstractWindowedCursor { - static final String TAG = "Cursor"; + static final String TAG = "SQLiteCursor"; static final int NO_COUNT = -1; /** The name of the table to edit */ - private String mEditTable; + private final String mEditTable; /** The names of the columns in the rows */ - private String[] mColumns; + private final String[] mColumns; /** The query object for the cursor */ private SQLiteQuery mQuery; - /** The database the cursor was created from */ - private SQLiteDatabase mDatabase; - /** The compiled query this cursor came from */ - private SQLiteCursorDriver mDriver; + private final SQLiteCursorDriver mDriver; /** The number of rows in the cursor */ - private int mCount = NO_COUNT; + private volatile int mCount = NO_COUNT; /** A mapping of column names to column indices, to speed up lookups */ private Map mColumnNameMap; /** Used to find out where a cursor was allocated in case it never got released. */ - private Throwable mStackTrace; + private final Throwable mStackTrace; /** * mMaxRead is the max items that each cursor window reads @@ -77,6 +73,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { private int mCursorState = 0; private ReentrantLock mLock = null; private boolean mPendingData = false; + + /** + * Used by {@link #requery()} to remember for which database we've already shown the warning. + */ + private static final HashMap sAlreadyWarned = new HashMap(); /** * support for a cursor variant that doesn't always read all results @@ -136,14 +137,22 @@ public class SQLiteCursor extends AbstractWindowedCursor { break; } try { - int count = mQuery.fillWindow(cw, mMaxRead, mCount); - // return -1 means not finished + int count = getQuery().fillWindow(cw, mMaxRead, mCount); + // return -1 means there is still more data to be retrieved from the resultset if (count != 0) { if (count == NO_COUNT){ mCount += mMaxRead; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received -1 from native_fill_window. read " + + mCount + " rows so far"); + } sendMessage(); } else { - mCount = count; + mCount += count; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received all data from native_fill_window. read " + + mCount + " rows."); + } sendMessage(); break; } @@ -201,24 +210,46 @@ public class SQLiteCursor extends AbstractWindowedCursor { * has package scope. * * @param db a reference to a Database object that is already constructed - * and opened + * and opened. This param is not used any longer * @param editTable the name of the table used for this query * @param query the rest of the query terms * cursor is finalized + * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead */ + @Deprecated public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { + this(driver, editTable, query); + } + + /** + * Execute a query and provide access to its result set through a Cursor + * interface. For a query such as: {@code SELECT name, birth, phone FROM + * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, + * phone) would be in the projection argument and everything from + * {@code FROM} onward would be in the params argument. This constructor + * has package scope. + * + * @param editTable the name of the table used for this query + * @param query the {@link SQLiteQuery} object associated with this cursor object. + */ + public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { // The AbstractCursor constructor needs to do some setup. super(); + if (query == null) { + throw new IllegalArgumentException("query object cannot be null"); + } + if (query.mDatabase == null) { + throw new IllegalArgumentException("query.mDatabase cannot be null"); + } mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - mDatabase = db; mDriver = driver; mEditTable = editTable; mColumnNameMap = null; mQuery = query; try { - db.lock(); + query.mDatabase.lock(); // Setup the list of columns int columnCount = mQuery.columnCountLocked(); @@ -239,7 +270,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { } } } finally { - db.unlock(); + query.mDatabase.unlock(); } } @@ -247,7 +278,9 @@ public class SQLiteCursor extends AbstractWindowedCursor { * @return the SQLiteDatabase that this cursor is associated with. */ public SQLiteDatabase getDatabase() { - return mDatabase; + synchronized (this) { + return mQuery.mDatabase; + } } @Override @@ -283,13 +316,27 @@ public class SQLiteCursor extends AbstractWindowedCursor { } } mWindow.setStartPosition(startPos); - mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); - // return -1 means not finished - if (mCount == NO_COUNT){ + int count = getQuery().fillWindow(mWindow, mInitialRead, 0); + // return -1 means there is still more data to be retrieved from the resultset + if (count == NO_COUNT){ mCount = startPos + mInitialRead; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received -1 from native_fill_window. read " + mCount + " rows so far"); + } Thread t = new Thread(new QueryThread(mCursorState), "query thread"); t.start(); - } + } else if (startPos == 0) { // native_fill_window returns count(*) only for startPos = 0 + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received count(*) from native_fill_window: " + count); + } + mCount = count; + } else if (mCount <= 0) { + throw new IllegalStateException("count should never be non-zero negative number"); + } + } + + private synchronized SQLiteQuery getQuery() { + return mQuery; } @Override @@ -321,166 +368,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { } } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() { - checkPosition(); - - // Only allow deletes if there is an ID column, and the ID has been read from it - if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { - Log.e(TAG, - "Could not delete row because either the row ID column is not available or it" + - "has not been read."); - return false; - } - - boolean success; - - /* - * Ensure we don't change the state of the database when another - * thread is holding the database lock. requery() and moveTo() are also - * synchronized here to make sure they get the state of the database - * immediately following the DELETE. - */ - mDatabase.lock(); - try { - try { - mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?", - new String[] {mCurrentRowID.toString()}); - success = true; - } catch (SQLException e) { - success = false; - } - - int pos = mPos; - requery(); - - /* - * Ensure proper cursor state. Note that mCurrentRowID changes - * in this call. - */ - moveToPosition(pos); - } finally { - mDatabase.unlock(); - } - - if (success) { - onChange(true); - return true; - } else { - return false; - } - } - @Override public String[] getColumnNames() { return mColumns; } - /** - * @hide - * @deprecated - */ - @Override - public boolean supportsUpdates() { - return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable); - } - - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates(Map> additionalValues) { - if (!supportsUpdates()) { - Log.e(TAG, "commitUpdates not supported on this cursor, did you " - + "include the _id column?"); - return false; - } - - /* - * Prevent other threads from changing the updated rows while they're - * being processed here. - */ - synchronized (mUpdatedRows) { - if (additionalValues != null) { - mUpdatedRows.putAll(additionalValues); - } - - if (mUpdatedRows.size() == 0) { - return true; - } - - /* - * Prevent other threads from changing the database state while - * we process the updated rows, and prevents us from changing the - * database behind the back of another thread. - */ - mDatabase.beginTransaction(); - try { - StringBuilder sql = new StringBuilder(128); - - // For each row that has been updated - for (Map.Entry> rowEntry : - mUpdatedRows.entrySet()) { - Map values = rowEntry.getValue(); - Long rowIdObj = rowEntry.getKey(); - - if (rowIdObj == null || values == null) { - throw new IllegalStateException("null rowId or values found! rowId = " - + rowIdObj + ", values = " + values); - } - - if (values.size() == 0) { - continue; - } - - long rowId = rowIdObj.longValue(); - - Iterator> valuesIter = - values.entrySet().iterator(); - - sql.setLength(0); - sql.append("UPDATE " + mEditTable + " SET "); - - // For each column value that has been updated - Object[] bindings = new Object[values.size()]; - int i = 0; - while (valuesIter.hasNext()) { - Map.Entry entry = valuesIter.next(); - sql.append(entry.getKey()); - sql.append("=?"); - bindings[i] = entry.getValue(); - if (valuesIter.hasNext()) { - sql.append(", "); - } - i++; - } - - sql.append(" WHERE " + mColumns[mRowIdColumnIndex] - + '=' + rowId); - sql.append(';'); - mDatabase.execSQL(sql.toString(), bindings); - mDatabase.rowUpdated(mEditTable, rowId); - } - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - - mUpdatedRows.clear(); - } - - // Let any change observers know about the update - onChange(true); - - return true; - } - private void deactivateCommon() { if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); mCursorState = 0; @@ -501,9 +393,32 @@ public class SQLiteCursor extends AbstractWindowedCursor { @Override public void close() { super.close(); - deactivateCommon(); - mQuery.close(); - mDriver.cursorClosed(); + synchronized (this) { + deactivateCommon(); + mQuery.close(); + mDriver.cursorClosed(); + } + } + + /** + * Show a warning against the use of requery() if called on the main thread. + * This warning is shown per database per process. + */ + private void warnIfUiThread() { + if (Looper.getMainLooper() == Looper.myLooper()) { + String databasePath = getQuery().mDatabase.getPath(); + // We show the warning once per database in order not to spam logcat. + if (!sAlreadyWarned.containsKey(databasePath)) { + sAlreadyWarned.put(databasePath, true); + String packageName = ActivityThread.currentPackageName(); + Throwable t = null; + // BEGIN STOPSHIP remove the following line + t = new RequeryOnUiThreadException(packageName); + // END STOPSHIP + Log.w(TAG, "should not attempt requery on main (UI) thread: app = " + + packageName == null ? "'unknown'" : packageName, t); + } + } } @Override @@ -511,20 +426,30 @@ public class SQLiteCursor extends AbstractWindowedCursor { if (isClosed()) { return false; } + warnIfUiThread(); long timeStart = 0; if (Config.LOGV) { timeStart = System.currentTimeMillis(); } - /* - * Synchronize on the database lock to ensure that mCount matches the - * results of mQuery.requery(). - */ - mDatabase.lock(); - try { + + synchronized (this) { if (mWindow != null) { mWindow.clear(); } mPos = -1; + SQLiteDatabase db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); + if (!db.equals(mQuery.mDatabase)) { + // since we need to use a different database connection handle, + // re-compile the query + db.lock(); + try { + // close the old mQuery object and open a new one + mQuery.close(); + mQuery = new SQLiteQuery(db, mQuery); + } finally { + db.unlock(); + } + } // This one will recreate the temp table, and get its count mDriver.cursorRequeried(this); mCount = NO_COUNT; @@ -535,8 +460,6 @@ public class SQLiteCursor extends AbstractWindowedCursor { } finally { queryThreadUnlock(); } - } finally { - mDatabase.unlock(); } if (Config.LOGV) { @@ -584,14 +507,14 @@ public class SQLiteCursor extends AbstractWindowedCursor { if (mWindow != null) { int len = mQuery.mSql.length(); Log.e(TAG, "Finalizing a Cursor that has not been deactivated or closed. " + - "database = " + mDatabase.getPath() + ", table = " + mEditTable + + "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len), mStackTrace); close(); SQLiteDebug.notifyActiveCursorFinalized(); } else { if (Config.LOGV) { - Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() + + Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + ", query = " + mQuery.mSql); } } @@ -599,4 +522,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { super.finalize(); } } + + /** + * this is only for testing purposes. + */ + /* package */ int getMCount() { + return mCount; + } } diff --git a/core/java/android/database/sqlite/SQLiteCursorDriver.java b/core/java/android/database/sqlite/SQLiteCursorDriver.java index eda1b78a9844b217dd81321f7e11a6728377bd34..b3963f9bcb1ed16852a51be3bde451af2463770b 100644 --- a/core/java/android/database/sqlite/SQLiteCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteCursorDriver.java @@ -40,8 +40,6 @@ public interface SQLiteCursorDriver { /** * Called by a SQLiteCursor when it is requeryed. - * - * @return The new count value. */ void cursorRequeried(Cursor cursor); diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index cdc9bbb6a510949df83397f75d5d9ebb784847b6..3df1790ffcfc5a16be28e2b8d2926559413540ef 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,16 +16,16 @@ package android.database.sqlite; -import com.google.android.collect.Maps; - -import android.app.ActivityThread; import android.app.AppGlobals; import android.content.ContentValues; import android.database.Cursor; +import android.database.DatabaseErrorHandler; import android.database.DatabaseUtils; +import android.database.DefaultDatabaseErrorHandler; import android.database.SQLException; import android.database.sqlite.SQLiteDebug.DbStats; import android.os.Debug; +import android.os.StatFs; import android.os.SystemClock; import android.os.SystemProperties; import android.text.TextUtils; @@ -38,15 +38,13 @@ import dalvik.system.BlockGuard; import java.io.File; import java.lang.ref.WeakReference; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Random; -import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Pattern; @@ -67,7 +65,7 @@ import java.util.regex.Pattern; * is the Unicode Collation Algorithm and not tailored to the current locale. */ public class SQLiteDatabase extends SQLiteClosable { - private static final String TAG = "Database"; + private static final String TAG = "SQLiteDatabase"; private static final int EVENT_DB_OPERATION = 52000; private static final int EVENT_DB_CORRUPT = 75004; @@ -192,6 +190,11 @@ public class SQLiteDatabase extends SQLiteClosable { */ private SQLiteTransactionListener mTransactionListener; + /** + * this member is set if {@link #execSQL(String)} is used to begin and end transactions. + */ + private boolean mTransactionUsingExecSql; + /** Synchronize on this when accessing the database */ private final ReentrantLock mLock = new ReentrantLock(true); @@ -233,94 +236,133 @@ public class SQLiteDatabase extends SQLiteClosable { // lock acquistions of the database. /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; - /** Used by native code, do not rename */ - /* package */ int mNativeHandle = 0; + /** Used by native code, do not rename. make it volatile, so it is thread-safe. */ + /* package */ volatile int mNativeHandle = 0; - /** Used to make temp table names unique */ - /* package */ int mTempTableSequence = 0; + /** + * The size, in bytes, of a block on "/data". This corresponds to the Unix + * statfs.f_bsize field. note that this field is lazily initialized. + */ + private static int sBlockSize = 0; /** The path for the database file */ - private String mPath; + private final String mPath; /** The anonymized path for the database file for logging purposes */ private String mPathForLogs = null; // lazily populated /** The flags passed to open/create */ - private int mFlags; + private final int mFlags; /** The optional factory to use when creating new Cursors */ - private CursorFactory mFactory; + private final CursorFactory mFactory; - private WeakHashMap mPrograms; + private final WeakHashMap mPrograms; /** - * for each instance of this class, a cache is maintained to store + * for each instance of this class, a LRU cache is maintained to store * the compiled query statement ids returned by sqlite database. - * key = sql statement with "?" for bind args + * key = SQL statement with "?" for bind args * value = {@link SQLiteCompiledSql} * If an application opens the database and keeps it open during its entire life, then - * there will not be an overhead of compilation of sql statements by sqlite. + * there will not be an overhead of compilation of SQL statements by sqlite. * * why is this cache NOT static? because sqlite attaches compiledsql statements to the * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is * invoked. * * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method - * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because - * most of the apps don't use "?" syntax in their sql, caching is not useful for them. - */ - /* package */ Map mCompiledQueries = Maps.newHashMap(); + * (@link #setMaxSqlCacheSize(int)}). + */ + // default statement-cache size per database connection ( = instance of this class) + private int mMaxSqlCacheSize = 25; + /* package */ final Map mCompiledQueries = + new LinkedHashMap(mMaxSqlCacheSize + 1, 0.75f, true) { + @Override + public boolean removeEldestEntry(Map.Entry eldest) { + // eldest = least-recently used entry + // if it needs to be removed to accommodate a new entry, + // close {@link SQLiteCompiledSql} represented by this entry, if not in use + // and then let it be removed from the Map. + // when this is called, the caller must be trying to add a just-compiled stmt + // to cache; i.e., caller should already have acquired database lock AND + // the lock on mCompiledQueries. do as assert of these two 2 facts. + verifyLockOwner(); + if (this.size() <= mMaxSqlCacheSize) { + // cache is not full. nothing needs to be removed + return false; + } + // cache is full. eldest will be removed. + eldest.getValue().releaseIfNotInUse(); + // return true, so that this entry is removed automatically by the caller. + return true; + } + }; /** - * @hide + * absolute max value that can be set by {@link #setMaxSqlCacheSize(int)} + * size of each prepared-statement is between 1K - 6K, depending on the complexity of the + * SQL statement & schema. */ - public static final int MAX_SQL_CACHE_SIZE = 250; - private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance - private int mCacheFullWarnings; - private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; + public static final int MAX_SQL_CACHE_SIZE = 100; + private boolean mCacheFullWarning; /** maintain stats about number of cache hits and misses */ private int mNumCacheHits; private int mNumCacheMisses; - /** the following 2 members maintain the time when a database is opened and closed */ - private String mTimeOpened = null; - private String mTimeClosed = null; - /** Used to find out where this object was created in case it never got closed. */ - private Throwable mStackTrace = null; + private final Throwable mStackTrace; // 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 + /** stores the list of statement ids that need to be finalized by sqlite */ + private final ArrayList mClosedStatementIds = new ArrayList(); + + /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors + * Corruption + * */ + private final DatabaseErrorHandler mErrorHandler; + + /** The Database connection pool {@link DatabaseConnectionPool}. + * Visibility is package-private for testing purposes. otherwise, private visibility is enough. */ - void addSQLiteClosable(SQLiteClosable closable) { - lock(); - try { - mPrograms.put(closable, null); - } finally { - unlock(); - } + /* package */ volatile DatabaseConnectionPool mConnectionPool = null; + + /** Each database connection handle in the pool is assigned a number 1..N, where N is the + * size of the connection pool. + * The main connection handle to which the pool is attached is assigned a value of 0. + */ + /* package */ final short mConnectionNum; + + /** on pooled database connections, this member points to the parent ( = main) + * database connection handle. + * package visibility only for testing purposes + */ + /* package */ SQLiteDatabase mParentConnObj = null; + + private static final String MEMORY_DB_PATH = ":memory:"; + + /** stores reference to all databases opened in the current process. */ + private static ArrayList> mActiveDatabases = + new ArrayList>(); + + synchronized void addSQLiteClosable(SQLiteClosable closable) { + // mPrograms is per instance of SQLiteDatabase and it doesn't actually touch the database + // itself. so, there is no need to lock(). + mPrograms.put(closable, null); } - void removeSQLiteClosable(SQLiteClosable closable) { - lock(); - try { - mPrograms.remove(closable); - } finally { - unlock(); - } + synchronized void removeSQLiteClosable(SQLiteClosable closable) { + mPrograms.remove(closable); } @Override protected void onAllReferencesReleased() { if (isOpen()) { - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } - dbclose(); + // close the database which will close all pending statements to be finalized also + close(); } } @@ -350,19 +392,8 @@ public class SQLiteDatabase extends SQLiteClosable { private boolean mLockingEnabled = true; /* package */ void onCorruption() { - Log.e(TAG, "Removing corrupt database: " + mPath); EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); - try { - // Close the database (if we can), which will cause subsequent operations to fail. - close(); - } finally { - // Delete the corrupt file. Don't re-create it now -- that would just confuse people - // -- but the next time someone tries to open it, they can set it up from scratch. - if (!mPath.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(mPath).delete(); - } - } + mErrorHandler.onCorruption(this); } /** @@ -374,6 +405,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @see #unlock() */ /* package */ void lock() { + verifyDbIsOpen(); if (!mLockingEnabled) return; mLock.lock(); if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { @@ -394,6 +426,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @see #unlockForced() */ private void lockForced() { + verifyDbIsOpen(); mLock.lock(); if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { if (mLock.getHoldCount() == 1) { @@ -460,11 +493,14 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of + * Begins a transaction in EXCLUSIVE mode. + *

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

    @@ -478,15 +514,42 @@ public class SQLiteDatabase extends SQLiteClosable {
          * 
    */ public void beginTransaction() { - beginTransactionWithListener(null /* transactionStatusCallback */); + beginTransaction(null /* transactionStatusCallback */, true); + } + + /** + * Begins a transaction in IMMEDIATE mode. 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.beginTransactionNonExclusive();
    +     *   try {
    +     *     ...
    +     *     db.setTransactionSuccessful();
    +     *   } finally {
    +     *     db.endTransaction();
    +     *   }
    +     * 
    + */ + public void beginTransactionNonExclusive() { + beginTransaction(null /* transactionStatusCallback */, false); } /** - * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of + * Begins a transaction in EXCLUSIVE mode. + *

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

    @@ -498,15 +561,48 @@ public class SQLiteDatabase extends SQLiteClosable {
          *     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) { + beginTransaction(transactionListener, true); + } + + /** + * Begins a transaction in IMMEDIATE mode. 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.beginTransactionWithListenerNonExclusive(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 beginTransactionWithListenerNonExclusive( + SQLiteTransactionListener transactionListener) { + beginTransaction(transactionListener, false); + } + + private void beginTransaction(SQLiteTransactionListener transactionListener, + boolean exclusive) { + verifyDbIsOpen(); lockForced(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } boolean ok = false; try { // If this thread already had the lock then get out @@ -524,7 +620,11 @@ public class SQLiteDatabase extends SQLiteClosable { // This thread didn't already have the lock, so begin a database // transaction now. - execSQL("BEGIN EXCLUSIVE;"); + if (exclusive && mConnectionPool == null) { + execSQL("BEGIN EXCLUSIVE;"); + } else { + execSQL("BEGIN IMMEDIATE;"); + } mTransactionListener = transactionListener; mTransactionIsSuccessful = true; mInnerTransactionIsSuccessful = false; @@ -551,12 +651,7 @@ public class SQLiteDatabase extends SQLiteClosable { * are committed and rolled back. */ public void endTransaction() { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("no transaction pending"); - } + verifyLockOwner(); try { if (mInnerTransactionIsSuccessful) { mInnerTransactionIsSuccessful = false; @@ -581,6 +676,18 @@ public class SQLiteDatabase extends SQLiteClosable { } if (mTransactionIsSuccessful) { execSQL(COMMIT_SQL); + // if write-ahead logging is used, we have to take care of checkpoint. + // TODO: should applications be given the flexibility of choosing when to + // trigger checkpoint? + // for now, do checkpoint after every COMMIT because that is the fastest + // way to guarantee that readers will see latest data. + // but this is the slowest way to run sqlite with in write-ahead logging mode. + if (this.mConnectionPool != null) { + execSQL("PRAGMA wal_checkpoint;"); + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + Log.i(TAG, "PRAGMA wal_Checkpoint done"); + } + } } else { try { execSQL("ROLLBACK;"); @@ -614,9 +721,7 @@ public class SQLiteDatabase extends SQLiteClosable { * transaction is already marked as successful. */ public void setTransactionSuccessful() { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } + verifyDbIsOpen(); if (!mLock.isHeldByCurrentThread()) { throw new IllegalStateException("no transaction pending"); } @@ -631,7 +736,50 @@ public class SQLiteDatabase extends SQLiteClosable { * return true if there is a transaction pending */ public boolean inTransaction() { - return mLock.getHoldCount() > 0; + return mLock.getHoldCount() > 0 || mTransactionUsingExecSql; + } + + /* package */ synchronized void setTransactionUsingExecSqlFlag() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.i(TAG, "found execSQL('begin transaction')"); + } + mTransactionUsingExecSql = true; + } + + /* package */ synchronized void resetTransactionUsingExecSqlFlag() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + if (mTransactionUsingExecSql) { + Log.i(TAG, "found execSQL('commit or end or rollback')"); + } + } + mTransactionUsingExecSql = false; + } + + /** + * Returns true if the caller is considered part of the current transaction, if any. + *

    + * Caller is part of the current transaction if either of the following is true + *

      + *
    1. If transaction is started by calling beginTransaction() methods AND if the caller is + * in the same thread as the thread that started the transaction. + *
    2. + *
    3. If the transaction is started by calling {@link #execSQL(String)} like this: + * execSQL("BEGIN transaction"). In this case, every thread in the process is considered + * part of the current transaction.
    4. + *
    + * + * @return true if the caller is considered part of the current transaction, if any. + */ + /* package */ synchronized boolean amIInTransaction() { + // always do this test on the main database connection - NOT on pooled database connection + // since transactions always occur on the main database connections only. + SQLiteDatabase db = (isPooledConnection()) ? mParentConnObj : this; + boolean b = (!db.inTransaction()) ? false : + db.mTransactionUsingExecSql || db.mLock.isHeldByCurrentThread(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.i(TAG, "amIinTransaction: " + b); + } + return b; } /** @@ -735,54 +883,12 @@ public class SQLiteDatabase extends SQLiteClosable { return true; } - /** Maps table names to info about what to which _sync_time column to set - * to NULL on an update. This is used to support syncing. */ - private final Map mSyncUpdateInfo = - new HashMap(); - - public Map getSyncedTables() { - synchronized(mSyncUpdateInfo) { - HashMap tables = new HashMap(); - for (String table : mSyncUpdateInfo.keySet()) { - SyncUpdateInfo info = mSyncUpdateInfo.get(table); - if (info.deletedTable != null) { - tables.put(table, info.deletedTable); - } - } - return tables; - } - } - /** - * Internal class used to keep track what needs to be marked as changed - * when an update occurs. This is used for syncing, so the sync engine - * knows what data has been updated locally. + * @deprecated This method no longer serves any useful purpose and has been deprecated. */ - static private class SyncUpdateInfo { - /** - * Creates the SyncUpdateInfo class. - * - * @param masterTable The table to set _sync_time to NULL in - * @param deletedTable The deleted table that corresponds to the - * master table - * @param foreignKey The key that refers to the primary key in table - */ - SyncUpdateInfo(String masterTable, String deletedTable, - String foreignKey) { - this.masterTable = masterTable; - this.deletedTable = deletedTable; - this.foreignKey = foreignKey; - } - - /** The table containing the _sync_time column */ - String masterTable; - - /** The deleted table that corresponds to the master table */ - String deletedTable; - - /** The key in the local table the row in table. It may be _id, if table - * is the local table. */ - String foreignKey; + @Deprecated + public Map getSyncedTables() { + return new HashMap(0); } /** @@ -791,8 +897,7 @@ public class SQLiteDatabase extends SQLiteClosable { public interface CursorFactory { /** * See - * {@link SQLiteCursor#SQLiteCursor(SQLiteDatabase, SQLiteCursorDriver, - * String, SQLiteQuery)}. + * {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}. */ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, String editTable, @@ -814,30 +919,80 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLiteException if the database cannot be opened */ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) { - SQLiteDatabase sqliteDatabase = null; + return openDatabase(path, factory, flags, new DefaultDatabaseErrorHandler()); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. + * + *

    Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

    + * + *

    Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.

    + * + * @param path to database file to open and/or create + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode + * @param errorHandler the {@link DatabaseErrorHandler} obj to be used to handle corruption + * when sqlite reports database corruption + * @return the newly opened database + * @throws SQLiteException if the database cannot be opened + */ + public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, + DatabaseErrorHandler errorHandler) { + SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler, + (short) 0 /* the main connection handle */); + + // set sqlite pagesize to mBlockSize + if (sBlockSize == 0) { + // TODO: "/data" should be a static final String constant somewhere. it is hardcoded + // in several places right now. + sBlockSize = new StatFs("/data").getBlockSize(); + } + sqliteDatabase.setPageSize(sBlockSize); + //STOPSHIP - uncomment the following line + //sqliteDatabase.setJournalMode(path, "TRUNCATE"); + // STOPSHIP remove the following lines + if (!path.equalsIgnoreCase(MEMORY_DB_PATH)) { + sqliteDatabase.enableWriteAheadLogging(); + } + // END STOPSHIP + + // add this database to the list of databases opened in this process + synchronized(mActiveDatabases) { + mActiveDatabases.add(new WeakReference(sqliteDatabase)); + } + return sqliteDatabase; + } + + private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, + DatabaseErrorHandler errorHandler, short connectionNum) { + SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum); try { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.i(TAG, "opening the db : " + path); + } // Open the database. - sqliteDatabase = new SQLiteDatabase(path, factory, flags); + db.dbopen(path, flags); + db.setLocale(Locale.getDefault()); if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - sqliteDatabase.enableSqlTracing(path); + db.enableSqlTracing(path, connectionNum); } if (SQLiteDebug.DEBUG_SQL_TIME) { - sqliteDatabase.enableSqlProfiling(path); + db.enableSqlProfiling(path, connectionNum); } + return db; } catch (SQLiteDatabaseCorruptException e) { - // 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); - if (!path.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(path).delete(); - } - sqliteDatabase = new SQLiteDatabase(path, factory, flags); + db.mErrorHandler.onCorruption(db); + return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler); + } catch (SQLiteException e) { + Log.e(TAG, "Failed to open the database. closing it.", e); + db.close(); + throw e; } - ActiveDatabases.getInstance().mActiveDatabases.add( - new WeakReference(sqliteDatabase)); - return sqliteDatabase; } /** @@ -854,6 +1009,25 @@ public class SQLiteDatabase extends SQLiteClosable { return openDatabase(path, factory, CREATE_IF_NECESSARY); } + /** + * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler); + } + + private void setJournalMode(final String dbPath, final String mode) { + // journal mode can be set only for non-memory databases + if (!dbPath.equalsIgnoreCase(MEMORY_DB_PATH)) { + String s = DatabaseUtils.stringForQuery(this, "PRAGMA journal_mode=" + mode, null); + if (!s.equalsIgnoreCase(mode)) { + Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + dbPath + + " (on pragma set journal_mode, sqlite returned:" + s); + } + } + } + /** * Create a memory backed SQLite database. Its contents will be destroyed * when the database is closed. @@ -867,7 +1041,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public static SQLiteDatabase create(CursorFactory factory) { // This is a magic string with special meaning for SQLite. - return openDatabase(":memory:", factory, CREATE_IF_NECESSARY); + return openDatabase(MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY); } /** @@ -877,18 +1051,31 @@ public class SQLiteDatabase extends SQLiteClosable { if (!isOpen()) { return; // already closed } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.i(TAG, "closing db: " + mPath + " (connection # " + mConnectionNum); + } lock(); try { closeClosable(); + // finalize ALL statements queued up so far + closePendingStatements(); + releaseCustomFunctions(); // close this database instance - regardless of its reference count value - onAllReferencesReleased(); + closeDatabase(); + if (mConnectionPool != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert mConnectionPool != null; + Log.i(TAG, mConnectionPool.toString()); + } + mConnectionPool.close(); + } } finally { - unlock(); + unlock(); } } private void closeClosable() { - /* deallocate all compiled sql statement objects from mCompiledQueries cache. + /* deallocate all compiled SQL statement objects from mCompiledQueries cache. * this should be done before de-referencing all {@link SQLiteClosable} objects * from this database object because calling * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database @@ -907,30 +1094,107 @@ public class SQLiteDatabase extends SQLiteClosable { } } + /** + * package level access for testing purposes + */ + /* package */ void closeDatabase() throws SQLiteException { + try { + dbclose(); + } catch (SQLiteUnfinalizedObjectsException e) { + String msg = e.getMessage(); + String[] tokens = msg.split(",", 2); + int stmtId = Integer.parseInt(tokens[0]); + // get extra info about this statement, if it is still to be released by closeClosable() + Iterator> iter = mPrograms.entrySet().iterator(); + boolean found = false; + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + SQLiteClosable program = entry.getKey(); + if (program != null && program instanceof SQLiteProgram) { + SQLiteCompiledSql compiledSql = ((SQLiteProgram)program).mCompiledSql; + if (compiledSql.nStatement == stmtId) { + msg = compiledSql.toString(); + found = true; + } + } + } + if (!found) { + // the statement is already released by closeClosable(). is it waiting to be + // finalized? + if (mClosedStatementIds.contains(stmtId)) { + Log.w(TAG, "this shouldn't happen. finalizing the statement now: "); + closePendingStatements(); + // try to close the database again + closeDatabase(); + } + } else { + // the statement is not yet closed. most probably programming error in the app. + Log.w(TAG, "dbclose failed due to un-close()d SQL statements: " + msg); + throw e; + } + } + } + /** * Native call to close the database. */ private native void dbclose(); + /** + * A callback interface for a custom sqlite3 function. + * This can be used to create a function that can be called from + * sqlite3 database triggers. + * @hide + */ + public interface CustomFunction { + public void callback(String[] args); + } + + /** + * Registers a CustomFunction callback as a function that can be called from + * sqlite3 database triggers. + * @param name the name of the sqlite3 function + * @param numArgs the number of arguments for the function + * @param function callback to call when the function is executed + * @hide + */ + public void addCustomFunction(String name, int numArgs, CustomFunction function) { + verifyDbIsOpen(); + synchronized (mCustomFunctions) { + int ref = native_addCustomFunction(name, numArgs, function); + if (ref != 0) { + // save a reference to the function for cleanup later + mCustomFunctions.add(new Integer(ref)); + } else { + throw new SQLiteException("failed to add custom function " + name); + } + } + } + + private void releaseCustomFunctions() { + synchronized (mCustomFunctions) { + for (int i = 0; i < mCustomFunctions.size(); i++) { + Integer function = mCustomFunctions.get(i); + native_releaseCustomFunction(function.intValue()); + } + mCustomFunctions.clear(); + } + } + + // list of CustomFunction references so we can clean up when the database closes + private final ArrayList mCustomFunctions = + new ArrayList(); + + private native int native_addCustomFunction(String name, int numArgs, CustomFunction function); + private native void native_releaseCustomFunction(int function); + /** * Gets the database version. * * @return the database version */ public int getVersion() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, "PRAGMA user_version;"); - long version = prog.simpleQueryForLong(); - return (int) version; - } finally { - if (prog != null) prog.close(); - unlock(); - } + return ((Long) DatabaseUtils.longForQuery(this, "PRAGMA user_version;", null)).intValue(); } /** @@ -948,20 +1212,8 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the new maximum database size */ public long getMaximumSize() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, - "PRAGMA max_page_count;"); - long pageCount = prog.simpleQueryForLong(); - return pageCount * getPageSize(); - } finally { - if (prog != null) prog.close(); - unlock(); - } + long pageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count;", null); + return pageCount * getPageSize(); } /** @@ -972,26 +1224,15 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the new maximum database size */ public long setMaximumSize(long numBytes) { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - long pageSize = getPageSize(); - long numPages = numBytes / pageSize; - // If numBytes isn't a multiple of pageSize, bump up a page - if ((numBytes % pageSize) != 0) { - numPages++; - } - prog = new SQLiteStatement(this, - "PRAGMA max_page_count = " + numPages); - long newPageCount = prog.simpleQueryForLong(); - return newPageCount * pageSize; - } finally { - if (prog != null) prog.close(); - unlock(); + long pageSize = getPageSize(); + long numPages = numBytes / pageSize; + // If numBytes isn't a multiple of pageSize, bump up a page + if ((numBytes % pageSize) != 0) { + numPages++; } + long newPageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count = " + numPages, + null); + return newPageCount * pageSize; } /** @@ -1000,20 +1241,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the database page size, in bytes */ public long getPageSize() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, - "PRAGMA page_size;"); - long size = prog.simpleQueryForLong(); - return size; - } finally { - if (prog != null) prog.close(); - unlock(); - } + return DatabaseUtils.longForQuery(this, "PRAGMA page_size;", null); } /** @@ -1034,25 +1262,10 @@ public class SQLiteDatabase extends SQLiteClosable { * @param table the table to mark as syncable * @param deletedTable The deleted table that corresponds to the * syncable table + * @deprecated This method no longer serves any useful purpose and has been deprecated. */ + @Deprecated public void markTableSyncable(String table, String deletedTable) { - markTableSyncable(table, "_id", table, deletedTable); - } - - /** - * Mark this table as syncable, with the _sync_dirty residing in another - * table. When an update occurs in this table the _sync_dirty field of the - * row in updateTable with the _id in foreignKey will be set to - * ensure proper syncing operation. - * - * @param table an update on this table will trigger a sync time removal - * @param foreignKey this is the column in table whose value is an _id in - * updateTable - * @param updateTable this is the table that will have its _sync_dirty - */ - public void markTableSyncable(String table, String foreignKey, - String updateTable) { - markTableSyncable(table, foreignKey, updateTable, null); } /** @@ -1065,44 +1278,10 @@ public class SQLiteDatabase extends SQLiteClosable { * @param foreignKey this is the column in table whose value is an _id in * updateTable * @param updateTable this is the table that will have its _sync_dirty - * @param deletedTable The deleted table that corresponds to the - * updateTable + * @deprecated This method no longer serves any useful purpose and has been deprecated. */ - private void markTableSyncable(String table, String foreignKey, - String updateTable, String deletedTable) { - lock(); - try { - native_execSQL("SELECT _sync_dirty FROM " + updateTable - + " LIMIT 0"); - native_execSQL("SELECT " + foreignKey + " FROM " + table - + " LIMIT 0"); - } finally { - unlock(); - } - - SyncUpdateInfo info = new SyncUpdateInfo(updateTable, deletedTable, - foreignKey); - synchronized (mSyncUpdateInfo) { - mSyncUpdateInfo.put(table, info); - } - } - - /** - * Call for each row that is updated in a cursor. - * - * @param table the table the row is in - * @param rowId the row ID of the updated row - */ - /* package */ void rowUpdated(String table, long rowId) { - SyncUpdateInfo info; - synchronized (mSyncUpdateInfo) { - info = mSyncUpdateInfo.get(table); - } - if (info != null) { - execSQL("UPDATE " + info.masterTable - + " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey - + " FROM " + table + " WHERE _id=" + rowId + ")"); - } + @Deprecated + public void markTableSyncable(String table, String foreignKey, String updateTable) { } /** @@ -1134,6 +1313,8 @@ public class SQLiteDatabase extends SQLiteClosable { * statement and fill in those values with {@link SQLiteProgram#bindString} * and {@link SQLiteProgram#bindLong} each time you want to run the * statement. Statements may not return result sets larger than 1x1. + *

    + * No two threads should be using the same {@link SQLiteStatement} at the same time. * * @param sql The raw SQL statement, may contain ? for unknown values to be * bound later. @@ -1141,15 +1322,8 @@ public class SQLiteDatabase extends SQLiteClosable { * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. */ public SQLiteStatement compileStatement(String sql) throws SQLException { - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - return new SQLiteStatement(this, sql); - } finally { - unlock(); - } + verifyDbIsOpen(); + return new SQLiteStatement(this, sql, null); } /** @@ -1226,9 +1400,7 @@ public class SQLiteDatabase extends SQLiteClosable { boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } + verifyDbIsOpen(); String sql = SQLiteQueryBuilder.buildQueryString( distinct, table, columns, selection, groupBy, having, orderBy, limit); @@ -1339,9 +1511,7 @@ public class SQLiteDatabase extends SQLiteClosable { public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } + verifyDbIsOpen(); BlockGuard.getThreadPolicy().onReadFromDisk(); long timeStart = 0; @@ -1349,7 +1519,8 @@ public class SQLiteDatabase extends SQLiteClosable { timeStart = System.currentTimeMillis(); } - SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); + SQLiteDatabase db = getDbConnection(sql); + SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable); Cursor cursor = null; try { @@ -1375,6 +1546,7 @@ public class SQLiteDatabase extends SQLiteClosable { : "") + ", count is " + count); } } + releaseDbConnection(db); } return cursor; } @@ -1501,84 +1673,41 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm) { - BlockGuard.getThreadPolicy().onWriteToDisk(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - - // Measurements show most sql lengths <= 152 - StringBuilder sql = new StringBuilder(152); + StringBuilder sql = new StringBuilder(); sql.append("INSERT"); sql.append(CONFLICT_VALUES[conflictAlgorithm]); sql.append(" INTO "); sql.append(table); - // Measurements show most values lengths < 40 - StringBuilder values = new StringBuilder(40); - - Set> entrySet = null; - if (initialValues != null && initialValues.size() > 0) { - entrySet = initialValues.valueSet(); - Iterator> entriesIter = entrySet.iterator(); - sql.append('('); - - boolean needSeparator = false; - while (entriesIter.hasNext()) { - if (needSeparator) { - sql.append(", "); - values.append(", "); - } - needSeparator = true; - Map.Entry entry = entriesIter.next(); - sql.append(entry.getKey()); - values.append('?'); + sql.append('('); + + Object[] bindArgs = null; + int size = (initialValues != null && initialValues.size() > 0) ? initialValues.size() : 0; + if (size > 0) { + bindArgs = new Object[size]; + int i = 0; + for (String colName : initialValues.keySet()) { + sql.append((i > 0) ? "," : ""); + sql.append(colName); + bindArgs[i++] = initialValues.get(colName); } - sql.append(')'); + sql.append(" VALUES ("); + for (i = 0; i < size; i++) { + sql.append((i > 0) ? ",?" : "?"); + } } else { - sql.append("(" + nullColumnHack + ") "); - values.append("NULL"); + sql.append(nullColumnHack + ") VALUES (NULL"); } + sql.append(')'); - sql.append(" VALUES("); - sql.append(values); - sql.append(");"); - - lock(); - SQLiteStatement statement = null; + SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); try { - statement = compileStatement(sql.toString()); - - // Bind the values - if (entrySet != null) { - int size = entrySet.size(); - Iterator> entriesIter = entrySet.iterator(); - for (int i = 0; i < size; i++) { - Map.Entry entry = entriesIter.next(); - DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue()); - } - } - - // Run the program and then cleanup - statement.execute(); - - long insertedRowId = lastInsertRow(); - if (insertedRowId == -1) { - Log.e(TAG, "Error inserting " + initialValues + " using " + sql); - } else { - if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Inserting row " + insertedRowId + " from " - + initialValues + " using " + sql); - } - } - return insertedRowId; + return statement.executeInsert(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { - if (statement != null) { - statement.close(); - } - unlock(); + statement.close(); } } @@ -1593,32 +1722,15 @@ public class SQLiteDatabase extends SQLiteClosable { * whereClause. */ public int delete(String table, String whereClause, String[] whereArgs) { - BlockGuard.getThreadPolicy().onWriteToDisk(); - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - SQLiteStatement statement = null; + SQLiteStatement statement = new SQLiteStatement(this, "DELETE FROM " + table + + (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs); try { - statement = compileStatement("DELETE FROM " + table - + (!TextUtils.isEmpty(whereClause) - ? " WHERE " + whereClause : "")); - if (whereArgs != null) { - int numArgs = whereArgs.length; - for (int i = 0; i < numArgs; i++) { - DatabaseUtils.bindObjectToProgram(statement, i + 1, whereArgs[i]); - } - } - statement.execute(); - return lastChangeCount(); + return statement.executeUpdateDelete(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { - if (statement != null) { - statement.close(); - } - unlock(); + statement.close(); } } @@ -1649,8 +1761,8 @@ public class SQLiteDatabase extends SQLiteClosable { */ public int updateWithOnConflict(String table, ContentValues values, String whereClause, String[] whereArgs, int conflictAlgorithm) { - BlockGuard.getThreadPolicy().onWriteToDisk(); - if (values == null || values.size() == 0) { + int setValuesSize = values.size(); + if (values == null || setValuesSize == 0) { throw new IllegalArgumentException("Empty values"); } @@ -1660,98 +1772,68 @@ public class SQLiteDatabase extends SQLiteClosable { sql.append(table); sql.append(" SET "); - Set> entrySet = values.valueSet(); - Iterator> entriesIter = entrySet.iterator(); - - while (entriesIter.hasNext()) { - Map.Entry entry = entriesIter.next(); - sql.append(entry.getKey()); + // move all bind args to one array + int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length); + Object[] bindArgs = new Object[bindArgsSize]; + int i = 0; + for (String colName : values.keySet()) { + sql.append((i > 0) ? "," : ""); + sql.append(colName); + bindArgs[i++] = values.get(colName); sql.append("=?"); - if (entriesIter.hasNext()) { - sql.append(", "); + } + if (whereArgs != null) { + for (i = setValuesSize; i < bindArgsSize; i++) { + bindArgs[i] = whereArgs[i - setValuesSize]; } } - if (!TextUtils.isEmpty(whereClause)) { sql.append(" WHERE "); sql.append(whereClause); } - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - SQLiteStatement statement = null; + SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); try { - statement = compileStatement(sql.toString()); - - // Bind the values - int size = entrySet.size(); - entriesIter = entrySet.iterator(); - int bindArg = 1; - for (int i = 0; i < size; i++) { - Map.Entry entry = entriesIter.next(); - DatabaseUtils.bindObjectToProgram(statement, bindArg, entry.getValue()); - bindArg++; - } - - if (whereArgs != null) { - size = whereArgs.length; - for (int i = 0; i < size; i++) { - statement.bindString(bindArg, whereArgs[i]); - bindArg++; - } - } - - // Run the program and then cleanup - statement.execute(); - int numChangedRows = lastChangeCount(); - if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Updated " + numChangedRows + " using " + values + " and " + sql); - } - return numChangedRows; + return statement.executeUpdateDelete(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; - } catch (SQLException e) { - Log.e(TAG, "Error updating " + values + " using " + sql); - throw e; } finally { - if (statement != null) { - statement.close(); - } - unlock(); + statement.close(); } } /** - * Execute a single SQL statement that is not a query. For example, CREATE - * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not - * supported. it takes a write lock + * Execute a single SQL statement that is NOT a SELECT + * or any other SQL statement that returns data. + *

    + * It has no means to return any data (such as the number of affected rows). + * Instead, you're encouraged to use {@link #insert(String, String, ContentValues)}, + * {@link #update(String, ContentValues, String, String[])}, et al, when possible. + *

    + *

    + * When using {@link #enableWriteAheadLogging()}, journal_mode is + * automatically managed by this class. So, do not set journal_mode + * using "PRAGMA journal_mode'" statement if your app is using + * {@link #enableWriteAheadLogging()} + *

    * + * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are + * not supported. * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql) throws SQLException { - BlockGuard.getThreadPolicy().onWriteToDisk(); - long timeStart = SystemClock.uptimeMillis(); - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); + int stmtType = DatabaseUtils.getSqlStatementType(sql); + if (stmtType == DatabaseUtils.STATEMENT_ATTACH) { + disableWriteAheadLogging(); } + long timeStart = SystemClock.uptimeMillis(); logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX); - try { - native_execSQL(sql); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; - } finally { - unlock(); - } + executeSql(sql, null); // Log commit statements along with the most recently executed - // SQL statement for disambiguation. Note that instance - // equality to COMMIT_SQL is safe here. - if (sql == COMMIT_SQL) { + // SQL statement for disambiguation. + if (stmtType == DatabaseUtils.STATEMENT_COMMIT) { logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL); } else { logTimeStat(sql, timeStart, null); @@ -1759,65 +1841,100 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Execute a single SQL statement that is not a query. For example, CREATE - * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not - * supported. it takes a write lock, + * Execute a single SQL statement that is NOT a SELECT/INSERT/UPDATE/DELETE. + *

    + * For INSERT statements, use any of the following instead. + *

      + *
    • {@link #insert(String, String, ContentValues)}
    • + *
    • {@link #insertOrThrow(String, String, ContentValues)}
    • + *
    • {@link #insertWithOnConflict(String, String, ContentValues, int)}
    • + *
    + *

    + * For UPDATE statements, use any of the following instead. + *

      + *
    • {@link #update(String, ContentValues, String, String[])}
    • + *
    • {@link #updateWithOnConflict(String, ContentValues, String, String[], int)}
    • + *
    + *

    + * For DELETE statements, use any of the following instead. + *

      + *
    • {@link #delete(String, String, String[])}
    • + *
    + *

    + * For example, the following are good candidates for using this method: + *

      + *
    • ALTER TABLE
    • + *
    • CREATE or DROP table / trigger / view / index / virtual table
    • + *
    • REINDEX
    • + *
    • RELEASE
    • + *
    • SAVEPOINT
    • + *
    • PRAGMA that returns no data
    • + *
    + *

    + *

    + * When using {@link #enableWriteAheadLogging()}, journal_mode is + * automatically managed by this class. So, do not set journal_mode + * using "PRAGMA journal_mode'" statement if your app is using + * {@link #enableWriteAheadLogging()} + *

    * - * @param sql + * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are + * not supported. * @param bindArgs only byte[], String, Long and Double are supported in bindArgs. * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql, Object[] bindArgs) throws SQLException { - BlockGuard.getThreadPolicy().onWriteToDisk(); if (bindArgs == null) { throw new IllegalArgumentException("Empty bindArgs"); } + executeSql(sql, bindArgs); + } + + private int executeSql(String sql, Object[] bindArgs) throws SQLException { long timeStart = SystemClock.uptimeMillis(); - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - SQLiteStatement statement = null; + int n; + SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); try { - statement = compileStatement(sql); - if (bindArgs != null) { - int numArgs = bindArgs.length; - for (int i = 0; i < numArgs; i++) { - DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]); - } - } - statement.execute(); + n = statement.executeUpdateDelete(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { - if (statement != null) { - statement.close(); - } - unlock(); + statement.close(); } logTimeStat(sql, timeStart); + return n; } @Override - protected void finalize() { - if (isOpen()) { - Log.e(TAG, "close() was never explicitly called on database '" + - mPath + "' ", mStackTrace); - closeClosable(); - onAllReferencesReleased(); + protected void finalize() throws Throwable { + try { + if (isOpen()) { + Log.e(TAG, "close() was never explicitly called on database '" + + mPath + "' ", mStackTrace); + closeClosable(); + onAllReferencesReleased(); + releaseCustomFunctions(); + } + } finally { + super.finalize(); } } /** - * Private constructor. See {@link #create} and {@link #openDatabase}. + * Private constructor. * * @param path The full path to the database * @param factory The factory to use when creating cursors, may be NULL. * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already * exists, mFlags will be updated appropriately. + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption. may be NULL. + * @param connectionNum 0 for main database connection handle. 1..N for pooled database + * connection handles. */ - private SQLiteDatabase(String path, CursorFactory factory, int flags) { + private SQLiteDatabase(String path, CursorFactory factory, int flags, + DatabaseErrorHandler errorHandler, short connectionNum) { if (path == null) { throw new IllegalArgumentException("path should not be null"); } @@ -1826,25 +1943,11 @@ public class SQLiteDatabase extends SQLiteClosable { mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); mFactory = factory; - dbopen(mPath, mFlags); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeOpened = getTime(); - } mPrograms = new WeakHashMap(); - try { - setLocale(Locale.getDefault()); - } catch (RuntimeException e) { - Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e); - dbclose(); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } - throw e; - } - } - - private String getTime() { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis()); + // Set the DatabaseErrorHandler to be used when SQLite reports corruption. + // If the caller sets errorHandler = null, then use default errorhandler. + mErrorHandler = (errorHandler == null) ? new DefaultDatabaseErrorHandler() : errorHandler; + mConnectionNum = connectionNum; } /** @@ -1901,7 +2004,7 @@ public class SQLiteDatabase extends SQLiteClosable { } if (durationMillis >= sQueryLogTimeInMillis) { samplePercent = 100; - } else {; + } else { samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1; if (mRandom.nextInt(100) >= samplePercent) return; } @@ -1970,66 +2073,55 @@ public class SQLiteDatabase extends SQLiteClosable { } } - /* - * ============================================================================ - * - * The following methods deal with compiled-sql cache - * ============================================================================ - */ + /* package */ void verifyDbIsOpen() { + if (!isOpen()) { + throw new IllegalStateException("database " + getPath() + " (conn# " + + mConnectionNum + ") already closed"); + } + } + + /* package */ void verifyLockOwner() { + verifyDbIsOpen(); + if (mLockingEnabled && !isDbLockedByCurrentThread()) { + throw new IllegalStateException("Don't have database lock!"); + } + } + /** - * adds the given sql and its compiled-statement-id-returned-by-sqlite to the + * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the * cache of compiledQueries attached to 'this'. - * - * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql, + *

    + * If there is already a {@link SQLiteCompiledSql} in compiledQueries for the given SQL, * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current * mapping is NOT replaced with the new mapping). */ /* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); - } - return; - } - - SQLiteCompiledSql compiledSql = null; synchronized(mCompiledQueries) { // don't insert the new mapping if a mapping already exists - compiledSql = mCompiledQueries.get(sql); - if (compiledSql != null) { + if (mCompiledQueries.containsKey(sql)) { return; } - // add this to the cache - if (mCompiledQueries.size() == mMaxSqlCacheSize) { + + if (!isCacheFullWarningLogged() && mCompiledQueries.size() == mMaxSqlCacheSize) { /* * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. - * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times - * chances are it is NOT using ? for bindargs - so caching is useless. - * TODO: either let the callers set max cchesize for their app, or intelligently - * figure out what should be cached for a given app. + * log a warning. + * chances are it is NOT using ? for bindargs - or cachesize is too small. */ - if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) { - Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + - getPath() + "; i.e., NO space for this sql statement in cache: " + - sql + ". Please change your sql statements to use '?' for " + - "bindargs, instead of using actual values"); - } - // don't add this entry to cache - } else { - // cache is NOT full. add this to cache. - mCompiledQueries.put(sql, compiledStatement); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + - mCompiledQueries.size() + "|" + sql); - } - } + Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + + getPath() + ". Use setMaxSqlCacheSize() to increase cachesize. "); + setCacheFullWarningLogged(); + } + /* add the given SQLiteCompiledSql compiledStatement to cache. + * no need to worry about the cache size - because {@link #mCompiledQueries} + * self-limits its size to {@link #mMaxSqlCacheSize}. + */ + mCompiledQueries.put(sql, compiledStatement); } - return; } - - private void deallocCachedSqlStatements() { + /** package-level access for testing purposes */ + /* package */ void deallocCachedSqlStatements() { synchronized (mCompiledQueries) { for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) { compiledSql.releaseSqlStatement(); @@ -2039,154 +2131,367 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * from the compiledQueries cache, returns the compiled-statement-id for the given sql. - * returns null, if not found in the cache. + * From the compiledQueries cache, returns the compiled-statement-id for the given SQL. + * Returns null, if not found in the cache. */ - /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) { - SQLiteCompiledSql compiledStatement = null; - boolean cacheHit; - synchronized(mCompiledQueries) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|cache NOT found|" + getPath()); - } - return null; - } - cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; - } - if (cacheHit) { - mNumCacheHits++; - } else { + /* package */ synchronized SQLiteCompiledSql getCompiledStatementForSql(String sql) { + SQLiteCompiledSql compiledStatement = mCompiledQueries.get(sql); + if (compiledStatement == null) { mNumCacheMisses++; + return null; } - - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|cache_stats|" + - getPath() + "|" + mCompiledQueries.size() + - "|" + mNumCacheHits + "|" + mNumCacheMisses + - "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); - } + mNumCacheHits++; return compiledStatement; } /** - * returns true if the given sql is cached in compiled-sql cache. - * @hide - */ - public boolean isInCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { + * Sets the maximum size of the prepared-statement cache for this database. + * (size of the cache = number of compiled-sql-statements stored in the cache). + *

    + * Maximum cache size can ONLY be increased from its current size (default = 10). + * If this method is called with smaller size than the current maximum value, + * then IllegalStateException is thrown. + *

    + * This method is thread-safe. + * + * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE}) + * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE} or + * the value set with previous setMaxSqlCacheSize() call. + */ + public void setMaxSqlCacheSize(int cacheSize) { + synchronized(this) { + if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { + throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); + } else if (cacheSize < mMaxSqlCacheSize) { + throw new IllegalStateException("cannot set cacheSize to a value less than the value " + + "set with previous setMaxSqlCacheSize() call."); + } + mMaxSqlCacheSize = cacheSize; + } + } + + /* package */ boolean isSqlInStatementCache(String sql) { + synchronized (mCompiledQueries) { return mCompiledQueries.containsKey(sql); } } - /** - * purges the given sql from the compiled-sql cache. - * @hide - */ - public void purgeFromCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { - mCompiledQueries.remove(sql); + private synchronized boolean isCacheFullWarningLogged() { + return mCacheFullWarning; + } + + private synchronized void setCacheFullWarningLogged() { + mCacheFullWarning = true; + } + + private synchronized int getCacheHitNum() { + return mNumCacheHits; + } + + private synchronized int getCacheMissNum() { + return mNumCacheMisses; + } + + private synchronized int getCachesize() { + return mCompiledQueries.size(); + } + + /* package */ void finalizeStatementLater(int id) { + if (!isOpen()) { + // database already closed. this statement will already have been finalized. + return; + } + synchronized(mClosedStatementIds) { + if (mClosedStatementIds.contains(id)) { + // this statement id is already queued up for finalization. + return; + } + mClosedStatementIds.add(id); } } - /** - * remove everything from the compiled sql cache - * @hide + /* package */ void closePendingStatements() { + if (!isOpen()) { + // since this database is already closed, no need to finalize anything. + mClosedStatementIds.clear(); + return; + } + verifyLockOwner(); + /* to minimize synchronization on mClosedStatementIds, make a copy of the list */ + ArrayList list = new ArrayList(mClosedStatementIds.size()); + synchronized(mClosedStatementIds) { + list.addAll(mClosedStatementIds); + mClosedStatementIds.clear(); + } + // finalize all the statements from the copied list + int size = list.size(); + for (int i = 0; i < size; i++) { + native_finalize(list.get(i)); + } + } + + /** + * for testing only + */ + /* package */ ArrayList getQueuedUpStmtList() { + return mClosedStatementIds; + } + + /** + * This method enables parallel execution of queries from multiple threads on the same database. + * It does this by opening multiple handles to the database and using a different + * database handle for each query. + *

    + * If a transaction is in progress on one connection handle and say, a table is updated in the + * transaction, then query on the same table on another connection handle will block for the + * transaction to complete. But this method enables such queries to execute by having them + * return old version of the data from the table. Most often it is the data that existed in the + * table prior to the above transaction updates on that table. + *

    + * Maximum number of simultaneous handles used to execute queries in parallel is + * dependent upon the device memory and possibly other properties. + *

    + * After calling this method, execution of queries in parallel is enabled as long as this + * database handle is open. To disable execution of queries in parallel, database should + * be closed and reopened. + *

    + * If a query is part of a transaction, then it is executed on the same database handle the + * transaction was begun. + *

    + * If the database has any attached databases, then execution of queries in paralel is NOT + * possible. In such cases, a message is printed to logcat and false is returned. + *

    + * This feature is not available for :memory: databases. In such cases, + * a message is printed to logcat and false is returned. + *

    + * A typical way to use this method is the following: + *

    +     *     SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
    +     *             CREATE_IF_NECESSARY, myDatabaseErrorHandler);
    +     *     db.enableWriteAheadLogging();
    +     * 
    + *

    + * Writers should use {@link #beginTransactionNonExclusive()} or + * {@link #beginTransactionWithListenerNonExclusive(SQLiteTransactionListener)} + * to start a trsnsaction. + * Non-exclusive mode allows database file to be in readable by threads executing queries. + *

    + * + * @return true if write-ahead-logging is set. false otherwise */ - public void resetCompiledSqlCache() { - synchronized(mCompiledQueries) { - mCompiledQueries.clear(); + public synchronized boolean enableWriteAheadLogging() { + if (mPath.equalsIgnoreCase(MEMORY_DB_PATH)) { + Log.i(TAG, "can't enable WAL for memory databases."); + return false; } + + // make sure this database has NO attached databases because sqlite's write-ahead-logging + // doesn't work for databases with attached databases + if (getAttachedDbs().size() > 1) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + "this database: " + mPath + " has attached databases. can't enable WAL."); + } + return false; + } + if (mConnectionPool == null) { + mConnectionPool = new DatabaseConnectionPool(this); + setJournalMode(mPath, "WAL"); + } + return true; } /** - * return the current maxCacheSqlCacheSize + * This method disables the features enabled by {@link #enableWriteAheadLogging()}. * @hide */ - public synchronized int getMaxSqlCacheSize() { - return mMaxSqlCacheSize; + public void disableWriteAheadLogging() { + synchronized (this) { + if (mConnectionPool == null) { + return; + } + mConnectionPool.close(); + mConnectionPool = null; + setJournalMode(mPath, "TRUNCATE"); + } + } + + /* package */ SQLiteDatabase getDatabaseHandle(String sql) { + if (isPooledConnection()) { + // this is a pooled database connection + // use it if it is open AND if I am not currently part of a transaction + if (isOpen() && !amIInTransaction()) { + // TODO: use another connection from the pool + // if this connection is currently in use by some other thread + // AND if there are free connections in the pool + return this; + } else { + // the pooled connection is not open! could have been closed either due + // to corruption on this or some other connection to the database + // OR, maybe the connection pool is disabled after this connection has been + // allocated to me. try to get some other pooled or main database connection + return getParentDbConnObj().getDbConnection(sql); + } + } else { + // this is NOT a pooled connection. can we get one? + return getDbConnection(sql); + } } /** - * set the max size of the compiled sql cache for this database after purging the cache. - * (size of the cache = number of compiled-sql-statements stored in the cache). + * Sets the database connection handle pool size to the given value. + * Database connection handle pool is enabled when the app calls + * {@link #enableWriteAheadLogging()}. + *

    + * The default connection handle pool is set by the system by taking into account various + * aspects of the device, such as memory, number of cores etc. It is recommended that + * applications use the default pool size set by the system. * - * max cache size can ONLY be increased from its current size (default = 0). - * if this method is called with smaller size than the current value of mMaxSqlCacheSize, - * then IllegalStateException is thrown - * - * synchronized because we don't want t threads to change cache size at the same time. - * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) - * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or - * < the value set with previous setMaxSqlCacheSize() call. - * - * @hide + * @param size the value the connection handle pool size should be set to. */ - public synchronized void setMaxSqlCacheSize(int cacheSize) { - if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { - throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); - } else if (cacheSize < mMaxSqlCacheSize) { - throw new IllegalStateException("cannot set cacheSize to a value less than the value " + - "set with previous setMaxSqlCacheSize() call."); + public void setConnectionPoolSize(int size) { + synchronized(this) { + if (mConnectionPool == null) { + throw new IllegalStateException("connection pool not enabled"); + } + int i = mConnectionPool.getMaxPoolSize(); + if (size < i) { + throw new IllegalArgumentException( + "cannot set max pool size to a value less than the current max value(=" + + i + ")"); + } + mConnectionPool.setMaxPoolSize(size); } - mMaxSqlCacheSize = cacheSize; } - static class ActiveDatabases { - private static final ActiveDatabases activeDatabases = new ActiveDatabases(); - private HashSet> mActiveDatabases = - new HashSet>(); - private ActiveDatabases() {} // disable instantiation of this class - static ActiveDatabases getInstance() {return activeDatabases;} + /* package */ SQLiteDatabase createPoolConnection(short connectionNum) { + SQLiteDatabase db = openDatabase(mPath, mFactory, mFlags, mErrorHandler, connectionNum); + db.mParentConnObj = this; + return db; + } + + private synchronized SQLiteDatabase getParentDbConnObj() { + return mParentConnObj; + } + + private boolean isPooledConnection() { + return this.mConnectionNum > 0; + } + + /* package */ SQLiteDatabase getDbConnection(String sql) { + verifyDbIsOpen(); + // this method should always be called with main database connection handle. + // the only time when it is called with pooled database connection handle is + // corruption occurs while trying to open a pooled database connection handle. + // in that case, simply return 'this' handle + if (isPooledConnection()) { + return this; + } + + // use the current connection handle if + // 1. if the caller is part of the ongoing transaction, if any + // 2. OR, if there is NO connection handle pool setup + if (amIInTransaction() || mConnectionPool == null) { + return this; + } else { + // get a connection handle from the pool + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert mConnectionPool != null; + Log.i(TAG, mConnectionPool.toString()); + } + return mConnectionPool.get(sql); + } + } + + private void releaseDbConnection(SQLiteDatabase db) { + // ignore this release call if + // 1. the database is closed + // 2. OR, if db is NOT a pooled connection handle + // 3. OR, if the database being released is same as 'this' (this condition means + // that we should always be releasing a pooled connection handle by calling this method + // from the 'main' connection handle + if (!isOpen() || !db.isPooledConnection() || (db == this)) { + return; + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert isPooledConnection(); + assert mConnectionPool != null; + Log.d(TAG, "releaseDbConnection threadid = " + Thread.currentThread().getId() + + ", releasing # " + db.mConnectionNum + ", " + getPath()); + } + mConnectionPool.release(db); } /** * this method is used to collect data about ALL open databases in the current process. - * bugreport is a user of this data. + * bugreport is a user of this data. */ /* package */ static ArrayList getDbStats() { ArrayList dbStatsList = new ArrayList(); - for (WeakReference w : ActiveDatabases.getInstance().mActiveDatabases) { + // make a local copy of mActiveDatabases - so that this method is not competing + // for synchronization lock on mActiveDatabases + ArrayList> tempList; + synchronized(mActiveDatabases) { + tempList = (ArrayList>)mActiveDatabases.clone(); + } + for (WeakReference w : tempList) { SQLiteDatabase db = w.get(); if (db == null || !db.isOpen()) { continue; } - // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db - int lookasideUsed = db.native_getDbLookaside(); - // get the lastnode of the dbname - String path = db.getPath(); - int indx = path.lastIndexOf("/"); - String lastnode = path.substring((indx != -1) ? ++indx : 0); - - // get list of attached dbs and for each db, get its size and pagesize - ArrayList> attachedDbs = getAttachedDbs(db); - if (attachedDbs == null) { - continue; - } - for (int i = 0; i < attachedDbs.size(); i++) { - Pair p = attachedDbs.get(i); - long pageCount = getPragmaVal(db, p.first + ".page_count;"); - - // first entry in the attached db list is always the main database - // don't worry about prefixing the dbname with "main" - String dbName; - if (i == 0) { - dbName = lastnode; - } else { - // lookaside is only relevant for the main db - lookasideUsed = 0; - dbName = " (attached) " + p.first; - // if the attached db has a path, attach the lastnode from the path to above - if (p.second.trim().length() > 0) { - int idx = p.second.lastIndexOf("/"); - dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); + synchronized (db) { + try { + // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db + int lookasideUsed = db.native_getDbLookaside(); + + // get the lastnode of the dbname + String path = db.getPath(); + int indx = path.lastIndexOf("/"); + String lastnode = path.substring((indx != -1) ? ++indx : 0); + + // get list of attached dbs and for each db, get its size and pagesize + ArrayList> attachedDbs = db.getAttachedDbs(); + if (attachedDbs == null) { + continue; } - } - if (pageCount > 0) { - dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), - lookasideUsed)); + for (int i = 0; i < attachedDbs.size(); i++) { + Pair p = attachedDbs.get(i); + long pageCount = DatabaseUtils.longForQuery(db, "PRAGMA " + p.first + + ".page_count;", null); + + // first entry in the attached db list is always the main database + // don't worry about prefixing the dbname with "main" + String dbName; + if (i == 0) { + dbName = lastnode; + } else { + // lookaside is only relevant for the main db + lookasideUsed = 0; + dbName = " (attached) " + p.first; + // if the attached db has a path, attach the lastnode from the path to above + if (p.second.trim().length() > 0) { + int idx = p.second.lastIndexOf("/"); + dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); + } + } + if (pageCount > 0) { + dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), + lookasideUsed, db.getCacheHitNum(), db.getCacheMissNum(), + db.getCachesize())); + } + } + // if there are pooled connections, return the cache stats for them also. + if (db.mConnectionPool != null) { + for (SQLiteDatabase pDb : db.mConnectionPool.getConnectionList()) { + dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") " + + lastnode, 0, 0, 0, pDb.getCacheHitNum(), + pDb.getCacheMissNum(), pDb.getCachesize())); + } + } + } catch (SQLiteException e) { + // ignore. we don't care about exceptions when we are taking adb + // bugreport! } } } @@ -2194,41 +2499,79 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * get the specified pragma value from sqlite for the specified database. - * only handles pragma's that return int/long. - * NO JAVA locks are held in this method. - * TODO: use this to do all pragma's in this class + * Returns list of full pathnames of all attached databases including the main database + * by executing 'pragma database_list' on the database. + * + * @return ArrayList of pairs of (database name, database file path) or null if the database + * is not open. */ - private static long getPragmaVal(SQLiteDatabase db, String pragma) { - if (!db.isOpen()) { - return 0; + public ArrayList> getAttachedDbs() { + if (!isOpen()) { + return null; } - SQLiteStatement prog = null; + ArrayList> attachedDbs = new ArrayList>(); + Cursor c = null; try { - prog = new SQLiteStatement(db, "PRAGMA " + pragma); - long val = prog.simpleQueryForLong(); - return val; + c = rawQuery("pragma database_list;", null); + while (c.moveToNext()) { + // sqlite returns a row for each database in the returned list of databases. + // in each row, + // 1st column is the database name such as main, or the database + // name specified on the "ATTACH" command + // 2nd column is the database file path. + attachedDbs.add(new Pair(c.getString(1), c.getString(2))); + } } finally { - if (prog != null) prog.close(); + if (c != null) { + c.close(); + } } + return attachedDbs; } /** - * returns list of full pathnames of all attached databases - * including the main database - * TODO: move this to {@link DatabaseUtils} + * Runs 'pragma integrity_check' on the given database (and all the attached databases) + * and returns true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + *

    + * If the result is false, then this method logs the errors reported by the integrity_check + * command execution. + *

    + * Note that 'pragma integrity_check' on a database can take a long time. + * + * @return true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. */ - private static ArrayList> getAttachedDbs(SQLiteDatabase dbObj) { - if (!dbObj.isOpen()) { - return null; - } - ArrayList> attachedDbs = new ArrayList>(); - Cursor c = dbObj.rawQuery("pragma database_list;", null); - while (c.moveToNext()) { - attachedDbs.add(new Pair(c.getString(1), c.getString(2))); + public boolean isDatabaseIntegrityOk() { + verifyDbIsOpen(); + ArrayList> attachedDbs = null; + try { + attachedDbs = getAttachedDbs(); + if (attachedDbs == null) { + throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " + + "be retrieved. probably because the database is closed"); + } + } catch (SQLiteException e) { + // can't get attachedDb list. do integrity check on the main database + attachedDbs = new ArrayList>(); + attachedDbs.add(new Pair("main", this.mPath)); + } + for (int i = 0; i < attachedDbs.size(); i++) { + Pair p = attachedDbs.get(i); + SQLiteStatement prog = null; + try { + prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);"); + String rslt = prog.simpleQueryForString(); + if (!rslt.equalsIgnoreCase("ok")) { + // integrity_checker failed on main or attached databases + Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt); + return false; + } + } finally { + if (prog != null) prog.close(); + } } - c.close(); - return attachedDbs; + return true; } /** @@ -2239,51 +2582,34 @@ public class SQLiteDatabase extends SQLiteClosable { private native void dbopen(String path, int flags); /** - * Native call to setup tracing of all sql statements + * Native call to setup tracing of all SQL statements * * @param path the full path to the database + * @param connectionNum connection number: 0 - N, where the main database + * connection handle is numbered 0 and the connection handles in the connection + * pool are numbered 1..N. */ - private native void enableSqlTracing(String path); + private native void enableSqlTracing(String path, short connectionNum); /** - * Native call to setup profiling of all sql statements. + * Native call to setup profiling of all SQL statements. * currently, sqlite's profiling = printing of execution-time - * (wall-clock time) of each of the sql statements, as they + * (wall-clock time) of each of the SQL statements, as they * are executed. * * @param path the full path to the database + * @param connectionNum connection number: 0 - N, where the main database + * connection handle is numbered 0 and the connection handles in the connection + * pool are numbered 1..N. */ - private native void enableSqlProfiling(String path); - - /** - * Native call to execute a raw SQL statement. {@link #lock} must be held - * when calling this method. - * - * @param sql The raw SQL string - * @throws SQLException - */ - /* package */ native void native_execSQL(String sql) throws SQLException; + private native void enableSqlProfiling(String path, short connectionNum); /** * Native call to set the locale. {@link #lock} must be held when calling * this method. * @throws SQLException */ - /* package */ native void native_setLocale(String loc, int flags); - - /** - * Returns the row ID of the last row inserted into the database. - * - * @return the row ID of the last row inserted into the database. - */ - /* package */ native long lastInsertRow(); - - /** - * Returns the number of changes made in the last statement executed. - * - * @return the number of changes made in the last statement executed. - */ - /* package */ native int lastChangeCount(); + private native void native_setLocale(String loc, int flags); /** * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here @@ -2291,4 +2617,11 @@ public class SQLiteDatabase extends SQLiteClosable { * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED */ private native int native_getDbLookaside(); + + /** + * finalizes the given statement id. + * + * @param statementId statement to be finzlied by sqlite + */ + private final native void native_finalize(int statementId); } diff --git a/core/java/android/database/sqlite/SQLiteDatabaseLockedException.java b/core/java/android/database/sqlite/SQLiteDatabaseLockedException.java new file mode 100644 index 0000000000000000000000000000000000000000..f0e2d811ce8d753fe8085fb74fbb8708c1f86622 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteDatabaseLockedException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +/** + * Thrown if the database engine was unable to acquire the + * database locks it needs to do its job. If the statement is a [COMMIT] + * or occurs outside of an explicit transaction, then you can retry the + * statement. If the statement is not a [COMMIT] and occurs within a + * explicit transaction then you should rollback the transaction before + * continuing. + */ +public class SQLiteDatabaseLockedException extends SQLiteException { + public SQLiteDatabaseLockedException() {} + + public SQLiteDatabaseLockedException(String error) { + super(error); + } +} diff --git a/core/java/android/database/sqlite/SQLiteDatatypeMismatchException.java b/core/java/android/database/sqlite/SQLiteDatatypeMismatchException.java new file mode 100644 index 0000000000000000000000000000000000000000..7f8253530090da4e359ce63f58095d79cc66b12a --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteDatatypeMismatchException.java @@ -0,0 +1,25 @@ +/* + * 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.database.sqlite; + +public class SQLiteDatatypeMismatchException extends SQLiteException { + public SQLiteDatatypeMismatchException() {} + + public SQLiteDatatypeMismatchException(String error) { + super(error); + } +} diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index b2a166b06fdf38d70d5d5a0c2ee2628a8307e1c3..9496079178e617bd4f7708375e5c003b30d618fc 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -132,11 +132,16 @@ public final class SQLiteDebug { /** documented here http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html */ public int lookaside; - public DbStats(String dbName, long pageCount, long pageSize, int lookaside) { + /** statement cache stats: hits/misses/cachesize */ + public String cache; + + public DbStats(String dbName, long pageCount, long pageSize, int lookaside, + int hits, int misses, int cachesize) { this.dbName = dbName; this.pageSize = pageSize / 1024; dbSize = (pageCount * pageSize) / 1024; this.lookaside = lookaside; + this.cache = hits + "/" + misses + "/" + cachesize; } } diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java index 2144fc3f756adff285aedc3aa21e20eccbd85eca..de2fca97d7d3d49860d9fc1d40cde45ab6bb1ed8 100644 --- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -39,18 +39,16 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { public Cursor query(CursorFactory factory, String[] selectionArgs) { // Compile the query - SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); + SQLiteQuery query = null; try { - // Arg binding - int numArgs = selectionArgs == null ? 0 : selectionArgs.length; - for (int i = 0; i < numArgs; i++) { - query.bindString(i + 1, selectionArgs[i]); - } + mDatabase.lock(); + mDatabase.closePendingStatements(); + query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); // Create the cursor if (factory == null) { - mCursor = new SQLiteCursor(mDatabase, this, mEditTable, query); + mCursor = new SQLiteCursor(this, mEditTable, query); } else { mCursor = factory.newCursor(mDatabase, this, mEditTable, query); } @@ -61,6 +59,7 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { } finally { // Make sure this object is cleaned up if something happens if (query != null) query.close(); + mDatabase.unlock(); } } @@ -69,10 +68,7 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { } public void setBindArguments(String[] bindArgs) { - final int numArgs = bindArgs.length; - for (int i = 0; i < numArgs; i++) { - mQuery.bindString(i + 1, bindArgs[i]); - } + mQuery.bindAllArgsAsStrings(bindArgs); } public void cursorDeactivated() { diff --git a/core/java/android/database/sqlite/SQLiteMisuseException.java b/core/java/android/database/sqlite/SQLiteMisuseException.java index 685f3ea394fe42d55776ff1b23d846e68b912335..546ec08cc76ac160594ce9c69b62fe87f07f4923 100644 --- a/core/java/android/database/sqlite/SQLiteMisuseException.java +++ b/core/java/android/database/sqlite/SQLiteMisuseException.java @@ -16,6 +16,18 @@ package android.database.sqlite; +/** + * This error can occur if the application creates a SQLiteStatement object and allows multiple + * threads in the application use it at the same time. + * Sqlite returns this error if bind and execute methods on this object occur at the same time + * from multiple threads, like so: + * thread # 1: in execute() method of the SQLiteStatement object + * while thread # 2: is in bind..() on the same object. + *

    + * FIX this by NEVER sharing the same SQLiteStatement object between threads. + * Create a local instance of the SQLiteStatement whenever it is needed, use it and close it ASAP. + * NEVER make it globally available. + */ public class SQLiteMisuseException extends SQLiteException { public SQLiteMisuseException() {} diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index b4615eb6b26140e4438d99607643c436a5da7008..ccf8d686de81bbb6cf54831a7f21cf04f3a19400 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -17,6 +17,8 @@ package android.database.sqlite; import android.content.Context; +import android.database.DatabaseErrorHandler; +import android.database.DefaultDatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.util.Log; @@ -51,6 +53,7 @@ public abstract class SQLiteOpenHelper { private SQLiteDatabase mDatabase = null; private boolean mIsInitializing = false; + private final DatabaseErrorHandler mErrorHandler; /** * Create a helper object to create, open, and/or manage a database. @@ -65,12 +68,37 @@ public abstract class SQLiteOpenHelper { * {@link #onUpgrade} will be used to upgrade the database */ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) { + this(context, name, factory, version, new DefaultDatabaseErrorHandler()); + } + + /** + * Create a helper object to create, open, and/or manage a database. + * The database is not actually created or opened until one of + * {@link #getWritableDatabase} or {@link #getReadableDatabase} is called. + * + *

    Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.

    + * + * @param context to use to open or create the database + * @param name of the database file, or null for an in-memory database + * @param factory to use for creating cursor objects, or null for the default + * @param version number of the database (starting at 1); if the database is older, + * {@link #onUpgrade} will be used to upgrade the database + * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption. + */ + public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, + DatabaseErrorHandler errorHandler) { if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version); + if (errorHandler == null) { + throw new IllegalArgumentException("DatabaseErrorHandler param value can't be null."); + } mContext = context; mName = name; mFactory = factory; mNewVersion = version; + mErrorHandler = errorHandler; } /** @@ -93,8 +121,13 @@ public abstract class SQLiteOpenHelper { * @return a read/write database object valid until {@link #close} is called */ public synchronized SQLiteDatabase getWritableDatabase() { - if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) { - return mDatabase; // The database is already open for business + if (mDatabase != null) { + if (!mDatabase.isOpen()) { + // darn! the user closed the database by calling mDatabase.close() + mDatabase = null; + } else if (!mDatabase.isReadOnly()) { + return mDatabase; // The database is already open for business + } } if (mIsInitializing) { @@ -115,10 +148,14 @@ public abstract class SQLiteOpenHelper { if (mName == null) { db = SQLiteDatabase.create(null); } else { - db = mContext.openOrCreateDatabase(mName, 0, mFactory); + db = mContext.openOrCreateDatabase(mName, 0, mFactory, mErrorHandler); } int version = db.getVersion(); + if (version > mNewVersion) { + throw new IllegalStateException("Database " + mName + + " cannot be downgraded. instead, please uninstall new version first."); + } if (version != mNewVersion) { db.beginTransaction(); try { @@ -175,8 +212,13 @@ public abstract class SQLiteOpenHelper { * or {@link #close} is called. */ public synchronized SQLiteDatabase getReadableDatabase() { - if (mDatabase != null && mDatabase.isOpen()) { - return mDatabase; // The database is already open for business + if (mDatabase != null) { + if (!mDatabase.isOpen()) { + // darn! the user closed the database by calling mDatabase.close() + mDatabase = null; + } else { + return mDatabase; // The database is already open for business + } } if (mIsInitializing) { @@ -194,7 +236,8 @@ public abstract class SQLiteOpenHelper { try { mIsInitializing = true; String path = mContext.getDatabasePath(mName).getPath(); - db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY); + db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY, + mErrorHandler); if (db.getVersion() != mNewVersion) { throw new SQLiteException("Can't upgrade read-only database from version " + db.getVersion() + " to " + mNewVersion + ": " + path); diff --git a/core/java/android/database/sqlite/SQLiteOutOfMemoryException.java b/core/java/android/database/sqlite/SQLiteOutOfMemoryException.java new file mode 100644 index 0000000000000000000000000000000000000000..98ce8b5fc8b8da1ee5dc3c25de127add7c011c2b --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteOutOfMemoryException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +public class SQLiteOutOfMemoryException extends SQLiteException { + public SQLiteOutOfMemoryException() {} + + public SQLiteOutOfMemoryException(String error) { + super(error); + } +} diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 4d96f12c794691cbaf02c845f4344858a274a069..4747a9e9fd1a17c9b54ca85878a14d1b471c59e0 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -16,11 +16,15 @@ package android.database.sqlite; +import android.database.DatabaseUtils; +import android.database.Cursor; import android.util.Log; +import java.util.HashMap; + /** * A base class for compiled SQLite programs. - * + *

    * SQLiteProgram is not internally synchronized so code using a SQLiteProgram from multiple * threads should perform its own synchronization when using the SQLiteProgram. */ @@ -43,12 +47,12 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @deprecated do not use this */ @Deprecated - protected int nHandle = 0; + protected int nHandle; /** * the SQLiteCompiledSql object for the given sql statement. */ - private SQLiteCompiledSql mCompiledSql; + /* package */ SQLiteCompiledSql mCompiledSql; /** * SQLiteCompiledSql statement id is populated with the corresponding object from the above @@ -56,41 +60,97 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @deprecated do not use this */ @Deprecated - protected int nStatement = 0; + protected int nStatement; + + /** + * In the case of {@link SQLiteStatement}, this member stores the bindargs passed + * to the following methods, instead of actually doing the binding. + *

      + *
    • {@link #bindBlob(int, byte[])}
    • + *
    • {@link #bindDouble(int, double)}
    • + *
    • {@link #bindLong(int, long)}
    • + *
    • {@link #bindNull(int)}
    • + *
    • {@link #bindString(int, String)}
    • + *
    + *

    + * Each entry in the array is a Pair of + *

      + *
    1. bind arg position number
    2. + *
    3. the value to be bound to the bindarg
    4. + *
    + *

    + * It is lazily initialized in the above bind methods + * and it is cleared in {@link #clearBindings()} method. + *

    + * It is protected (in multi-threaded environment) by {@link SQLiteProgram}.this + */ + /* package */ HashMap mBindArgs = null; + /* package */ final int mStatementType; + /* package */ static final int STATEMENT_CACHEABLE = 16; + /* package */ static final int STATEMENT_DONT_PREPARE = 32; + /* package */ static final int STATEMENT_USE_POOLED_CONN = 64; + /* package */ static final int STATEMENT_TYPE_MASK = 0x0f; /* package */ SQLiteProgram(SQLiteDatabase db, String sql) { - mDatabase = db; + this(db, sql, null, true); + } + + /* package */ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs, + boolean compileFlag) { mSql = sql.trim(); + int n = DatabaseUtils.getSqlStatementType(mSql); + switch (n) { + case DatabaseUtils.STATEMENT_UPDATE: + mStatementType = n | STATEMENT_CACHEABLE; + break; + case DatabaseUtils.STATEMENT_SELECT: + mStatementType = n | STATEMENT_CACHEABLE | STATEMENT_USE_POOLED_CONN; + break; + case DatabaseUtils.STATEMENT_ATTACH: + case DatabaseUtils.STATEMENT_BEGIN: + case DatabaseUtils.STATEMENT_COMMIT: + case DatabaseUtils.STATEMENT_ABORT: + case DatabaseUtils.STATEMENT_DDL: + case DatabaseUtils.STATEMENT_UNPREPARED: + mStatementType = n | STATEMENT_DONT_PREPARE; + break; + default: + mStatementType = n; + } db.acquireReference(); db.addSQLiteClosable(this); - this.nHandle = db.mNativeHandle; + mDatabase = db; + nHandle = db.mNativeHandle; + if (bindArgs != null) { + int size = bindArgs.length; + for (int i = 0; i < size; i++) { + this.addToBindArgs(i + 1, bindArgs[i]); + } + } + if (compileFlag) { + compileAndbindAllArgs(); + } + } + private void compileSql() { // only cache CRUD statements - String prefixSql = mSql.substring(0, 6); - if (!prefixSql.equalsIgnoreCase("INSERT") && !prefixSql.equalsIgnoreCase("UPDATE") && - !prefixSql.equalsIgnoreCase("REPLAC") && - !prefixSql.equalsIgnoreCase("DELETE") && !prefixSql.equalsIgnoreCase("SELECT")) { - mCompiledSql = new SQLiteCompiledSql(db, sql); + if ((mStatementType & STATEMENT_CACHEABLE) == 0) { + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); nStatement = mCompiledSql.nStatement; // since it is not in the cache, no need to acquire() it. return; } - // it is not pragma - mCompiledSql = db.getCompiledStatementForSql(sql); + mCompiledSql = mDatabase.getCompiledStatementForSql(mSql); if (mCompiledSql == null) { // create a new compiled-sql obj - mCompiledSql = new SQLiteCompiledSql(db, sql); + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); // add it to the cache of compiled-sqls // but before adding it and thus making it available for anyone else to use it, // make sure it is acquired by me. mCompiledSql.acquire(); - db.addToCompiledQueries(sql, mCompiledSql); - if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.v(TAG, "Created DbObj (id#" + mCompiledSql.nStatement + - ") for sql: " + sql); - } + mDatabase.addToCompiledQueries(mSql, mCompiledSql); } else { // it is already in compiled-sql cache. // try to acquire the object. @@ -100,13 +160,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { // we can't have two different SQLiteProgam objects can't share the same // CompiledSql object. create a new one. // finalize it when I am done with it in "this" object. - mCompiledSql = new SQLiteCompiledSql(db, sql); - if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.v(TAG, "** possible bug ** Created NEW DbObj (id#" + - mCompiledSql.nStatement + - ") because the previously created DbObj (id#" + last + - ") was not released for sql:" + sql); - } + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); // since it is not in the cache, no need to acquire() it. } } @@ -115,42 +169,59 @@ public abstract class SQLiteProgram extends SQLiteClosable { @Override protected void onAllReferencesReleased() { - releaseCompiledSqlIfNotInCache(); - mDatabase.releaseReference(); + release(); mDatabase.removeSQLiteClosable(this); + mDatabase.releaseReference(); } @Override protected void onAllReferencesReleasedFromContainer() { - releaseCompiledSqlIfNotInCache(); + release(); mDatabase.releaseReference(); } - private void releaseCompiledSqlIfNotInCache() { + /* package */ synchronized void release() { if (mCompiledSql == null) { return; } - synchronized(mDatabase.mCompiledQueries) { - if (!mDatabase.mCompiledQueries.containsValue(mCompiledSql)) { - // it is NOT in compiled-sql cache. i.e., responsibility of - // releasing this statement is on me. - mCompiledSql.releaseSqlStatement(); - mCompiledSql = null; - nStatement = 0; - } else { - // it is in compiled-sql cache. reset its CompiledSql#mInUse flag - mCompiledSql.release(); + if ((mStatementType & STATEMENT_CACHEABLE) == 0) { + // this SQL statement was never in cache + mCompiledSql.releaseSqlStatement(); + } else { + synchronized(mDatabase.mCompiledQueries) { + if (!mDatabase.mCompiledQueries.containsValue(mCompiledSql)) { + // it is NOT in compiled-sql cache. i.e., responsibility of + // releasing this statement is on me. + mCompiledSql.releaseSqlStatement(); + } else { + // it is in compiled-sql cache. reset its CompiledSql#mInUse flag + mCompiledSql.release(); + } } - } + } + mCompiledSql = null; + nStatement = 0; } /** * Returns a unique identifier for this program. * * @return a unique identifier for this program + * @deprecated do not use this method. it is not guaranteed to be the same across executions of + * the SQL statement contained in this object. */ + @Deprecated public final int getUniqueId() { - return nStatement; + return -1; + } + + /** + * used only for testing purposes + */ + /* package */ int getSqlStatementId() { + synchronized(this) { + return (mCompiledSql == null) ? 0 : nStatement; + } } /* package */ String getSqlString() { @@ -169,6 +240,39 @@ public abstract class SQLiteProgram extends SQLiteClosable { // TODO is there a need for this? } + private void bind(int type, int index, Object value) { + synchronized (this) { + mDatabase.verifyDbIsOpen(); + addToBindArgs(index, (type == Cursor.FIELD_TYPE_NULL) ? null : value); + if (nStatement > 0) { + // bind only if the SQL statement is compiled + acquireReference(); + try { + switch (type) { + case Cursor.FIELD_TYPE_NULL: + native_bind_null(index); + break; + case Cursor.FIELD_TYPE_BLOB: + native_bind_blob(index, (byte[]) value); + break; + case Cursor.FIELD_TYPE_FLOAT: + native_bind_double(index, (Double) value); + break; + case Cursor.FIELD_TYPE_INTEGER: + native_bind_long(index, (Long) value); + break; + case Cursor.FIELD_TYPE_STRING: + default: + native_bind_string(index, (String) value); + break; + } + } finally { + releaseReference(); + } + } + } + } + /** * Bind a NULL value to this statement. The value remains bound until * {@link #clearBindings} is called. @@ -176,34 +280,18 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param index The 1-based index to the parameter to bind null to */ public void bindNull(int index) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_null(index); - } finally { - releaseReference(); - } + bind(Cursor.FIELD_TYPE_NULL, index, null); } /** * Bind a long value to this statement. The value remains bound until * {@link #clearBindings} is called. - * + *addToBindArgs * @param index The 1-based index to the parameter to bind * @param value The value to bind */ public void bindLong(int index, long value) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_long(index, value); - } finally { - releaseReference(); - } + bind(Cursor.FIELD_TYPE_INTEGER, index, value); } /** @@ -214,15 +302,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindDouble(int index, double value) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_double(index, value); - } finally { - releaseReference(); - } + bind(Cursor.FIELD_TYPE_FLOAT, index, value); } /** @@ -236,15 +316,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_string(index, value); - } finally { - releaseReference(); - } + bind(Cursor.FIELD_TYPE_STRING, index, value); } /** @@ -258,29 +330,25 @@ public abstract class SQLiteProgram extends SQLiteClosable { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_blob(index, value); - } finally { - releaseReference(); - } + bind(Cursor.FIELD_TYPE_BLOB, index, value); } /** * Clears all existing bindings. Unset bindings are treated as NULL. */ public void clearBindings() { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_clear_bindings(); - } finally { - releaseReference(); + synchronized (this) { + mBindArgs = null; + if (this.nStatement == 0) { + return; + } + mDatabase.verifyDbIsOpen(); + acquireReference(); + try { + native_clear_bindings(); + } finally { + releaseReference(); + } } } @@ -288,17 +356,85 @@ public abstract class SQLiteProgram extends SQLiteClosable { * Release this program's resources, making it invalid. */ public void close() { - if (!mDatabase.isOpen()) { + synchronized (this) { + mBindArgs = null; + if (nHandle == 0 || !mDatabase.isOpen()) { + return; + } + releaseReference(); + } + } + + private synchronized void addToBindArgs(int index, Object value) { + if (mBindArgs == null) { + mBindArgs = new HashMap(); + } + mBindArgs.put(index, value); + } + + /* package */ synchronized void compileAndbindAllArgs() { + if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { + // no need to prepare this SQL statement + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + if (mBindArgs != null) { + throw new IllegalArgumentException("no need to pass bindargs for this sql :" + + mSql); + } + } return; } - mDatabase.lock(); - try { - releaseReference(); - } finally { - mDatabase.unlock(); + if (nStatement == 0) { + // SQL statement is not compiled yet. compile it now. + compileSql(); + } + if (mBindArgs == null) { + return; + } + for (int index : mBindArgs.keySet()) { + Object value = mBindArgs.get(index); + if (value == null) { + native_bind_null(index); + } else if (value instanceof Double || value instanceof Float) { + native_bind_double(index, ((Number) value).doubleValue()); + } else if (value instanceof Number) { + native_bind_long(index, ((Number) value).longValue()); + } else if (value instanceof Boolean) { + Boolean bool = (Boolean)value; + native_bind_long(index, (bool) ? 1 : 0); + if (bool) { + native_bind_long(index, 1); + } else { + native_bind_long(index, 0); + } + } else if (value instanceof byte[]){ + native_bind_blob(index, (byte[]) value); + } else { + native_bind_string(index, value.toString()); + } + } + } + + /** + * Given an array of String bindArgs, this method binds all of them in one single call. + * + * @param bindArgs the String array of bind args. + */ + public void bindAllArgsAsStrings(String[] bindArgs) { + if (bindArgs == null) { + return; + } + int size = bindArgs.length; + synchronized(this) { + for (int i = 0; i < size; i++) { + bindString(i + 1, bindArgs[i]); + } } } + /* package */ synchronized final void setNativeHandle(int nHandle) { + this.nHandle = nHandle; + } + /** * @deprecated This method is deprecated and must not be used. * Compiles SQL into a SQLite program. diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 905b66b78cd6e573651a626b81b46c2a46c52b49..e9e01723eedb4c6d4930702ecbf7f67e58c6d634 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -28,28 +28,38 @@ import android.util.Log; * threads should perform its own synchronization when using the SQLiteQuery. */ public class SQLiteQuery extends SQLiteProgram { - private static final String TAG = "Cursor"; + private static final String TAG = "SQLiteQuery"; /** The index of the unbound OFFSET parameter */ - private int mOffsetIndex; - - /** Args to bind on requery */ - private String[] mBindArgs; + private int mOffsetIndex = 0; private boolean mClosed = false; /** * Create a persistent query object. - * + * * @param db The database that this query object is associated with * @param query The SQL string for this query. * @param offsetIndex The 1-based index to the OFFSET parameter, */ /* package */ SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex, String[] bindArgs) { super(db, query); - mOffsetIndex = offsetIndex; - mBindArgs = bindArgs; + bindAllArgsAsStrings(bindArgs); + } + + /** + * Constructor used to create new instance to replace a given instance of this class. + * This constructor is used when the current Query object is now associated with a different + * {@link SQLiteDatabase} object. + * + * @param db The database that this query object is associated with + * @param query the instance of {@link SQLiteQuery} to be replaced + */ + /* package */ SQLiteQuery(SQLiteDatabase db, SQLiteQuery query) { + super(db, query.mSql); + this.mBindArgs = query.mBindArgs; + this.mOffsetIndex = query.mOffsetIndex; } /** @@ -70,13 +80,8 @@ public class SQLiteQuery extends SQLiteProgram { // if the start pos is not equal to 0, then most likely window is // too small for the data set, loading by another thread // is not safe in this situation. the native code will ignore maxRead - int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex, - maxRead, lastPos); - - // Logging - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.d(TAG, "fillWindow(): " + mSql); - } + int numRows = native_fill_window(window, window.getStartPosition(), + mOffsetIndex, maxRead, lastPos); mDatabase.logTimeStat(mSql, timeStart); return numRows; } catch (IllegalStateException e){ @@ -85,6 +90,9 @@ public class SQLiteQuery extends SQLiteProgram { } catch (SQLiteDatabaseCorruptException e) { mDatabase.onCorruption(); throw e; + } catch (SQLiteException e) { + Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql); + throw e; } finally { window.releaseReference(); } @@ -141,51 +149,13 @@ public class SQLiteQuery extends SQLiteProgram { * Called by SQLiteCursor when it is requeried. */ /* package */ void requery() { - if (mBindArgs != null) { - int len = mBindArgs.length; - try { - for (int i = 0; i < len; i++) { - super.bindString(i + 1, mBindArgs[i]); - } - } catch (SQLiteMisuseException e) { - StringBuilder errMsg = new StringBuilder("mSql " + mSql); - for (int i = 0; i < len; i++) { - errMsg.append(" "); - errMsg.append(mBindArgs[i]); - } - errMsg.append(" "); - IllegalStateException leakProgram = new IllegalStateException( - errMsg.toString(), e); - throw leakProgram; - } + if (mClosed) { + throw new IllegalStateException("requerying a closed cursor"); } + compileAndbindAllArgs(); } - @Override - public void bindNull(int index) { - mBindArgs[index - 1] = null; - if (!mClosed) super.bindNull(index); - } - - @Override - public void bindLong(int index, long value) { - mBindArgs[index - 1] = Long.toString(value); - if (!mClosed) super.bindLong(index, value); - } - - @Override - public void bindDouble(int index, double value) { - mBindArgs[index - 1] = Double.toString(value); - if (!mClosed) super.bindDouble(index, value); - } - - @Override - public void bindString(int index, String value) { - mBindArgs[index - 1] = value; - if (!mClosed) super.bindString(index, value); - } - - private final native int native_fill_window(CursorWindow window, + private final native int native_fill_window(CursorWindow window, int startPos, int offsetParam, int maxRead, int lastPos); private final native int native_column_count(); diff --git a/core/java/android/database/sqlite/SQLiteReadOnlyDatabaseException.java b/core/java/android/database/sqlite/SQLiteReadOnlyDatabaseException.java new file mode 100644 index 0000000000000000000000000000000000000000..5b633c62bbcba9a5ab5d4a3b08061a22f4186b5b --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteReadOnlyDatabaseException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +public class SQLiteReadOnlyDatabaseException extends SQLiteException { + public SQLiteReadOnlyDatabaseException() {} + + public SQLiteReadOnlyDatabaseException(String error) { + super(error); + } +} diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index 9e425c34c05afccf7c549e72c8cfd04f27695cae..bd05e247cdab66b968141718901c3383001f1a1c 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -16,7 +16,12 @@ package android.database.sqlite; +import android.database.DatabaseUtils; +import android.os.ParcelFileDescriptor; import android.os.SystemClock; +import android.util.Log; + +import java.io.IOException; import dalvik.system.BlockGuard; @@ -25,44 +30,72 @@ import dalvik.system.BlockGuard; * The statement cannot return multiple rows, but 1x1 result sets are allowed. * Don't use SQLiteStatement constructor directly, please use * {@link SQLiteDatabase#compileStatement(String)} - * + *

    * SQLiteStatement is not internally synchronized so code using a SQLiteStatement from multiple * threads should perform its own synchronization when using the SQLiteStatement. */ +@SuppressWarnings("deprecation") public class SQLiteStatement extends SQLiteProgram { + private static final String TAG = "SQLiteStatement"; + + private static final boolean READ = true; + private static final boolean WRITE = false; + + private SQLiteDatabase mOrigDb; + private int mState; + /** possible value for {@link #mState}. indicates that a transaction is started. */ + private static final int TRANS_STARTED = 1; + /** possible value for {@link #mState}. indicates that a lock is acquired. */ + private static final int LOCK_ACQUIRED = 2; + /** * Don't use SQLiteStatement constructor directly, please use * {@link SQLiteDatabase#compileStatement(String)} * @param db * @param sql */ - /* package */ SQLiteStatement(SQLiteDatabase db, String sql) { - super(db, sql); + /* package */ SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) { + super(db, sql, bindArgs, false /* don't compile sql statement */); } /** - * Execute this SQL statement, if it is not a query. For example, - * CREATE TABLE, DELTE, INSERT, etc. + * Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example + * CREATE / DROP table, view, trigger, index etc. * * @throws android.database.SQLException If the SQL string is invalid for * some reason */ public void execute() { - BlockGuard.getThreadPolicy().onWriteToDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); + executeUpdateDelete(); + } - acquireReference(); - try { - native_execute(); - mDatabase.logTimeStat(mSql, timeStart); - } finally { - releaseReference(); - mDatabase.unlock(); + /** + * Execute this SQL statement, if the the number of rows affected by execution of this SQL + * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements. + * + * @return the number of rows affected by this SQL statement execution. + * @throws android.database.SQLException If the SQL string is invalid for + * some reason + */ + public int executeUpdateDelete() { + synchronized(this) { + try { + long timeStart = acquireAndLock(WRITE); + int numChanges = 0; + if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { + // since the statement doesn't have to be prepared, + // call the following native method which will not prepare + // the query plan + native_executeSql(mSql); + } else { + numChanges = native_execute(); + } + mDatabase.logTimeStat(mSql, timeStart); + return numChanges; + } finally { + releaseAndUnlock(); + } } } @@ -76,21 +109,15 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public long executeInsert() { - BlockGuard.getThreadPolicy().onWriteToDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); - - acquireReference(); - try { - native_execute(); - mDatabase.logTimeStat(mSql, timeStart); - return (mDatabase.lastChangeCount() > 0) ? mDatabase.lastInsertRow() : -1; - } finally { - releaseReference(); - mDatabase.unlock(); + synchronized(this) { + try { + long timeStart = acquireAndLock(WRITE); + long lastInsertedRowId = native_executeInsert(); + mDatabase.logTimeStat(mSql, timeStart); + return lastInsertedRowId; + } finally { + releaseAndUnlock(); + } } } @@ -103,21 +130,15 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public long simpleQueryForLong() { - BlockGuard.getThreadPolicy().onReadFromDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); - - acquireReference(); - try { - long retValue = native_1x1_long(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } finally { - releaseReference(); - mDatabase.unlock(); + synchronized(this) { + try { + long timeStart = acquireAndLock(READ); + long retValue = native_1x1_long(); + mDatabase.logTimeStat(mSql, timeStart); + return retValue; + } finally { + releaseAndUnlock(); + } } } @@ -130,25 +151,140 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public String simpleQueryForString() { - BlockGuard.getThreadPolicy().onReadFromDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); + synchronized(this) { + try { + long timeStart = acquireAndLock(READ); + String retValue = native_1x1_string(); + mDatabase.logTimeStat(mSql, timeStart); + return retValue; + } finally { + releaseAndUnlock(); + } + } + } + + /** + * Executes a statement that returns a 1 by 1 table with a blob value. + * + * @return A read-only file descriptor for a copy of the blob value, or {@code null} + * if the value is null or could not be read for some reason. + * + * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows + */ + public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() { + synchronized(this) { + try { + long timeStart = acquireAndLock(READ); + ParcelFileDescriptor retValue = native_1x1_blob_ashmem(); + mDatabase.logTimeStat(mSql, timeStart); + return retValue; + } catch (IOException ex) { + Log.e(TAG, "simpleQueryForBlobFileDescriptor() failed", ex); + return null; + } finally { + releaseAndUnlock(); + } + } + } + + /** + * Called before every method in this class before executing a SQL statement, + * this method does the following: + *

      + *
    • make sure the database is open
    • + *
    • get a database connection from the connection pool,if possible
    • + *
    • notifies {@link BlockGuard} of read/write
    • + *
    • if the SQL statement is an update, start transaction if not already in one. + * otherwise, get lock on the database
    • + *
    • acquire reference on this object
    • + *
    • and then return the current time _before_ the database lock was acquired
    • + *
    + *

    + * This method removes the duplicate code from the other public + * methods in this class. + */ + private long acquireAndLock(boolean rwFlag) { + mState = 0; + // use pooled database connection handles for SELECT SQL statements + mDatabase.verifyDbIsOpen(); + SQLiteDatabase db = ((mStatementType & SQLiteProgram.STATEMENT_USE_POOLED_CONN) > 0) + ? mDatabase.getDbConnection(mSql) : mDatabase; + // use the database connection obtained above + mOrigDb = mDatabase; + mDatabase = db; + setNativeHandle(mDatabase.mNativeHandle); + if (rwFlag == WRITE) { + BlockGuard.getThreadPolicy().onWriteToDisk(); + } else { + BlockGuard.getThreadPolicy().onReadFromDisk(); + } + + /* + * Special case handling of SQLiteDatabase.execSQL("BEGIN transaction"). + * we know it is execSQL("BEGIN transaction") from the caller IF there is no lock held. + * beginTransaction() methods in SQLiteDatabase call lockForced() before + * calling execSQL("BEGIN transaction"). + */ + if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_BEGIN) { + if (!mDatabase.isDbLockedByCurrentThread()) { + // transaction is NOT started by calling beginTransaction() methods in + // SQLiteDatabase + mDatabase.setTransactionUsingExecSqlFlag(); + } + } else if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == + DatabaseUtils.STATEMENT_UPDATE) { + // got update SQL statement. if there is NO pending transaction, start one + if (!mDatabase.inTransaction()) { + mDatabase.beginTransactionNonExclusive(); + mState = TRANS_STARTED; + } + } + // do I have database lock? if not, grab it. + if (!mDatabase.isDbLockedByCurrentThread()) { + mDatabase.lock(); + mState = LOCK_ACQUIRED; } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); acquireReference(); - try { - String retValue = native_1x1_string(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } finally { - releaseReference(); + long startTime = SystemClock.uptimeMillis(); + mDatabase.closePendingStatements(); + compileAndbindAllArgs(); + return startTime; + } + + /** + * this method releases locks and references acquired in {@link #acquireAndLock(boolean)} + */ + private void releaseAndUnlock() { + releaseReference(); + if (mState == TRANS_STARTED) { + try { + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + } else if (mState == LOCK_ACQUIRED) { mDatabase.unlock(); } + if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == + DatabaseUtils.STATEMENT_COMMIT || + (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == + DatabaseUtils.STATEMENT_ABORT) { + mDatabase.resetTransactionUsingExecSqlFlag(); + } + clearBindings(); + // release the compiled sql statement so that the caller's SQLiteStatement no longer + // has a hard reference to a database object that may get deallocated at any point. + release(); + // restore the database connection handle to the original value + mDatabase = mOrigDb; + setNativeHandle(mDatabase.mNativeHandle); } - private final native void native_execute(); + private final native int native_execute(); + private final native long native_executeInsert(); private final native long native_1x1_long(); private final native String native_1x1_string(); + private final native ParcelFileDescriptor native_1x1_blob_ashmem() throws IOException; + private final native void native_executeSql(String sql); } diff --git a/core/java/android/database/sqlite/SQLiteTableLockedException.java b/core/java/android/database/sqlite/SQLiteTableLockedException.java new file mode 100644 index 0000000000000000000000000000000000000000..8278df07043b1bace4dddcc67e9250742d661ae6 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteTableLockedException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +public class SQLiteTableLockedException extends SQLiteException { + public SQLiteTableLockedException() {} + + public SQLiteTableLockedException(String error) { + super(error); + } +} diff --git a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java b/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java similarity index 52% rename from core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java rename to core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java index 67db62ce86b5143b6d11d77168f7d5ec08d030c1..bcf95e2ef1fe249013335190db2c8b7feb96121c 100644 --- a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java +++ b/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +14,18 @@ * limitations under the License. */ -package android.pim.vcard.exception; +package android.database.sqlite; /** - * 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. + * Thrown if the database can't be closed because of some un-closed + * Cursor or SQLiteStatement objects. Could happen when a thread is trying to close + * the database while another thread still hasn't closed a Cursor on that database. + * @hide */ -public class VCardInvalidCommentLineException extends VCardInvalidLineException { - public VCardInvalidCommentLineException() { - super(); - } +public class SQLiteUnfinalizedObjectsException extends SQLiteException { + public SQLiteUnfinalizedObjectsException() {} - public VCardInvalidCommentLineException(final String message) { - super(message); + public SQLiteUnfinalizedObjectsException(String error) { + super(error); } } diff --git a/core/java/android/hardware/Usb.java b/core/java/android/hardware/Usb.java index 57271d4b72f875581fe6d73f1f6172f1e96a35c7..ebb8296e02e76fd0f57b664f7f36f762e089c01b 100644 --- a/core/java/android/hardware/Usb.java +++ b/core/java/android/hardware/Usb.java @@ -17,6 +17,10 @@ package android.hardware; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + /** * Class for accessing USB state information. * @hide @@ -55,6 +59,24 @@ public class Usb { public static final String ACTION_USB_STATE = "android.hardware.action.USB_STATE"; + /** + * Broadcast Action: A broadcast for USB camera attached event. + * + * This intent is sent when a USB device supporting PTP is attached to the host USB bus. + * The intent's data contains a Uri for the device in the MTP provider. + */ + public static final String ACTION_USB_CAMERA_ATTACHED = + "android.hardware.action.USB_CAMERA_ATTACHED"; + + /** + * Broadcast Action: A broadcast for USB camera detached event. + * + * This intent is sent when a USB device supporting PTP is detached from the host USB bus. + * The intent's data contains a Uri for the device in the MTP provider. + */ + public static final String ACTION_USB_CAMERA_DETACHED = + "android.hardware.action.USB_CAMERA_DETACHED"; + /** * Boolean extra indicating whether USB is connected or disconnected. * Used in extras for the {@link #ACTION_USB_STATE} broadcast. @@ -96,4 +118,30 @@ public class Usb { * Used in extras for the {@link #ACTION_USB_CONNECTED} broadcast */ public static final String USB_FUNCTION_DISABLED = "disabled"; + + private static File getFunctionEnableFile(String function) { + return new File("/sys/class/usb_composite/" + function + "/enable"); + } + + /** + * Returns true if the specified USB function is supported by the kernel. + * Note that a USB function maybe supported but disabled. + */ + public static boolean isFunctionSupported(String function) { + return getFunctionEnableFile(function).exists(); + } + + /** + * Returns true if the specified USB function is currently enabled. + */ + public static boolean isFunctionEnabled(String function) { + try { + FileInputStream stream = new FileInputStream(getFunctionEnableFile(function)); + boolean enabled = (stream.read() == '1'); + stream.close(); + return enabled; + } catch (IOException e) { + return false; + } + } } diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index 8a52e4025b720883098cdd96e7e2653e21cafadd..22968b09dfd54b42c2898e77cd8db19af4549a34 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -18,7 +18,6 @@ package android.inputmethodservice; import android.content.Context; import android.util.AttributeSet; -import android.view.ContextMenu; import android.view.inputmethod.ExtractedText; import android.widget.EditText; @@ -29,7 +28,6 @@ import android.widget.EditText; public class ExtractEditText extends EditText { private InputMethodService mIME; private int mSettingExtractedText; - private boolean mContextMenuShouldBeHandledBySuper = false; public ExtractEditText(Context context) { super(context, null); @@ -99,19 +97,12 @@ public class ExtractEditText extends EditText { return false; } - @Override - protected void onCreateContextMenu(ContextMenu menu) { - super.onCreateContextMenu(menu); - mContextMenuShouldBeHandledBySuper = true; - } - @Override public boolean onTextContextMenuItem(int id) { - if (mIME != null && !mContextMenuShouldBeHandledBySuper) { + if (mIME != null) { if (mIME.onExtractTextContextMenuItem(id)) { return true; } } - mContextMenuShouldBeHandledBySuper = false; return super.onTextContextMenuItem(id); } diff --git a/core/java/android/inputmethodservice/Keyboard.java b/core/java/android/inputmethodservice/Keyboard.java index 269e1c9d3e900021a6c609d250ebcd1d48b8b338..75c945b827cbdad0efda19d087f1e886c8475f12 100644 --- a/core/java/android/inputmethodservice/Keyboard.java +++ b/core/java/android/inputmethodservice/Keyboard.java @@ -504,7 +504,30 @@ public class Keyboard { public Keyboard(Context context, int xmlLayoutResId) { this(context, xmlLayoutResId, 0); } - + + /** + * Creates a keyboard from the given xml key layout file. Weeds out rows + * that have a keyboard mode defined but don't match the specified mode. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param modeId keyboard mode identifier + * @param width sets width of keyboard + * @param height sets height of keyboard + */ + public Keyboard(Context context, int xmlLayoutResId, int modeId, int width, int height) { + mDisplayWidth = width; + mDisplayHeight = height; + + mDefaultHorizontalGap = 0; + mDefaultWidth = mDisplayWidth / 10; + mDefaultVerticalGap = 0; + mDefaultHeight = mDefaultWidth; + mKeys = new ArrayList(); + mModifierKeys = new ArrayList(); + mKeyboardMode = modeId; + loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); + } + /** * Creates a keyboard from the given xml key layout file. Weeds out rows * that have a keyboard mode defined but don't match the specified mode. diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index 4b484092d02e84dd4c9d4ba4dfd2ccabd6049eb7..ab5c78a92078b6289d037625d2174c4317fd27f0 100644 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -1128,7 +1128,9 @@ public class KeyboardView extends View implements View.OnClickListener { private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { int touchX = (int) me.getX() - mPaddingLeft; - int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop; + int touchY = (int) me.getY() - mPaddingTop; + if (touchY >= -mVerticalCorrection) + touchY += mVerticalCorrection; final int action = me.getAction(); final long eventTime = me.getEventTime(); mOldEventTime = eventTime; diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 39681912dec30a84555e3c39be9b725de300bbd5..dd9c8f09bd16e13ea1d969448b6c440eeac59654 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -21,6 +21,9 @@ import android.annotation.SdkConstant.SdkConstantType; import android.os.Binder; import android.os.RemoteException; +import java.net.InetAddress; +import java.net.UnknownHostException; + /** * Class that answers queries about the state of network connectivity. It also * notifies applications when network connectivity changes. Get an instance @@ -261,6 +264,24 @@ public class ConnectivityManager } } + /** @hide */ + public LinkProperties getActiveLinkProperties() { + try { + return mService.getActiveLinkProperties(); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public LinkProperties getLinkProperties(int networkType) { + try { + return mService.getLinkProperties(networkType); + } catch (RemoteException e) { + return null; + } + } + /** {@hide} */ public boolean setRadios(boolean turnOn) { try { @@ -328,8 +349,29 @@ public class ConnectivityManager * @return {@code true} on success, {@code false} on failure */ public boolean requestRouteToHost(int networkType, int hostAddress) { + InetAddress inetAddress = NetworkUtils.intToInetAddress(hostAddress); + + if (inetAddress == null) { + return false; + } + + return requestRouteToHostAddress(networkType, inetAddress); + } + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via the specified network interface. An attempt to add a route that + * already exists is ignored, but treated as successful. + * @param networkType the type of the network over which traffic to the specified + * host is to be routed + * @param hostAddress the IP address of the host to which the route is desired + * @return {@code true} on success, {@code false} on failure + * @hide + */ + public boolean requestRouteToHostAddress(int networkType, InetAddress hostAddress) { + byte[] address = hostAddress.getAddress(); try { - return mService.requestRouteToHost(networkType, hostAddress); + return mService.requestRouteToHostAddress(networkType, address); } catch (RemoteException e) { return false; } @@ -343,14 +385,14 @@ public class ConnectivityManager *

    * All applications that have background services that use the network * should listen to {@link #ACTION_BACKGROUND_DATA_SETTING_CHANGED}. - * + * * @return Whether background data usage is allowed. */ public boolean getBackgroundDataSetting() { try { return mService.getBackgroundDataSetting(); } catch (RemoteException e) { - // Err on the side of safety + // Err on the side of safety return false; } } @@ -508,6 +550,17 @@ public class ConnectivityManager } } + /** + * {@hide} + */ + public String[] getTetherableBluetoothRegexs() { + try { + return mService.getTetherableBluetoothRegexs(); + } catch (RemoteException e) { + return new String[0]; + } + } + /** {@hide} */ public static final int TETHER_ERROR_NO_ERROR = 0; /** {@hide} */ @@ -545,6 +598,21 @@ public class ConnectivityManager } } + /** + * Ensure the device stays awake until we connect with the next network + * @param forWhome The name of the network going down for logging purposes + * @return {@code true} on success, {@code false} on failure + * {@hide} + */ + public boolean requestNetworkTransitionWakelock(String forWhom) { + try { + mService.requestNetworkTransitionWakelock(forWhom); + return true; + } catch (RemoteException e) { + return false; + } + } + /** * @param networkType The type of network you want to report on * @param percentage The quality of the connection 0 is bad, 100 is good diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java index 1178bec39b50e09fe3805dfae931a250fe8fbd73..9c81c193672c18b9dc00125dd857b0671515b28d 100644 --- a/core/java/android/net/DhcpInfo.java +++ b/core/java/android/net/DhcpInfo.java @@ -37,6 +37,19 @@ public class DhcpInfo implements Parcelable { super(); } + /** copy constructor {@hide} */ + public DhcpInfo(DhcpInfo source) { + if (source != null) { + ipAddress = source.ipAddress; + gateway = source.gateway; + netmask = source.netmask; + dns1 = source.dns1; + dns2 = source.dns2; + serverAddress = source.serverAddress; + leaseDuration = source.leaseDuration; + } + } + public String toString() { StringBuffer str = new StringBuffer(); diff --git a/core/java/android/net/DownloadManager.java b/core/java/android/net/DownloadManager.java index 0e671e930062e1844174830b6526305074844016..fc5ebb30a1754fdb60d9cc7c8194106b22a5f055 100644 --- a/core/java/android/net/DownloadManager.java +++ b/core/java/android/net/DownloadManager.java @@ -293,8 +293,8 @@ public class DownloadManager { throw new NullPointerException(); } String scheme = uri.getScheme(); - if (scheme == null || !scheme.equals("http")) { - throw new IllegalArgumentException("Can only download HTTP URIs: " + uri); + if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) { + throw new IllegalArgumentException("Can only download HTTP/HTTPS URIs: " + uri); } mUri = uri; } diff --git a/core/java/android/net/Downloads.java b/core/java/android/net/Downloads.java index fd33781981216237657263e46c15d9c9a7933604..ddde5c1bbc4144f0e73bbd003d65f3ca48832319 100644 --- a/core/java/android/net/Downloads.java +++ b/core/java/android/net/Downloads.java @@ -430,11 +430,10 @@ public final class Downloads { ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query( - downloadUri, DOWNLOADS_PROJECTION, null /* selection */, null /* selection args */, - null /* sort order */); + Cursor c = cr.query(downloadUri, DOWNLOADS_PROJECTION, null /* selection */, + null /* selection args */, null /* sort order */); try { - if (!c.moveToNext()) { + if (c == null || !c.moveToNext()) { return result; } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index b734ac7ebb9c5b35ea8606e859a772ae04cc4edb..35054d6c409451b8a99535afd7d6e5d7fc9418ee 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -1,21 +1,22 @@ /** * 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 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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. */ package android.net; +import android.net.LinkProperties; import android.net.NetworkInfo; import android.os.IBinder; @@ -36,6 +37,10 @@ interface IConnectivityManager NetworkInfo[] getAllNetworkInfo(); + LinkProperties getActiveLinkProperties(); + + LinkProperties getLinkProperties(int networkType); + boolean setRadios(boolean onOff); boolean setRadio(int networkType, boolean turnOn); @@ -47,6 +52,8 @@ interface IConnectivityManager boolean requestRouteToHost(int networkType, int hostAddress); + boolean requestRouteToHostAddress(int networkType, in byte[] hostAddress); + boolean getBackgroundDataSetting(); void setBackgroundDataSetting(boolean allowBackgroundData); @@ -73,5 +80,9 @@ interface IConnectivityManager String[] getTetherableWifiRegexs(); + String[] getTetherableBluetoothRegexs(); + + void requestNetworkTransitionWakelock(in String forWhom); + void reportInetCondition(int networkType, int percentage); } diff --git a/core/java/android/net/LinkCapabilities.aidl b/core/java/android/net/LinkCapabilities.aidl new file mode 100644 index 0000000000000000000000000000000000000000..df7259902febc7d19a7ca8214fa24eca6a753a9e --- /dev/null +++ b/core/java/android/net/LinkCapabilities.aidl @@ -0,0 +1,21 @@ +/* +** +** Copyright (C) 2010 The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.net; + +parcelable LinkCapabilities; + diff --git a/core/java/android/net/LinkCapabilities.java b/core/java/android/net/LinkCapabilities.java new file mode 100644 index 0000000000000000000000000000000000000000..eb9166ffe36022e642d30e6ce528315b917e3bf4 --- /dev/null +++ b/core/java/android/net/LinkCapabilities.java @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.os.Parcelable; +import android.os.Parcel; +import android.util.Log; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * A class representing the capabilities of a link + * + * @hide + */ +public class LinkCapabilities implements Parcelable { + private static final String TAG = "LinkCapabilities"; + private static final boolean DBG = false; + + /** The Map of Keys to Values */ + private HashMap mCapabilities; + + + /** + * The set of keys defined for a links capabilities. + * + * Keys starting with RW are read + write, i.e. the application + * can request for a certain requirement corresponding to that key. + * Keys starting with RO are read only, i.e. the the application + * can read the value of that key from the socket but cannot request + * a corresponding requirement. + * + * TODO: Provide a documentation technique for concisely and precisely + * define the syntax for each value string associated with a key. + */ + public static final class Key { + /** No constructor */ + private Key() {} + + /** + * An integer representing the network type. + * @see ConnectivityManager + */ + public final static int RO_NETWORK_TYPE = 1; + + /** + * Desired minimum forward link (download) bandwidth for the + * in kilobits per second (kbps). Values should be strings such + * "50", "100", "1500", etc. + */ + public final static int RW_DESIRED_FWD_BW = 2; + + /** + * Required minimum forward link (download) bandwidth, in + * per second (kbps), below which the socket cannot function. + * Values should be strings such as "50", "100", "1500", etc. + */ + public final static int RW_REQUIRED_FWD_BW = 3; + + /** + * Available forward link (download) bandwidth for the socket. + * This value is in kilobits per second (kbps). + * Values will be strings such as "50", "100", "1500", etc. + */ + public final static int RO_AVAILABLE_FWD_BW = 4; + + /** + * Desired minimum reverse link (upload) bandwidth for the socket + * in kilobits per second (kbps). + * Values should be strings such as "50", "100", "1500", etc. + *

    + * This key is set via the needs map. + */ + public final static int RW_DESIRED_REV_BW = 5; + + /** + * Required minimum reverse link (upload) bandwidth, in kilobits + * per second (kbps), below which the socket cannot function. + * If a rate is not specified, the default rate of kbps will be + * Values should be strings such as "50", "100", "1500", etc. + */ + public final static int RW_REQUIRED_REV_BW = 6; + + /** + * Available reverse link (upload) bandwidth for the socket. + * This value is in kilobits per second (kbps). + * Values will be strings such as "50", "100", "1500", etc. + */ + public final static int RO_AVAILABLE_REV_BW = 7; + + /** + * Maximum latency for the socket, in milliseconds, above which + * socket cannot function. + * Values should be strings such as "50", "300", "500", etc. + */ + public final static int RW_MAX_ALLOWED_LATENCY = 8; + + /** + * Interface that the socket is bound to. This can be a virtual + * interface (e.g. VPN or Mobile IP) or a physical interface + * (e.g. wlan0 or rmnet0). + * Values will be strings such as "wlan0", "rmnet0" + */ + public final static int RO_BOUND_INTERFACE = 9; + + /** + * Physical interface that the socket is routed on. + * This can be different from BOUND_INTERFACE in cases such as + * VPN or Mobile IP. The physical interface may change over time + * if seamless mobility is supported. + * Values will be strings such as "wlan0", "rmnet0" + */ + public final static int RO_PHYSICAL_INTERFACE = 10; + } + + /** + * Role informs the LinkSocket about the data usage patterns of your + * application. + *

    + * {@code Role.DEFAULT} is the default role, and is used whenever + * a role isn't set. + */ + public static final class Role { + /** No constructor */ + private Role() {} + + // examples only, discuss which roles should be defined, and then + // code these to match + + /** Default Role */ + public static final String DEFAULT = "default"; + /** Bulk down load */ + public static final String BULK_DOWNLOAD = "bulk.download"; + /** Bulk upload */ + public static final String BULK_UPLOAD = "bulk.upload"; + + /** VoIP Application at 24kbps */ + public static final String VOIP_24KBPS = "voip.24k"; + /** VoIP Application at 32kbps */ + public static final String VOIP_32KBPS = "voip.32k"; + + /** Video Streaming at 480p */ + public static final String VIDEO_STREAMING_480P = "video.streaming.480p"; + /** Video Streaming at 720p */ + public static final String VIDEO_STREAMING_720I = "video.streaming.720i"; + + /** Video Chat Application at 360p */ + public static final String VIDEO_CHAT_360P = "video.chat.360p"; + /** Video Chat Application at 480p */ + public static final String VIDEO_CHAT_480P = "video.chat.480i"; + } + + /** + * Constructor + */ + public LinkCapabilities() { + mCapabilities = new HashMap(); + } + + /** + * Copy constructor. + * + * @param source + */ + public LinkCapabilities(LinkCapabilities source) { + if (source != null) { + mCapabilities = new HashMap(source.mCapabilities); + } else { + mCapabilities = new HashMap(); + } + } + + /** + * Create the {@code LinkCapabilities} with values depending on role type. + * @param applicationRole a {@code LinkSocket.Role} + * @return the {@code LinkCapabilities} associated with the applicationRole, empty if none + */ + public static LinkCapabilities createNeedsMap(String applicationRole) { + if (DBG) log("createNeededCapabilities(applicationRole) EX"); + return new LinkCapabilities(); + } + + /** + * Remove all capabilities + */ + public void clear() { + mCapabilities.clear(); + } + + /** + * Returns whether this map is empty. + */ + public boolean isEmpty() { + return mCapabilities.isEmpty(); + } + + /** + * Returns the number of elements in this map. + * + * @return the number of elements in this map. + */ + public int size() { + return mCapabilities.size(); + } + + /** + * Given the key return the capability string + * + * @param key + * @return the capability string + */ + public String get(int key) { + return mCapabilities.get(key); + } + + /** + * Store the key/value capability pair + * + * @param key + * @param value + */ + public void put(int key, String value) { + mCapabilities.put(key, value); + } + + /** + * Returns whether this map contains the specified key. + * + * @param key to search for. + * @return {@code true} if this map contains the specified key, + * {@code false} otherwise. + */ + public boolean containsKey(int key) { + return mCapabilities.containsKey(key); + } + + /** + * Returns whether this map contains the specified value. + * + * @param value to search for. + * @return {@code true} if this map contains the specified value, + * {@code false} otherwise. + */ + public boolean containsValue(String value) { + return mCapabilities.containsValue(value); + } + + /** + * Returns a set containing all of the mappings in this map. Each mapping is + * an instance of {@link Map.Entry}. As the set is backed by this map, + * changes in one will be reflected in the other. + * + * @return a set of the mappings. + */ + public Set> entrySet() { + return mCapabilities.entrySet(); + } + + /** + * @return the set of the keys. + */ + public Set keySet() { + return mCapabilities.keySet(); + } + + /** + * @return the set of values + */ + public Collection values() { + return mCapabilities.values(); + } + + /** + * Implement the Parcelable interface + * @hide + */ + public int describeContents() { + return 0; + } + + /** + * Convert to string for debugging + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + boolean firstTime = true; + for (Entry entry : mCapabilities.entrySet()) { + if (firstTime) { + firstTime = false; + } else { + sb.append(","); + } + sb.append(entry.getKey()); + sb.append(":\""); + sb.append(entry.getValue()); + sb.append("\""); + return mCapabilities.toString(); + } + return sb.toString(); + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mCapabilities.size()); + for (Entry entry : mCapabilities.entrySet()) { + dest.writeInt(entry.getKey().intValue()); + dest.writeString(entry.getValue()); + } + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public static final Creator CREATOR = + new Creator() { + public LinkCapabilities createFromParcel(Parcel in) { + LinkCapabilities capabilities = new LinkCapabilities(); + int size = in.readInt(); + while (size-- != 0) { + int key = in.readInt(); + String value = in.readString(); + capabilities.mCapabilities.put(key, value); + } + return capabilities; + } + + public LinkCapabilities[] newArray(int size) { + return new LinkCapabilities[size]; + } + }; + + /** + * Debug logging + */ + protected static void log(String s) { + Log.d(TAG, s); + } +} diff --git a/core/java/android/net/LinkProperties.aidl b/core/java/android/net/LinkProperties.aidl new file mode 100644 index 0000000000000000000000000000000000000000..9cd06d57844ef8a74437c49319c05b75fa4dde36 --- /dev/null +++ b/core/java/android/net/LinkProperties.aidl @@ -0,0 +1,21 @@ +/* +** +** Copyright (C) 2010 The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.net; + +parcelable LinkProperties; + diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..f411eac1b2d9d740ff777e26e87ee4175e0eed60 --- /dev/null +++ b/core/java/android/net/LinkProperties.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.ProxyProperties; +import android.os.Parcelable; +import android.os.Parcel; +import android.util.Log; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * Describes the properties of a network link. + * TODO - consider adding optional fields like Apn and ApnType + * @hide + */ +public class LinkProperties implements Parcelable { + + private NetworkInterface mIface; + private Collection mAddresses; + private Collection mDnses; + private InetAddress mGateway; + private ProxyProperties mHttpProxy; + + public LinkProperties() { + clear(); + } + + // copy constructor instead of clone + public LinkProperties(LinkProperties source) { + if (source != null) { + mIface = source.getInterface(); + mAddresses = source.getAddresses(); + mDnses = source.getDnses(); + mGateway = source.getGateway(); + mHttpProxy = new ProxyProperties(source.getHttpProxy()); + } + } + + public void setInterface(NetworkInterface iface) { + mIface = iface; + } + public NetworkInterface getInterface() { + return mIface; + } + public String getInterfaceName() { + return (mIface == null ? null : mIface.getName()); + } + + public void addAddress(InetAddress address) { + mAddresses.add(address); + } + public Collection getAddresses() { + return Collections.unmodifiableCollection(mAddresses); + } + + public void addDns(InetAddress dns) { + mDnses.add(dns); + } + public Collection getDnses() { + return Collections.unmodifiableCollection(mDnses); + } + + public void setGateway(InetAddress gateway) { + mGateway = gateway; + } + public InetAddress getGateway() { + return mGateway; + } + + public void setHttpProxy(ProxyProperties proxy) { + mHttpProxy = proxy; + } + public ProxyProperties getHttpProxy() { + return mHttpProxy; + } + + public void clear() { + mIface = null; + mAddresses = new ArrayList(); + mDnses = new ArrayList(); + mGateway = null; + mHttpProxy = null; + } + + /** + * Implement the Parcelable interface + * @hide + */ + public int describeContents() { + return 0; + } + + @Override + public String toString() { + String ifaceName = (mIface == null ? "" : "InterfaceName: " + mIface.getName() + " "); + + String ip = "IpAddresses: ["; + for (InetAddress addr : mAddresses) ip += addr.getHostAddress() + ","; + ip += "] "; + + String dns = "DnsAddresses: ["; + for (InetAddress addr : mDnses) dns += addr.getHostAddress() + ","; + dns += "] "; + + String proxy = (mHttpProxy == null ? "" : "HttpProxy: " + mHttpProxy.toString() + " "); + String gateway = (mGateway == null ? "" : "Gateway: " + mGateway.getHostAddress() + " "); + + return ifaceName + ip + gateway + dns + proxy; + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getInterfaceName()); + dest.writeInt(mAddresses.size()); + //TODO: explore an easy alternative to preserve hostname + // without doing a lookup + for(InetAddress a : mAddresses) { + dest.writeByteArray(a.getAddress()); + } + dest.writeInt(mDnses.size()); + for(InetAddress d : mDnses) { + dest.writeByteArray(d.getAddress()); + } + if (mGateway != null) { + dest.writeByte((byte)1); + dest.writeByteArray(mGateway.getAddress()); + } else { + dest.writeByte((byte)0); + } + if (mHttpProxy != null) { + dest.writeByte((byte)1); + dest.writeParcelable(mHttpProxy, flags); + } else { + dest.writeByte((byte)0); + } + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public static final Creator CREATOR = + new Creator() { + public LinkProperties createFromParcel(Parcel in) { + LinkProperties netProp = new LinkProperties(); + String iface = in.readString(); + if (iface != null) { + try { + netProp.setInterface(NetworkInterface.getByName(iface)); + } catch (Exception e) { + return null; + } + } + int addressCount = in.readInt(); + for (int i=0; i capabilities) { + if (DBG) log("getCapabilities(capabilities) EX"); + return new LinkCapabilities(); + } + + /** + * Provide the set of capabilities the application is interested in tracking + * for this LinkSocket. + * @param capabilities a {@code Set} of capabilities to track + */ + public void setTrackedCapabilities(Set capabilities) { + if (DBG) log("setTrackedCapabilities(capabilities) EX"); + } + + /** + * @return the {@code LinkCapabilities} that are tracked, empty if none has been set. + */ + public Set getTrackedCapabilities() { + if (DBG) log("getTrackedCapabilities(capabilities) EX"); + return new HashSet(); + } + + /** + * Connects this socket to the given remote host address and port specified + * by dstName and dstPort. + * @param dstName the address of the remote host to connect to + * @param dstPort the port to connect to on the remote host + * @param timeout the timeout value in milliseconds or 0 for infinite timeout + * @throws UnknownHostException if the given dstName is invalid + * @throws IOException if the socket is already connected or an error occurs + * while connecting + * @throws SocketTimeoutException if the timeout fires + */ + public void connect(String dstName, int dstPort, int timeout) + throws UnknownHostException, IOException, SocketTimeoutException { + if (DBG) log("connect(dstName, dstPort, timeout) EX"); + } + + /** + * Connects this socket to the given remote host address and port specified + * by dstName and dstPort. + * @param dstName the address of the remote host to connect to + * @param dstPort the port to connect to on the remote host + * @throws UnknownHostException if the given dstName is invalid + * @throws IOException if the socket is already connected or an error occurs + * while connecting + */ + public void connect(String dstName, int dstPort) + throws UnknownHostException, IOException { + if (DBG) log("connect(dstName, dstPort, timeout) EX"); + } + + /** + * Connects this socket to the given remote host address and port specified + * by the SocketAddress with the specified timeout. + * @deprecated Use {@code connect(String dstName, int dstPort, int timeout)} + * instead. Using this method may result in reduced functionality. + * @param remoteAddr the address and port of the remote host to connect to + * @throws IllegalArgumentException if the given SocketAddress is invalid + * @throws IOException if the socket is already connected or an error occurs + * while connecting + * @throws SocketTimeoutException if the timeout expires + */ + @Override + @Deprecated + public void connect(SocketAddress remoteAddr, int timeout) + throws IOException, SocketTimeoutException { + if (DBG) log("connect(remoteAddr, timeout) EX DEPRECATED"); + } + + /** + * Connects this socket to the given remote host address and port specified + * by the SocketAddress. + * TODO add comment on all these that the network selection happens during connect + * and may take 30 seconds + * @deprecated Use {@code connect(String dstName, int dstPort)} + * Using this method may result in reduced functionality. + * @param remoteAddr the address and port of the remote host to connect to. + * @throws IllegalArgumentException if the SocketAddress is invalid or not supported. + * @throws IOException if the socket is already connected or an error occurs + * while connecting + */ + @Override + @Deprecated + public void connect(SocketAddress remoteAddr) throws IOException { + if (DBG) log("connect(remoteAddr) EX DEPRECATED"); + } + + /** + * Connect a duplicate socket socket to the same remote host address and port + * as the original with a timeout parameter. + * @param timeout the timeout value in milliseconds or 0 for infinite timeout + * @throws IOException if the socket is already connected or an error occurs + * while connecting + */ + public void connect(int timeout) throws IOException { + if (DBG) log("connect(timeout) EX"); + } + + /** + * Connect a duplicate socket socket to the same remote host address and port + * as the original. + * @throws IOException if the socket is already connected or an error occurs + * while connecting + */ + public void connect() throws IOException { + if (DBG) log("connect() EX"); + } + + /** + * Closes the socket. It is not possible to reconnect or rebind to this + * socket thereafter which means a new socket instance has to be created. + * @throws IOException if an error occurs while closing the socket + */ + @Override + public synchronized void close() throws IOException { + if (DBG) log("close() EX"); + } + + /** + * Request that a new LinkSocket be created using a different radio + * (such as WiFi or 3G) than the current LinkSocket. If a different + * radio is available a call back will be made via {@code onBetterLinkAvail}. + * If unable to find a better radio, application will be notified via + * {@code onNewLinkUnavailable} + * @see LinkSocketNotifier#onBetterLinkAvailable(LinkSocket, LinkSocket) + * @param linkRequestReason reason for requesting a new link. + */ + public void requestNewLink(LinkRequestReason linkRequestReason) { + if (DBG) log("requestNewLink(linkRequestReason) EX"); + } + + /** + * @deprecated LinkSocket will automatically pick the optimum interface + * to bind to + * @param localAddr the specific address and port on the local machine + * to bind to + * @throws IOException always as this method is deprecated for LinkSocket + */ + @Override + @Deprecated + public void bind(SocketAddress localAddr) throws UnsupportedOperationException { + if (DBG) log("bind(localAddr) EX throws IOException"); + throw new UnsupportedOperationException("bind is deprecated for LinkSocket"); + } + + /** + * Reason codes an application can specify when requesting for a new link. + * TODO: need better documentation + */ + public static final class LinkRequestReason { + /** No constructor */ + private LinkRequestReason() {} + + /** This link is working properly */ + public static final int LINK_PROBLEM_NONE = 0; + /** This link has an unknown issue */ + public static final int LINK_PROBLEM_UNKNOWN = 1; + } + + /** + * Debug logging + */ + protected static void log(String s) { + Log.d(TAG, s); + } +} diff --git a/core/java/android/net/LinkSocketNotifier.java b/core/java/android/net/LinkSocketNotifier.java new file mode 100644 index 0000000000000000000000000000000000000000..28e2834dc555d9c110bcdcaff5fa444da5548d64 --- /dev/null +++ b/core/java/android/net/LinkSocketNotifier.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import java.util.Map; + +/** + * Interface used to get feedback about a {@link android.net.LinkSocket}. Instance is optionally + * passed when a LinkSocket is constructed. Multiple LinkSockets may use the same notifier. + * @hide + */ +public interface LinkSocketNotifier { + /** + * This callback function will be called if a better link + * becomes available. + * TODO - this shouldn't be checked for all cases - what's the conditional + * flag? + * If the duplicate socket is accepted, the original will be marked invalid + * and additional use will throw exceptions. + * @param original the original LinkSocket + * @param duplicate the new LinkSocket that better meets the application + * requirements + * @return {@code true} if the application intends to use this link + * + * REM + * TODO - how agressive should we be? + * At a minimum CS tracks which LS have this turned on and tracks the requirements + * When a new link becomes available, automatically check if any of the LinkSockets + * will care. + * If found, grab a refcount on the link so it doesn't go away and send notification + * Optionally, periodically setup connection on available networks to check for better links + * Maybe pass this info into the LinkFactories so condition changes can be acted on more quickly + */ + public boolean onBetterLinkAvailable(LinkSocket original, LinkSocket duplicate); + + /** + * This callback function will be called when a LinkSocket no longer has + * an active link. + * @param socket the LinkSocket that lost its link + * + * REM + * NetworkStateTracker tells us it is disconnected + * CS checks the table for LS on that link + * CS calls each callback (need to work out p2p cross process callback) + */ + public void onLinkLost(LinkSocket socket); + + /** + * This callback function will be called when an application calls + * requestNewLink on a LinkSocket but the LinkSocket is unable to find + * a suitable new link. + * @param socket the LinkSocket for which a new link was not found + * TODO - why the diff between initial request (sync) and requestNewLink? + * + * REM + * CS process of trying to find a new link must track the LS that started it + * on failure, call callback + */ + public void onNewLinkUnavailable(LinkSocket socket); + + /** + * This callback function will be called when any of the notification-marked + * capabilities of the LinkSocket (e.g. upstream bandwidth) have changed. + * @param socket the linkSocet for which capabilities have changed + * @param changedCapabilities the set of capabilities that the application + * is interested in and have changed (with new values) + * + * REM + * Maybe pass the interesting capabilities into the Links. + * Get notified of every capability change + * check for LinkSockets on that Link that are interested in that Capability - call them + */ + public void onCapabilitiesChanged(LinkSocket socket, LinkCapabilities changedCapabilities); +} diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index eb7117bdee5dc4ad04c1aa83a69c3338bb4b4f2a..3df8ec0529464a993106bd6770b1427aa9fac87e 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -22,12 +22,15 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.RemoteException; import android.os.Handler; +import android.os.IBinder; +import android.os.Message; import android.os.ServiceManager; -import android.os.SystemProperties; import com.android.internal.telephony.ITelephony; import com.android.internal.telephony.Phone; import com.android.internal.telephony.TelephonyIntents; import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo; +import android.net.LinkProperties; import android.telephony.TelephonyManager; import android.util.Log; import android.text.TextUtils; @@ -39,7 +42,7 @@ import android.text.TextUtils; * * {@hide} */ -public class MobileDataStateTracker extends NetworkStateTracker { +public class MobileDataStateTracker implements NetworkStateTracker { private static final String TAG = "MobileDataStateTracker"; private static final boolean DBG = true; @@ -48,10 +51,16 @@ public class MobileDataStateTracker extends NetworkStateTracker { private ITelephony mPhoneService; private String mApnType; - private String mApnTypeToWatchFor; - private String mApnName; - private boolean mEnabled; - private BroadcastReceiver mStateReceiver; + private static String[] sDnsPropNames; + private NetworkInfo mNetworkInfo; + private boolean mTeardownRequested = false; + private Handler mTarget; + private Context mContext; + private LinkProperties mLinkProperties; + private LinkCapabilities mLinkCapabilities; + private boolean mPrivateDnsRouteSet = false; + private int mDefaultGatewayAddr = 0; + private boolean mDefaultRouteSet = false; // DEFAULT and HIPRI are the same connection. If we're one of these we need to check if // the other is also disconnected before we reset sockets @@ -59,35 +68,22 @@ public class MobileDataStateTracker extends NetworkStateTracker { /** * 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, int netType, String tag) { - super(context, target, netType, + public MobileDataStateTracker(int netType, String tag) { + mNetworkInfo = new NetworkInfo(netType, TelephonyManager.getDefault().getNetworkType(), tag, TelephonyManager.getDefault().getNetworkTypeName()); mApnType = networkTypeToApnType(netType); - if (TextUtils.equals(mApnType, Phone.APN_TYPE_HIPRI)) { - mApnTypeToWatchFor = Phone.APN_TYPE_DEFAULT; - } else { - mApnTypeToWatchFor = mApnType; - } if (netType == ConnectivityManager.TYPE_MOBILE || netType == ConnectivityManager.TYPE_MOBILE_HIPRI) { mIsDefaultOrHipri = true; } mPhoneService = null; - if(netType == ConnectivityManager.TYPE_MOBILE) { - mEnabled = true; - } else { - mEnabled = false; - } - mDnsPropNames = new String[] { + sDnsPropNames = new String[] { "net.rmnet0.dns1", "net.rmnet0.dns2", "net.eth0.dns1", @@ -98,165 +94,168 @@ public class MobileDataStateTracker extends NetworkStateTracker { "net.gprs.dns2", "net.ppp0.dns1", "net.ppp0.dns2"}; - } /** - * Begin monitoring mobile data connectivity. + * Begin monitoring data connectivity. + * + * @param context is the current Android context + * @param target is the Hander to which to return the events. */ - public void startMonitoring() { - IntentFilter filter = - new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); + public void startMonitoring(Context context, Handler target) { + mTarget = target; + mContext = context; + + IntentFilter filter = new IntentFilter(); + filter.addAction(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED); - filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); - - mStateReceiver = new MobileDataStateReceiver(); - Intent intent = mContext.registerReceiver(mStateReceiver, filter); - if (intent != null) - mMobileDataState = getMobileDataState(intent); - else - mMobileDataState = Phone.DataState.DISCONNECTED; + + mContext.registerReceiver(new MobileDataStateReceiver(), filter); + mMobileDataState = Phone.DataState.DISCONNECTED; } - private Phone.DataState getMobileDataState(Intent intent) { - String str = intent.getStringExtra(Phone.STATE_KEY); - if (str != null) { - String apnTypeList = - intent.getStringExtra(Phone.DATA_APN_TYPES_KEY); - if (isApnTypeIncluded(apnTypeList)) { - return Enum.valueOf(Phone.DataState.class, str); - } - } - return Phone.DataState.DISCONNECTED; + /** + * 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[] getDnsPropNames() { + return sDnsPropNames; } - private boolean isApnTypeIncluded(String typeList) { - /* comma seperated list - split and check */ - if (typeList == null) - return false; + public boolean isPrivateDnsRouteSet() { + return mPrivateDnsRouteSet; + } - String[] list = typeList.split(","); - for(int i=0; i< list.length; i++) { - if (TextUtils.equals(list[i], mApnTypeToWatchFor) || - TextUtils.equals(list[i], Phone.APN_TYPE_ALL)) { - return true; - } - } - return false; + public void privateDnsRouteSet(boolean enabled) { + mPrivateDnsRouteSet = enabled; + } + + public NetworkInfo getNetworkInfo() { + return mNetworkInfo; + } + + public int getDefaultGatewayAddr() { + return mDefaultGatewayAddr; + } + + public boolean isDefaultRouteSet() { + return mDefaultRouteSet; + } + + public void defaultRouteSet(boolean enabled) { + mDefaultRouteSet = enabled; + } + + /** + * This is not implemented. + */ + public void releaseWakeLock() { } private class MobileDataStateReceiver extends BroadcastReceiver { - ConnectivityManager mConnectivityManager; + IConnectivityManager mConnectivityManager; + 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); + if (intent.getAction().equals(TelephonyIntents. + ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) { + String apnType = intent.getStringExtra(Phone.DATA_APN_TYPE_KEY); + if (!TextUtils.equals(apnType, mApnType)) { + return; + } + Phone.DataState state = Enum.valueOf(Phone.DataState.class, + intent.getStringExtra(Phone.STATE_KEY)); + String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY); + String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); + + mNetworkInfo.setIsAvailable(!intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY, + false)); + + if (DBG) Log.d(TAG, mApnType + " Received state= " + state + ", old= " + + mMobileDataState + ", reason= " + + (reason == null ? "(unspecified)" : reason)); + + if (mMobileDataState != state) { + mMobileDataState = state; + switch (state) { + case DISCONNECTED: + if(isTeardownRequested()) { + setTeardownRequested(false); } - return; - } - } else { - return; - } - 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); + boolean doReset = true; + if (mIsDefaultOrHipri == true) { + // both default and hipri must go down before we reset + int typeToCheck = (Phone.APN_TYPE_DEFAULT.equals(mApnType) ? + ConnectivityManager.TYPE_MOBILE_HIPRI : + ConnectivityManager.TYPE_MOBILE); + if (mConnectivityManager == null) { + IBinder b = ServiceManager.getService( + Context.CONNECTIVITY_SERVICE); + mConnectivityManager = IConnectivityManager.Stub.asInterface(b); } - - setDetailedState(DetailedState.DISCONNECTED, reason, apnName); - boolean doReset = true; - if (mIsDefaultOrHipri == true) { - // both default and hipri must go down before we reset - int typeToCheck = (Phone.APN_TYPE_DEFAULT.equals(mApnType) ? - ConnectivityManager.TYPE_MOBILE_HIPRI : - ConnectivityManager.TYPE_MOBILE); - if (mConnectivityManager == null) { - mConnectivityManager = - (ConnectivityManager)context.getSystemService( - Context.CONNECTIVITY_SERVICE); - } + try { if (mConnectivityManager != null) { NetworkInfo info = mConnectivityManager.getNetworkInfo( - typeToCheck); - if (info != null && info.isConnected() == true) { + typeToCheck); + if (info.isConnected() == true) { doReset = false; } } + } catch (RemoteException e) { + // just go ahead with the reset + Log.e(TAG, "Exception trying to contact ConnService: " + + e); } - if (doReset && 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."); - } - mDefaultGatewayAddr = intent.getIntExtra(Phone.DATA_GATEWAY_KEY, 0); - setDetailedState(DetailedState.CONNECTED, reason, apnName); - break; - } + } + if (doReset && mLinkProperties != null) { + String iface = mLinkProperties.getInterfaceName(); + if (iface != null) NetworkUtils.resetConnections(iface); + } + // TODO - check this + // 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: + mLinkProperties = intent.getParcelableExtra( + Phone.DATA_LINK_PROPERTIES_KEY); + if (mLinkProperties == null) { + Log.d(TAG, "CONNECTED event did not supply link properties."); + mLinkProperties = new LinkProperties(); + } + mLinkCapabilities = intent.getParcelableExtra( + Phone.DATA_LINK_CAPABILITIES_KEY); + if (mLinkCapabilities == null) { + Log.d(TAG, "CONNECTED event did not supply link capabilities."); + mLinkCapabilities = new LinkCapabilities(); + } + 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()); + } else if (intent.getAction(). + equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) { + String apnType = intent.getStringExtra(Phone.DATA_APN_TYPE_KEY); + if (!TextUtils.equals(apnType, mApnType)) { + return; + } + String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY); + String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); + if (DBG) Log.d(TAG, mApnType + "Received " + intent.getAction() + + " broadcast" + reason == null ? "" : "(" + reason + ")"); + setDetailedState(DetailedState.FAILED, reason, apnName); } } } @@ -271,33 +270,7 @@ public class MobileDataStateTracker extends NetworkStateTracker { * Report whether data connectivity is possible. */ public boolean isAvailable() { - 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) break; - - try { - return mPhoneService.isDataConnectivityPossible(); - } catch (RemoteException e) { - // First-time failed, get the phone service again - if (retry == 0) getPhoneService(true); - } - } - - return false; - } - - /** - * {@inheritDoc} - * The mobile data network subtype indicates what generation network technology is in effect, - * e.g., GPRS, EDGE, UMTS, etc. - */ - public int getNetworkSubtype() { - return TelephonyManager.getDefault().getNetworkType(); + return mNetworkInfo.isAvailable(); } /** @@ -349,70 +322,74 @@ public class MobileDataStateTracker extends NetworkStateTracker { /** * Tear down mobile data connectivity, i.e., disable the ability to create * mobile data connections. + * TODO - make async and return nothing? */ - @Override public boolean teardown() { - // 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); } + /** + * Record the detailed state of a network, and if it is a + * change from the previous state, send a notification to + * any listeners. + * @param state the new @{code DetailedState} + * @param reason a {@code String} indicating a reason for the state change, + * if one was supplied. May be {@code null}. + * @param extraInfo optional {@code String} providing extra information about the state change + */ + private 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(); + /* + * If a reason was supplied when the CONNECTING state was entered, and no + * reason was supplied for entering the CONNECTED state, then retain the + * reason that was supplied when going to CONNECTING. + */ + if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null + && lastReason != null) + reason = lastReason; + mNetworkInfo.setDetailedState(state, reason, extraInfo); + Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); + msg.sendToTarget(); + } + } + + public void setTeardownRequested(boolean isRequested) { + mTeardownRequested = isRequested; + } + + public boolean isTeardownRequested() { + return mTeardownRequested; + } + /** * Re-enable mobile data connectivity after a {@link #teardown()}. + * TODO - make async and always get a notification? */ public boolean reconnect() { + boolean retValue = false; //connected or expect to be? 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, mApnTypeToWatchFor); - 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); + retValue = true; break; case Phone.APN_REQUEST_STARTED: - mEnabled = true; // no need to do anything - we're already due some status update intents + retValue = true; 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 - // since the phone will come up enabled, go with that. - // TODO - this also comes up on telephony crash: if we think mobile data is - // off and the telephony stuff crashes and has to restart it will come up - // enabled (making a data connection). We will then be out of sync. - // A possible solution is a broadcast when telephony restarts. - mEnabled = true; - return false; - } - // else fall through case Phone.APN_TYPE_NOT_AVAILABLE: - // Default is always available, but may be off due to - // AirplaneMode or E-Call or whatever.. - if (mApnType != Phone.APN_TYPE_DEFAULT) { - mEnabled = false; - } break; default: Log.e(TAG, "Error in reconnect - unexpected response."); - mEnabled = false; break; } - return mEnabled; + return retValue; } /** @@ -485,26 +462,6 @@ public class MobileDataStateTracker extends NetworkStateTracker { return -1; } - /** - * Ensure that a network route exists to deliver traffic to the specified - * host via the mobile data network. - * @param hostAddress the IP address of the host to which the route is desired, - * in network byte order. - * @return {@code true} on success, {@code false} on failure - */ - @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) { - return NetworkUtils.addHostRoute(mInterfaceName, hostAddress) == 0; - } else { - return false; - } - } - @Override public String toString() { StringBuffer sb = new StringBuffer("Mobile data state: "); @@ -566,4 +523,18 @@ public class MobileDataStateTracker extends NetworkStateTracker { return null; } } + + /** + * @see android.net.NetworkStateTracker#getLinkProperties() + */ + public LinkProperties getLinkProperties() { + return new LinkProperties(mLinkProperties); + } + + /** + * @see android.net.NetworkStateTracker#getLinkCapabilities() + */ + public LinkCapabilities getLinkCapabilities() { + return new LinkCapabilities(mLinkCapabilities); + } } diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 649cb8cfc850724a7d2b1e23219dc3c93819e6bc..5f5e11c96a2b60ef7b05aecb65d66bfb0e96476b 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -97,7 +97,7 @@ public class NetworkInfo implements Parcelable { stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED); stateMap.put(DetailedState.FAILED, State.DISCONNECTED); } - + private int mNetworkType; private int mSubtype; private String mTypeName; @@ -121,7 +121,10 @@ public class NetworkInfo implements Parcelable { */ public NetworkInfo(int type) {} - NetworkInfo(int type, int subtype, String typeName, String subtypeName) { + /** + * @hide + */ + public NetworkInfo(int type, int subtype, String typeName, String subtypeName) { if (!ConnectivityManager.isNetworkTypeValid(type)) { throw new IllegalArgumentException("Invalid network type: " + type); } @@ -141,7 +144,9 @@ public class NetworkInfo implements Parcelable { * @return the network type */ public int getType() { - return mNetworkType; + synchronized (this) { + return mNetworkType; + } } /** @@ -150,12 +155,16 @@ public class NetworkInfo implements Parcelable { * @return the network subtype */ public int getSubtype() { - return mSubtype; + synchronized (this) { + return mSubtype; + } } void setSubtype(int subtype, String subtypeName) { - mSubtype = subtype; - mSubtypeName = subtypeName; + synchronized (this) { + mSubtype = subtype; + mSubtypeName = subtypeName; + } } /** @@ -164,7 +173,9 @@ public class NetworkInfo implements Parcelable { * @return the name of the network type */ public String getTypeName() { - return mTypeName; + synchronized (this) { + return mTypeName; + } } /** @@ -172,7 +183,9 @@ public class NetworkInfo implements Parcelable { * @return the name of the network subtype */ public String getSubtypeName() { - return mSubtypeName; + synchronized (this) { + return mSubtypeName; + } } /** @@ -185,7 +198,9 @@ public class NetworkInfo implements Parcelable { * of being established, {@code false} otherwise. */ public boolean isConnectedOrConnecting() { - return mState == State.CONNECTED || mState == State.CONNECTING; + synchronized (this) { + return mState == State.CONNECTED || mState == State.CONNECTING; + } } /** @@ -194,7 +209,9 @@ public class NetworkInfo implements Parcelable { * @return {@code true} if network connectivity exists, {@code false} otherwise. */ public boolean isConnected() { - return mState == State.CONNECTED; + synchronized (this) { + return mState == State.CONNECTED; + } } /** @@ -210,7 +227,9 @@ public class NetworkInfo implements Parcelable { * @return {@code true} if the network is available, {@code false} otherwise */ public boolean isAvailable() { - return mIsAvailable; + synchronized (this) { + return mIsAvailable; + } } /** @@ -220,7 +239,9 @@ public class NetworkInfo implements Parcelable { * @hide */ public void setIsAvailable(boolean isAvailable) { - mIsAvailable = isAvailable; + synchronized (this) { + mIsAvailable = isAvailable; + } } /** @@ -231,7 +252,9 @@ public class NetworkInfo implements Parcelable { * otherwise. */ public boolean isFailover() { - return mIsFailover; + synchronized (this) { + return mIsFailover; + } } /** @@ -241,7 +264,9 @@ public class NetworkInfo implements Parcelable { * @hide */ public void setFailover(boolean isFailover) { - mIsFailover = isFailover; + synchronized (this) { + mIsFailover = isFailover; + } } /** @@ -251,11 +276,15 @@ public class NetworkInfo implements Parcelable { * @return {@code true} if roaming is in effect, {@code false} otherwise. */ public boolean isRoaming() { - return mIsRoaming; + synchronized (this) { + return mIsRoaming; + } } void setRoaming(boolean isRoaming) { - mIsRoaming = isRoaming; + synchronized (this) { + mIsRoaming = isRoaming; + } } /** @@ -263,7 +292,9 @@ public class NetworkInfo implements Parcelable { * @return the coarse-grained state */ public State getState() { - return mState; + synchronized (this) { + return mState; + } } /** @@ -271,7 +302,9 @@ public class NetworkInfo implements Parcelable { * @return the fine-grained state */ public DetailedState getDetailedState() { - return mDetailedState; + synchronized (this) { + return mDetailedState; + } } /** @@ -281,12 +314,15 @@ public class NetworkInfo implements Parcelable { * if one was supplied. May be {@code null}. * @param extraInfo an optional {@code String} providing addditional network state * information passed up from the lower networking layers. + * @hide */ - void setDetailedState(DetailedState detailedState, String reason, String extraInfo) { - this.mDetailedState = detailedState; - this.mState = stateMap.get(detailedState); - this.mReason = reason; - this.mExtraInfo = extraInfo; + public void setDetailedState(DetailedState detailedState, String reason, String extraInfo) { + synchronized (this) { + this.mDetailedState = detailedState; + this.mState = stateMap.get(detailedState); + this.mReason = reason; + this.mExtraInfo = extraInfo; + } } /** @@ -295,7 +331,9 @@ public class NetworkInfo implements Parcelable { * @return the reason for failure, or null if not available */ public String getReason() { - return mReason; + synchronized (this) { + return mReason; + } } /** @@ -305,20 +343,24 @@ public class NetworkInfo implements Parcelable { * @return the extra information, or null if not available */ public String getExtraInfo() { - return mExtraInfo; + synchronized (this) { + return mExtraInfo; + } } @Override public String toString() { - StringBuilder builder = new StringBuilder("NetworkInfo: "); - builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()). - append("], state: ").append(mState).append("/").append(mDetailedState). - append(", reason: ").append(mReason == null ? "(unspecified)" : mReason). - append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo). - append(", roaming: ").append(mIsRoaming). - append(", failover: ").append(mIsFailover). - append(", isAvailable: ").append(mIsAvailable); - return builder.toString(); + synchronized (this) { + StringBuilder builder = new StringBuilder("NetworkInfo: "); + builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()). + append("], state: ").append(mState).append("/").append(mDetailedState). + append(", reason: ").append(mReason == null ? "(unspecified)" : mReason). + append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo). + append(", roaming: ").append(mIsRoaming). + append(", failover: ").append(mIsFailover). + append(", isAvailable: ").append(mIsAvailable); + return builder.toString(); + } } /** @@ -334,17 +376,19 @@ public class NetworkInfo implements Parcelable { * @hide */ public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mNetworkType); - dest.writeInt(mSubtype); - dest.writeString(mTypeName); - dest.writeString(mSubtypeName); - dest.writeString(mState.name()); - dest.writeString(mDetailedState.name()); - dest.writeInt(mIsFailover ? 1 : 0); - dest.writeInt(mIsAvailable ? 1 : 0); - dest.writeInt(mIsRoaming ? 1 : 0); - dest.writeString(mReason); - dest.writeString(mExtraInfo); + synchronized (this) { + dest.writeInt(mNetworkType); + dest.writeInt(mSubtype); + dest.writeString(mTypeName); + dest.writeString(mSubtypeName); + dest.writeString(mState.name()); + dest.writeString(mDetailedState.name()); + dest.writeInt(mIsFailover ? 1 : 0); + dest.writeInt(mIsAvailable ? 1 : 0); + dest.writeInt(mIsRoaming ? 1 : 0); + dest.writeString(mReason); + dest.writeString(mExtraInfo); + } } /** diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index c5a327744bb833106ad014b5b2954c3530bae937..97c31faf86a3ad47191f89dc43e129145898ed99 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -16,358 +16,143 @@ package android.net; -import java.io.FileWriter; -import java.io.IOException; - -import android.os.Handler; -import android.os.Message; -import android.os.SystemProperties; import android.content.Context; -import android.text.TextUtils; -import android.util.Config; -import android.util.Log; - +import android.os.Handler; /** - * Each subclass of this class keeps track of the state of connectivity - * of a network interface. All state information for a network should - * be kept in a Tracker class. This superclass manages the - * network-type-independent aspects of network state. + * Interface provides the {@link com.android.server.ConnectivityService} + * with three services. Events to the ConnectivityService when + * changes occur, an API for controlling the network and storage + * for network specific information. + * + * The Connectivity will call startMonitoring before any other + * method is called. * * {@hide} */ -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 mTeardownRequested; - - private static boolean DBG = true; - private static final String TAG = "NetworkStateTracker"; +public interface NetworkStateTracker { - public static final int EVENT_STATE_CHANGED = 1; - public static final int EVENT_SCAN_RESULTS_AVAILABLE = 2; - /** - * arg1: 1 to show, 0 to hide - * arg2: ID of the notification - * obj: Notification (if showing) - */ - public static final int EVENT_NOTIFICATION_CHANGED = 3; - 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; /** - * arg1: network type - * arg2: condition (0 bad, 100 good) + * ------------------------------------------------------------- + * Event Interface back to ConnectivityService. + * + * The events that are to be sent back to the Handler passed + * to startMonitoring when the particular event occurs. + * ------------------------------------------------------------- */ - public static final int EVENT_INET_CONDITION_CHANGE = 8; + /** - * arg1: network type + * The network state has changed and the NetworkInfo object + * contains the new state. + * + * msg.what = EVENT_STATE_CHANGED + * msg.obj = NetworkInfo object */ - public static final int EVENT_INET_CONDITION_HOLD_END = 9; - - public NetworkStateTracker(Context context, - Handler target, - int networkType, - int subType, - String typeName, - String subtypeName) { - super(); - mContext = context; - mTarget = target; - mTeardownRequested = false; - - this.mNetworkInfo = new NetworkInfo(networkType, subType, typeName, subtypeName); - } - - public NetworkInfo getNetworkInfo() { - return mNetworkInfo; - } + public static final int EVENT_STATE_CHANGED = 1; /** - * Return the system properties name associated with the tcp buffer sizes - * for this network. + * msg.what = EVENT_CONFIGURATION_CHANGED + * msg.obj = NetworkInfo object */ - public abstract String getTcpBufferSizesPropName(); + public static final int EVENT_CONFIGURATION_CHANGED = 3; /** - * Return the IP addresses of the DNS servers available for the mobile data - * network interface. - * @return a list of DNS addresses, with no holes. + * msg.what = EVENT_RESTORE_DEFAULT_NETWORK + * msg.obj = FeatureUser object */ - public String[] getNameServers() { - return getNameServerList(mDnsPropNames); - } + public static final int EVENT_RESTORE_DEFAULT_NETWORK = 6; /** - * Return the IP addresses of the DNS servers available for this - * network interface. - * @param propertyNames the names of the system properties whose values - * give the IP addresses. Properties with no values are skipped. - * @return an array of {@code String}s containing the IP addresses - * of the DNS servers, in dot-notation. This may have fewer - * non-null entries than the list of names passed in, since - * some of the passed-in names may have empty values. + * USED by ConnectivityService only + * + * msg.what = EVENT_CLEAR_NET_TRANSITION_WAKELOCK + * msg.arg1 = mNetTransitionWakeLockSerialNumber */ - static protected String[] getNameServerList(String[] propertyNames) { - String[] dnsAddresses = new String[propertyNames.length]; - int i, j; - - for (i = 0, j = 0; i < propertyNames.length; i++) { - String value = SystemProperties.get(propertyNames[i]); - // The GSM layer sometimes sets a bogus DNS server address of - // 0.0.0.0 - if (!TextUtils.isEmpty(value) && !TextUtils.equals(value, "0.0.0.0")) { - dnsAddresses[j++] = value; - } - } - 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)) { - if (DBG) { - Log.d(TAG, "addDefaultRoute for " + mNetworkInfo.getTypeName() + - " (" + mInterfaceName + "), GatewayAddr=" + mDefaultGatewayAddr); - } - NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr); - } - } - - public void removeDefaultRoute() { - if (mInterfaceName != null) { - if (DBG) { - Log.d(TAG, "removeDefaultRoute for " + mNetworkInfo.getTypeName() + " (" + - mInterfaceName + ")"); - } - NetworkUtils.removeDefaultRoute(mInterfaceName); - } - } + public static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 7; /** - * Reads the network specific TCP buffer sizes from SystemProperties - * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system - * wide use + * msg.arg1 = network type + * msg.arg2 = condition (0 bad, 100 good) */ - public void updateNetworkSettings() { - String key = getTcpBufferSizesPropName(); - String bufferSizes = SystemProperties.get(key); - - if (bufferSizes.length() == 0) { - Log.e(TAG, key + " not found in system properties. Using defaults"); - - // Setting to default values so we won't be stuck to previous values - key = "net.tcp.buffersize.default"; - bufferSizes = SystemProperties.get(key); - } - - // Set values in kernel - if (bufferSizes.length() != 0) { - if (DBG) { - Log.v(TAG, "Setting TCP values: [" + bufferSizes - + "] which comes from [" + key + "]"); - } - setBufferSize(bufferSizes); - } - } + public static final int EVENT_INET_CONDITION_CHANGE = 8; /** - * Release the wakelock, if any, that may be held while handling a - * disconnect operation. + * msg.arg1 = network type + * msg.arg2 = default connection sequence number */ - public void releaseWakeLock() { - } + public static final int EVENT_INET_CONDITION_HOLD_END = 9; /** - * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max] - * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem - * - * @param bufferSizes in the format of "readMin, readInitial, readMax, - * writeMin, writeInitial, writeMax" + * ------------------------------------------------------------- + * Control Interface + * ------------------------------------------------------------- */ - private void setBufferSize(String bufferSizes) { - try { - String[] values = bufferSizes.split(","); - - if (values.length == 6) { - final String prefix = "/sys/kernel/ipv4/tcp_"; - stringToFile(prefix + "rmem_min", values[0]); - stringToFile(prefix + "rmem_def", values[1]); - stringToFile(prefix + "rmem_max", values[2]); - stringToFile(prefix + "wmem_min", values[3]); - stringToFile(prefix + "wmem_def", values[4]); - stringToFile(prefix + "wmem_max", values[5]); - } else { - Log.e(TAG, "Invalid buffersize string: " + bufferSizes); - } - } catch (IOException e) { - Log.e(TAG, "Can't set tcp buffer sizes:" + e); - } - } - /** - * Writes string to file. Basically same as "echo -n $string > $filename" - * - * @param filename - * @param string - * @throws IOException + * Begin monitoring data connectivity. + * + * This is the first method called when this interface is used. + * + * @param context is the current Android context + * @param target is the Hander to which to return the events. */ - private void stringToFile(String filename, String string) throws IOException { - FileWriter out = new FileWriter(filename); - try { - out.write(string); - } finally { - out.close(); - } - } + public void startMonitoring(Context context, Handler target); /** - * Record the detailed state of a network, and if it is a - * change from the previous state, send a notification to - * any listeners. - * @param state the new @{code DetailedState} + * Fetch NetworkInfo for the network */ - public void setDetailedState(NetworkInfo.DetailedState state) { - setDetailedState(state, null, null); - } + public NetworkInfo getNetworkInfo(); /** - * Record the detailed state of a network, and if it is a - * change from the previous state, send a notification to - * any listeners. - * @param state the new @{code DetailedState} - * @param reason a {@code String} indicating a reason for the state change, - * if one was supplied. May be {@code null}. - * @param extraInfo optional {@code String} providing extra information about the state change + * Return the LinkProperties for the connection. + * + * @return a copy of the LinkProperties, is never null. */ - 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(); - /* - * If a reason was supplied when the CONNECTING state was entered, and no - * reason was supplied for entering the CONNECTED state, then retain the - * reason that was supplied when going to CONNECTING. - */ - if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null - && lastReason != null) - reason = lastReason; - mNetworkInfo.setDetailedState(state, reason, extraInfo); - Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); - msg.sendToTarget(); - } - } - - protected void setDetailedStateInternal(NetworkInfo.DetailedState state) { - mNetworkInfo.setDetailedState(state, null, null); - } + public LinkProperties getLinkProperties(); - public void setTeardownRequested(boolean isRequested) { - mTeardownRequested = isRequested; - } - - public boolean isTeardownRequested() { - return mTeardownRequested; - } - /** - * Send a notification that the results of a scan for network access - * points has completed, and results are available. + * A capability is an Integer/String pair, the capabilities + * are defined in the class LinkSocket#Key. + * + * @return a copy of this connections capabilities, may be empty but never null. */ - protected void sendScanResultsAvailable() { - Message msg = mTarget.obtainMessage(EVENT_SCAN_RESULTS_AVAILABLE, mNetworkInfo); - msg.sendToTarget(); - } + public LinkCapabilities getLinkCapabilities(); /** - * Record the roaming status of the device, and if it is a change from the previous - * status, send a notification to any listeners. - * @param isRoaming {@code true} if the device is now roaming, {@code false} - * if it is no longer roaming. + * Return the system properties name associated with the tcp buffer sizes + * for this network. */ - protected void setRoamingStatus(boolean isRoaming) { - if (isRoaming != mNetworkInfo.isRoaming()) { - mNetworkInfo.setRoaming(isRoaming); - Message msg = mTarget.obtainMessage(EVENT_ROAMING_CHANGED, mNetworkInfo); - msg.sendToTarget(); - } - } - - protected void setSubtype(int subtype, String subtypeName) { - if (mNetworkInfo.isConnected()) { - int oldSubtype = mNetworkInfo.getSubtype(); - if (subtype != oldSubtype) { - mNetworkInfo.setSubtype(subtype, subtypeName); - Message msg = mTarget.obtainMessage( - EVENT_NETWORK_SUBTYPE_CHANGED, oldSubtype, 0, mNetworkInfo); - msg.sendToTarget(); - } - } - } - - public abstract void startMonitoring(); + public String getTcpBufferSizesPropName(); /** * Disable connectivity to a network * @return {@code true} if a teardown occurred, {@code false} if the * teardown did not occur. */ - public abstract boolean teardown(); + public boolean teardown(); /** * Reenable connectivity to a network after a {@link #teardown()}. + * @return {@code true} if we're connected or expect to be connected */ - public abstract boolean reconnect(); + public boolean reconnect(); /** * Turn the wireless radio off for a network. * @param turnOn {@code true} to turn the radio on, {@code false} */ - public abstract boolean setRadio(boolean turnOn); + public boolean setRadio(boolean turnOn); /** * Returns an indication of whether this network is available for * connections. A value of {@code false} means that some quasi-permanent * condition prevents connectivity to this network. */ - public abstract boolean isAvailable(); + public boolean isAvailable(); + + /** + * Fetch default gateway address for the network + */ + public int getDefaultGatewayAddr(); /** * Tells the underlying networking system that the caller wants to @@ -381,7 +166,7 @@ public abstract class NetworkStateTracker extends Handler { * implementation+feature combination, except that the value {@code -1} * always indicates failure. */ - public abstract int startUsingNetworkFeature(String feature, int callingPid, int callingUid); + public int startUsingNetworkFeature(String feature, int callingPid, int callingUid); /** * Tells the underlying networking system that the caller is finished @@ -395,23 +180,43 @@ public abstract class NetworkStateTracker extends Handler { * implementation+feature combination, except that the value {@code -1} * always indicates failure. */ - public abstract int stopUsingNetworkFeature(String feature, int callingPid, int callingUid); + public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid); + + /** + * ------------------------------------------------------------- + * Storage API used by ConnectivityService for saving + * Network specific information. + * ------------------------------------------------------------- + */ + + /** + * Check if private DNS route is set for the network + */ + public boolean isPrivateDnsRouteSet(); + + /** + * Set a flag indicating private DNS route is set + */ + public void privateDnsRouteSet(boolean enabled); + + /** + * Check if default route is set + */ + public boolean isDefaultRouteSet(); + + /** + * Set a flag indicating default route is set for the network + */ + public void defaultRouteSet(boolean enabled); /** - * Ensure that a network route exists to deliver traffic to the specified - * host via this network interface. - * @param hostAddress the IP address of the host to which the route is desired - * @return {@code true} on success, {@code false} on failure + * Check if tear down was requested */ - public boolean requestRouteToHost(int hostAddress) { - return false; - } + public boolean isTeardownRequested(); /** - * Interprets scan results. This will be called at a safe time for - * processing, and from a safe thread. + * Indicate tear down requested from connectivity */ - public void interpretScanResultsAvailable() { - } + public void setTeardownRequested(boolean isRequested); } diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index e4f3d5c0f104feb28365965ed8dbdae12decd5c4..01004c24cd042305cc855ec4c4ebf31f9ba6bb3d 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -17,28 +17,46 @@ package android.net; import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.UnknownHostException; +import android.util.Log; + /** * Native methods for managing network interfaces. * * {@hide} */ public class NetworkUtils { + + private static final String TAG = "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); - /** Add a route to the specified host via the named interface. */ - public native static int addHostRoute(String interfaceName, int hostaddr); - - /** Add a default route for the named interface. */ - public native static int setDefaultRoute(String interfaceName, int gwayAddr); + /** + * Add a route to the routing table. + * + * @param interfaceName the interface to route through. + * @param dst the network or host to route to. May be IPv4 or IPv6, e.g. + * "0.0.0.0" or "2001:4860::". + * @param prefixLength the prefix length of the route. + * @param gw the gateway to use, e.g., "192.168.251.1". If null, + * indicates a directly-connected route. + */ + public native static int addRoute(String interfaceName, String dst, + int prefixLength, String gw); /** Return the gateway address for the default route for the named interface. */ - public native static int getDefaultRoute(String interfaceName); + public static InetAddress getDefaultRoute(String interfaceName) { + int addr = getDefaultRouteNative(interfaceName); + return intToInetAddress(addr); + } + private native static int getDefaultRouteNative(String interfaceName); /** Remove host routes that uses the named interface. */ public native static int removeHostRoutes(String interfaceName); @@ -106,41 +124,78 @@ public class NetworkUtils { String interfaceName, int ipAddress, int netmask, int gateway, int dns1, int dns2); /** - * Look up a host name and return the result as an int. Works if the argument - * is an IP address in dot notation. Obviously, this can only be used for IPv4 - * addresses. - * @param hostname the name of the host (or the IP address) - * @return the IP address as an {@code int} in network byte order + * Convert a IPv4 address from an integer to an InetAddress. + * @param hostAddr is an Int corresponding to the IPv4 address in network byte order + * @return the IP address as an {@code InetAddress}, returns null if + * unable to convert or if the int is an invalid address. */ - public static int lookupHost(String hostname) { + public static InetAddress intToInetAddress(int hostAddress) { InetAddress inetAddress; + byte[] addressBytes = { (byte)(0xff & hostAddress), + (byte)(0xff & (hostAddress >> 8)), + (byte)(0xff & (hostAddress >> 16)), + (byte)(0xff & (hostAddress >> 24)) }; + try { - inetAddress = InetAddress.getByName(hostname); - } catch (UnknownHostException e) { - return -1; + inetAddress = InetAddress.getByAddress(addressBytes); + } catch(UnknownHostException e) { + return null; } - byte[] addrBytes; - int addr; - addrBytes = inetAddress.getAddress(); - addr = ((addrBytes[3] & 0xff) << 24) - | ((addrBytes[2] & 0xff) << 16) - | ((addrBytes[1] & 0xff) << 8) - | (addrBytes[0] & 0xff); - return addr; + + return inetAddress; } - public static int v4StringToInt(String str) { - int result = 0; - String[] array = str.split("\\."); - if (array.length != 4) return 0; - try { - result = Integer.parseInt(array[3]); - result = (result << 8) + Integer.parseInt(array[2]); - result = (result << 8) + Integer.parseInt(array[1]); - result = (result << 8) + Integer.parseInt(array[0]); - } catch (NumberFormatException e) { - return 0; + /** + * Add a default route through the specified gateway. + * @param interfaceName interface on which the route should be added + * @param gw the IP address of the gateway to which the route is desired, + * @return {@code true} on success, {@code false} on failure + */ + public static boolean addDefaultRoute(String interfaceName, InetAddress gw) { + String dstStr; + String gwStr = gw.getHostAddress(); + + if (gw instanceof Inet4Address) { + dstStr = "0.0.0.0"; + } else if (gw instanceof Inet6Address) { + dstStr = "::"; + } else { + Log.w(TAG, "addDefaultRoute failure: address is neither IPv4 nor IPv6" + + "(" + gwStr + ")"); + return false; + } + return addRoute(interfaceName, dstStr, 0, gwStr) == 0; + } + + /** + * Add a host route. + * @param interfaceName interface on which the route should be added + * @param dst the IP address of the host to which the route is desired, + * this should not be null. + * @param gw the IP address of the gateway to which the route is desired, + * if null, indicates a directly-connected route. + * @return {@code true} on success, {@code false} on failure + */ + public static boolean addHostRoute(String interfaceName, InetAddress dst, + InetAddress gw) { + if (dst == null) { + Log.w(TAG, "addHostRoute: dst should not be null"); + return false; + } + + int prefixLength; + String dstStr = dst.getHostAddress(); + String gwStr = (gw != null) ? gw.getHostAddress() : null; + + if (dst instanceof Inet4Address) { + prefixLength = 32; + } else if (dst instanceof Inet6Address) { + prefixLength = 128; + } else { + Log.w(TAG, "addHostRoute failure: address is neither IPv4 nor IPv6" + + "(" + dst + ")"); + return false; } - return result; + return addRoute(interfaceName, dstStr, prefixLength, gwStr) == 0; } } diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java index 66eefb26af97fbafcee49d6a261bbffcfd971a99..830ff06b5c62445011e3774ed5a7aa62343e1574 100644 --- a/core/java/android/net/Proxy.java +++ b/core/java/android/net/Proxy.java @@ -18,24 +18,171 @@ package android.net; import android.content.ContentResolver; import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; import android.os.SystemProperties; +import android.text.TextUtils; import android.provider.Settings; import android.util.Log; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import junit.framework.Assert; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.impl.conn.ProxySelectorRoutePlanner; +import org.apache.http.protocol.HttpContext; + /** * A convenience class for accessing the user and default proxy * settings. */ -final public class Proxy { +public final class Proxy { // Set to true to enable extra debugging. - static final private boolean DEBUG = false; + private static final boolean DEBUG = false; - static final public String PROXY_CHANGE_ACTION = + // Used to notify an app that's caching the default connection proxy + // that either the default connection or its proxy has changed + public static final String PROXY_CHANGE_ACTION = "android.intent.action.PROXY_CHANGE"; + private static ReadWriteLock sProxyInfoLock = new ReentrantReadWriteLock(); + + private static SettingsObserver sGlobalProxyChangedObserver = null; + + private static ProxySpec sGlobalProxySpec = null; + + private static ConnectivityManager sConnectivityManager = null; + + // Hostname / IP REGEX validation + // Matches blank input, ips, and domain names + private static final String NAME_IP_REGEX = + "[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*"; + + private static final String HOSTNAME_REGEXP = "^$|^" + NAME_IP_REGEX + "$"; + + private static final Pattern HOSTNAME_PATTERN; + + private static final String EXCLLIST_REGEXP = "$|^(.?" + NAME_IP_REGEX + + ")+(,(.?" + NAME_IP_REGEX + "))*$"; + + private static final Pattern EXCLLIST_PATTERN; + + static { + HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP); + EXCLLIST_PATTERN = Pattern.compile(EXCLLIST_REGEXP); + } + + // useful because it holds the processed exclusion list - don't want to reparse it each time + private static class ProxySpec { + String[] exclusionList; + InetSocketAddress address = null; + public ProxySpec() { + exclusionList = new String[0]; + }; + } + + private static boolean isURLInExclusionList(String url, String[] exclusionList) { + if (url == null) { + return false; + } + Uri u = Uri.parse(url); + String urlDomain = u.getHost(); + // If the domain is defined as ".android.com" or "android.com", we wish to match + // http://android.com as well as http://xxx.android.com , but not + // http://myandroid.com . This code works out the logic. + for (String excludedDomain : exclusionList) { + String dotDomain = "." + excludedDomain; + if (urlDomain.equals(excludedDomain)) { + return true; + } + if (urlDomain.endsWith(dotDomain)) { + return true; + } + } + // No match + return false; + } + + private static String parseHost(String proxySpec) { + int i = proxySpec.indexOf(':'); + if (i == -1) { + if (DEBUG) { + Assert.assertTrue(proxySpec.length() == 0); + } + return null; + } + return proxySpec.substring(0, i); + } + + private static int parsePort(String proxySpec) { + int i = proxySpec.indexOf(':'); + if (i == -1) { + if (DEBUG) { + Assert.assertTrue(proxySpec.length() == 0); + } + return -1; + } + if (DEBUG) { + Assert.assertTrue(i < proxySpec.length()); + } + return Integer.parseInt(proxySpec.substring(i+1)); + } + + /** + * Return the proxy object to be used for the URL given as parameter. + * @param ctx A Context used to get the settings for the proxy host. + * @param url A URL to be accessed. Used to evaluate exclusion list. + * @return Proxy (java.net) object containing the host name. If the + * user did not set a hostname it returns the default host. + * A null value means that no host is to be used. + * {@hide} + */ + public static final java.net.Proxy getProxy(Context ctx, String url) { + sProxyInfoLock.readLock().lock(); + java.net.Proxy retval; + try { + if (sGlobalProxyChangedObserver == null) { + registerContentObserversReadLocked(ctx); + parseGlobalProxyInfoReadLocked(ctx); + } + if (sGlobalProxySpec != null) { + // Proxy defined - Apply exclusion rules + if (isURLInExclusionList(url, sGlobalProxySpec.exclusionList)) { + // Return no proxy + retval = java.net.Proxy.NO_PROXY; + } else { + retval = + new java.net.Proxy(java.net.Proxy.Type.HTTP, sGlobalProxySpec.address); + } + } else { + retval = getDefaultProxy(ctx, url); + } + } finally { + sProxyInfoLock.readLock().unlock(); + } + if ((retval != java.net.Proxy.NO_PROXY) && (isLocalHost(url))) { + retval = java.net.Proxy.NO_PROXY; + } + return retval; + } + + // TODO: deprecate this function /** * Return the proxy host set by the user. * @param ctx A Context used to get the settings for the proxy host. @@ -43,81 +190,291 @@ final public class Proxy { * name it returns the default host. A null value means that no * host is to be used. */ - static final public String getHost(Context ctx) { - ContentResolver contentResolver = ctx.getContentResolver(); - Assert.assertNotNull(contentResolver); - String host = Settings.Secure.getString( - contentResolver, - Settings.Secure.HTTP_PROXY); - if (host != null) { - int i = host.indexOf(':'); - if (i == -1) { - if (DEBUG) { - Assert.assertTrue(host.length() == 0); - } - return null; - } - return host.substring(0, i); + public static final String getHost(Context ctx) { + java.net.Proxy proxy = getProxy(ctx, null); + if (proxy == java.net.Proxy.NO_PROXY) return null; + try { + return ((InetSocketAddress)(proxy.address())).getHostName(); + } catch (Exception e) { + return null; } - return getDefaultHost(); } + // TODO: deprecate this function /** * Return the proxy port set by the user. * @param ctx A Context used to get the settings for the proxy port. * @return The port number to use or -1 if no proxy is to be used. */ - static final public int getPort(Context ctx) { - ContentResolver contentResolver = ctx.getContentResolver(); - Assert.assertNotNull(contentResolver); - String host = Settings.Secure.getString( - contentResolver, - Settings.Secure.HTTP_PROXY); - if (host != null) { - int i = host.indexOf(':'); - if (i == -1) { - if (DEBUG) { - Assert.assertTrue(host.length() == 0); - } - return -1; - } - if (DEBUG) { - Assert.assertTrue(i < host.length()); - } - return Integer.parseInt(host.substring(i+1)); + public static final int getPort(Context ctx) { + java.net.Proxy proxy = getProxy(ctx, null); + if (proxy == java.net.Proxy.NO_PROXY) return -1; + try { + return ((InetSocketAddress)(proxy.address())).getPort(); + } catch (Exception e) { + return -1; } - return getDefaultPort(); } + // TODO: deprecate this function /** * Return the default proxy host specified by the carrier. * @return String containing the host name or null if there is no proxy for * this carrier. */ - static final public String getDefaultHost() { - String host = SystemProperties.get("net.gprs.http-proxy"); - if (host != null) { - Uri u = Uri.parse(host); - host = u.getHost(); - return host; - } else { - return null; - } + public static final String getDefaultHost() { + return null; } + // TODO: deprecate this function /** * Return the default proxy port specified by the carrier. * @return The port number to be used with the proxy host or -1 if there is * no proxy for this carrier. */ - static final public int getDefaultPort() { - String host = SystemProperties.get("net.gprs.http-proxy"); - if (host != null) { - Uri u = Uri.parse(host); - return u.getPort(); + public static final int getDefaultPort() { + return -1; + } + + // TODO - cache the details for each network so we don't have to fetch and parse + // on each request + private static final java.net.Proxy getDefaultProxy(Context context, String url) { + if (sConnectivityManager == null) { + sConnectivityManager = (ConnectivityManager)context.getSystemService( + Context.CONNECTIVITY_SERVICE); + } + if (sConnectivityManager == null) return java.net.Proxy.NO_PROXY; + + LinkProperties linkProperties = sConnectivityManager.getActiveLinkProperties(); + + if (linkProperties != null) { + ProxyProperties proxyProperties = linkProperties.getHttpProxy(); + + if (proxyProperties != null) { + String exclusionList = proxyProperties.getExclusionList(); + SocketAddress socketAddr = proxyProperties.getSocketAddress(); + if (socketAddr != null) { + String[] parsedExclusionArray = + parsedExclusionArray = parseExclusionList(exclusionList); + if (!isURLInExclusionList(url, parsedExclusionArray)) { + return new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddr); + } + } + } + } + return java.net.Proxy.NO_PROXY; + } + + // TODO: remove this function / deprecate + /** + * Returns the preferred proxy to be used by clients. This is a wrapper + * around {@link android.net.Proxy#getHost()}. Currently no proxy will + * be returned for localhost or if the active network is Wi-Fi. + * + * @param context the context which will be passed to + * {@link android.net.Proxy#getHost()} + * @param url the target URL for the request + * @note Calling this method requires permission + * android.permission.ACCESS_NETWORK_STATE + * @return The preferred proxy to be used by clients, or null if there + * is no proxy. + * {@hide} + */ + public static final HttpHost getPreferredHttpHost(Context context, + String url) { + java.net.Proxy prefProxy = getProxy(context, url); + if (prefProxy.equals(java.net.Proxy.NO_PROXY)) { + return null; } else { - return -1; + InetSocketAddress sa = (InetSocketAddress)prefProxy.address(); + return new HttpHost(sa.getHostName(), sa.getPort(), "http"); + } + } + + private static final boolean isLocalHost(String url) { + if (url == null) { + return false; + } + try { + final URI uri = URI.create(url); + final String host = uri.getHost(); + if (host != null) { + if (host.equalsIgnoreCase("localhost")) { + return true; + } + if (InetAddress.getByName(host).isLoopbackAddress()) { + return true; + } + } + } catch (UnknownHostException uex) { + // Ignore (INetworkSystem.ipStringToByteArray) + } catch (IllegalArgumentException iex) { + // Ignore (URI.create) + } + return false; + } + + private static class SettingsObserver extends ContentObserver { + + private Context mContext; + + SettingsObserver(Context ctx) { + super(new Handler(ctx.getMainLooper())); + mContext = ctx; + } + + @Override + public void onChange(boolean selfChange) { + sProxyInfoLock.readLock().lock(); + parseGlobalProxyInfoReadLocked(mContext); + sProxyInfoLock.readLock().unlock(); + } + } + + private static final void registerContentObserversReadLocked(Context ctx) { + Uri uriGlobalProxy = Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY); + Uri uriGlobalExclList = + Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY_EXCLUSION_LIST); + + // No lock upgrading (from read to write) allowed + sProxyInfoLock.readLock().unlock(); + sProxyInfoLock.writeLock().lock(); + try { + sGlobalProxyChangedObserver = new SettingsObserver(ctx); + } finally { + // Downgrading locks (from write to read) is allowed + sProxyInfoLock.readLock().lock(); + sProxyInfoLock.writeLock().unlock(); + } + ctx.getContentResolver().registerContentObserver(uriGlobalProxy, false, + sGlobalProxyChangedObserver); + ctx.getContentResolver().registerContentObserver(uriGlobalExclList, false, + sGlobalProxyChangedObserver); + } + + private static final void parseGlobalProxyInfoReadLocked(Context ctx) { + ContentResolver contentResolver = ctx.getContentResolver(); + String proxyHost = Settings.Secure.getString( + contentResolver, + Settings.Secure.HTTP_PROXY); + if (TextUtils.isEmpty(proxyHost)) { + // Clear signal + sProxyInfoLock.readLock().unlock(); + sProxyInfoLock.writeLock().lock(); + sGlobalProxySpec = null; + sProxyInfoLock.readLock().lock(); + sProxyInfoLock.writeLock().unlock(); + return; + } + String exclusionListSpec = Settings.Secure.getString( + contentResolver, + Settings.Secure.HTTP_PROXY_EXCLUSION_LIST); + String host = parseHost(proxyHost); + int port = parsePort(proxyHost); + ProxySpec tmpProxySpec = null; + if (proxyHost != null) { + tmpProxySpec = new ProxySpec(); + tmpProxySpec.address = new InetSocketAddress(host, port); + tmpProxySpec.exclusionList = parseExclusionList(exclusionListSpec); } + sProxyInfoLock.readLock().unlock(); + sProxyInfoLock.writeLock().lock(); + sGlobalProxySpec = tmpProxySpec; + sProxyInfoLock.readLock().lock(); + sProxyInfoLock.writeLock().unlock(); } -}; + private static String[] parseExclusionList(String exclusionList) { + String[] processedArray = new String[0]; + if (!TextUtils.isEmpty(exclusionList)) { + String[] exclusionListArray = exclusionList.toLowerCase().split(","); + processedArray = new String[exclusionListArray.length]; + for (int i = 0; i < exclusionListArray.length; i++) { + String entry = exclusionListArray[i].trim(); + if (entry.startsWith(".")) { + entry = entry.substring(1); + } + processedArray[i] = entry; + } + } + return processedArray; + } + + /** + * Validate syntax of hostname, port and exclusion list entries + * {@hide} + */ + public static void validate(String hostname, String port, String exclList) { + Matcher match = HOSTNAME_PATTERN.matcher(hostname); + Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList); + + if (!match.matches()) { + throw new IllegalArgumentException(); + } + + if (!listMatch.matches()) { + throw new IllegalArgumentException(); + } + + if (hostname.length() > 0 && port.length() == 0) { + throw new IllegalArgumentException(); + } + + if (port.length() > 0) { + if (hostname.length() == 0) { + throw new IllegalArgumentException(); + } + int portVal = -1; + try { + portVal = Integer.parseInt(port); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(); + } + if (portVal <= 0 || portVal > 0xFFFF) { + throw new IllegalArgumentException(); + } + } + } + + static class AndroidProxySelectorRoutePlanner + extends org.apache.http.impl.conn.ProxySelectorRoutePlanner { + + private Context mContext; + + public AndroidProxySelectorRoutePlanner(SchemeRegistry schreg, ProxySelector prosel, + Context context) { + super(schreg, prosel); + mContext = context; + } + + @Override + protected java.net.Proxy chooseProxy(List proxies, HttpHost target, + HttpRequest request, HttpContext context) { + return getProxy(mContext, target.getHostName()); + } + + @Override + protected HttpHost determineProxy(HttpHost target, HttpRequest request, + HttpContext context) { + return getPreferredHttpHost(mContext, target.getHostName()); + } + + @Override + public HttpRoute determineRoute(HttpHost target, HttpRequest request, + HttpContext context) { + HttpHost proxy = getPreferredHttpHost(mContext, target.getHostName()); + if (proxy == null) { + return new HttpRoute(target); + } else { + return new HttpRoute(target, null, proxy, false); + } + } + } + + /** @hide */ + public static final HttpRoutePlanner getAndroidProxySelectorRoutePlanner(Context context) { + AndroidProxySelectorRoutePlanner ret = new AndroidProxySelectorRoutePlanner( + new SchemeRegistry(), ProxySelector.getDefault(), context); + return ret; + } +} diff --git a/core/java/android/net/ProxyProperties.java b/core/java/android/net/ProxyProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..140b71fd129af628e0849c089e905ee6ca10e679 --- /dev/null +++ b/core/java/android/net/ProxyProperties.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + + +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +/** + * A container class for the http proxy info + * @hide + */ +public class ProxyProperties implements Parcelable { + + private InetSocketAddress mProxy; + private String mExclusionList; + + public ProxyProperties() { + } + + // copy constructor instead of clone + public ProxyProperties(ProxyProperties source) { + if (source != null) { + mProxy = source.getSocketAddress(); + String exclusionList = source.getExclusionList(); + if (exclusionList != null) { + mExclusionList = new String(exclusionList); + } + } + } + + public InetSocketAddress getSocketAddress() { + return mProxy; + } + + public void setSocketAddress(InetSocketAddress proxy) { + mProxy = proxy; + } + + public String getExclusionList() { + return mExclusionList; + } + + public void setExclusionList(String exclusionList) { + mExclusionList = exclusionList; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (mProxy != null) { + sb.append(mProxy.toString()); + if (mExclusionList != null) { + sb.append(" xl=").append(mExclusionList); + } + } + return sb.toString(); + } + + /** + * Implement the Parcelable interface + * @hide + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public void writeToParcel(Parcel dest, int flags) { + if (mProxy != null) { + InetAddress addr = mProxy.getAddress(); + if (addr != null) { + dest.writeByte((byte)1); + dest.writeByteArray(addr.getAddress()); + dest.writeInt(mProxy.getPort()); + } + } else { + dest.writeByte((byte)0); + } + dest.writeString(mExclusionList); + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public static final Creator CREATOR = + new Creator() { + public ProxyProperties createFromParcel(Parcel in) { + ProxyProperties proxyProperties = new ProxyProperties(); + if (in.readByte() == 1) { + try { + InetAddress addr = InetAddress.getByAddress(in.createByteArray()); + proxyProperties.setSocketAddress(new InetSocketAddress(addr, in.readInt())); + } catch (UnknownHostException e) { } + } + proxyProperties.setExclusionList(in.readString()); + return proxyProperties; + } + + public ProxyProperties[] newArray(int size) { + return new ProxyProperties[size]; + } + }; +} diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 47faaba8d0c570779b4d7d8c0a255129732d219d..3b21590bbb9b3c83e9a657003b6a1f27078728ca 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -28,8 +28,10 @@ import java.net.URLEncoder; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.RandomAccess; +import java.util.Set; /** * Immutable URI reference. A URI reference includes a URI and a fragment, the @@ -47,7 +49,7 @@ public abstract class Uri implements Parcelable, Comparable { /* This class aims to do as little up front work as possible. To accomplish - that, we vary the implementation dependending on what the user passes in. + that, we vary the implementation depending on what the user passes in. For example, we have one implementation if the user passes in a URI string (StringUri) and another if the user passes in the individual components (OpaqueUri). @@ -1253,14 +1255,16 @@ public abstract class Uri implements Parcelable, Comparable { * concurrent use. * *

    An absolute hierarchical URI reference follows the pattern: - * {@code <scheme>://<authority><absolute path>?<query>#<fragment>} + * {@code ://?#} * *

    Relative URI references (which are always hierarchical) follow one - * of two patterns: {@code <relative or absolute path>?<query>#<fragment>} - * or {@code //<authority><absolute path>?<query>#<fragment>} + * of two patterns: {@code ?#} + * or {@code //?#} * *

    An opaque URI follows this pattern: - * {@code <scheme>:<opaque part>#<fragment>} + * {@code :#} + * + *

    Use {@link Uri#buildUpon()} to obtain a builder representing an existing URI. */ public static final class Builder { @@ -1446,6 +1450,13 @@ public abstract class Uri implements Parcelable, Comparable { return this; } + /** + * Clears the the previously set query. + */ + public Builder clearQuery() { + return query((Part) null); + } + /** * Constructs a Uri with the current attributes. * @@ -1490,6 +1501,45 @@ public abstract class Uri implements Parcelable, Comparable { } } + /** + * Returns a set of the unique names of all query parameters. Iterating + * over the set will return the names in order of their first occurrence. + * + * @throws UnsupportedOperationException if this isn't a hierarchical URI + * + * @return a set of decoded names + */ + public Set getQueryParameterNames() { + if (isOpaque()) { + throw new UnsupportedOperationException(NOT_HIERARCHICAL); + } + + String query = getEncodedQuery(); + if (query == null) { + return Collections.emptySet(); + } + + Set names = new LinkedHashSet(); + int start = 0; + do { + int next = query.indexOf('&', start); + int end = (next == -1) ? query.length() : next; + + int separator = query.indexOf('=', start); + if (separator > end || separator == -1) { + separator = end; + } + + String name = query.substring(start, separator); + names.add(decode(name)); + + // Move start to end of name. + start = end + 1; + } while (start < query.length()); + + return Collections.unmodifiableSet(names); + } + /** * Searches the query string for parameter values with the given key. * @@ -1497,13 +1547,15 @@ public abstract class Uri implements Parcelable, Comparable { * * @throws UnsupportedOperationException if this isn't a hierarchical URI * @throws NullPointerException if key is null - * * @return a list of decoded values */ public List getQueryParameters(String key) { if (isOpaque()) { throw new UnsupportedOperationException(NOT_HIERARCHICAL); } + if (key == null) { + throw new NullPointerException("key"); + } String query = getEncodedQuery(); if (query == null) { @@ -1517,39 +1569,34 @@ public abstract class Uri implements Parcelable, Comparable { throw new AssertionError(e); } - // Prepend query with "&" making the first parameter the same as the - // rest. - query = "&" + query; - - // Parameter prefix. - String prefix = "&" + encodedKey + "="; - ArrayList values = new ArrayList(); int start = 0; - int length = query.length(); - while (start < length) { - start = query.indexOf(prefix, start); + do { + int nextAmpersand = query.indexOf('&', start); + int end = nextAmpersand != -1 ? nextAmpersand : query.length(); - if (start == -1) { - // No more values. - break; + int separator = query.indexOf('=', start); + if (separator > end || separator == -1) { + separator = end; } - // Move start to start of value. - start += prefix.length(); - - // Find end of value. - int end = query.indexOf('&', start); - if (end == -1) { - end = query.length(); + if (separator - start == encodedKey.length() + && query.regionMatches(start, encodedKey, 0, encodedKey.length())) { + if (separator == end) { + values.add(""); + } else { + values.add(decode(query.substring(separator + 1, end))); + } } - String value = query.substring(start, end); - values.add(decode(value)); - - start = end; - } + // Move start to end of name. + if (nextAmpersand != -1) { + start = nextAmpersand + 1; + } else { + break; + } + } while (true); return Collections.unmodifiableList(values); } @@ -1560,7 +1607,6 @@ public abstract class Uri implements Parcelable, Comparable { * @param key which will be encoded * @throws UnsupportedOperationException if this isn't a hierarchical URI * @throws NullPointerException if key is null - * * @return the decoded value or null if no parameter is found */ public String getQueryParameter(String key) { @@ -1568,7 +1614,7 @@ public abstract class Uri implements Parcelable, Comparable { throw new UnsupportedOperationException(NOT_HIERARCHICAL); } if (key == null) { - throw new NullPointerException("key"); + throw new NullPointerException("key"); } final String query = getEncodedQuery(); @@ -1577,37 +1623,54 @@ public abstract class Uri implements Parcelable, Comparable { } final String encodedKey = encode(key, null); - final int encodedKeyLength = encodedKey.length(); - - int encodedKeySearchIndex = 0; - final int encodedKeySearchEnd = query.length() - (encodedKeyLength + 1); + final int length = query.length(); + int start = 0; + do { + int nextAmpersand = query.indexOf('&', start); + int end = nextAmpersand != -1 ? nextAmpersand : length; - while (encodedKeySearchIndex <= encodedKeySearchEnd) { - int keyIndex = query.indexOf(encodedKey, encodedKeySearchIndex); - if (keyIndex == -1) { - break; + int separator = query.indexOf('=', start); + if (separator > end || separator == -1) { + separator = end; } - final int equalsIndex = keyIndex + encodedKeyLength; - if (equalsIndex >= query.length()) { - break; - } - if (query.charAt(equalsIndex) != '=') { - encodedKeySearchIndex = equalsIndex + 1; - continue; - } - if (keyIndex == 0 || query.charAt(keyIndex - 1) == '&') { - int end = query.indexOf('&', equalsIndex); - if (end == -1) { - end = query.length(); + + if (separator - start == encodedKey.length() + && query.regionMatches(start, encodedKey, 0, encodedKey.length())) { + if (separator == end) { + return ""; + } else { + return decode(query.substring(separator + 1, end)); } - return decode(query.substring(equalsIndex + 1, end)); + } + + // Move start to end of name. + if (nextAmpersand != -1) { + start = nextAmpersand + 1; } else { - encodedKeySearchIndex = equalsIndex + 1; + break; } - } + } while (true); return null; } + /** + * Searches the query string for the first value with the given key and interprets it + * as a boolean value. "false" and "0" are interpreted as false, everything + * else is interpreted as true. + * + * @param key which will be decoded + * @param defaultValue the default value to return if there is no query parameter for key + * @return the boolean interpretation of the query parameter key + */ + public boolean getBooleanQueryParameter(String key, boolean defaultValue) { + String flag = getQueryParameter(key); + if (flag == null) { + return defaultValue; + } + flag = flag.toLowerCase(); + return (!"false".equals(flag) && !"0".equals(flag)); + } + /** Identifies a null parcelled Uri. */ private static final int NULL_TYPE_ID = 0; diff --git a/core/java/android/net/WebAddress.java b/core/java/android/net/WebAddress.java index 4101ab489308d3dcab903f2368d84d2b00b2ee18..9c4d6e882b59578ed3b4cec21a02f6d1a0beba58 100644 --- a/core/java/android/net/WebAddress.java +++ b/core/java/android/net/WebAddress.java @@ -56,7 +56,7 @@ public class WebAddress { static Pattern sAddressPattern = Pattern.compile( /* scheme */ "(?:(http|https|file)\\:\\/\\/)?" + /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" + - /* host */ "([-" + GOOD_IRI_CHAR + "%_]+(?:\\.[-" + GOOD_IRI_CHAR + "%_]+)*|\\[[0-9a-fA-F:\\.]+\\])?" + + /* host */ "([" + GOOD_IRI_CHAR + "%_-][" + GOOD_IRI_CHAR + "%_\\.-]*|\\[[0-9a-fA-F:\\.]+\\])?" + /* port */ "(?:\\:([0-9]*))?" + /* path */ "(\\/?[^#]*)?" + /* anchor */ ".*", Pattern.CASE_INSENSITIVE); diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java index e07ee59a3904284e849bbcdc3b41ececf2d1fd8e..915e342b0f29d60efdfa00b56a65e10ca633b137 100644 --- a/core/java/android/net/http/AndroidHttpClient.java +++ b/core/java/android/net/http/AndroidHttpClient.java @@ -61,6 +61,7 @@ import android.content.ContentResolver; import android.net.SSLCertificateSocketFactory; import android.net.SSLSessionCache; import android.os.Looper; +import android.util.Base64; import android.util.Log; /** @@ -81,6 +82,11 @@ public final class AndroidHttpClient implements HttpClient { private static final String TAG = "AndroidHttpClient"; + private static String[] textContentTypes = new String[] { + "text/", + "application/xml", + "application/json" + }; /** Interceptor throws an exception if the executing thread is blocked */ private static final HttpRequestInterceptor sThreadCheckInterceptor = @@ -358,7 +364,7 @@ public final class AndroidHttpClient implements HttpClient { } if (level < Log.VERBOSE || level > Log.ASSERT) { throw new IllegalArgumentException("Level is out of range [" - + Log.VERBOSE + ".." + Log.ASSERT + "]"); + + Log.VERBOSE + ".." + Log.ASSERT + "]"); } curlConfiguration = new LoggingConfiguration(name, level); @@ -431,12 +437,17 @@ public final class AndroidHttpClient implements HttpClient { if (entity.getContentLength() < 1024) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); entity.writeTo(stream); - String entityString = stream.toString(); - // TODO: Check the content type, too. - builder.append(" --data-ascii \"") - .append(entityString) - .append("\""); + if (isBinaryContent(request)) { + String base64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP); + builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; "); + builder.append(" --data-binary @/tmp/$$.bin"); + } else { + String entityString = stream.toString(); + builder.append(" --data-ascii \"") + .append(entityString) + .append("\""); + } } else { builder.append(" [TOO MUCH DATA TO INCLUDE]"); } @@ -446,6 +457,30 @@ public final class AndroidHttpClient implements HttpClient { return builder.toString(); } + private static boolean isBinaryContent(HttpUriRequest request) { + Header[] headers; + headers = request.getHeaders(Headers.CONTENT_ENCODING); + if (headers != null) { + for (Header header : headers) { + if ("gzip".equalsIgnoreCase(header.getValue())) { + return true; + } + } + } + + headers = request.getHeaders(Headers.CONTENT_TYPE); + if (headers != null) { + for (Header header : headers) { + for (String contentType : textContentTypes) { + if (header.getValue().startsWith(contentType)) { + return false; + } + } + } + } + return true; + } + /** * Returns the date of the given HTTP date string. This method can identify * and parse the date formats emitted by common HTTP servers, such as diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java index 4ca59031270ef2abd4ab54e1c04c88fa3036f7d4..503c4706fc488cdc90e8daeee1b55da34fd47de1 100644 --- a/core/java/android/net/http/CertificateChainValidator.java +++ b/core/java/android/net/http/CertificateChainValidator.java @@ -80,14 +80,10 @@ class CertificateChainValidator { throws IOException { X509Certificate[] serverCertificates = null; - // start handshake, close the socket if we fail - try { - sslSocket.setUseClientMode(true); - sslSocket.startHandshake(); - } catch (IOException e) { - closeSocketThrowException( - sslSocket, e.getMessage(), - "failed to perform SSL handshake"); + // get a valid SSLSession, close the socket if we fail + SSLSession sslSession = sslSession = sslSocket.getSession(); + if (!sslSession.isValid()) { + closeSocketThrowException(sslSocket, "failed to perform SSL handshake"); } // retrieve the chain of the server peer certificates diff --git a/core/java/android/net/http/Headers.java b/core/java/android/net/http/Headers.java index 09f6f4f894674005a83542c7d75cfddbb5576216..74c0de871a789b51bbdaa1b217643894918ee5c5 100644 --- a/core/java/android/net/http/Headers.java +++ b/core/java/android/net/http/Headers.java @@ -262,7 +262,14 @@ public final class Headers { break; case HASH_CACHE_CONTROL: if (name.equals(CACHE_CONTROL)) { - mHeaders[IDX_CACHE_CONTROL] = val; + // In case where we receive more than one header, create a ',' separated list. + // This should be ok, according to RFC 2616 chapter 4.2 + if (mHeaders[IDX_CACHE_CONTROL] != null && + mHeaders[IDX_CACHE_CONTROL].length() > 0) { + mHeaders[IDX_CACHE_CONTROL] += (',' + val); + } else { + mHeaders[IDX_CACHE_CONTROL] = val; + } } break; case HASH_LAST_MODIFIED: diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java index b361dca7faa7faddd1cca8de132159438a91cfd1..d77e9d9c153d4cbe95928d3362901f6074cbd225 100644 --- a/core/java/android/net/http/HttpsConnection.java +++ b/core/java/android/net/http/HttpsConnection.java @@ -205,10 +205,13 @@ public class HttpsConnection extends Connection { BasicHttpRequest proxyReq = new BasicHttpRequest ("CONNECT", mHost.toHostString()); - // add all 'proxy' headers from the original request + // add all 'proxy' headers from the original request, we also need + // to add 'host' header unless we want proxy to answer us with a + // 400 Bad Request for (Header h : req.mHttpRequest.getAllHeaders()) { String headerName = h.getName().toLowerCase(); - if (headerName.startsWith("proxy") || headerName.equals("keep-alive")) { + if (headerName.startsWith("proxy") || headerName.equals("keep-alive") + || headerName.equals("host")) { proxyReq.addHeader(h); } } diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 5fb1d7c32879352170abbd6c56c1aa81f5fae175..f0309d68e6561d5c7e86b22f0a05c3269d465fa3 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -16,16 +16,16 @@ package android.os; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.Callable; -import java.util.concurrent.FutureTask; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -36,8 +36,8 @@ import java.util.concurrent.atomic.AtomicInteger; *

    An asynchronous task is defined by a computation that runs on a background thread and * whose result is published on the UI thread. An asynchronous task is defined by 3 generic * types, called Params, Progress and Result, - * and 4 steps, called begin, doInBackground, - * processProgress and end.

    + * and 4 steps, called onPreExecute, doInBackground, + * onProgressUpdate and onPostExecute.

    * *

    Usage

    *

    AsyncTask must be subclassed to be used. The subclass will override at least @@ -123,6 +123,16 @@ import java.util.concurrent.atomic.AtomicInteger; *

  • The task can be executed only once (an exception will be thrown if * a second execution is attempted.)
  • * + * + *

    Memory observability

    + *

    AsyncTask guarantees that all callback calls are synchronized in such a way that the following + * operations are safe without explicit synchronizations.

    + *
      + *
    • Set member fields in the constructor or {@link #onPreExecute}, and refer to them + * in {@link #doInBackground}. + *
    • Set member fields in {@link #doInBackground}, and refer to them in + * {@link #onProgressUpdate} and {@link #onPostExecute}. + *
    */ public abstract class AsyncTask { private static final String LOG_TAG = "AsyncTask"; @@ -175,6 +185,11 @@ public abstract class AsyncTask { FINISHED, } + /** @hide Used to force static handler to be created. */ + public static void init() { + sHandler.getLooper(); + } + /** * Creates a new asynchronous task. This constructor must be invoked on the UI thread. */ @@ -187,6 +202,17 @@ public abstract class AsyncTask { }; mFuture = new FutureTask(mWorker) { + + @Override + protected void set(Result v) { + super.set(v); + if (isCancelled()) { + Message message = sHandler.obtainMessage(MESSAGE_POST_CANCEL, + new AsyncTaskResult(AsyncTask.this, (Result[]) null)); + message.sendToTarget(); + } + } + @Override protected void done() { Message message; @@ -402,14 +428,19 @@ public abstract class AsyncTask { * still running. Each call to this method will trigger the execution of * {@link #onProgressUpdate} on the UI thread. * + * {@link #onProgressUpdate} will note be called if the task has been + * canceled. + * * @param values The progress values to update the UI with. * * @see #onProgressUpdate * @see #doInBackground */ protected final void publishProgress(Progress... values) { - sHandler.obtainMessage(MESSAGE_POST_PROGRESS, - new AsyncTaskResult(this, values)).sendToTarget(); + if (!isCancelled()) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult(this, values)).sendToTarget(); + } } private void finish(Result result) { diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 8925bd05cd83170573c8ab84b832189c26671514..9268cd17f0900cff0961ad7ae3bf0d3f2e7bb7c8 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -199,6 +199,18 @@ public class Build { * */ public static final int GINGERBREAD = CUR_DEVELOPMENT; + + /** + * Next next version of Android. + * + *

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

    + *
      + *
    • The default theme for applications is now dark holographic: + * {@link android.R.style#Theme_Holo}. + *
    + */ + public static final int HONEYCOMB = CUR_DEVELOPMENT; } /** 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 2e14667a0fa4933605f944a0e5ce31cae734307f..a58e70b1e1e8e0d5f49c929089d700dae06820ea 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -94,7 +94,8 @@ public final class Debug /** * Default trace file path and file */ - private static final String DEFAULT_TRACE_PATH_PREFIX = "/sdcard/"; + private static final String DEFAULT_TRACE_PATH_PREFIX = + Environment.getExternalStorageDirectory().getPath() + "/"; private static final String DEFAULT_TRACE_BODY = "dmtrace"; private static final String DEFAULT_TRACE_EXTENSION = ".trace"; private static final String DEFAULT_TRACE_FILE_PATH = @@ -127,7 +128,7 @@ public final class Debug public int otherPrivateDirty; /** The shared dirty pages used by everything else. */ public int otherSharedDirty; - + public MemoryInfo() { } @@ -137,21 +138,21 @@ public final class Debug 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; } @@ -179,7 +180,7 @@ public final class Debug otherPrivateDirty = source.readInt(); otherSharedDirty = source.readInt(); } - + public static final Creator CREATOR = new Creator() { public MemoryInfo createFromParcel(Parcel source) { return new MemoryInfo(source); @@ -460,7 +461,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * Like startMethodTracing(String, int, int), but taking an already-opened * FileDescriptor in which the trace is written. The file name is also * supplied simply for logging. Makes a dup of the file descriptor. - * + * * Not exposed in the SDK unless we are really comfortable with supporting * this and find it would be useful. * @hide @@ -730,7 +731,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** - * Dump "hprof" data to the specified file. This will cause a GC. + * Dump "hprof" data to the specified file. This may cause a GC. * * @param fileName Full pathname of output file (e.g. "/sdcard/dump.hprof"). * @throws UnsupportedOperationException if the VM was built without @@ -742,17 +743,37 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** - * Collect "hprof" and send it to DDMS. This will cause a GC. + * Like dumpHprofData(String), but takes an already-opened + * FileDescriptor to which the trace is written. The file name is also + * supplied simply for logging. Makes a dup of the file descriptor. + * + * Primarily for use by the "am" shell command. + * + * @hide + */ + public static void dumpHprofData(String fileName, FileDescriptor fd) + throws IOException { + VMDebug.dumpHprofData(fileName, fd); + } + + /** + * Collect "hprof" and send it to DDMS. This may cause a GC. * * @throws UnsupportedOperationException if the VM was built without * HPROF support. - * * @hide */ public static void dumpHprofDataDdms() { VMDebug.dumpHprofDataDdms(); } + /** + * Writes native heap data to the specified file descriptor. + * + * @hide + */ + public static native void dumpNativeHeap(FileDescriptor fd); + /** * Returns the number of sent transactions from this process. * @return The number of sent transactions or -1 if it could not read t. @@ -1070,7 +1091,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * static { * // Sets all the fields * Debug.setFieldsOn(MyDebugVars.class); - * + * * // Sets only the fields annotated with @Debug.DebugProperty * // Debug.setFieldsOn(MyDebugVars.class, true); * } diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index a17b7feef38e9ad2cfb59cba949701b3d726e8b1..72e21deaec1957367df6327e66f446c6c742ee2e 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -85,6 +85,8 @@ public class FileUtils public static native int getPermissions(String file, int[] outPermissions); + public static native int setUMask(int mask); + /** returns the FAT file system volume ID for the volume mounted * at the given mount point, or -1 for failure * @param mount point for FAT volume diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java index 3b2bf1e81c80edf30bf05c3514a7cc3ea92ca386..165e438f597d91212ba0e4c11cda08b9e11d838e 100644 --- a/core/java/android/os/Handler.java +++ b/core/java/android/os/Handler.java @@ -58,7 +58,7 @@ import java.lang.reflect.Modifier; * they create. You can create your own threads, and communicate back with * the main application thread through a Handler. This is done by calling * the same post or sendMessage methods as before, but from - * your new thread. The given Runnable or Message will than be scheduled + * your new thread. The given Runnable or Message will then be scheduled * in the Handler's message queue and processed when appropriate. */ public class Handler { diff --git a/core/java/android/os/MemoryFile.java b/core/java/android/os/MemoryFile.java index a81e16bacfb2dcfa00a9eec991490384066b55e3..f82702a5acebcaaff031fbe431dfa2fb6ed3b58a 100644 --- a/core/java/android/os/MemoryFile.java +++ b/core/java/android/os/MemoryFile.java @@ -58,7 +58,6 @@ public class MemoryFile private int mAddress; // address of ashmem memory private int mLength; // total length of our ashmem region private boolean mAllowPurging = false; // true if our ashmem region is unpinned - private final boolean mOwnsRegion; // false if this is a ref to an existing ashmem region /** * Allocates a new ashmem region. The region is initially not purgable. @@ -70,38 +69,11 @@ public class MemoryFile public MemoryFile(String name, int length) throws IOException { mLength = length; mFD = native_open(name, length); - mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE); - mOwnsRegion = true; - } - - /** - * Creates a reference to an existing memory file. Changes to the original file - * will be available through this reference. - * Calls to {@link #allowPurging(boolean)} on the returned MemoryFile will fail. - * - * @param fd File descriptor for an existing memory file, as returned by - * {@link #getFileDescriptor()}. This file descriptor will be closed - * by {@link #close()}. - * @param length Length of the memory file in bytes. - * @param mode File mode. Currently only "r" for read-only access is supported. - * @throws NullPointerException if fd is null. - * @throws IOException If fd does not refer to an existing memory file, - * or if the file mode of the existing memory file is more restrictive - * than mode. - * - * @hide - */ - public MemoryFile(FileDescriptor fd, int length, String mode) throws IOException { - if (fd == null) { - throw new NullPointerException("File descriptor is null."); - } - if (!isMemoryFile(fd)) { - throw new IllegalArgumentException("Not a memory file."); + if (length > 0) { + mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE); + } else { + mAddress = 0; } - mLength = length; - mFD = fd; - mAddress = native_mmap(mFD, length, modeToProt(mode)); - mOwnsRegion = false; } /** @@ -122,7 +94,7 @@ public class MemoryFile * * @hide */ - public void deactivate() { + void deactivate() { if (!isDeactivated()) { try { native_munmap(mAddress, mLength); @@ -181,9 +153,6 @@ public class MemoryFile * @return previous value of allowPurging */ synchronized public boolean allowPurging(boolean allowPurging) throws IOException { - if (!mOwnsRegion) { - throw new IOException("Only the owner can make ashmem regions purgable."); - } boolean oldValue = mAllowPurging; if (oldValue != allowPurging) { native_pin(mFD, !allowPurging); @@ -260,28 +229,7 @@ public class MemoryFile } /** - * Gets a ParcelFileDescriptor for the memory file. See {@link #getFileDescriptor()} - * for caveats. This must be here to allow classes outside android.osfd is not a valid file descriptor. - * - * @hide - */ - public static boolean isMemoryFile(FileDescriptor fd) throws IOException { - return (native_get_size(fd) >= 0); - } - /** * Returns the size of the memory file that the file descriptor refers to, * or -1 if the file descriptor does not refer to a memory file. @@ -316,20 +253,6 @@ public class MemoryFile return native_get_size(fd); } - /** - * Converts a file mode string to a prot value as expected by - * native_mmap(). - * - * @throws IllegalArgumentException if the file mode is invalid. - */ - private static int modeToProt(String mode) { - if ("r".equals(mode)) { - return PROT_READ; - } else { - throw new IllegalArgumentException("Unsupported file mode: '" + mode + "'"); - } - } - private class MemoryInputStream extends InputStream { private int mMark = 0; diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java index 49b72fee22d3f6c3e8bfd7c0bf43aff7f1312391..eb941e43fe63704d1a9c6c1521d8337d94a3f325 100644 --- a/core/java/android/os/Message.java +++ b/core/java/android/os/Message.java @@ -73,7 +73,18 @@ public final class Message implements Parcelable { * receiver. */ public Messenger replyTo; - + + /** If set message is in use */ + /*package*/ static final int FLAG_IN_USE = 1; + + /** Flags reserved for future use (All are reserved for now) */ + /*package*/ static final int FLAGS_RESERVED = ~FLAG_IN_USE; + + /** Flags to clear in the copyFrom method */ + /*package*/ static final int FLAGS_TO_CLEAR_ON_COPY_FROM = FLAGS_RESERVED | FLAG_IN_USE; + + /*package*/ int flags; + /*package*/ long when; /*package*/ Bundle data; @@ -253,6 +264,7 @@ public final class Message implements Parcelable { * target/callback of the original message. */ public void copyFrom(Message o) { + this.flags = o.flags & ~FLAGS_TO_CLEAR_ON_COPY_FROM; this.what = o.what; this.arg1 = o.arg1; this.arg2 = o.arg2; @@ -350,6 +362,7 @@ public final class Message implements Parcelable { } /*package*/ void clearForRecycle() { + flags = 0; what = 0; arg1 = 0; arg2 = 0; @@ -361,6 +374,14 @@ public final class Message implements Parcelable { data = null; } + /*package*/ boolean isInUse() { + return ((flags & FLAG_IN_USE) == FLAG_IN_USE); + } + + /*package*/ void markInUse() { + flags |= FLAG_IN_USE; + } + /** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}). */ public Message() { @@ -453,4 +474,3 @@ public final class Message implements Parcelable { replyTo = Messenger.readMessengerOrNullFromParcel(source); } } - diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index 6237c0e4e523907f9ff6d45d06d002d3afa489f9..be5b685d895f3899db1abdc040b502b1294d826e 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -124,6 +124,7 @@ public class MessageQueue { if (now >= when) { mMessages = msg.next; if (Config.LOGV) Log.v("MessageQueue", "Returning message: " + msg); + msg.markInUse(); return msg; } else { nextPollTimeoutMillis = (int) Math.min(when - now, Integer.MAX_VALUE); @@ -177,7 +178,7 @@ public class MessageQueue { } final boolean enqueueMessage(Message msg, long when) { - if (msg.when != 0) { + if (msg.isInUse()) { throw new AndroidRuntimeException(msg + " This message is already in use."); } diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 9d213b344dc845b9646fe2e6eaafea3d25915437..c1a1809d49621570193c192edf629de0625f248e 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -133,6 +133,45 @@ public class ParcelFileDescriptor implements Parcelable { // Extracts the file descriptor from the specified socket and returns it untouched private static native FileDescriptor getFileDescriptorFromSocket(Socket socket); + /** + * Create two ParcelFileDescriptors structured as a data pipe. The first + * ParcelFileDescriptor in the returned array is the read side; the second + * is the write side. + */ + public static ParcelFileDescriptor[] createPipe() throws IOException { + FileDescriptor[] fds = new FileDescriptor[2]; + int res = createPipeNative(fds); + if (res == 0) { + ParcelFileDescriptor[] pfds = new ParcelFileDescriptor[2]; + pfds[0] = new ParcelFileDescriptor(fds[0]); + pfds[1] = new ParcelFileDescriptor(fds[1]); + return pfds; + } + throw new IOException("Unable to create pipe: errno=" + -res); + } + + private static native int createPipeNative(FileDescriptor[] outFds); + + /** + * Gets a file descriptor for a read-only copy of the given data. + * + * @param data Data to copy. + * @param name Name for the shared memory area that may back the file descriptor. + * This is purely informative and may be {@code null}. + * @return A ParcelFileDescriptor. + * @throws IOException if there is an error while creating the shared memory area. + */ + public static ParcelFileDescriptor fromData(byte[] data, String name) throws IOException { + if (data == null) return null; + MemoryFile file = new MemoryFile(name, data.length); + if (data.length > 0) { + file.writeBytes(data, 0, 0, data.length); + } + file.deactivate(); + FileDescriptor fd = file.getFileDescriptor(); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } + /** * Retrieve the actual FileDescriptor associated with this object. * diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java index 7b883a7e85f7edec6b3d4a88b2304beeda38d969..d3d39d6db782fcee746c4e9f8f628be255e96208 100644 --- a/core/java/android/os/storage/StorageEventListener.java +++ b/core/java/android/os/storage/StorageEventListener.java @@ -18,7 +18,6 @@ package android.os.storage; /** * Used for receiving notifications from the StorageManager - * @hide */ public abstract class StorageEventListener { /** diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index df0b69c9fe248f40487d41f5f7fd47d1f03af4c6..4a0296b2888fe1126ef67c98b7e4565cbe071809 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -30,6 +30,7 @@ import java.util.ArrayList; * Get an instance of this class by calling * {@link android.content.Context#getSystemService(java.lang.String)} with an argument * of {@link android.content.Context#STORAGE_SERVICE}. + * */ public class StorageManager @@ -203,7 +204,6 @@ public class StorageManager * * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. * - * @hide */ public void registerListener(StorageEventListener listener) { if (listener == null) { @@ -220,7 +220,6 @@ public class StorageManager * * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. * - * @hide */ public void unregisterListener(StorageEventListener listener) { if (listener == null) { @@ -241,8 +240,6 @@ public class StorageManager /** * Enables USB Mass Storage (UMS) on the device. - * - * @hide */ public void enableUsbMassStorage() { try { @@ -254,8 +251,6 @@ public class StorageManager /** * Disables USB Mass Storage (UMS) on the device. - * - * @hide */ public void disableUsbMassStorage() { try { @@ -268,8 +263,6 @@ public class StorageManager /** * Query if a USB Mass Storage (UMS) host is connected. * @return true if UMS host is connected. - * - * @hide */ public boolean isUsbMassStorageConnected() { try { @@ -283,8 +276,6 @@ public class StorageManager /** * Query if a USB Mass Storage (UMS) is enabled on the device. * @return true if UMS host is enabled. - * - * @hide */ public boolean isUsbMassStorageEnabled() { try { diff --git a/core/java/android/os/storage/StorageResultCode.java b/core/java/android/os/storage/StorageResultCode.java index 075f47fc6fd5304e1aaccb1495ea0d4b54d22de7..07d95df96bcffdfe1b2f7d00f4e2cd8bca0a6250 100644 --- a/core/java/android/os/storage/StorageResultCode.java +++ b/core/java/android/os/storage/StorageResultCode.java @@ -19,8 +19,6 @@ package android.os.storage; /** * Class that provides access to constants returned from StorageManager * and lower level MountService APIs. - * - * @hide */ public class StorageResultCode { diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index 635323e9fcb0b7444a9df94c5308815a97568bad..282417d42081787c7a91fbf2c878336aafa04ebb 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -181,7 +181,9 @@ public class RecurrenceSet { boolean inUtc = start.parse(dtstart); boolean allDay = start.allDay; - if (inUtc) { + // We force TimeZone to UTC for "all day recurring events" as the server is sending no + // TimeZone in DTSTART for them + if (inUtc || allDay) { tzid = Time.TIMEZONE_UTC; } @@ -204,10 +206,7 @@ public class RecurrenceSet { } if (allDay) { - // TODO: also change tzid to be UTC? that would be consistent, but - // that would not reflect the original timezone value back to the - // server. - start.timezone = Time.TIMEZONE_UTC; + start.timezone = Time.TIMEZONE_UTC; } long millis = start.toMillis(false /* use isDst */); values.put(Calendar.Events.DTSTART, millis); diff --git a/core/java/android/pim/vcard/JapaneseUtils.java b/core/java/android/pim/vcard/JapaneseUtils.java deleted file mode 100644 index dcfe980bfcaf3399f66ae583487d871be73325b6..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/JapaneseUtils.java +++ /dev/null @@ -1,379 +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.pim.vcard; - -import java.util.HashMap; -import java.util.Map; - -/** - * TextUtils especially for Japanese. - */ -/* package */ class JapaneseUtils { - static private final Map sHalfWidthMap = - new HashMap(); - - static { - 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"); - } - - /** - * Returns half-width version of that character if possible. Returns null if not possible - * @param ch input character - * @return CharSequence object if the mapping for ch exists. Return null otherwise. - */ - public static String tryGetHalfWidthText(final char ch) { - if (sHalfWidthMap.containsKey(ch)) { - return sHalfWidthMap.get(ch); - } else { - return null; - } - } -} diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java deleted file mode 100644 index b2007f287836da6868c4fc80a83bf3e20a9ad69c..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardBuilder.java +++ /dev/null @@ -1,2145 +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.pim.vcard; - -import android.content.ContentValues; -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.Relation; -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.Base64; -import android.util.CharsetUtils; -import android.util.Log; - -import java.io.UnsupportedEncodingException; -import java.nio.charset.UnsupportedCharsetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - *

    - * The class which lets users create their own vCard String. Typical usage is as follows: - *

    - *
    final VCardBuilder builder = new VCardBuilder(vcardType);
    - * builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
    - *     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
    - *     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
    - *     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
    - *     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
    - *     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
    - *     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE))
    - *     .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
    - *     .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
    - *     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
    - *     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
    - *     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
    - * return builder.toString();
    - */ -public class VCardBuilder { - private static final String LOG_TAG = "VCardBuilder"; - - // If you add the other element, please check all the columns are able to be - // converted to String. - // - // e.g. BLOB is not what we can handle here now. - private static final Set sAllowedAndroidPropertySet = - Collections.unmodifiableSet(new HashSet(Arrays.asList( - Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, - Relation.CONTENT_ITEM_TYPE))); - - public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; - public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; - public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; - - private static final String VCARD_DATA_VCARD = "VCARD"; - private static final String VCARD_DATA_PUBLIC = "PUBLIC"; - - private static final String VCARD_PARAM_SEPARATOR = ";"; - private static final String VCARD_END_OF_LINE = "\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_PARAM_EQUAL = "="; - - private static final String VCARD_PARAM_ENCODING_QP = - "ENCODING=" + VCardConstants.PARAM_ENCODING_QP; - private static final String VCARD_PARAM_ENCODING_BASE64_V21 = - "ENCODING=" + VCardConstants.PARAM_ENCODING_BASE64; - private static final String VCARD_PARAM_ENCODING_BASE64_AS_B = - "ENCODING=" + VCardConstants.PARAM_ENCODING_B; - - private static final String SHIFT_JIS = "SHIFT_JIS"; - - private final int mVCardType; - - private final boolean mIsV30OrV40; - private final boolean mIsJapaneseMobilePhone; - private final boolean mOnlyOneNoteFieldIsAvailable; - private final boolean mIsDoCoMo; - private final boolean mShouldUseQuotedPrintable; - private final boolean mUsesAndroidProperty; - private final boolean mUsesDefactProperty; - private final boolean mAppendTypeParamName; - private final boolean mRefrainsQPToNameProperties; - private final boolean mNeedsToConvertPhoneticString; - - private final boolean mShouldAppendCharsetParam; - - private final String mCharset; - private final String mVCardCharsetParameter; - - private StringBuilder mBuilder; - private boolean mEndAppended; - - public VCardBuilder(final int vcardType) { - // Default charset should be used - this(vcardType, null); - } - - /** - * @param vcardType - * @param charset If null, we use default charset for export. - * @hide - */ - public VCardBuilder(final int vcardType, String charset) { - mVCardType = vcardType; - - Log.w(LOG_TAG, - "Should not use vCard 4.0 when building vCard. " + - "It is not officially published yet."); - - mIsV30OrV40 = VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType); - mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType); - mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType); - mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); - mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); - mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); - mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType); - mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); - mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); - - // vCard 2.1 requires charset. - // vCard 3.0 does not allow it but we found some devices use it to determine - // the exact charset. - // We currently append it only when charset other than UTF_8 is used. - mShouldAppendCharsetParam = - !(VCardConfig.isVersion30(vcardType) && "UTF-8".equalsIgnoreCase(charset)); - - if (VCardConfig.isDoCoMo(vcardType)) { - if (!SHIFT_JIS.equalsIgnoreCase(charset)) { - Log.w(LOG_TAG, - "The charset \"" + charset + "\" is used while " - + SHIFT_JIS + " is needed to be used."); - if (TextUtils.isEmpty(charset)) { - mCharset = SHIFT_JIS; - } else { - try { - charset = CharsetUtils.charsetForVendor(charset).name(); - } catch (UnsupportedCharsetException e) { - Log.i(LOG_TAG, - "Career-specific \"" + charset + "\" was not found (as usual). " - + "Use it as is."); - } - mCharset = charset; - } - } else { - if (mIsDoCoMo) { - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, - "DoCoMo-specific SHIFT_JIS was not found. " - + "Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - } else { - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, - "Career-specific SHIFT_JIS was not found. " - + "Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - } - mCharset = charset; - } - mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; - } else { - if (TextUtils.isEmpty(charset)) { - Log.i(LOG_TAG, - "Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET - + "\" for export."); - mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET; - mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET; - } else { - try { - charset = CharsetUtils.charsetForVendor(charset).name(); - } catch (UnsupportedCharsetException e) { - Log.i(LOG_TAG, - "Career-specific \"" + charset + "\" was not found (as usual). " - + "Use it as is."); - } - mCharset = charset; - mVCardCharsetParameter = "CHARSET=" + charset; - } - } - clear(); - } - - public void clear() { - mBuilder = new StringBuilder(); - mEndAppended = false; - appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (VCardConfig.isVersion40(mVCardType)) { - appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V40); - } else if (VCardConfig.isVersion30(mVCardType)) { - appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30); - } else { - if (!VCardConfig.isVersion21(mVCardType)) { - Log.w(LOG_TAG, "Unknown vCard version detected."); - } - appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21); - } - } - - private boolean containsNonEmptyName(final 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 phoneticFamilyName = - contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); - final String phoneticMiddleName = - contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - final String phoneticGivenName = - contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); - 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(phoneticFamilyName) && - TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) && - TextUtils.isEmpty(displayName)); - } - - private ContentValues getPrimaryContentValue(final List contentValuesList) { - 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. - final 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(); - } - } - - return primaryContentValues; - } - - /** - * To avoid unnecessary complication in logic, we use this method to construct N, FN - * properties for vCard 4.0. - */ - private VCardBuilder appendNamePropertiesV40(final List contentValuesList) { - if (mIsDoCoMo || mNeedsToConvertPhoneticString) { - // Ignore all flags that look stale from the view of vCard 4.0 to - // simplify construction algorithm. Actually we don't have any vCard file - // available from real world yet, so we may need to re-enable some of these - // in the future. - Log.w(LOG_TAG, "Invalid flag is used in vCard 4.0 construction. Ignored."); - } - - if (contentValuesList == null || contentValuesList.isEmpty()) { - appendLine(VCardConstants.PROPERTY_FN, ""); - return this; - } - - // We have difficulty here. How can we appropriately handle StructuredName with - // missing parts necessary for displaying while it has suppremental information. - // - // e.g. How to handle non-empty phonetic names with empty structured names? - - final ContentValues contentValues = getPrimaryContentValue(contentValuesList); - 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 formattedName = contentValues.getAsString(StructuredName.DISPLAY_NAME); - if (TextUtils.isEmpty(familyName) - && TextUtils.isEmpty(givenName) - && TextUtils.isEmpty(middleName) - && TextUtils.isEmpty(prefix) - && TextUtils.isEmpty(suffix)) { - if (TextUtils.isEmpty(formattedName)) { - appendLine(VCardConstants.PROPERTY_FN, ""); - return this; - } - familyName = formattedName; - } - - final String phoneticFamilyName = - contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); - final String phoneticMiddleName = - contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - final String phoneticGivenName = - contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); - final String escapedFamily = escapeCharacters(familyName); - final String escapedGiven = escapeCharacters(givenName); - final String escapedMiddle = escapeCharacters(middleName); - final String escapedPrefix = escapeCharacters(prefix); - final String escapedSuffix = escapeCharacters(suffix); - - mBuilder.append(VCardConstants.PROPERTY_N); - - if (!(TextUtils.isEmpty(phoneticFamilyName) && - TextUtils.isEmpty(phoneticMiddleName) && - TextUtils.isEmpty(phoneticGivenName))) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - final String sortAs = escapeCharacters(phoneticFamilyName) - + ';' + escapeCharacters(phoneticGivenName) - + ';' + escapeCharacters(phoneticMiddleName); - mBuilder.append("SORT-AS=").append( - VCardUtils.toStringAsV40ParamValue(sortAs)); - } - - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(escapedFamily); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(escapedGiven); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(escapedMiddle); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(escapedPrefix); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(escapedSuffix); - mBuilder.append(VCARD_END_OF_LINE); - - if (TextUtils.isEmpty(formattedName)) { - // Note: - // DISPLAY_NAME doesn't exist while some other elements do, which is usually - // weird in Android, as DISPLAY_NAME should (usually) be constructed - // from the others using locale information and its code points. - Log.w(LOG_TAG, "DISPLAY_NAME is empty."); - - final String escaped = escapeCharacters(VCardUtils.constructNameFromElements( - VCardConfig.getNameOrderType(mVCardType), - familyName, middleName, givenName, prefix, suffix)); - appendLine(VCardConstants.PROPERTY_FN, escaped); - } else { - final String escapedFormatted = escapeCharacters(formattedName); - mBuilder.append(VCardConstants.PROPERTY_FN); - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(escapedFormatted); - mBuilder.append(VCARD_END_OF_LINE); - } - - // We may need X- properties for phonetic names. - appendPhoneticNameFields(contentValues); - return this; - } - - /** - * 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. - */ - public VCardBuilder appendNameProperties(final List contentValuesList) { - if (VCardConfig.isVersion40(mVCardType)) { - return appendNamePropertiesV40(contentValuesList); - } - - if (contentValuesList == null || contentValuesList.isEmpty()) { - if (VCardConfig.isVersion30(mVCardType)) { - // vCard 3.0 requires "N" and "FN" properties. - // vCard 4.0 does NOT require N, but we take care of possible backward - // compatibility issues. - appendLine(VCardConstants.PROPERTY_N, ""); - appendLine(VCardConstants.PROPERTY_FN, ""); - } else if (mIsDoCoMo) { - appendLine(VCardConstants.PROPERTY_N, ""); - } - return this; - } - - final ContentValues contentValues = getPrimaryContentValue(contentValuesList); - 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); - - if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { - final boolean reallyAppendCharsetParameterToName = - shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix); - final boolean reallyUseQuotedPrintableToName = - (!mRefrainsQPToNameProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); - - final String formattedName; - if (!TextUtils.isEmpty(displayName)) { - formattedName = displayName; - } else { - formattedName = VCardUtils.constructNameFromElements( - VCardConfig.getNameOrderType(mVCardType), - familyName, middleName, givenName, prefix, suffix); - } - final boolean reallyAppendCharsetParameterToFN = - shouldAppendCharsetParam(formattedName); - final boolean reallyUseQuotedPrintableToFN = - !mRefrainsQPToNameProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); - - final String encodedFamily; - final String encodedGiven; - final String encodedMiddle; - final String encodedPrefix; - final String encodedSuffix; - 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); - } - - final String encodedFormattedname = - (reallyUseQuotedPrintableToFN ? - encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); - - mBuilder.append(VCardConstants.PROPERTY_N); - if (mIsDoCoMo) { - if (reallyAppendCharsetParameterToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - // DoCoMo phones require that all the elements in the "family name" field. - mBuilder.append(formattedName); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - } else { - if (reallyAppendCharsetParameterToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedFamily); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedGiven); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedMiddle); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedPrefix); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedSuffix); - } - mBuilder.append(VCARD_END_OF_LINE); - - // FN property - mBuilder.append(VCardConstants.PROPERTY_FN); - if (reallyAppendCharsetParameterToFN) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToFN) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedFormattedname); - mBuilder.append(VCARD_END_OF_LINE); - } else if (!TextUtils.isEmpty(displayName)) { - final boolean reallyUseQuotedPrintableToDisplayName = - (!mRefrainsQPToNameProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); - final String encodedDisplayName = - reallyUseQuotedPrintableToDisplayName ? - encodeQuotedPrintable(displayName) : - escapeCharacters(displayName); - - // N - mBuilder.append(VCardConstants.PROPERTY_N); - if (shouldAppendCharsetParam(displayName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToDisplayName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedDisplayName); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_END_OF_LINE); - - // FN - mBuilder.append(VCardConstants.PROPERTY_FN); - - // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it - // when it would be useful or necessary for external importers, - // assuming the external importer allows this vioration of the spec. - if (shouldAppendCharsetParam(displayName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedDisplayName); - mBuilder.append(VCARD_END_OF_LINE); - } else if (VCardConfig.isVersion30(mVCardType)) { - appendLine(VCardConstants.PROPERTY_N, ""); - appendLine(VCardConstants.PROPERTY_FN, ""); - } else if (mIsDoCoMo) { - appendLine(VCardConstants.PROPERTY_N, ""); - } - - appendPhoneticNameFields(contentValues); - return this; - } - - /** - * Emits SOUND;IRMC, SORT-STRING, and de-fact values for phonetic names like X-PHONETIC-FAMILY. - */ - private void appendPhoneticNameFields(final ContentValues contentValues) { - final String phoneticFamilyName; - final String phoneticMiddleName; - final String phoneticGivenName; - { - final String tmpPhoneticFamilyName = - contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); - final String tmpPhoneticMiddleName = - contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - final String tmpPhoneticGivenName = - contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); - if (mNeedsToConvertPhoneticString) { - phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); - phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); - phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); - } else { - phoneticFamilyName = tmpPhoneticFamilyName; - phoneticMiddleName = tmpPhoneticMiddleName; - phoneticGivenName = tmpPhoneticGivenName; - } - } - - if (TextUtils.isEmpty(phoneticFamilyName) - && TextUtils.isEmpty(phoneticMiddleName) - && TextUtils.isEmpty(phoneticGivenName)) { - if (mIsDoCoMo) { - mBuilder.append(VCardConstants.PROPERTY_SOUND); - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_END_OF_LINE); - } - return; - } - - if (VCardConfig.isVersion40(mVCardType)) { - // We don't want SORT-STRING anyway. - } else if (VCardConfig.isVersion30(mVCardType)) { - final String sortString = - VCardUtils.constructNameFromElements(mVCardType, - phoneticFamilyName, phoneticMiddleName, phoneticGivenName); - mBuilder.append(VCardConstants.PROPERTY_SORT_STRING); - if (VCardConfig.isVersion30(mVCardType) && shouldAppendCharsetParam(sortString)) { - // vCard 3.0 does not force us to use UTF-8 and actually we see some - // programs which emit this value. It is incorrect from the view of - // specification, but actually necessary for parsing vCard with non-UTF-8 - // charsets, expecting other parsers not get confused with this value. - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(escapeCharacters(sortString)); - mBuilder.append(VCARD_END_OF_LINE); - } else if (mIsJapaneseMobilePhone) { - // Note: There is no appropriate property for expressing - // phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in - // vCard 3.0 (SORT-STRING). - // We use DoCoMo's way when the device is Japanese one since it is already - // supported by a lot of Japanese mobile phones. - // This is "X-" property, so any parser hopefully would not get - // confused with this. - // - // Also, DoCoMo's specification requires vCard composer to use just the first - // column. - // i.e. - // good: SOUND;X-IRMC-N:Miyakawa Daisuke;;;; - // bad : SOUND;X-IRMC-N:Miyakawa;Daisuke;;; - mBuilder.append(VCardConstants.PROPERTY_SOUND); - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); - - boolean reallyUseQuotedPrintable = - (!mRefrainsQPToNameProperties - && !(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 (shouldAppendCharsetParam(encodedPhoneticFamilyName, - encodedPhoneticMiddleName, encodedPhoneticGivenName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - { - boolean first = true; - if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { - mBuilder.append(encodedPhoneticFamilyName); - first = false; - } - if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { - if (first) { - first = false; - } else { - mBuilder.append(' '); - } - mBuilder.append(encodedPhoneticMiddleName); - } - if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { - if (!first) { - mBuilder.append(' '); - } - mBuilder.append(encodedPhoneticGivenName); - } - } - mBuilder.append(VCARD_ITEM_SEPARATOR); // family;given - mBuilder.append(VCARD_ITEM_SEPARATOR); // given;middle - mBuilder.append(VCARD_ITEM_SEPARATOR); // middle;prefix - mBuilder.append(VCARD_ITEM_SEPARATOR); // prefix;suffix - mBuilder.append(VCARD_END_OF_LINE); - } - - Log.d("@@@", "hoge"); - if (mUsesDefactProperty) { - if (!TextUtils.isEmpty(phoneticGivenName)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); - final String encodedPhoneticGivenName; - if (reallyUseQuotedPrintable) { - encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); - } else { - encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); - } - mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME); - if (shouldAppendCharsetParam(phoneticGivenName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedPhoneticGivenName); - mBuilder.append(VCARD_END_OF_LINE); - } // if (!TextUtils.isEmpty(phoneticGivenName)) - if (!TextUtils.isEmpty(phoneticMiddleName)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); - final String encodedPhoneticMiddleName; - if (reallyUseQuotedPrintable) { - encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); - } else { - encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); - } - mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME); - if (shouldAppendCharsetParam(phoneticMiddleName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedPhoneticMiddleName); - mBuilder.append(VCARD_END_OF_LINE); - } // if (!TextUtils.isEmpty(phoneticGivenName)) - if (!TextUtils.isEmpty(phoneticFamilyName)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); - final String encodedPhoneticFamilyName; - if (reallyUseQuotedPrintable) { - encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); - } else { - encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); - } - mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME); - if (shouldAppendCharsetParam(phoneticFamilyName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedPhoneticFamilyName); - mBuilder.append(VCARD_END_OF_LINE); - } // if (!TextUtils.isEmpty(phoneticFamilyName)) - } - } - - public VCardBuilder appendNickNames(final List contentValuesList) { - final boolean useAndroidProperty; - if (mIsV30OrV40) { // These specifications have NICKNAME property. - useAndroidProperty = false; - } else if (mUsesAndroidProperty) { - useAndroidProperty = true; - } else { - // There's no way to add this field. - return this; - } - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - final String nickname = contentValues.getAsString(Nickname.NAME); - if (TextUtils.isEmpty(nickname)) { - continue; - } - if (useAndroidProperty) { - appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues); - } else { - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname); - } - } - } - return this; - } - - public VCardBuilder appendPhones(final List contentValuesList) { - 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); - final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - String phoneNumber = contentValues.getAsString(Phone.NUMBER); - if (phoneNumber != null) { - phoneNumber = phoneNumber.trim(); - } - if (TextUtils.isEmpty(phoneNumber)) { - continue; - } - - // PAGER number needs unformatted "phone number". - final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); - if (type == Phone.TYPE_PAGER || - VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { - phoneLineExists = true; - if (!phoneSet.contains(phoneNumber)) { - phoneSet.add(phoneNumber); - appendTelLine(type, label, phoneNumber, isPrimary); - } - } else { - final List phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber); - if (phoneNumberList.isEmpty()) { - continue; - } - phoneLineExists = true; - for (String actualPhoneNumber : phoneNumberList) { - if (!phoneSet.contains(actualPhoneNumber)) { - final int format = VCardUtils.getPhoneNumberFormat(mVCardType); - final String formattedPhoneNumber = - PhoneNumberUtils.formatNumber(actualPhoneNumber, format); - phoneSet.add(actualPhoneNumber); - appendTelLine(type, label, formattedPhoneNumber, isPrimary); - } - } // for (String actualPhoneNumber : phoneNumberList) { - } - } - } - - if (!phoneLineExists && mIsDoCoMo) { - appendTelLine(Phone.TYPE_HOME, "", "", false); - } - - return this; - } - - /** - *

    - * Splits a given string expressing phone numbers into several strings, and remove - * unnecessary characters inside them. The size of a returned list becomes 1 when - * no split is needed. - *

    - *

    - * The given number "may" have several phone numbers when the contact entry is corrupted - * because of its original source. - * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)" - *

    - *

    - * This kind of "phone numbers" will not be created with Android vCard implementation, - * but we may encounter them if the source of the input data has already corrupted - * implementation. - *

    - *

    - * To handle this case, this method first splits its input into multiple parts - * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and - * removes unnecessary strings like "(Miami)". - *

    - *

    - * Do not call this method when trimming is inappropriate for its receivers. - *

    - */ - private List splitAndTrimPhoneNumbers(final String phoneNumber) { - final 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) || 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; - } - - public VCardBuilder appendEmails(final List contentValuesList) { - boolean emailAddressExists = false; - if (contentValuesList != null) { - final Set addressSet = new HashSet(); - for (ContentValues contentValues : contentValuesList) { - String emailAddress = contentValues.getAsString(Email.DATA); - if (emailAddress != null) { - emailAddress = emailAddress.trim(); - } - if (TextUtils.isEmpty(emailAddress)) { - continue; - } - Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); - final int type = (typeAsObject != null ? - typeAsObject : DEFAULT_EMAIL_TYPE); - final String label = contentValues.getAsString(Email.LABEL); - Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - emailAddressExists = true; - if (!addressSet.contains(emailAddress)) { - addressSet.add(emailAddress); - appendEmailLine(type, label, emailAddress, isPrimary); - } - } - } - - if (!emailAddressExists && mIsDoCoMo) { - appendEmailLine(Email.TYPE_HOME, "", "", false); - } - - return this; - } - - public VCardBuilder appendPostals(final List contentValuesList) { - if (contentValuesList == null || contentValuesList.isEmpty()) { - if (mIsDoCoMo) { - mBuilder.append(VCardConstants.PROPERTY_ADR); - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCardConstants.PARAM_TYPE_HOME); - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(VCARD_END_OF_LINE); - } - } else { - if (mIsDoCoMo) { - appendPostalsForDoCoMo(contentValuesList); - } else { - appendPostalsForGeneric(contentValuesList); - } - } - - return this; - } - - private static final Map sPostalTypePriorityMap; - - static { - sPostalTypePriorityMap = new HashMap(); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3); - } - - /** - * Tries to append just one line. If there's no appropriate address - * information, append an empty line. - */ - private void appendPostalsForDoCoMo(final List contentValuesList) { - int currentPriority = Integer.MAX_VALUE; - int currentType = Integer.MAX_VALUE; - ContentValues currentContentValues = null; - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); - final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger); - final int priority = - (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE); - if (priority < currentPriority) { - currentPriority = priority; - currentType = typeAsInteger; - currentContentValues = contentValues; - if (priority == 0) { - break; - } - } - } - - if (currentContentValues == null) { - Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); - return; - } - - final String label = currentContentValues.getAsString(StructuredPostal.LABEL); - appendPostalLine(currentType, label, currentContentValues, false, true); - } - - private void appendPostalsForGeneric(final List contentValuesList) { - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); - final int type = (typeAsInteger != null ? - typeAsInteger : DEFAULT_POSTAL_TYPE); - final String label = contentValues.getAsString(StructuredPostal.LABEL); - final Integer isPrimaryAsInteger = - contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - appendPostalLine(type, label, contentValues, isPrimary, false); - } - } - - private static class PostalStruct { - final boolean reallyUseQuotedPrintable; - final boolean appendCharset; - final String addressData; - public PostalStruct(final boolean reallyUseQuotedPrintable, - final boolean appendCharset, final String addressData) { - this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; - this.appendCharset = appendCharset; - this.addressData = addressData; - } - } - - /** - * @return null when there's no information available to construct the data. - */ - private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { - // adr-value = 0*6(text-value ";") text-value - // ; PO Box, Extended Address, Street, Locality, Region, Postal - // ; Code, Country Name - final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX); - final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); - final String rawStreet = contentValues.getAsString(StructuredPostal.STREET); - final String rawLocality = contentValues.getAsString(StructuredPostal.CITY); - final String rawRegion = contentValues.getAsString(StructuredPostal.REGION); - final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE); - final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY); - final String[] rawAddressArray = new String[]{ - rawPoBox, rawNeighborhood, rawStreet, rawLocality, - rawRegion, rawPostalCode, rawCountry}; - if (!VCardUtils.areAllEmpty(rawAddressArray)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray)); - final boolean appendCharset = - !VCardUtils.containsOnlyPrintableAscii(rawAddressArray); - final String encodedPoBox; - final String encodedStreet; - final String encodedLocality; - final String encodedRegion; - final String encodedPostalCode; - final String encodedCountry; - final String encodedNeighborhood; - - final String rawLocality2; - // This looks inefficient since we encode rawLocality and rawNeighborhood twice, - // but this is intentional. - // - // QP encoding may add line feeds when needed and the result of - // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood) - // may be different from - // - encodedLocality + " " + encodedNeighborhood. - // - // We use safer way. - if (TextUtils.isEmpty(rawLocality)) { - if (TextUtils.isEmpty(rawNeighborhood)) { - rawLocality2 = ""; - } else { - rawLocality2 = rawNeighborhood; - } - } else { - if (TextUtils.isEmpty(rawNeighborhood)) { - rawLocality2 = rawLocality; - } else { - rawLocality2 = rawLocality + " " + rawNeighborhood; - } - } - if (reallyUseQuotedPrintable) { - encodedPoBox = encodeQuotedPrintable(rawPoBox); - encodedStreet = encodeQuotedPrintable(rawStreet); - encodedLocality = encodeQuotedPrintable(rawLocality2); - encodedRegion = encodeQuotedPrintable(rawRegion); - encodedPostalCode = encodeQuotedPrintable(rawPostalCode); - encodedCountry = encodeQuotedPrintable(rawCountry); - } else { - encodedPoBox = escapeCharacters(rawPoBox); - encodedStreet = escapeCharacters(rawStreet); - encodedLocality = escapeCharacters(rawLocality2); - encodedRegion = escapeCharacters(rawRegion); - encodedPostalCode = escapeCharacters(rawPostalCode); - encodedCountry = escapeCharacters(rawCountry); - encodedNeighborhood = escapeCharacters(rawNeighborhood); - } - final StringBuilder addressBuilder = new StringBuilder(); - addressBuilder.append(encodedPoBox); - addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street - addressBuilder.append(encodedStreet); - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality - addressBuilder.append(encodedLocality); - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region - addressBuilder.append(encodedRegion); - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code - addressBuilder.append(encodedPostalCode); - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country - addressBuilder.append(encodedCountry); - return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); - } else { // VCardUtils.areAllEmpty(rawAddressArray) == true - // Try to use FORMATTED_ADDRESS instead. - final String rawFormattedAddress = - contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); - if (TextUtils.isEmpty(rawFormattedAddress)) { - return null; - } - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress)); - final boolean appendCharset = - !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress); - final String encodedFormattedAddress; - if (reallyUseQuotedPrintable) { - encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress); - } else { - encodedFormattedAddress = escapeCharacters(rawFormattedAddress); - } - - // We use the second value ("Extended Address") just because Japanese mobile phones - // do so. If the other importer expects the value be in the other field, some flag may - // be needed. - final StringBuilder addressBuilder = new StringBuilder(); - addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address - addressBuilder.append(encodedFormattedAddress); - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code - addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country - return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); - } - } - - public VCardBuilder appendIms(final List contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); - if (protocolAsObject == null) { - continue; - } - final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); - if (propertyName == null) { - continue; - } - String data = contentValues.getAsString(Im.DATA); - if (data != null) { - data = data.trim(); - } - if (TextUtils.isEmpty(data)) { - continue; - } - final String typeAsString; - { - final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); - switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { - case Im.TYPE_HOME: { - typeAsString = VCardConstants.PARAM_TYPE_HOME; - break; - } - case Im.TYPE_WORK: { - typeAsString = VCardConstants.PARAM_TYPE_WORK; - break; - } - case Im.TYPE_CUSTOM: { - final String label = contentValues.getAsString(Im.LABEL); - typeAsString = (label != null ? "X-" + label : null); - break; - } - case Im.TYPE_OTHER: // Ignore - default: { - typeAsString = null; - break; - } - } - } - - final List parameterList = new ArrayList(); - if (!TextUtils.isEmpty(typeAsString)) { - parameterList.add(typeAsString); - } - final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - - appendLineWithCharsetAndQPDetection(propertyName, parameterList, data); - } - } - return this; - } - - public VCardBuilder appendWebsites(final List contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String website = contentValues.getAsString(Website.URL); - if (website != null) { - website = website.trim(); - } - - // Note: vCard 3.0 does not allow any parameter addition toward "URL" - // property, while there's no document in vCard 2.1. - if (!TextUtils.isEmpty(website)) { - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website); - } - } - } - return this; - } - - public VCardBuilder appendOrganizations(final List contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String company = contentValues.getAsString(Organization.COMPANY); - if (company != null) { - company = company.trim(); - } - String department = contentValues.getAsString(Organization.DEPARTMENT); - if (department != null) { - department = department.trim(); - } - String title = contentValues.getAsString(Organization.TITLE); - if (title != null) { - title = title.trim(); - } - - StringBuilder orgBuilder = new StringBuilder(); - if (!TextUtils.isEmpty(company)) { - orgBuilder.append(company); - } - if (!TextUtils.isEmpty(department)) { - if (orgBuilder.length() > 0) { - orgBuilder.append(';'); - } - orgBuilder.append(department); - } - final String orgline = orgBuilder.toString(); - appendLine(VCardConstants.PROPERTY_ORG, orgline, - !VCardUtils.containsOnlyPrintableAscii(orgline), - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); - - if (!TextUtils.isEmpty(title)) { - appendLine(VCardConstants.PROPERTY_TITLE, title, - !VCardUtils.containsOnlyPrintableAscii(title), - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); - } - } - } - return this; - } - - public VCardBuilder appendPhotos(final List contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - byte[] data = contentValues.getAsByteArray(Photo.PHOTO); - if (data == null) { - continue; - } - final String photoType = VCardUtils.guessImageType(data); - if (photoType == null) { - Log.d(LOG_TAG, "Unknown photo type. Ignored."); - continue; - } - // TODO: check this works fine. - final String photoString = new String(Base64.encode(data, Base64.NO_WRAP)); - if (!TextUtils.isEmpty(photoString)) { - appendPhotoLine(photoString, photoType); - } - } - } - return this; - } - - public VCardBuilder appendNotes(final List contentValuesList) { - if (contentValuesList != null) { - if (mOnlyOneNoteFieldIsAvailable) { - final StringBuilder noteBuilder = new StringBuilder(); - boolean first = true; - for (final 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 = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendLine(VCardConstants.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 = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendLine(VCardConstants.PROPERTY_NOTE, noteStr, - shouldAppendCharsetInfo, reallyUseQuotedPrintable); - } - } - } - } - return this; - } - - public VCardBuilder appendEvents(final List contentValuesList) { - // There's possibility where a given object may have more than one birthday, which - // is inappropriate. We just build one birthday. - if (contentValuesList != null) { - String primaryBirthday = null; - String secondaryBirthday = null; - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE); - final int eventType; - if (eventTypeAsInteger != null) { - eventType = eventTypeAsInteger; - } else { - eventType = Event.TYPE_OTHER; - } - if (eventType == Event.TYPE_BIRTHDAY) { - final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); - if (birthdayCandidate == null) { - continue; - } - final Integer isSuperPrimaryAsInteger = - contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); - final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? - (isSuperPrimaryAsInteger > 0) : false); - if (isSuperPrimary) { - // "super primary" birthday should the prefered one. - primaryBirthday = birthdayCandidate; - break; - } - final Integer isPrimaryAsInteger = - contentValues.getAsInteger(Event.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - if (isPrimary) { - // We don't break here since "super primary" birthday may exist later. - primaryBirthday = birthdayCandidate; - } else if (secondaryBirthday == null) { - // First entry is set to the "secondary" candidate. - secondaryBirthday = birthdayCandidate; - } - } else if (mUsesAndroidProperty) { - // Event types other than Birthday is not supported by vCard. - appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues); - } - } - if (primaryBirthday != null) { - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, - primaryBirthday.trim()); - } else if (secondaryBirthday != null){ - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, - secondaryBirthday.trim()); - } - } - return this; - } - - public VCardBuilder appendRelation(final List contentValuesList) { - if (mUsesAndroidProperty && contentValuesList != null) { - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues); - } - } - return this; - } - - /** - * @param emitEveryTime If true, builder builds the line even when there's no entry. - */ - public void appendPostalLine(final int type, final String label, - final ContentValues contentValues, - final boolean isPrimary, final boolean emitEveryTime) { - final boolean reallyUseQuotedPrintable; - final boolean appendCharset; - final String addressValue; - { - PostalStruct postalStruct = tryConstructPostalStruct(contentValues); - if (postalStruct == null) { - if (emitEveryTime) { - reallyUseQuotedPrintable = false; - appendCharset = false; - addressValue = ""; - } else { - return; - } - } else { - reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; - appendCharset = postalStruct.appendCharset; - addressValue = postalStruct.addressData; - } - } - - List parameterList = new ArrayList(); - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - switch (type) { - case StructuredPostal.TYPE_HOME: { - parameterList.add(VCardConstants.PARAM_TYPE_HOME); - break; - } - case StructuredPostal.TYPE_WORK: { - parameterList.add(VCardConstants.PARAM_TYPE_WORK); - break; - } - case StructuredPostal.TYPE_CUSTOM: { - if (!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. - parameterList.add("X-" + label); - } - break; - } - case StructuredPostal.TYPE_OTHER: { - break; - } - default: { - Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); - break; - } - } - - mBuilder.append(VCardConstants.PROPERTY_ADR); - if (!parameterList.isEmpty()) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameters(parameterList); - } - if (appendCharset) { - // 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 parameter in vCard 3.0. - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(addressValue); - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendEmailLine(final int type, final String label, - final String rawValue, final boolean isPrimary) { - final String typeAsString; - switch (type) { - case Email.TYPE_CUSTOM: { - if (VCardUtils.isMobilePhoneLabel(label)) { - typeAsString = VCardConstants.PARAM_TYPE_CELL; - } else if (!TextUtils.isEmpty(label) - && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - typeAsString = "X-" + label; - } else { - typeAsString = null; - } - break; - } - case Email.TYPE_HOME: { - typeAsString = VCardConstants.PARAM_TYPE_HOME; - break; - } - case Email.TYPE_WORK: { - typeAsString = VCardConstants.PARAM_TYPE_WORK; - break; - } - case Email.TYPE_OTHER: { - typeAsString = null; - break; - } - case Email.TYPE_MOBILE: { - typeAsString = VCardConstants.PARAM_TYPE_CELL; - break; - } - default: { - Log.e(LOG_TAG, "Unknown Email type: " + type); - typeAsString = null; - break; - } - } - - final List parameterList = new ArrayList(); - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - if (!TextUtils.isEmpty(typeAsString)) { - parameterList.add(typeAsString); - } - - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList, - rawValue); - } - - public void appendTelLine(final Integer typeAsInteger, final String label, - final String encodedValue, boolean isPrimary) { - mBuilder.append(VCardConstants.PROPERTY_TEL); - mBuilder.append(VCARD_PARAM_SEPARATOR); - - final int type; - if (typeAsInteger == null) { - type = Phone.TYPE_OTHER; - } else { - type = typeAsInteger; - } - - ArrayList parameterList = new ArrayList(); - switch (type) { - case Phone.TYPE_HOME: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_HOME)); - break; - } - case Phone.TYPE_WORK: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_WORK)); - break; - } - case Phone.TYPE_FAX_HOME: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX)); - break; - } - case Phone.TYPE_FAX_WORK: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX)); - break; - } - case Phone.TYPE_MOBILE: { - parameterList.add(VCardConstants.PARAM_TYPE_CELL); - break; - } - case Phone.TYPE_PAGER: { - if (mIsDoCoMo) { - // Not sure about the reason, but previous implementation had - // used "VOICE" instead of "PAGER" - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - } else { - parameterList.add(VCardConstants.PARAM_TYPE_PAGER); - } - break; - } - case Phone.TYPE_OTHER: { - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - break; - } - case Phone.TYPE_CAR: { - parameterList.add(VCardConstants.PARAM_TYPE_CAR); - break; - } - case Phone.TYPE_COMPANY_MAIN: { - // There's no relevant field in vCard (at least 2.1). - parameterList.add(VCardConstants.PARAM_TYPE_WORK); - isPrimary = true; - break; - } - case Phone.TYPE_ISDN: { - parameterList.add(VCardConstants.PARAM_TYPE_ISDN); - break; - } - case Phone.TYPE_MAIN: { - isPrimary = true; - break; - } - case Phone.TYPE_OTHER_FAX: { - parameterList.add(VCardConstants.PARAM_TYPE_FAX); - break; - } - case Phone.TYPE_TELEX: { - parameterList.add(VCardConstants.PARAM_TYPE_TLX); - break; - } - case Phone.TYPE_WORK_MOBILE: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL)); - break; - } - case Phone.TYPE_WORK_PAGER: { - parameterList.add(VCardConstants.PARAM_TYPE_WORK); - // See above. - if (mIsDoCoMo) { - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - } else { - parameterList.add(VCardConstants.PARAM_TYPE_PAGER); - } - break; - } - case Phone.TYPE_MMS: { - parameterList.add(VCardConstants.PARAM_TYPE_MSG); - break; - } - case Phone.TYPE_CUSTOM: { - if (TextUtils.isEmpty(label)) { - // Just ignore the custom type. - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - } else if (VCardUtils.isMobilePhoneLabel(label)) { - parameterList.add(VCardConstants.PARAM_TYPE_CELL); - } else if (mIsV30OrV40) { - // This label is appropriately encoded in appendTypeParameters. - parameterList.add(label); - } else { - final String upperLabel = label.toUpperCase(); - if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) { - parameterList.add(upperLabel); - } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - // Note: Strictly, vCard 2.1 does not allow "X-" parameter without - // "TYPE=" string. - parameterList.add("X-" + label); - } - } - break; - } - case Phone.TYPE_RADIO: - case Phone.TYPE_TTY_TDD: - default: { - break; - } - } - - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - - if (parameterList.isEmpty()) { - appendUncommonPhoneType(mBuilder, type); - } else { - appendTypeParameters(parameterList); - } - - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedValue); - mBuilder.append(VCARD_END_OF_LINE); - } - - /** - * 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(VCardConstants.PARAM_TYPE_VOICE); - } else { - String phoneType = VCardUtils.getPhoneTypeString(type); - if (phoneType != null) { - appendTypeParameter(phoneType); - } else { - Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); - } - } - } - - /** - * @param encodedValue Must be encoded by BASE64 - * @param photoType - */ - public void appendPhotoLine(final String encodedValue, final String photoType) { - StringBuilder tmpBuilder = new StringBuilder(); - tmpBuilder.append(VCardConstants.PROPERTY_PHOTO); - tmpBuilder.append(VCARD_PARAM_SEPARATOR); - if (mIsV30OrV40) { - tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B); - } else { - tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); - } - tmpBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameter(tmpBuilder, photoType); - tmpBuilder.append(VCARD_DATA_SEPARATOR); - tmpBuilder.append(encodedValue); - - final String tmpStr = tmpBuilder.toString(); - tmpBuilder = new StringBuilder(); - int lineCount = 0; - final int length = tmpStr.length(); - final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30 - - VCARD_END_OF_LINE.length(); - final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length(); - int maxNum = maxNumForFirstLine; - for (int i = 0; i < length; i++) { - tmpBuilder.append(tmpStr.charAt(i)); - lineCount++; - if (lineCount > maxNum) { - tmpBuilder.append(VCARD_END_OF_LINE); - tmpBuilder.append(VCARD_WS); - maxNum = maxNumInGeneral; - lineCount = 0; - } - } - mBuilder.append(tmpBuilder.toString()); - mBuilder.append(VCARD_END_OF_LINE); - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendAndroidSpecificProperty( - final String mimeType, ContentValues contentValues) { - if (!sAllowedAndroidPropertySet.contains(mimeType)) { - return; - } - final List rawValueList = new ArrayList(); - for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) { - String value = contentValues.getAsString("data" + i); - if (value == null) { - value = ""; - } - rawValueList.add(value); - } - - boolean needCharset = - (mShouldAppendCharsetParam && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM); - if (needCharset) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(mimeType); // Should not be encoded. - for (String rawValue : rawValueList) { - final String encodedValue; - if (reallyUseQuotedPrintable) { - encodedValue = encodeQuotedPrintable(rawValue); - } else { - // TODO: one line may be too huge, which may be invalid in vCard 3.0 - // (which says "When generating a content line, lines longer than - // 75 characters SHOULD be folded"), though several - // (even well-known) applications do not care this. - encodedValue = escapeCharacters(rawValue); - } - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedValue); - } - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendLineWithCharsetAndQPDetection(final String propertyName, - final String rawValue) { - appendLineWithCharsetAndQPDetection(propertyName, null, rawValue); - } - - public void appendLineWithCharsetAndQPDetection( - final String propertyName, final List rawValueList) { - appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList); - } - - public void appendLineWithCharsetAndQPDetection(final String propertyName, - final List parameterList, final String rawValue) { - final boolean needCharset = - !VCardUtils.containsOnlyPrintableAscii(rawValue); - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue)); - appendLine(propertyName, parameterList, - rawValue, needCharset, reallyUseQuotedPrintable); - } - - public void appendLineWithCharsetAndQPDetection(final String propertyName, - final List parameterList, final List rawValueList) { - boolean needCharset = - (mShouldAppendCharsetParam && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - appendLine(propertyName, parameterList, rawValueList, - needCharset, reallyUseQuotedPrintable); - } - - /** - * Appends one line with a given property name and value. - */ - public void appendLine(final String propertyName, final String rawValue) { - appendLine(propertyName, rawValue, false, false); - } - - public void appendLine(final String propertyName, final List rawValueList) { - appendLine(propertyName, rawValueList, false, false); - } - - public void appendLine(final String propertyName, - final String rawValue, final boolean needCharset, - boolean reallyUseQuotedPrintable) { - appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable); - } - - public void appendLine(final String propertyName, final List parameterList, - final String rawValue) { - appendLine(propertyName, parameterList, rawValue, false, false); - } - - public void appendLine(final String propertyName, final List parameterList, - final String rawValue, final boolean needCharset, - boolean reallyUseQuotedPrintable) { - mBuilder.append(propertyName); - if (parameterList != null && parameterList.size() > 0) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameters(parameterList); - } - if (needCharset) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - - final String encodedValue; - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - encodedValue = encodeQuotedPrintable(rawValue); - } else { - // TODO: one line may be too huge, which may be invalid in vCard spec, though - // several (even well-known) applications do not care that violation. - encodedValue = escapeCharacters(rawValue); - } - - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedValue); - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendLine(final String propertyName, final List rawValueList, - final boolean needCharset, boolean needQuotedPrintable) { - appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable); - } - - public void appendLine(final String propertyName, final List parameterList, - final List rawValueList, final boolean needCharset, - final boolean needQuotedPrintable) { - mBuilder.append(propertyName); - if (parameterList != null && parameterList.size() > 0) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameters(parameterList); - } - if (needCharset) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (needQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - - mBuilder.append(VCARD_DATA_SEPARATOR); - boolean first = true; - for (String rawValue : rawValueList) { - final String encodedValue; - if (needQuotedPrintable) { - encodedValue = encodeQuotedPrintable(rawValue); - } else { - // TODO: one line may be too huge, which may be invalid in vCard 3.0 - // (which says "When generating a content line, lines longer than - // 75 characters SHOULD be folded"), though several - // (even well-known) applications do not care this. - encodedValue = escapeCharacters(rawValue); - } - - if (first) { - first = false; - } else { - mBuilder.append(VCARD_ITEM_SEPARATOR); - } - mBuilder.append(encodedValue); - } - mBuilder.append(VCARD_END_OF_LINE); - } - - /** - * VCARD_PARAM_SEPARATOR must be appended before this method being called. - */ - private void appendTypeParameters(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 (final String typeValue : types) { - if (VCardConfig.isVersion30(mVCardType)) { - final String encoded = VCardUtils.toStringAsV30ParamValue(typeValue); - if (TextUtils.isEmpty(encoded)) { - continue; - } - - // Note: vCard 3.0 specifies the different type of acceptable type Strings, but - // we don't emit that kind of vCard 3.0 specific type since there should be - // high probabilyty in which external importers cannot understand them. - // - // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they - // are quoted.) - if (first) { - first = false; - } else { - mBuilder.append(VCARD_PARAM_SEPARATOR); - } - appendTypeParameter(encoded); - } else { // vCard 2.1 - if (!VCardUtils.isV21Word(typeValue)) { - continue; - } - if (first) { - first = false; - } else { - mBuilder.append(VCARD_PARAM_SEPARATOR); - } - appendTypeParameter(typeValue); - } - } - } - - /** - * VCARD_PARAM_SEPARATOR must be appended before this method being called. - */ - private void appendTypeParameter(final String type) { - appendTypeParameter(mBuilder, type); - } - - private void appendTypeParameter(final StringBuilder builder, final String type) { - // Refrain from using appendType() so that "TYPE=" is not be appended when the - // device is DoCoMo's (just for safety). - // - // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" - if (VCardConfig.isVersion40(mVCardType) || - ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) { - builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); - } - builder.append(type); - } - - /** - * Returns true when the property line should contain charset parameter - * 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 charset we use 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 shouldAppendCharsetParam(String...propertyValueList) { - if (!mShouldAppendCharsetParam) { - return false; - } - for (String propertyValue : propertyValueList) { - if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { - return true; - } - } - return false; - } - - private String encodeQuotedPrintable(final String str) { - if (TextUtils.isEmpty(str)) { - return ""; - } - - final StringBuilder builder = new StringBuilder(); - int index = 0; - int lineCount = 0; - byte[] strArray = null; - - try { - strArray = str.getBytes(mCharset); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. " - + "Try default charset"); - strArray = str.getBytes(); - } - while (index < strArray.length) { - builder.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 - builder.append("=\r\n"); - lineCount = 0; - } - } - - return builder.toString(); - } - - /** - * 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++) { - final 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') { - break; - } 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 (mIsV30OrV40) { - tmpBuilder.append("\\\\"); - break; - } else { - // fall through - } - } - case '<': - case '>': { - if (mIsDoCoMo) { - tmpBuilder.append('\\'); - tmpBuilder.append(ch); - } else { - tmpBuilder.append(ch); - } - break; - } - case ',': { - if (mIsV30OrV40) { - tmpBuilder.append("\\,"); - } else { - tmpBuilder.append(ch); - } - break; - } - default: { - tmpBuilder.append(ch); - break; - } - } - } - return tmpBuilder.toString(); - } - - @Override - public String toString() { - if (!mEndAppended) { - if (mIsDoCoMo) { - appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); - appendLine(VCardConstants.PROPERTY_X_REDUCTION, ""); - appendLine(VCardConstants.PROPERTY_X_NO, ""); - appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, ""); - } - appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD); - mEndAppended = true; - } - return mBuilder.toString(); - } -} diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java deleted file mode 100644 index 193cf1e32b923c4fa24253812ecd432f4b0b2ce4..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardComposer.java +++ /dev/null @@ -1,678 +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.pim.vcard; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Entity; -import android.content.Entity.NamedContentValues; -import android.content.EntityIterator; -import android.database.Cursor; -import android.database.sqlite.SQLiteException; -import android.net.Uri; -import android.pim.vcard.exception.VCardException; -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.Relation; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; -import android.provider.ContactsContract.CommonDataKinds.Website; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.RawContactsEntity; -import android.text.TextUtils; -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.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.charset.UnsupportedCharsetException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - *

    - * The class for composing vCard from Contacts information. - *

    - *

    - * 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();
    - *     }
    - * }
    - *

    - * Users have to manually take care of memory efficiency. Even one vCard may contain - * image of non-trivial size for mobile devices. - *

    - *

    - * {@link VCardBuilder} is used to build each vCard. - *

    - */ -public class VCardComposer { - private static final String LOG_TAG = "VCardComposer"; - - 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"; - - /** Should be visible only from developers... (no need to translate, hopefully) */ - public static final String FAILURE_REASON_UNSUPPORTED_URI = - "The Uri vCard composer received is not supported by the composer."; - - public static final String NO_ERROR = "No error"; - - public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; - - // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, - // since usual vCard devices for Japanese devices already use it. - private static final String SHIFT_JIS = "SHIFT_JIS"; - private static final String UTF_8 = "UTF-8"; - - /** - * Special URI for testing. - */ - public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; - public static final Uri VCARD_TEST_AUTHORITY_URI = - Uri.parse("content://" + VCARD_TEST_AUTHORITY); - public static final Uri CONTACTS_TEST_CONTENT_URI = - Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); - - private static final Map sImMap; - - static { - sImMap = new HashMap(); - sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); - sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); - sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); - sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); - sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); - sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); - // We don't add Google talk here since it has to be handled separately. - } - - public static interface OneEntryHandler { - public boolean onInit(Context context); - public boolean onEntryCreated(String vcard); - public void onTerminate(); - } - - /** - *

    - * An useful handler for emitting vCard String to an OutputStream object one by one. - *

    - *

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

    - */ - public final class HandlerForOutputStream implements OneEntryHandler { - @SuppressWarnings("hiding") - private static final String LOG_TAG = "VCardComposer.HandlerForOutputStream"; - - private boolean mOnTerminateIsCalled = false; - - private final OutputStream mOutputStream; // mWriter will close this. - private Writer mWriter; - - /** - * Input stream will be closed on the detruction of this object. - */ - public HandlerForOutputStream(final OutputStream outputStream) { - mOutputStream = outputStream; - } - - public boolean onInit(final Context context) { - try { - mWriter = new BufferedWriter(new OutputStreamWriter( - mOutputStream, mCharset)); - } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Unsupported charset: " + mCharset); - mErrorReason = "Encoding is not supported (usually this does not happen!): " - + mCharset; - return false; - } - - if (mIsDoCoMo) { - try { - // Create one empty entry. - mWriter.write(createOneEntryInternal("-1", null)); - } catch (VCardException e) { - Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " + - e.getMessage()); - return false; - } 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 { - closeOutputStream(); - } - } - } - - public void closeOutputStream() { - try { - mWriter.close(); - } catch (IOException e) { - Log.w(LOG_TAG, "IOException is thrown during close(). Ignoring."); - } - } - - @Override - public void finalize() { - if (!mOnTerminateIsCalled) { - onTerminate(); - } - } - } - - private final Context mContext; - private final int mVCardType; - private final boolean mCareHandlerErrors; - private final ContentResolver mContentResolver; - - private final boolean mIsDoCoMo; - private Cursor mCursor; - private int mIdColumn; - - private final String mCharset; - private boolean mTerminateIsCalled; - private final List mHandlerList; - - private String mErrorReason = NO_ERROR; - - private static final String[] sContactsProjection = new String[] { - Contacts._ID, - }; - - public VCardComposer(Context context) { - this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true); - } - - /** - * The variant which sets charset to null and sets careHandlerErrors to true. - */ - public VCardComposer(Context context, int vcardType) { - this(context, vcardType, null, true); - } - - public VCardComposer(Context context, int vcardType, String charset) { - this(context, vcardType, charset, true); - } - - /** - * The variant which sets charset to null. - */ - public VCardComposer(final Context context, final int vcardType, - final boolean careHandlerErrors) { - this(context, vcardType, null, careHandlerErrors); - } - - /** - * Construct for supporting call log entry vCard composing. - * - * @param context Context to be used during the composition. - * @param vcardType The type of vCard, typically available via {@link VCardConfig}. - * @param charset The charset to be used. Use null when you don't need the charset. - * @param careHandlerErrors If true, This object returns false everytime - * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false. - * If false, this ignores those errors. - */ - public VCardComposer(final Context context, final int vcardType, String charset, - final boolean careHandlerErrors) { - mContext = context; - mVCardType = vcardType; - mCareHandlerErrors = careHandlerErrors; - mContentResolver = context.getContentResolver(); - - mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mHandlerList = new ArrayList(); - - charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset); - final boolean shouldAppendCharsetParam = !( - VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset)); - - if (mIsDoCoMo || shouldAppendCharsetParam) { - if (SHIFT_JIS.equalsIgnoreCase(charset)) { - if (mIsDoCoMo) { - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, - "DoCoMo-specific SHIFT_JIS was not found. " - + "Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - } else { - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, - "Career-specific SHIFT_JIS was not found. " - + "Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - } - mCharset = charset; - } else { - Log.w(LOG_TAG, - "The charset \"" + charset + "\" is used while " - + SHIFT_JIS + " is needed to be used."); - if (TextUtils.isEmpty(charset)) { - mCharset = SHIFT_JIS; - } else { - try { - charset = CharsetUtils.charsetForVendor(charset).name(); - } catch (UnsupportedCharsetException e) { - Log.i(LOG_TAG, - "Career-specific \"" + charset + "\" was not found (as usual). " - + "Use it as is."); - } - mCharset = charset; - } - } - } else { - if (TextUtils.isEmpty(charset)) { - mCharset = UTF_8; - } else { - try { - charset = CharsetUtils.charsetForVendor(charset).name(); - } catch (UnsupportedCharsetException e) { - Log.i(LOG_TAG, - "Career-specific \"" + charset + "\" was not found (as usual). " - + "Use it as is."); - } - mCharset = charset; - } - } - - Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\""); - } - - /** - * Must be called before {@link #init()}. - */ - public void addHandler(OneEntryHandler handler) { - if (handler != null) { - mHandlerList.add(handler); - } - } - - /** - * @return Returns true when initialization is successful and all the other - * methods are available. Returns false otherwise. - */ - public boolean init() { - return init(null, null); - } - - public boolean init(final String selection, final String[] selectionArgs) { - return init(Contacts.CONTENT_URI, selection, selectionArgs, null); - } - - /** - * Note that this is unstable interface, may be deleted in the future. - */ - public boolean init(final Uri contentUri, final String selection, - final String[] selectionArgs, final String sortOrder) { - if (contentUri == null) { - return false; - } - - if (mCareHandlerErrors) { - final 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); - } - } - - final String[] projection; - if (Contacts.CONTENT_URI.equals(contentUri) || - CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { - projection = sContactsProjection; - } else { - mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; - return false; - } - mCursor = mContentResolver.query( - contentUri, projection, selection, selectionArgs, sortOrder); - - 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; - } - - mIdColumn = mCursor.getColumnIndex(Contacts._ID); - - return true; - } - - public boolean createOneEntry() { - return createOneEntry(null); - } - - /** - * @param getEntityIteratorMethod For Dependency Injection. - * @hide just for testing. - */ - public boolean createOneEntry(Method getEntityIteratorMethod) { - if (mCursor == null || mCursor.isAfterLast()) { - mErrorReason = FAILURE_REASON_NOT_INITIALIZED; - return false; - } - final String vcard; - try { - if (mIdColumn >= 0) { - vcard = createOneEntryInternal(mCursor.getString(mIdColumn), - getEntityIteratorMethod); - } else { - Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); - return true; - } - } catch (VCardException e) { - Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage()); - return false; - } 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."); - 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; - } - - private String createOneEntryInternal(final String contactId, - final Method getEntityIteratorMethod) throws VCardException { - final Map> contentValuesListMap = - new HashMap>(); - // The resolver may return the entity iterator with no data. It is possible. - // 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. - EntityIterator entityIterator = null; - try { - final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() - // .appendQueryParameter("for_export_only", "1") - .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") - .build(); - final String selection = Data.CONTACT_ID + "=?"; - final String[] selectionArgs = new String[] {contactId}; - if (getEntityIteratorMethod != null) { - // Please note that this branch is executed by unit tests only - try { - entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, - mContentResolver, uri, selection, selectionArgs, null); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + - e.getMessage()); - } catch (IllegalAccessException e) { - Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + - e.getMessage()); - } catch (InvocationTargetException e) { - Log.e(LOG_TAG, "InvocationTargetException has been thrown: "); - StackTraceElement[] stackTraceElements = e.getCause().getStackTrace(); - for (StackTraceElement element : stackTraceElements) { - Log.e(LOG_TAG, " at " + element.toString()); - } - throw new VCardException("InvocationTargetException has been thrown: " + - e.getCause().getMessage()); - } - } else { - entityIterator = RawContacts.newEntityIterator(mContentResolver.query( - uri, null, selection, selectionArgs, null)); - } - - if (entityIterator == null) { - Log.e(LOG_TAG, "EntityIterator is null"); - return ""; - } - - if (!entityIterator.hasNext()) { - Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); - return ""; - } - - 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); - } - } - } - } finally { - if (entityIterator != null) { - entityIterator.close(); - } - } - - return buildVCard(contentValuesListMap); - } - - /** - * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in - * {ContactsContract}. Developers can override this method to customize the output. - */ - public String buildVCard(final Map> contentValuesListMap) { - if (contentValuesListMap == null) { - Log.e(LOG_TAG, "The given map is null. Ignore and return empty String"); - return ""; - } else { - final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset); - builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) - .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) - .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) - .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) - .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) - .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) - .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); - if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { - builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); - } - builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) - .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) - .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) - .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); - 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) { - Log.w(LOG_TAG, "terminate() is not called yet. We call it in finalize() step."); - terminate(); - } - } - - /** - * @return returns the number of available entities. The return value is undefined - * when this object is not ready yet (typically when {{@link #init()} is not called - * or when {@link #terminate()} is already called). - */ - public int getCount() { - if (mCursor == null) { - Log.w(LOG_TAG, "This object is not ready yet."); - return 0; - } - return mCursor.getCount(); - } - - /** - * @return true when there's no entity to be built. The return value is undefined - * when this object is not ready yet. - */ - public boolean isAfterLast() { - if (mCursor == null) { - Log.w(LOG_TAG, "This object is not ready yet."); - return false; - } - return mCursor.isAfterLast(); - } - - /** - * @return Returns the error reason. - */ - public String getErrorReason() { - return mErrorReason; - } -} diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java deleted file mode 100644 index 8e759e3960b2c5c8dbd70c9c4d6f4696a2247b9a..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardConfig.java +++ /dev/null @@ -1,505 +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.pim.vcard; - -import android.telephony.PhoneNumberUtils; -import android.util.Log; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * The class representing VCard related configurations. Useful static methods are not in this class - * but in VCardUtils. - */ -public class VCardConfig { - private static final String LOG_TAG = "VCardConfig"; - - /* 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; - - /** - *

    - * The charset used during import. - *

    - *

    - * We cannot determine which charset should be used to interpret lines in vCard, - * while Java requires us to specify it when InputStream is used. - * We need to rely on the mechanism due to some performance reason. - *

    - *

    - * In order to avoid "misinterpretation" of charset and lose any data in vCard, - * "ISO-8859-1" is first used for reading the stream. - * When a charset is specified in a property (with "CHARSET=..." parameter), - * the string is decoded to raw bytes and encoded into the specific charset, - *

    - *

    - * Unicode specification there's a one to one mapping between each byte in ISO-8859-1 - * and a codepoint, and Java specification requires runtime must have the charset. - * Thus, ISO-8859-1 is one effective mapping for intermediate mapping. - *

    - */ - public static final String DEFAULT_INTERMEDIATE_CHARSET = "ISO-8859-1"; - - /** - * The charset used when there's no information affbout what charset should be used to - * encode the binary given from vCard. - */ - public static final String DEFAULT_IMPORT_CHARSET = "UTF-8"; - public static final String DEFAULT_EXPORT_CHARSET = "UTF-8"; - - /** - * Do not use statically like "version == VERSION_V21" - */ - public static final int VERSION_21 = 0; - public static final int VERSION_30 = 1; - public static final int VERSION_40 = 2; - public static final int VERSION_MASK = 3; - - 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 - - /** - *

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

    - */ - 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 does "NOT" use Quoted-Printable toward "primary" - * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0). - *

    - *

    - * We actually cannot define what is the "primary" property. Note that this is NOT defined - * in vCard specification either. Also be aware that it is NOT related to "primary" notion - * used in {@link android.provider.ContactsContract}. - * This notion is just for vCard composition in Android. - *

    - *

    - * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1 - * do NOT use Quoted-Printable encoding toward some properties related names like "N", "FN", etc. - * even when their values contain non-ascii or/and CR/LF, while they use the encoding in the - * other properties like "ADR", "ORG", etc. - *

    - * We are afraid of the case where some vCard importer also forget handling QP presuming QP is - * not used in such fields. - *

    - *

    - * This flag is useful when some target importer you are going to focus on does not accept - * such properties with Quoted-Printable encoding. - *

    - *

    - * Again, we should not use this flag at all for complying vCard 2.1 spec. - *

    - *

    - * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this - * kind of problem (hopefully). - *

    - * @hide - */ - public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000; - - /** - *

    - * The flag indicating that phonetic name related fields must be converted to - * appropriate form. Note that "appropriate" is not defined in any vCard specification. - * This is Android-specific. - *

    - *

    - * One typical (and currently sole) example where we need this flag is the time when - * we need to emit Japanese phonetic names into vCard entries. The property values - * should be encoded into half-width katakana when the target importer is Japanese mobile - * phones', which are probably not able to parse full-width hiragana/katakana for - * historical reasons, while the vCard importers embedded to softwares for PC should be - * able to parse them as we expect. - *

    - */ - public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x08000000; - - /** - *

    - * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string toward TYPE params - * every time possible. The default behavior does not emit it and is valid in the spec. - * In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in vCard 3.0 specification. - *

    - *

    - * Detail: - * 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. - *
    3. Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."
    4. - *
    5. Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."
    6. - *
    - *

    - * If you are targeting to the importer which cannot accept TYPE params without "TYPE=" - * strings (which should be rare though), please use this flag. - *

    - *

    - * Example usage: - *

    int type = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM);
    - *

    - */ - public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; - - /** - *

    - * The flag indicating the vCard composer does touch nothing toward phone number Strings - * but leave it as is. - *

    - *

    - * The vCard specifications mention nothing toward phone numbers, while some devices - * do (wrongly, but with innevitable reasons). - * For example, there's a possibility Japanese mobile phones are expected to have - * just numbers, hypens, plus, etc. but not usual alphabets, while US mobile phones - * should get such characters. To make exported vCard simple for external parsers, - * we have used {@link PhoneNumberUtils#formatNumber(String)} during export, and - * removed unnecessary characters inside the number (e.g. "111-222-3333 (Miami)" - * becomes "111-222-3333"). - * Unfortunate side effect of that use was some control characters used in the other - * areas may be badly affected by the formatting. - *

    - *

    - * This flag disables that formatting, affecting both importer and exporter. - * If the user is aware of some side effects due to the implicit formatting, use this flag. - *

    - */ - public static final int FLAG_REFRAIN_PHONE_NUMBER_FORMATTING = 0x02000000; - - /** - *

    - * For importer only. Ignored in exporter. - *

    - *

    - * The flag indicating the parser should handle a nested vCard, in which vCard clause starts - * in another vCard clause. Here's a typical example. - *

    - *
    BEGIN:VCARD
    -     * BEGIN:VCARD
    -     * VERSION:2.1
    -     * ...
    -     * END:VCARD
    -     * END:VCARD
    - *

    - * The vCard 2.1 specification allows the nest, but also let parsers ignore nested entries, - * while some mobile devices emit nested ones as primary data to be imported. - *

    - *

    - * This flag forces a vCard parser to torelate such a nest and understand its content. - *

    - */ - public static final int FLAG_TORELATE_NEST = 0x01000000; - - //// The followings are VCard types available from importer/exporter. //// - - public static final int FLAG_REFRAIN_IMAGE_EXPORT = 0x00800000; - - /** - *

    - * The type indicating nothing. Used by {@link VCardSourceDetector} when it - * was not able to guess the exact vCard type. - *

    - */ - public static final int VCARD_TYPE_UNKNOWN = 0; - - /** - *

    - * Generic vCard format with the vCard 2.1. When composing a vCard entry, - * the US convension will be used toward formatting some values. - *

    - *

    - * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", - * while it should be "Prefix Family Middle Given Suffix" in Japan for example. - *

    - *

    - * Uses UTF-8 for the charset as a charset for exporting. Note that old vCard importer - * outside Android cannot accept it since vCard 2.1 specifically does not allow - * that charset, while we need to use it to support various languages around the world. - *

    - *

    - * If you want to use alternative charset, you should notify the charset to the other - * compontent to be used. - *

    - */ - public static final int VCARD_TYPE_V21_GENERIC = - (VERSION_21 | NAME_ORDER_DEFAULT | 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. - *

    - *

    - * Not fully ready yet. Use with caution when you use this. - *

    - */ - public static final int VCARD_TYPE_V30_GENERIC = - (VERSION_30 | NAME_ORDER_DEFAULT | 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 4.0. - * @hide vCard 4.0 is not published yet. - */ - public static final int VCARD_TYPE_V40_GENERIC = - (VERSION_40 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V40_GENERIC_STR = "v40_generic"; - - /** - *

    - * General vCard format for the vCard 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 = - (VERSION_21 | NAME_ORDER_EUROPE | 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. - *

    - *

    - * Not ready yet. Use with caution when you use this. - *

    - */ - public static final int VCARD_TYPE_V30_EUROPE = - (VERSION_30 | NAME_ORDER_EUROPE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; - - /** - *

    - * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. - *

    - *

    - * Not ready yet. Use with caution when you use this. - *

    - */ - public static final int VCARD_TYPE_V21_JAPANESE = - (VERSION_21 | NAME_ORDER_JAPANESE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese_utf8"; - - /** - *

    - * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. - *

    - *

    - * Not ready yet. Use with caution when you use this. - *

    - */ - public static final int VCARD_TYPE_V30_JAPANESE = - (VERSION_30 | NAME_ORDER_JAPANESE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese_utf8"; - - /** - *

    - * The vCard 2.1 based format which (partially) considers the convention in Japanese - * mobile phones, where phonetic names are translated to half-width katakana if - * possible, etc. It would be better to use Shift_JIS as a charset for maximum - * compatibility. - *

    - * @hide Should not be available world wide. - */ - public static final int VCARD_TYPE_V21_JAPANESE_MOBILE = - (VERSION_21 | NAME_ORDER_JAPANESE | - FLAG_CONVERT_PHONETIC_NAME_STRINGS | FLAG_REFRAIN_QP_TO_NAME_PROPERTIES); - - /* package */ static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile"; - - /** - *

    - * The 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. The "Primary" properties - * are NOT encoded to Quoted-Printable. - *

    - * @hide Should not be available world wide. - */ - public static final int VCARD_TYPE_DOCOMO = - (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO); - - /* package */ static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - - public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; - - private static final Map sVCardTypeMap; - private static final Set sJapaneseMobileTypeSet; - - static { - sVCardTypeMap = new HashMap(); - sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); - sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); - sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); - sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); - sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE); - sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); - - sJapaneseMobileTypeSet = new HashSet(); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE); - sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO); - } - - public static int getVCardTypeFromString(final String vcardTypeString) { - final String loweredKey = vcardTypeString.toLowerCase(); - if (sVCardTypeMap.containsKey(loweredKey)) { - return sVCardTypeMap.get(loweredKey); - } else if ("default".equalsIgnoreCase(vcardTypeString)) { - return VCARD_TYPE_DEFAULT; - } else { - Log.e(LOG_TAG, "Unknown vCard type String: \"" + vcardTypeString + "\""); - return VCARD_TYPE_DEFAULT; - } - } - - public static boolean isVersion21(final int vcardType) { - return (vcardType & VERSION_MASK) == VERSION_21; - } - - public static boolean isVersion30(final int vcardType) { - return (vcardType & VERSION_MASK) == VERSION_30; - } - - public static boolean isVersion40(final int vcardType) { - return (vcardType & VERSION_MASK) == VERSION_40; - } - - public static boolean shouldUseQuotedPrintable(final int vcardType) { - return !isVersion30(vcardType); - } - - public static int getNameOrderType(final int vcardType) { - return vcardType & NAME_ORDER_MASK; - } - - public static boolean usesAndroidSpecificProperty(final int vcardType) { - return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0); - } - - public static boolean usesDefactProperty(final int vcardType) { - return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); - } - - public static boolean showPerformanceLog() { - return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; - } - - public static boolean shouldRefrainQPToNameProperties(final int vcardType) { - return (!shouldUseQuotedPrintable(vcardType) || - ((vcardType & FLAG_REFRAIN_QP_TO_NAME_PROPERTIES) != 0)); - } - - public static boolean appendTypeParamName(final int vcardType) { - return (isVersion30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 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(final int vcardType) { - // TODO: Some mask will be required so that this method wrongly interpret - // Japanese"-like" vCard type. - // e.g. VCARD_TYPE_V21_JAPANESE_SJIS | FLAG_APPEND_TYPE_PARAMS - return sJapaneseMobileTypeSet.contains(vcardType); - } - - /* package */ static boolean refrainPhoneNumberFormatting(final int vcardType) { - return ((vcardType & FLAG_REFRAIN_PHONE_NUMBER_FORMATTING) != 0); - } - - public static boolean needsToConvertPhoneticString(final int vcardType) { - return ((vcardType & FLAG_CONVERT_PHONETIC_NAME_STRINGS) != 0); - } - - public static boolean onlyOneNoteFieldIsAvailable(final int vcardType) { - return vcardType == VCARD_TYPE_DOCOMO; - } - - public static boolean isDoCoMo(final int vcardType) { - return ((vcardType & FLAG_DOCOMO) != 0); - } - - private VCardConfig() { - } -} \ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardConstants.java b/core/java/android/pim/vcard/VCardConstants.java deleted file mode 100644 index 76371ef9b02a1d3cf97c3ce4b2d839380a3d640a..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardConstants.java +++ /dev/null @@ -1,175 +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.pim.vcard; - -/** - * Constants used in both exporter and importer code. - */ -public class VCardConstants { - public static final String VERSION_V21 = "2.1"; - public static final String VERSION_V30 = "3.0"; - public static final String VERSION_V40 = "4.0"; - - // The property names valid both in vCard 2.1 and 3.0. - public static final String PROPERTY_BEGIN = "BEGIN"; - public static final String PROPERTY_VERSION = "VERSION"; - public static final String PROPERTY_N = "N"; - public static final String PROPERTY_FN = "FN"; - public static final String PROPERTY_ADR = "ADR"; - public static final String PROPERTY_EMAIL = "EMAIL"; - public static final String PROPERTY_NOTE = "NOTE"; - public static final String PROPERTY_ORG = "ORG"; - public static final String PROPERTY_SOUND = "SOUND"; // Not fully supported. - public static final String PROPERTY_TEL = "TEL"; - public static final String PROPERTY_TITLE = "TITLE"; - public static final String PROPERTY_ROLE = "ROLE"; - public static final String PROPERTY_PHOTO = "PHOTO"; - public static final String PROPERTY_LOGO = "LOGO"; - public static final String PROPERTY_URL = "URL"; - public static final String PROPERTY_BDAY = "BDAY"; // Birthday (3.0, 4.0) - public static final String PROPERTY_BIRTH = "BIRTH"; // Place of birth (4.0) - public static final String PROPERTY_ANNIVERSARY = "ANNIVERSARY"; // Date of marriage (4.0) - public static final String PROPERTY_NAME = "NAME"; // (3.0, 4,0) - public static final String PROPERTY_NICKNAME = "NICKNAME"; // (3.0, 4.0) - public static final String PROPERTY_SORT_STRING = "SORT-STRING"; // (3.0, 4.0) - public static final String PROPERTY_END = "END"; - - // Valid property names not supported (not appropriately handled) by our importer. - // TODO: Should be removed from the view of memory efficiency? - public static final String PROPERTY_REV = "REV"; - public static final String PROPERTY_AGENT = "AGENT"; // (3.0) - public static final String PROPERTY_DDAY = "DDAY"; // Date of death (4.0) - public static final String PROPERTY_DEATH = "DEATH"; // Place of death (4.0) - - // Available in vCard 3.0. Shoud not use when composing vCard 2.1 file. - - // De-fact property values expressing phonetic names. - public static final String PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; - public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; - public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; - - // Properties both 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"; - // Properties only ContactsStruct has. We alse use this. - public static final String PROPERTY_X_QQ = "X-QQ"; - public static final String PROPERTY_X_NETMEETING = "X-NETMEETING"; - - // Phone number for Skype, available as usual phone. - public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; - - // Property for Android-specific fields. - public static final String PROPERTY_X_ANDROID_CUSTOM = "X-ANDROID-CUSTOM"; - - // Properties for DoCoMo vCard. - public static final String PROPERTY_X_CLASS = "X-CLASS"; - public static final String PROPERTY_X_REDUCTION = "X-REDUCTION"; - public static final String PROPERTY_X_NO = "X-NO"; - public static final String PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; - - public static final String PARAM_TYPE = "TYPE"; - - public static final String PARAM_TYPE_HOME = "HOME"; - public static final String PARAM_TYPE_WORK = "WORK"; - public static final String PARAM_TYPE_FAX = "FAX"; - public static final String PARAM_TYPE_CELL = "CELL"; - public static final String PARAM_TYPE_VOICE = "VOICE"; - public static final String PARAM_TYPE_INTERNET = "INTERNET"; - - public static final String PARAM_CHARSET = "CHARSET"; - public static final String PARAM_ENCODING = "ENCODING"; - - // Abbreviation of "prefered" according to vCard 2.1 specification. - // We interpret this value as "primary" property during import/export. - // - // Note: Both vCard specs does not mention anything about the requirement for this parameter, - // but there may be some vCard importer which will get confused with more than - // one "PREF"s in one property name, while Android accepts them. - public static final String PARAM_TYPE_PREF = "PREF"; - - // Phone type parameters valid in vCard and known to ContactsContract, but not so common. - public static final String PARAM_TYPE_CAR = "CAR"; - public static final String PARAM_TYPE_ISDN = "ISDN"; - public static final String PARAM_TYPE_PAGER = "PAGER"; - public static final String PARAM_TYPE_TLX = "TLX"; // Telex - - // Phone types existing in vCard 2.1 but not known to ContactsContract. - public static final String PARAM_TYPE_MODEM = "MODEM"; - public static final String PARAM_TYPE_MSG = "MSG"; - public static final String PARAM_TYPE_BBS = "BBS"; - public static final String PARAM_TYPE_VIDEO = "VIDEO"; - - public static final String PARAM_ENCODING_7BIT = "7BIT"; - public static final String PARAM_ENCODING_8BIT = "8BIT"; - public static final String PARAM_ENCODING_QP = "QUOTED-PRINTABLE"; - public static final String PARAM_ENCODING_BASE64 = "BASE64"; // Available in vCard 2.1 - public static final String PARAM_ENCODING_B = "B"; // Available in vCard 3.0 - - // TYPE parameters for Phones, which are not formally valid in vCard (at least 2.1). - // These types are basically encoded to "X-" parameters when composing vCard. - // Parser passes these when "X-" is added to the parameter or not. - public static final String PARAM_PHONE_EXTRA_TYPE_CALLBACK = "CALLBACK"; - public static final String PARAM_PHONE_EXTRA_TYPE_RADIO = "RADIO"; - public static final String PARAM_PHONE_EXTRA_TYPE_TTY_TDD = "TTY-TDD"; - public static final String PARAM_PHONE_EXTRA_TYPE_ASSISTANT = "ASSISTANT"; - // vCard composer translates this type to "WORK" + "PREF". Just for parsing. - public static final String PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN"; - // vCard composer translates this type to "VOICE" Just for parsing. - public static final String PARAM_PHONE_EXTRA_TYPE_OTHER = "OTHER"; - - // TYPE parameters for postal addresses. - public static final String PARAM_ADR_TYPE_PARCEL = "PARCEL"; - public static final String PARAM_ADR_TYPE_DOM = "DOM"; - public static final String PARAM_ADR_TYPE_INTL = "INTL"; - - public static final String PARAM_LANGUAGE = "LANGUAGE"; - - // SORT-AS parameter introduced in vCard 4.0 (as of rev.13) - public static final String PARAM_SORT_AS = "SORT-AS"; - - // TYPE parameters not officially valid but used in some vCard exporter. - // Do not use in composer side. - public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY"; - - public interface ImportOnly { - public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; - // Some device emits this "X-" parameter for expressing Google Talk, - // 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"; - } - - //// Mainly for package constants. - - // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of - // SORT-STRING invCard 3.0. - /* package */ static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; - - // Used in unit test. - public static final int MAX_DATA_COLUMN = 15; - - /* package */ static final int MAX_CHARACTER_NUMS_QP = 76; - static final int MAX_CHARACTER_NUMS_BASE64_V30 = 75; - - private VCardConstants() { - } -} \ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardEntry.java b/core/java/android/pim/vcard/VCardEntry.java deleted file mode 100644 index 94f7c5fe1b0d6f46c19afec203bfed2cb3e190f0..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardEntry.java +++ /dev/null @@ -1,1493 +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.pim.vcard; - -import android.accounts.Account; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentResolver; -import android.content.OperationApplicationException; -import android.net.Uri; -import android.os.RemoteException; -import android.provider.ContactsContract; -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.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.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -/** - * This class bridges between data structure of Contact app and VCard data. - */ -public class VCardEntry { - private static final String LOG_TAG = "VCardEntry"; - - private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; - - private static final Map sImMap = new HashMap(); - - static { - sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); - sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); - sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); - sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); - sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); - sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); - sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); - sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, - Im.PROTOCOL_GOOGLE_TALK); - } - - public static class PhoneData { - public final int type; - public final String data; - public final String label; - // isPrimary is (not final but) 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); - } - } - - public static class EmailData { - public final int type; - public final String data; - // Used only when TYPE is TYPE_CUSTOM. - public final String label; - 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); - } - } - - public static class PostalData { - // Determined by vCard specification. - // - 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; - public final String label; - public boolean isPrimary; - - public PostalData(final int type, final List propValueList, - final 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; - } - final 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(final 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(' '); - } else { - empty = false; - } - builder.append(addressPart); - } - } - } else { - for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) { - String addressPart = dataArray[i]; - if (!TextUtils.isEmpty(addressPart)) { - if (!empty) { - builder.append(' '); - } else { - empty = false; - } - builder.append(addressPart); - } - } - } - - return builder.toString().trim(); - } - - @Override - public String toString() { - return String.format("type: %d, label: %s, isPrimary: %s", - type, label, isPrimary); - } - } - - public static class OrganizationData { - public final int type; - // non-final is Intentional: we may change the values since this info is separated into - // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in - // different timing. - public String companyName; - public String departmentName; - public String titleName; - public final String phoneticName; // We won't have this in "TITLE" property. - public boolean isPrimary; - - public OrganizationData(int type, - final String companyName, - final String departmentName, - final String titleName, - final String phoneticName, - final boolean isPrimary) { - this.type = type; - this.companyName = companyName; - this.departmentName = departmentName; - this.titleName = titleName; - this.phoneticName = phoneticName; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof OrganizationData)) { - return false; - } - OrganizationData organization = (OrganizationData)obj; - return (type == organization.type && - TextUtils.equals(companyName, organization.companyName) && - TextUtils.equals(departmentName, organization.departmentName) && - TextUtils.equals(titleName, organization.titleName) && - isPrimary == organization.isPrimary); - } - - public String getFormattedString() { - final StringBuilder builder = new StringBuilder(); - if (!TextUtils.isEmpty(companyName)) { - builder.append(companyName); - } - - if (!TextUtils.isEmpty(departmentName)) { - if (builder.length() > 0) { - builder.append(", "); - } - builder.append(departmentName); - } - - if (!TextUtils.isEmpty(titleName)) { - if (builder.length() > 0) { - builder.append(", "); - } - builder.append(titleName); - } - - return builder.toString(); - } - - @Override - public String toString() { - return String.format( - "type: %d, company: %s, department: %s, title: %s, isPrimary: %s", - type, companyName, departmentName, titleName, isPrimary); - } - } - - public static class ImData { - public final int protocol; - public final String customProtocol; - public final int type; - public final String data; - public final boolean isPrimary; - - public ImData(final int protocol, final String customProtocol, final int type, - final String data, final boolean isPrimary) { - this.protocol = protocol; - this.customProtocol = customProtocol; - this.type = type; - this.data = data; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ImData)) { - return false; - } - ImData imData = (ImData)obj; - return (type == imData.type && protocol == imData.protocol - && (customProtocol != null ? customProtocol.equals(imData.customProtocol) : - (imData.customProtocol == null)) - && (data != null ? data.equals(imData.data) : (imData.data == null)) - && isPrimary == imData.isPrimary); - } - - @Override - public String toString() { - return String.format( - "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", - type, protocol, customProtocol, data, isPrimary); - } - } - - public static 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 final boolean isPrimary; - - public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) { - this.type = type; - this.formatName = formatName; - this.photoBytes = photoBytes; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof PhotoData)) { - return false; - } - PhotoData photoData = (PhotoData)obj; - return (type == photoData.type && - (formatName == null ? (photoData.formatName == null) : - formatName.equals(photoData.formatName)) && - (Arrays.equals(photoBytes, photoData.photoBytes)) && - (isPrimary == photoData.isPrimary)); - } - - @Override - public String toString() { - return String.format("type: %d, format: %s: size: %d, isPrimary: %s", - type, formatName, photoBytes.length, isPrimary); - } - } - - /* package */ static class Property { - private String mPropertyName; - private Map> mParameterMap = - new HashMap>(); - private List mPropertyValueList = new ArrayList(); - private byte[] mPropertyBytes; - - 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(); - mPropertyBytes = null; - } - } - - // TODO(dmiyakawa): vCard 4.0 logically has multiple formatted names and we need to - // select the most preferable one using PREF parameter. - // - // e.g. (based on rev.13) - // FN;PREF=1:John M. Doe - // FN;PREF=2:John Doe - // FN;PREF=3;John - - 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 mFormattedName; - - private String mPhoneticFamilyName; - private String mPhoneticGivenName; - private String mPhoneticMiddleName; - - private String mPhoneticFullName; - - private List mNickNameList; - - private String mDisplayName; - - private String mBirthday; - private String mAnniversary; - - 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 List> mAndroidCustomPropertyList; - - private final int mVCardType; - private final Account mAccount; - - public VCardEntry() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC); - } - - public VCardEntry(int vcardType) { - this(vcardType, null); - } - - public VCardEntry(int vcardType, Account account) { - mVCardType = vcardType; - mAccount = account; - } - - private void addPhone(int type, String data, String label, boolean isPrimary) { - if (mPhoneList == null) { - mPhoneList = new ArrayList(); - } - final StringBuilder builder = new StringBuilder(); - final String trimed = data.trim(); - final String formattedNumber; - if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { - formattedNumber = trimed; - } else { - final 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); - } - } - - final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType); - formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType); - } - PhoneData phoneData = new PhoneData(type, formattedNumber, 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(0); - } - mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); - } - - /** - * Should be called via {@link #handleOrgValue(int, List, Map, boolean) or - * {@link #handleTitleValue(String)}. - */ - private void addNewOrganization(int type, final String companyName, - final String departmentName, - final String titleName, - final String phoneticName, - final boolean isPrimary) { - if (mOrganizationList == null) { - mOrganizationList = new ArrayList(); - } - mOrganizationList.add(new OrganizationData(type, companyName, - departmentName, titleName, phoneticName, isPrimary)); - } - - private static final List sEmptyList = - Collections.unmodifiableList(new ArrayList(0)); - - private String buildSinglePhoneticNameFromSortAsParam(Map> paramMap) { - final Collection sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); - if (sortAsCollection != null && sortAsCollection.size() != 0) { - if (sortAsCollection.size() > 1) { - Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " + - Arrays.toString(sortAsCollection.toArray())); - } - final List sortNames = - VCardUtils.constructListFromValue(sortAsCollection.iterator().next(), - mVCardType); - final StringBuilder builder = new StringBuilder(); - for (final String elem : sortNames) { - builder.append(elem); - } - return builder.toString(); - } else { - return null; - } - } - - /** - * Set "ORG" related values to the appropriate data. If there's more than one - * {@link OrganizationData} objects, this input data are attached to the last one which - * does not have valid values (not including empty but only null). If there's no - * {@link OrganizationData} object, a new {@link OrganizationData} is created, - * whose title is set to null. - */ - private void handleOrgValue(final int type, List orgList, - Map> paramMap, boolean isPrimary) { - final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap); - if (orgList == null) { - orgList = sEmptyList; - } - final String companyName; - final String departmentName; - final int size = orgList.size(); - switch (size) { - case 0: { - companyName = ""; - departmentName = null; - break; - } - case 1: { - companyName = orgList.get(0); - departmentName = null; - break; - } - default: { // More than 1. - companyName = orgList.get(0); - // We're not sure which is the correct string for department. - // In order to keep all the data, concatinate the rest of elements. - StringBuilder builder = new StringBuilder(); - for (int i = 1; i < size; i++) { - if (i > 1) { - builder.append(' '); - } - builder.append(orgList.get(i)); - } - departmentName = builder.toString(); - } - } - if (mOrganizationList == null) { - // Create new first organization entry, with "null" title which may be - // added via handleTitleValue(). - addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary); - return; - } - for (OrganizationData organizationData : mOrganizationList) { - // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty. - // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null. - if (organizationData.companyName == null && - organizationData.departmentName == null) { - // Probably the "TITLE" property comes before the "ORG" property via - // handleTitleLine(). - organizationData.companyName = companyName; - organizationData.departmentName = departmentName; - organizationData.isPrimary = isPrimary; - return; - } - } - // No OrganizatioData is available. Create another one, with "null" title, which may be - // added via handleTitleValue(). - addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary); - } - - /** - * Set "title" value to the appropriate data. If there's more than one - * OrganizationData objects, this input is attached to the last one which does not - * have valid title value (not including empty but only null). If there's no - * OrganizationData object, a new OrganizationData is created, whose company name is - * set to null. - */ - private void handleTitleValue(final String title) { - if (mOrganizationList == null) { - // Create new first organization entry, with "null" other info, which may be - // added via handleOrgValue(). - addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false); - return; - } - for (OrganizationData organizationData : mOrganizationList) { - if (organizationData.titleName == null) { - organizationData.titleName = title; - return; - } - } - // No Organization is available. Create another one, with "null" other info, which may be - // added via handleOrgValue(). - addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false); - } - - private void addIm(int protocol, String customProtocol, int type, - String propValue, boolean isPrimary) { - if (mImList == null) { - mImList = new ArrayList(); - } - mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary)); - } - - private void addNote(final String note) { - if (mNoteList == null) { - mNoteList = new ArrayList(1); - } - mNoteList.add(note); - } - - private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { - if (mPhotoList == null) { - mPhotoList = new ArrayList(1); - } - final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary); - mPhotoList.add(photoData); - } - - /** - * Tries to extract paramMap, constructs SORT-AS parameter values, and store them in - * appropriate phonetic name variables. - * - * This method does not care the vCard version. Even when we have SORT-AS parameters in - * invalid versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't drop - * meaningful information. If we had this parameter in the N field of vCard 3.0, and - * the contact data also have SORT-STRING, we will prefer SORT-STRING, since it is - * regitimate property to be understood. - */ - private void tryHandleSortAsName(final Map> paramMap) { - if (VCardConfig.isVersion30(mVCardType) && - !(TextUtils.isEmpty(mPhoneticFamilyName) && - TextUtils.isEmpty(mPhoneticMiddleName) && - TextUtils.isEmpty(mPhoneticGivenName))) { - return; - } - - final Collection sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); - if (sortAsCollection != null && sortAsCollection.size() != 0) { - if (sortAsCollection.size() > 1) { - Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " + - Arrays.toString(sortAsCollection.toArray())); - } - final List sortNames = - VCardUtils.constructListFromValue(sortAsCollection.iterator().next(), - mVCardType); - int size = sortNames.size(); - if (size > 3) { - size = 3; - } - switch (size) { - case 3: mPhoneticMiddleName = sortNames.get(2); //$FALL-THROUGH$ - case 2: mPhoneticGivenName = sortNames.get(1); //$FALL-THROUGH$ - default: mPhoneticFamilyName = sortNames.get(0); break; - } - } - } - - @SuppressWarnings("fallthrough") - private void handleNProperty(final List paramValues, - Map> paramMap) { - // in vCard 4.0, SORT-AS parameter is available. - tryHandleSortAsName(paramMap); - - // Family, Given, Middle, Prefix, Suffix. (1 - 5) - int size; - if (paramValues == null || (size = paramValues.size()) < 1) { - return; - } - if (size > 5) { - size = 5; - } - - switch (size) { - // Fall-through. - case 5: mSuffix = paramValues.get(4); - case 4: mPrefix = paramValues.get(3); - case 3: mMiddleName = paramValues.get(2); - case 2: mGivenName = paramValues.get(1); - default: mFamilyName = paramValues.get(0); - } - } - - /** - * Note: 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) { - if (!(TextUtils.isEmpty(mPhoneticFamilyName) && - TextUtils.isEmpty(mPhoneticMiddleName) && - TextUtils.isEmpty(mPhoneticGivenName))) { - // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found. - // Ignore "SOUND;X-IRMC-N". - return; - } - - int size; - if (elems == null || (size = elems.size()) < 1) { - return; - } - - // Assume that the order is "Family, Given, Middle". - // This is not from specification but mere assumption. Some Japanese phones use this order. - if (size > 3) { - size = 3; - } - - if (elems.get(0).length() > 0) { - boolean onlyFirstElemIsNonEmpty = true; - for (int i = 1; i < size; i++) { - if (elems.get(i).length() > 0) { - onlyFirstElemIsNonEmpty = false; - break; - } - } - if (onlyFirstElemIsNonEmpty) { - final String[] namesArray = elems.get(0).split(" "); - final int nameArrayLength = namesArray.length; - if (nameArrayLength == 3) { - // Assume the string is "Family Middle Given". - mPhoneticFamilyName = namesArray[0]; - mPhoneticMiddleName = namesArray[1]; - mPhoneticGivenName = namesArray[2]; - } else if (nameArrayLength == 2) { - // Assume the string is "Family Given" based on the Japanese mobile - // phones' preference. - mPhoneticFamilyName = namesArray[0]; - mPhoneticGivenName = namesArray[1]; - } else { - mPhoneticFullName = elems.get(0); - } - return; - } - } - - switch (size) { - // fallthrough - case 3: mPhoneticMiddleName = elems.get(2); - case 2: mPhoneticGivenName = elems.get(1); - default: mPhoneticFamilyName = elems.get(0); - } - } - - public void addProperty(final Property property) { - final 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(VCardConstants.PROPERTY_VERSION)) { - // vCard version. Ignore this. - } else if (propName.equals(VCardConstants.PROPERTY_FN)) { - mFormattedName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFormattedName == 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. - mFormattedName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_N)) { - handleNProperty(propValueList, paramMap); - } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) { - mPhoneticFullName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) || - propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) { - addNickName(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) { - Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null - && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) { - // As of 2009-10-08, Parser side does not split a property value into separated - // values using ';' (in other words, propValueList.size() == 1), - // which is correct behavior from the view of vCard 2.1. - // But we want it to be separated, so do the separation here. - final List phoneticNameList = - VCardUtils.constructListFromValue(propValue, mVCardType); - handlePhoneticNameFromSound(phoneticNameList); - } else { - // Ignore this field since Android cannot understand what it is. - } - } else if (propName.equals(VCardConstants.PROPERTY_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(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - typeString = typeString.toUpperCase(); - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { - type = StructuredPostal.TYPE_HOME; - label = ""; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) || - typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_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(VCardConstants.PARAM_ADR_TYPE_PARCEL) || - typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) || - typeString.equals(VCardConstants.PARAM_ADR_TYPE_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(VCardConstants.PROPERTY_EMAIL)) { - int type = -1; - String label = null; - boolean isPrimary = false; - Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - typeString = typeString.toUpperCase(); - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { - type = Email.TYPE_HOME; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) { - type = Email.TYPE_WORK; - } else if (typeString.equals(VCardConstants.PARAM_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(VCardConstants.PROPERTY_ORG)) { - // vCard specification does not specify other types. - final int type = Organization.TYPE_WORK; - boolean isPrimary = false; - Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } - } - } - handleOrgValue(type, propValueList, paramMap, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) { - handleTitleValue(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) { - // This conflicts with TITLE. Ignore for now... - // handleTitleValue(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) || - propName.equals(VCardConstants.PROPERTY_LOGO)) { - Collection paramMapValue = paramMap.get("VALUE"); - if (paramMapValue != null && paramMapValue.contains("URL")) { - // Currently we do not have appropriate example for testing this case. - } else { - final Collection typeCollection = paramMap.get("TYPE"); - String formatName = null; - boolean isPrimary = false; - if (typeCollection != null) { - for (String typeValue : typeCollection) { - if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) { - isPrimary = true; - } else if (formatName == null){ - formatName = typeValue; - } - } - } - addPhotoBytes(formatName, propBytes, isPrimary); - } - } else if (propName.equals(VCardConstants.PROPERTY_TEL)) { - final Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - final Object typeObject = - VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue); - 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 (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else { - isPrimary = false; - } - addPhone(type, propValue, label, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) { - // The phone number available via Skype. - Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - final int type = Phone.TYPE_OTHER; - final boolean isPrimary; - if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else { - isPrimary = false; - } - addPhone(type, propValue, null, isPrimary); - } else if (sImMap.containsKey(propName)) { - final int protocol = sImMap.get(propName); - boolean isPrimary = false; - int type = -1; - final Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else if (type < 0) { - if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) { - type = Im.TYPE_HOME; - } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) { - type = Im.TYPE_WORK; - } - } - } - } - if (type < 0) { - type = Im.TYPE_HOME; - } - addIm(protocol, null, type, propValue, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) { - addNote(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_URL)) { - if (mWebsiteList == null) { - mWebsiteList = new ArrayList(1); - } - mWebsiteList.add(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) { - mBirthday = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) { - mAnniversary = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { - mPhoneticGivenName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { - mPhoneticMiddleName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) { - mPhoneticFamilyName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { - final List customPropertyList = - VCardUtils.constructListFromValue(propValue, mVCardType); - handleAndroidCustomProperty(customPropertyList); - } else { - } - } - - private void handleAndroidCustomProperty(final List customPropertyList) { - if (mAndroidCustomPropertyList == null) { - mAndroidCustomPropertyList = new ArrayList>(); - } - mAndroidCustomPropertyList.add(customPropertyList); - } - - /** - * Construct the display name. The constructed data must not be null. - */ - private void constructDisplayName() { - // FullName (created via "FN" or "NAME" field) is prefered. - if (!TextUtils.isEmpty(mFormattedName)) { - mDisplayName = mFormattedName; - } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { - mDisplayName = VCardUtils.constructNameFromElements(mVCardType, - mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix); - } 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); - } else if (mOrganizationList != null && mOrganizationList.size() > 0) { - mDisplayName = mOrganizationList.get(0).getFormattedString(); - } - - if (mDisplayName == null) { - mDisplayName = ""; - } - } - - /** - * Consolidate several fielsds (like mName) using name candidates, - */ - public void consolidateFields() { - constructDisplayName(); - - if (mPhoneticFullName != null) { - mPhoneticFullName = mPhoneticFullName.trim(); - } - } - - public Uri pushIntoContentResolver(ContentResolver resolver) { - ArrayList operationList = - new ArrayList(); - // After applying the batch the first result's Uri is returned so it is important that - // the RawContact is the first operation that gets inserted into the list - 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); - } else { - builder.withValue(RawContacts.ACCOUNT_NAME, null); - builder.withValue(RawContacts.ACCOUNT_TYPE, null); - } - operationList.add(builder.build()); - - if (!nameFieldsAreEmpty()) { - 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); - - if (!(TextUtils.isEmpty(mPhoneticGivenName) - && TextUtils.isEmpty(mPhoneticFamilyName) - && TextUtils.isEmpty(mPhoneticMiddleName))) { - builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); - builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); - builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); - } else if (!TextUtils.isEmpty(mPhoneticFullName)) { - builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName); - } - - builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); - operationList.add(builder.build()); - } - - if (mNickNameList != null && mNickNameList.size() > 0) { - 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); - 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(Phone.IS_PRIMARY, 1); - } - operationList.add(builder.build()); - } - } - - if (mOrganizationList != null) { - 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); - builder.withValue(Organization.TYPE, organizationData.type); - if (organizationData.companyName != null) { - builder.withValue(Organization.COMPANY, organizationData.companyName); - } - if (organizationData.departmentName != null) { - builder.withValue(Organization.DEPARTMENT, organizationData.departmentName); - } - if (organizationData.titleName != null) { - builder.withValue(Organization.TITLE, organizationData.titleName); - } - if (organizationData.phoneticName != null) { - builder.withValue(Organization.PHONETIC_NAME, organizationData.phoneticName); - } - if (organizationData.isPrimary) { - builder.withValue(Organization.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); - builder.withValue(Im.PROTOCOL, imData.protocol); - builder.withValue(Im.DATA, imData.data); - if (imData.protocol == Im.PROTOCOL_CUSTOM) { - builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol); - } - if (imData.isPrimary) { - builder.withValue(Data.IS_PRIMARY, 1); - } - operationList.add(builder.build()); - } - } - - 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) { - 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 (photoData.isPrimary) { - builder.withValue(Photo.IS_PRIMARY, 1); - } - 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_HOMEPAGE for safety. - builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE); - 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 (!TextUtils.isEmpty(mAnniversary)) { - 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, mAnniversary); - builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY); - operationList.add(builder.build()); - } - - if (mAndroidCustomPropertyList != null) { - for (List customPropertyList : mAndroidCustomPropertyList) { - int size = customPropertyList.size(); - if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) { - continue; - } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) { - size = VCardConstants.MAX_DATA_COLUMN + 1; - customPropertyList = - customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2); - } - - int i = 0; - for (final String customPropertyValue : customPropertyList) { - if (i == 0) { - final String mimeType = customPropertyValue; - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, mimeType); - } else { // 1 <= i && i <= MAX_DATA_COLUMNS - if (!TextUtils.isEmpty(customPropertyValue)) { - builder.withValue("data" + i, customPropertyValue); - } - } - - i++; - } - 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 { - ContentProviderResult[] results = resolver.applyBatch( - ContactsContract.AUTHORITY, operationList); - // the first result is always the raw_contact. return it's uri so - // that it can be found later. do null checking for badly behaving - // ContentResolvers - return (results == null || results.length == 0 || results[0] == null) - ? null - : results[0].uri; - } catch (RemoteException e) { - Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); - return null; - } catch (OperationApplicationException e) { - Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); - return null; - } - } - - public static VCardEntry buildFromResolver(ContentResolver resolver) { - return buildFromResolver(resolver, Contacts.CONTENT_URI); - } - - public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) { - - return null; - } - - private boolean nameFieldsAreEmpty() { - return (TextUtils.isEmpty(mFamilyName) - && TextUtils.isEmpty(mMiddleName) - && TextUtils.isEmpty(mGivenName) - && TextUtils.isEmpty(mPrefix) - && TextUtils.isEmpty(mSuffix) - && TextUtils.isEmpty(mFormattedName) - && TextUtils.isEmpty(mPhoneticFamilyName) - && TextUtils.isEmpty(mPhoneticMiddleName) - && TextUtils.isEmpty(mPhoneticGivenName) - && TextUtils.isEmpty(mPhoneticFullName)); - } - - 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 ""; - } - } - - // All getter methods should be used carefully, since they may change - // in the future as of 2009-10-05, on which I cannot be sure this structure - // is completely consolidated. - // - // 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. - - public String getFamilyName() { - return mFamilyName; - } - - public String getGivenName() { - return mGivenName; - } - - public String getMiddleName() { - return mMiddleName; - } - - public String getPrefix() { - return mPrefix; - } - - public String getSuffix() { - return mSuffix; - } - - public String getFullName() { - return mFormattedName; - } - - public String getPhoneticFamilyName() { - return mPhoneticFamilyName; - } - - public String getPhoneticGivenName() { - return mPhoneticGivenName; - } - - public String getPhoneticMiddleName() { - return mPhoneticMiddleName; - } - - public String getPhoneticFullName() { - return mPhoneticFullName; - } - - public final List getNickNameList() { - return mNickNameList; - } - - public String getBirthday() { - return mBirthday; - } - - public final List getNotes() { - return mNoteList; - } - - public final List getPhoneList() { - return mPhoneList; - } - - public final List getEmailList() { - return mEmailList; - } - - public final List getPostalList() { - return mPostalList; - } - - public final List getOrganizationList() { - return mOrganizationList; - } - - public final List getImList() { - return mImList; - } - - public final List getPhotoList() { - return mPhotoList; - } - - public final List getWebsiteList() { - return mWebsiteList; - } - - public String getDisplayName() { - if (mDisplayName == null) { - constructDisplayName(); - } - return mDisplayName; - } -} diff --git a/core/java/android/pim/vcard/VCardEntryCommitter.java b/core/java/android/pim/vcard/VCardEntryCommitter.java deleted file mode 100644 index a8c8057a58b64c8796ddd3d1942d66d175a19c97..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardEntryCommitter.java +++ /dev/null @@ -1,68 +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.pim.vcard; - -import android.content.ContentResolver; -import android.net.Uri; -import android.util.Log; - -import java.util.ArrayList; - -/** - *

    - * {@link VCardEntryHandler} implementation which commits the entry to ContentResolver. - *

    - *

    - * Note:
    - * Each vCard may contain big photo images encoded by BASE64, - * If we store all vCard entries in memory, OutOfMemoryError may be thrown. - * Thus, this class push each VCard entry into ContentResolver immediately. - *

    - */ -public class VCardEntryCommitter implements VCardEntryHandler { - public static String LOG_TAG = "VCardEntryComitter"; - - private final ContentResolver mContentResolver; - private long mTimeToCommit; - private ArrayList mCreatedUris = new ArrayList(); - - public VCardEntryCommitter(ContentResolver resolver) { - mContentResolver = resolver; - } - - public void onStart() { - } - - public void onEnd() { - if (VCardConfig.showPerformanceLog()) { - Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit)); - } - } - - public void onEntryCreated(final VCardEntry vcardEntry) { - long start = System.currentTimeMillis(); - mCreatedUris.add(vcardEntry.pushIntoContentResolver(mContentResolver)); - mTimeToCommit += System.currentTimeMillis() - start; - } - - /** - * Returns the list of created Uris. This list should not be modified by the caller as it is - * not a clone. - */ - public ArrayList getCreatedUris() { - return mCreatedUris; - } -} \ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardEntryConstructor.java b/core/java/android/pim/vcard/VCardEntryConstructor.java deleted file mode 100644 index aa3e3e2f585f4d0c66fd83910282c92193feeca4..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardEntryConstructor.java +++ /dev/null @@ -1,234 +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.pim.vcard; - -import android.accounts.Account; -import android.text.TextUtils; -import android.util.Base64; -import android.util.CharsetUtils; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - *

    - * The {@link VCardInterpreter} implementation which enables {@link VCardEntryHandler} objects - * to easily handle each vCard entry. - *

    - *

    - * This class understand details inside vCard and translates it to {@link VCardEntry}. - * Then the class throw it to {@link VCardEntryHandler} registered via - * {@link #addEntryHandler(VCardEntryHandler)}, so that all those registered objects - * are able to handle the {@link VCardEntry} object. - *

    - *

    - * If you want to know the detail inside vCard, it would be better to implement - * {@link VCardInterpreter} directly, instead of relying on this class and - * {@link VCardEntry} created by the object. - *

    - */ -public class VCardEntryConstructor implements VCardInterpreter { - private static String LOG_TAG = "VCardEntryConstructor"; - - private VCardEntry.Property mCurrentProperty = new VCardEntry.Property(); - private VCardEntry mCurrentVCardEntry; - private String mParamType; - - // The charset using which {@link VCardInterpreter} parses the text. - // Each String is first decoded into binary stream with this charset, and encoded back - // to "target charset", which may be explicitly specified by the vCard with "CHARSET" - // property or implicitly mentioned by its version (e.g. vCard 3.0 recommends UTF-8). - private final String mSourceCharset; - - private final boolean mStrictLineBreaking; - private final int mVCardType; - private final Account mAccount; - - // For measuring performance. - private long mTimePushIntoContentResolver; - - private final List mEntryHandlers = new ArrayList(); - - public VCardEntryConstructor() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC, null); - } - - public VCardEntryConstructor(final int vcardType) { - this(vcardType, null, null, false); - } - - public VCardEntryConstructor(final int vcardType, final Account account) { - this(vcardType, account, null, false); - } - - public VCardEntryConstructor(final int vcardType, final Account account, - final String inputCharset) { - this(vcardType, account, inputCharset, false); - } - - /** - * @hide Just for testing. - */ - public VCardEntryConstructor(final int vcardType, final Account account, - final String inputCharset, final boolean strictLineBreakParsing) { - if (inputCharset != null) { - mSourceCharset = inputCharset; - } else { - mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; - } - mStrictLineBreaking = strictLineBreakParsing; - mVCardType = vcardType; - mAccount = account; - } - - public void addEntryHandler(VCardEntryHandler entryHandler) { - mEntryHandlers.add(entryHandler); - } - - @Override - public void start() { - for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onStart(); - } - } - - @Override - public void end() { - for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onEnd(); - } - } - - public void clear() { - mCurrentVCardEntry = null; - mCurrentProperty = new VCardEntry.Property(); - } - - @Override - public void startEntry() { - if (mCurrentVCardEntry != null) { - Log.e(LOG_TAG, "Nested VCard code is not supported now."); - } - mCurrentVCardEntry = new VCardEntry(mVCardType, mAccount); - } - - @Override - public void endEntry() { - mCurrentVCardEntry.consolidateFields(); - for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onEntryCreated(mCurrentVCardEntry); - } - mCurrentVCardEntry = null; - } - - @Override - public void startProperty() { - mCurrentProperty.clear(); - } - - @Override - public void endProperty() { - mCurrentVCardEntry.addProperty(mCurrentProperty); - } - - @Override - public void propertyName(String name) { - mCurrentProperty.setPropertyName(name); - } - - @Override - public void propertyGroup(String group) { - } - - @Override - public void propertyParamType(String type) { - if (mParamType != null) { - Log.e(LOG_TAG, "propertyParamType() is called more than once " + - "before propertyParamValue() is called"); - } - mParamType = type; - } - - @Override - public void propertyParamValue(String value) { - if (mParamType == null) { - // From vCard 2.1 specification. vCard 3.0 formally does not allow this case. - mParamType = "TYPE"; - } - if (!VCardUtils.containsOnlyAlphaDigitHyphen(value)) { - value = VCardUtils.convertStringCharset( - value, mSourceCharset, VCardConfig.DEFAULT_IMPORT_CHARSET); - } - mCurrentProperty.addParameter(mParamType, value); - mParamType = null; - } - - private String handleOneValue(String value, - String sourceCharset, String targetCharset, String encoding) { - // It is possible when some of multiple values are empty. - // e.g. N:;a;;; -> values are "", "a", "", "", and "". - if (TextUtils.isEmpty(value)) { - return ""; - } - - if (encoding != null) { - if (encoding.equals("BASE64") || encoding.equals("B")) { - mCurrentProperty.setPropertyBytes(Base64.decode(value.getBytes(), Base64.DEFAULT)); - return value; - } else if (encoding.equals("QUOTED-PRINTABLE")) { - return VCardUtils.parseQuotedPrintable( - value, mStrictLineBreaking, sourceCharset, targetCharset); - } - Log.w(LOG_TAG, "Unknown encoding. Fall back to default."); - } - - // Just translate the charset of a given String from inputCharset to a system one. - return VCardUtils.convertStringCharset(value, sourceCharset, targetCharset); - } - - public void propertyValues(List values) { - if (values == null || values.isEmpty()) { - return; - } - - final Collection charsetCollection = - mCurrentProperty.getParameters(VCardConstants.PARAM_CHARSET); - final Collection encodingCollection = - mCurrentProperty.getParameters(VCardConstants.PARAM_ENCODING); - final String encoding = - ((encodingCollection != null) ? encodingCollection.iterator().next() : null); - String targetCharset = CharsetUtils.nameForDefaultVendor( - ((charsetCollection != null) ? charsetCollection.iterator().next() : null)); - if (TextUtils.isEmpty(targetCharset)) { - targetCharset = VCardConfig.DEFAULT_IMPORT_CHARSET; - } - - for (final String value : values) { - mCurrentProperty.addToPropertyValueList( - handleOneValue(value, mSourceCharset, targetCharset, encoding)); - } - } - - /** - * @hide - */ - public void showPerformanceInfo() { - Log.d(LOG_TAG, "time for insert ContactStruct to database: " + - mTimePushIntoContentResolver + " ms"); - } -} diff --git a/core/java/android/pim/vcard/VCardEntryCounter.java b/core/java/android/pim/vcard/VCardEntryCounter.java deleted file mode 100644 index 7bab50dcdb59183627d51253aa39c9e14af45498..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardEntryCounter.java +++ /dev/null @@ -1,63 +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.pim.vcard; - -import java.util.List; - -/** - * The class which just counts the number of vCard entries in the specified input. - */ -public class VCardEntryCounter implements VCardInterpreter { - private int mCount; - - public int getCount() { - return mCount; - } - - public void start() { - } - - public void end() { - } - - public void startEntry() { - } - - public void endEntry() { - mCount++; - } - - public void startProperty() { - } - - public void endProperty() { - } - - public void propertyGroup(String group) { - } - - public void propertyName(String name) { - } - - public void propertyParamType(String type) { - } - - public void propertyParamValue(String value) { - } - - public void propertyValues(List values) { - } -} diff --git a/core/java/android/pim/vcard/VCardEntryHandler.java b/core/java/android/pim/vcard/VCardEntryHandler.java deleted file mode 100644 index 56bf69d5d37175d4af699f594613edb0988ddaa6..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardEntryHandler.java +++ /dev/null @@ -1,43 +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.pim.vcard; - -/** - *

    - * The interface called by {@link VCardEntryConstructor}. - *

    - *

    - * This class is useful when you don't want to know vCard data in detail. If you want to know - * it, it would be better to consider using {@link VCardInterpreter}. - *

    - */ -public interface VCardEntryHandler { - /** - * Called when the parsing started. - */ - public void onStart(); - - /** - * The method called when one VCard entry is successfully created - */ - public void onEntryCreated(final VCardEntry entry); - - /** - * Called when the parsing ended. - * Able to be use this method for showing performance log, etc. - */ - public void onEnd(); -} diff --git a/core/java/android/pim/vcard/VCardInterpreter.java b/core/java/android/pim/vcard/VCardInterpreter.java deleted file mode 100644 index 03704a22a962af8be7e2ad8b40955ba276e930da..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardInterpreter.java +++ /dev/null @@ -1,102 +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.pim.vcard; - -import java.util.List; - -/** - *

    - * The interface which should be implemented by the classes which have to analyze each - * vCard entry minutely. - *

    - *

    - * Here, there are several terms specific to vCard (and this library). - *

    - *

    - * The term "entry" is one vCard representation in the input, which should start with "BEGIN:VCARD" - * and end with "END:VCARD". - *

    - *

    - * The term "property" is one line in vCard entry, which consists of "group", "property name", - * "parameter(param) names and values", and "property values". - *

    - *

    - * e.g. group1.propName;paramName1=paramValue1;paramName2=paramValue2;propertyValue1;propertyValue2... - *

    - */ -public interface VCardInterpreter { - /** - * Called when vCard interpretation started. - */ - void start(); - - /** - * Called when vCard interpretation finished. - */ - void end(); - - /** - * Called when parsing one vCard entry started. - * More specifically, this method is called when "BEGIN:VCARD" is read. - */ - void startEntry(); - - /** - * Called when parsing one vCard entry ended. - * More specifically, this method is called when "END:VCARD" is read. - * Note that {@link #startEntry()} may be called since - * vCard (especially 2.1) allows nested vCard. - */ - void endEntry(); - - /** - * Called when reading one property started. - */ - void startProperty(); - - /** - * Called when reading one property ended. - */ - void endProperty(); - - /** - * @param group A group name. This method may be called more than once or may not be - * called at all, depending on how many gruoups are appended to the property. - */ - void propertyGroup(String group); - - /** - * @param name A property name like "N", "FN", "ADR", etc. - */ - void propertyName(String name); - - /** - * @param type A parameter name like "ENCODING", "CHARSET", etc. - */ - void propertyParamType(String type); - - /** - * @param value A parameter value. This method may be called without - * {@link #propertyParamType(String)} being called (when the vCard is vCard 2.1). - */ - void propertyParamValue(String value); - - /** - * @param values List of property values. The size of values would be 1 unless - * coressponding property name is "N", "ADR", or "ORG". - */ - void propertyValues(List values); -} diff --git a/core/java/android/pim/vcard/VCardInterpreterCollection.java b/core/java/android/pim/vcard/VCardInterpreterCollection.java deleted file mode 100644 index 77e44a02b2e0c181c21ba6b951a8006d77c378be..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardInterpreterCollection.java +++ /dev/null @@ -1,102 +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.pim.vcard; - -import java.util.Collection; -import java.util.List; - -/** - * The {@link VCardInterpreter} implementation which aggregates more than one - * {@link VCardInterpreter} objects and make a user object treat them as one - * {@link VCardInterpreter} object. - */ -public final class VCardInterpreterCollection implements VCardInterpreter { - private final Collection mInterpreterCollection; - - public VCardInterpreterCollection(Collection interpreterCollection) { - mInterpreterCollection = interpreterCollection; - } - - public Collection getCollection() { - return mInterpreterCollection; - } - - public void start() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.start(); - } - } - - public void end() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.end(); - } - } - - public void startEntry() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.startEntry(); - } - } - - public void endEntry() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.endEntry(); - } - } - - public void startProperty() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.startProperty(); - } - } - - public void endProperty() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.endProperty(); - } - } - - public void propertyGroup(String group) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyGroup(group); - } - } - - public void propertyName(String name) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyName(name); - } - } - - public void propertyParamType(String type) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyParamType(type); - } - } - - public void propertyParamValue(String value) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyParamValue(value); - } - } - - public void propertyValues(List values) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyValues(values); - } - } -} diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java deleted file mode 100644 index 31b9369231f52f56a0c6a38a8c5af5eabde8da7d..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardParser.java +++ /dev/null @@ -1,55 +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.pim.vcard; - -import android.pim.vcard.exception.VCardException; - -import java.io.IOException; -import java.io.InputStream; - -public interface VCardParser { - /** - *

    - * 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 allowed in vCard 3.0. In vCard 2.1, - * In some exreme case, it is allowed for vCard to have different charsets in one vCard. - *

    - *

    - * We recommend you use {@link 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 interepreter A {@link VCardInterpreter} object which used to construct data. - * @throws IOException, VCardException - */ - public void parse(InputStream is, VCardInterpreter interepreter) - throws IOException, VCardException; - - /** - *

    - * Cancel parsing vCard. Useful when you want to stop the parse in the other threads. - *

    - *

    - * Actual cancel is done after parsing the current vcard. - *

    - */ - public abstract void cancel(); -} diff --git a/core/java/android/pim/vcard/VCardParserImpl_V21.java b/core/java/android/pim/vcard/VCardParserImpl_V21.java deleted file mode 100644 index 8b365eb417adfa44be0e165bafbc01b8c5b84c6b..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardParserImpl_V21.java +++ /dev/null @@ -1,1046 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.pim.vcard; - -import android.pim.vcard.exception.VCardAgentNotSupportedException; -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.VCardVersionException; -import android.text.TextUtils; -import android.util.Log; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - *

    - * Basic implementation achieving vCard parsing. Based on vCard 2.1, - *

    - * @hide - */ -/* package */ class VCardParserImpl_V21 { - private static final String LOG_TAG = "VCardParserImpl_V21"; - - private static final class EmptyInterpreter implements VCardInterpreter { - @Override - public void end() { - } - @Override - public void endEntry() { - } - @Override - public void endProperty() { - } - @Override - public void propertyGroup(String group) { - } - @Override - public void propertyName(String name) { - } - @Override - public void propertyParamType(String type) { - } - @Override - public void propertyParamValue(String value) { - } - @Override - public void propertyValues(List values) { - } - @Override - public void start() { - } - @Override - public void startEntry() { - } - @Override - public void startProperty() { - } - } - - protected static final class CustomBufferedReader extends BufferedReader { - private long mTime; - - /** - * Needed since "next line" may be null due to end of line. - */ - private boolean mNextLineIsValid; - private String mNextLine; - - public CustomBufferedReader(Reader in) { - super(in); - } - - @Override - public String readLine() throws IOException { - if (mNextLineIsValid) { - final String ret = mNextLine; - mNextLine = null; - mNextLineIsValid = false; - return ret; - } - - long start = System.currentTimeMillis(); - final String line = super.readLine(); - long end = System.currentTimeMillis(); - mTime += end - start; - return line; - } - - /** - * Read one line, but make this object store it in its queue. - */ - public String peekLine() throws IOException { - if (!mNextLineIsValid) { - long start = System.currentTimeMillis(); - final String line = super.readLine(); - long end = System.currentTimeMillis(); - mTime += end - start; - - mNextLine = line; - mNextLineIsValid = true; - } - - return mNextLine; - } - - public long getTotalmillisecond() { - return mTime; - } - } - - private static final String DEFAULT_ENCODING = "8BIT"; - - protected boolean mCanceled; - protected VCardInterpreter mInterpreter; - - protected final String mIntermediateCharset; - - /** - *

    - * The encoding type for deconding byte streams. This member variable is - * reset to a default encoding every time when a new item comes. - *

    - *

    - * "Encoding" in vCard is different from "Charset". It is mainly used for - * addresses, notes, images. "7BIT", "8BIT", "BASE64", and - * "QUOTED-PRINTABLE" are known examples. - *

    - */ - protected String mCurrentEncoding; - - /** - *

    - * The reader object to be used internally. - *

    - *

    - * Developers should not directly read a line from this object. Use - * getLine() unless there some reason. - *

    - */ - protected CustomBufferedReader mReader; - - /** - *

    - * Set for storing unkonwn TYPE attributes, which is not acceptable in vCard - * specification, but happens to be seen in real world vCard. - *

    - */ - protected final Set mUnknownTypeSet = new HashSet(); - - /** - *

    - * Set for storing unkonwn VALUE attributes, which is not acceptable in - * vCard specification, but happens to be seen in real world vCard. - *

    - */ - protected final Set mUnknownValueSet = new HashSet(); - - - // 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. - // TODO: Don't ignore by using count, but read all of information outside vCard. - private int mNestCount; - - // Used only for parsing END:VCARD. - private String mPreviousLine; - - // For measuring performance. - private long mTimeTotal; - private long mTimeReadStartRecord; - private long mTimeReadEndRecord; - private long mTimeStartProperty; - private long mTimeEndProperty; - private long mTimeParseItems; - private long mTimeParseLineAndHandleGroup; - private long mTimeParsePropertyValues; - private long mTimeParseAdrOrgN; - private long mTimeHandleMiscPropertyValue; - private long mTimeHandleQuotedPrintable; - private long mTimeHandleBase64; - - public VCardParserImpl_V21() { - this(VCardConfig.VCARD_TYPE_DEFAULT); - } - - public VCardParserImpl_V21(int vcardType) { - if ((vcardType & VCardConfig.FLAG_TORELATE_NEST) != 0) { - mNestCount = 1; - } - - mIntermediateCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; - } - - /** - *

    - * Parses the file at the given position. - *

    - */ - //
    vcard_file = [wsls] vcard [wsls]
    - protected void parseVCardFile() throws IOException, VCardException { - boolean readingFirstFile = true; - while (true) { - if (mCanceled) { - break; - } - if (!parseOneVCard(readingFirstFile)) { - break; - } - readingFirstFile = false; - } - - if (mNestCount > 0) { - boolean useCache = true; - for (int i = 0; i < mNestCount; i++) { - readEndVCard(useCache, true); - useCache = false; - } - } - } - - /** - * @return true when a given property name is a valid property name. - */ - protected boolean isValidPropertyName(final String propertyName) { - if (!(getKnownPropertyNameSet().contains(propertyName.toUpperCase()) || - propertyName.startsWith("X-")) - && !mUnknownTypeSet.contains(propertyName)) { - mUnknownTypeSet.add(propertyName); - Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); - } - return true; - } - - /** - * @return String. It may be null, or its length may be 0 - * @throws IOException - */ - protected String getLine() throws IOException { - return mReader.readLine(); - } - - protected String peekLine() throws IOException { - return mReader.peekLine(); - } - - /** - * @return String with it's length > 0 - * @throws IOException - * @throws VCardException when the stream reached end of line - */ - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Reached end of buffer."); - } else if (line.trim().length() > 0) { - return line; - } - } - } - - /* - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF - * "END" [ws] ":" [ws] "VCARD" - */ - private boolean parseOneVCard(boolean firstRead) throws IOException, VCardException { - boolean allowGarbage = false; - if (firstRead) { - if (mNestCount > 0) { - for (int i = 0; i < mNestCount; i++) { - if (!readBeginVCard(allowGarbage)) { - return false; - } - allowGarbage = true; - } - } - } - - if (!readBeginVCard(allowGarbage)) { - return false; - } - final long beforeStartEntry = System.currentTimeMillis(); - mInterpreter.startEntry(); - mTimeReadStartRecord += System.currentTimeMillis() - beforeStartEntry; - - final long beforeParseItems = System.currentTimeMillis(); - parseItems(); - mTimeParseItems += System.currentTimeMillis() - beforeParseItems; - - readEndVCard(true, false); - - final long beforeEndEntry = System.currentTimeMillis(); - mInterpreter.endEntry(); - mTimeReadEndRecord += System.currentTimeMillis() - beforeEndEntry; - return true; - } - - /** - * @return True when successful. False when reaching the end of line - * @throws IOException - * @throws VCardException - */ - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - String line; - do { - while (true) { - line = getLine(); - if (line == null) { - return false; - } else if (line.trim().length() > 0) { - break; - } - } - final String[] strArray = line.split(":", 2); - final int length = strArray.length; - - // Although vCard 2.1/3.0 specification does not allow lower cases, - // we found vCard file emitted by some external vCard expoter have such - // invalid Strings. - // So we allow it. - // e.g. - // BEGIN:vCard - if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") - && strArray[1].trim().equalsIgnoreCase("VCARD")) { - return true; - } else if (!allowGarbage) { - if (mNestCount > 0) { - mPreviousLine = line; - return false; - } else { - throw new VCardException("Expected String \"BEGIN:VCARD\" did not come " - + "(Instead, \"" + line + "\" came)"); - } - } - } while (allowGarbage); - - throw new VCardException("Reached where must not be reached."); - } - - /** - *

    - * The arguments useCache and allowGarbase are usually true and false - * accordingly when this function is called outside this function itself. - *

    - * - * @param useCache When true, line is obtained from mPreviousline. - * Otherwise, getLine() is used. - * @param allowGarbage When true, ignore non "END:VCARD" line. - * @throws IOException - * @throws VCardException - */ - protected void readEndVCard(boolean useCache, boolean allowGarbage) throws IOException, - VCardException { - String line; - do { - if (useCache) { - // Though vCard specification does not allow lower cases, - // some data may have them, so we allow it. - line = mPreviousLine; - } else { - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Expected END:VCARD was not found."); - } else if (line.trim().length() > 0) { - break; - } - } - } - - String[] strArray = line.split(":", 2); - if (strArray.length == 2 && strArray[0].trim().equalsIgnoreCase("END") - && strArray[1].trim().equalsIgnoreCase("VCARD")) { - return; - } else if (!allowGarbage) { - throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); - } - useCache = false; - } while (allowGarbage); - } - - /* - * items = *CRLF item / item - */ - protected void parseItems() throws IOException, VCardException { - boolean ended = false; - - final long beforeBeginProperty = System.currentTimeMillis(); - mInterpreter.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - beforeBeginProperty; - ended = parseItem(); - if (!ended) { - final long beforeEndProperty = System.currentTimeMillis(); - mInterpreter.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty; - } - - while (!ended) { - final long beforeStartProperty = System.currentTimeMillis(); - mInterpreter.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - beforeStartProperty; - try { - ended = parseItem(); - } catch (VCardInvalidCommentLineException e) { - Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); - ended = false; - } - - if (!ended) { - final long beforeEndProperty = System.currentTimeMillis(); - mInterpreter.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - beforeEndProperty; - } - } - } - - /* - * 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 { - mCurrentEncoding = DEFAULT_ENCODING; - - final String line = getNonEmptyLine(); - long start = System.currentTimeMillis(); - - String[] propertyNameAndValue = separateLineAndHandleGroup(line); - if (propertyNameAndValue == null) { - return true; - } - if (propertyNameAndValue.length != 2) { - throw new VCardInvalidLineException("Invalid line \"" + line + "\""); - } - String propertyName = propertyNameAndValue[0].toUpperCase(); - String propertyValue = propertyNameAndValue[1]; - - mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; - - if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { - start = System.currentTimeMillis(); - handleMultiplePropertyValue(propertyName, propertyValue); - mTimeParseAdrOrgN += System.currentTimeMillis() - start; - return false; - } else if (propertyName.equals("AGENT")) { - handleAgent(propertyValue); - return false; - } else if (isValidPropertyName(propertyName)) { - if (propertyName.equals("BEGIN")) { - if (propertyValue.equals("VCARD")) { - throw new VCardNestedException("This vCard has nested vCard data in it."); - } else { - throw new VCardException("Unknown BEGIN type: " + propertyValue); - } - } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersionString())) { - throw new VCardVersionException("Incompatible version: " + propertyValue + " != " - + getVersionString()); - } - start = System.currentTimeMillis(); - handlePropertyValue(propertyName, propertyValue); - mTimeParsePropertyValues += System.currentTimeMillis() - start; - return false; - } - - throw new VCardException("Unknown property name: \"" + propertyName + "\""); - } - - // For performance reason, the states for group and property name are merged into one. - static private final int STATE_GROUP_OR_PROPERTY_NAME = 0; - static private final int STATE_PARAMS = 1; - // vCard 3.0 specification allows double-quoted parameters, while vCard 2.1 does not. - static private final int STATE_PARAMS_IN_DQUOTE = 2; - - protected String[] separateLineAndHandleGroup(String line) throws VCardException { - final String[] propertyNameAndValue = new String[2]; - final int length = line.length(); - if (length > 0 && line.charAt(0) == '#') { - throw new VCardInvalidCommentLineException(); - } - - int state = STATE_GROUP_OR_PROPERTY_NAME; - int nameIndex = 0; - - // This loop is developed so that we don't have to take care of bottle neck here. - // Refactor carefully when you need to do so. - for (int i = 0; i < length; i++) { - final char ch = line.charAt(i); - switch (state) { - case STATE_GROUP_OR_PROPERTY_NAME: { - if (ch == ':') { // End of a property name. - final String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - mInterpreter.propertyName(propertyName); - propertyNameAndValue[0] = propertyName; - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } else if (ch == '.') { // Each group is followed by the dot. - final String groupName = line.substring(nameIndex, i); - if (groupName.length() == 0) { - Log.w(LOG_TAG, "Empty group found. Ignoring."); - } else { - mInterpreter.propertyGroup(groupName); - } - nameIndex = i + 1; // Next should be another group or a property name. - } else if (ch == ';') { // End of property name and beginneng of parameters. - final String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - mInterpreter.propertyName(propertyName); - propertyNameAndValue[0] = propertyName; - nameIndex = i + 1; - state = STATE_PARAMS; // Start parameter parsing. - } - // TODO: comma support (in vCard 3.0 and 4.0). - break; - } - case STATE_PARAMS: { - if (ch == '"') { - if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { - Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + - "Silently allow it"); - } - state = STATE_PARAMS_IN_DQUOTE; - } else if (ch == ';') { // Starts another param. - handleParams(line.substring(nameIndex, i)); - nameIndex = i + 1; - } else if (ch == ':') { // End of param and beginenning of values. - handleParams(line.substring(nameIndex, i)); - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } - break; - } - case STATE_PARAMS_IN_DQUOTE: { - if (ch == '"') { - if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { - Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + - "Silently allow it"); - } - state = STATE_PARAMS; - } - break; - } - } - } - - throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); - } - - /* - * params = ";" [ws] paramlist paramlist = paramlist [ws] ";" [ws] param / - * param param = "TYPE" [ws] "=" [ws] ptypeval / "VALUE" [ws] "=" [ws] - * pvalueval / "ENCODING" [ws] "=" [ws] pencodingval / "CHARSET" [ws] "=" - * [ws] charsetval / "LANGUAGE" [ws] "=" [ws] langval / "X-" word [ws] "=" - * [ws] word / knowntype - */ - protected void handleParams(String params) throws VCardException { - final String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - final String paramName = strArray[0].trim().toUpperCase(); - String paramValue = strArray[1].trim(); - if (paramName.equals("TYPE")) { - handleType(paramValue); - } else if (paramName.equals("VALUE")) { - handleValue(paramValue); - } else if (paramName.equals("ENCODING")) { - handleEncoding(paramValue); - } else if (paramName.equals("CHARSET")) { - handleCharset(paramValue); - } else if (paramName.equals("LANGUAGE")) { - handleLanguage(paramValue); - } else if (paramName.startsWith("X-")) { - handleAnyParam(paramName, paramValue); - } else { - throw new VCardException("Unknown type \"" + paramName + "\""); - } - } else { - handleParamWithoutName(strArray[0]); - } - } - - /** - * vCard 3.0 parser implementation may throw VCardException. - */ - @SuppressWarnings("unused") - protected void handleParamWithoutName(final String paramValue) throws VCardException { - handleType(paramValue); - } - - /* - * ptypeval = knowntype / "X-" word - */ - protected void handleType(final String ptypeval) { - if (!(getKnownTypeSet().contains(ptypeval.toUpperCase()) - || ptypeval.startsWith("X-")) - && !mUnknownTypeSet.contains(ptypeval)) { - mUnknownTypeSet.add(ptypeval); - Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval)); - } - mInterpreter.propertyParamType("TYPE"); - mInterpreter.propertyParamValue(ptypeval); - } - - /* - * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word - */ - protected void handleValue(final String pvalueval) { - if (!(getKnownValueSet().contains(pvalueval.toUpperCase()) - || pvalueval.startsWith("X-") - || mUnknownValueSet.contains(pvalueval))) { - mUnknownValueSet.add(pvalueval); - Log.w(LOG_TAG, String.format( - "The value unsupported by TYPE of %s: ", getVersion(), pvalueval)); - } - mInterpreter.propertyParamType("VALUE"); - mInterpreter.propertyParamValue(pvalueval); - } - - /* - * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word - */ - protected void handleEncoding(String pencodingval) throws VCardException { - if (getAvailableEncodingSet().contains(pencodingval) || - pencodingval.startsWith("X-")) { - mInterpreter.propertyParamType("ENCODING"); - mInterpreter.propertyParamValue(pencodingval); - mCurrentEncoding = pencodingval; - } else { - throw new VCardException("Unknown encoding \"" + pencodingval + "\""); - } - } - - /** - *

    - * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), - * but recent vCard files often contain other charset like UTF-8, SHIFT_JIS, etc. - * We allow any charset. - *

    - */ - protected void handleCharset(String charsetval) { - mInterpreter.propertyParamType("CHARSET"); - mInterpreter.propertyParamValue(charsetval); - } - - /** - * See also Section 7.1 of RFC 1521 - */ - protected void handleLanguage(String langval) throws VCardException { - String[] strArray = langval.split("-"); - if (strArray.length != 2) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - String tmp = strArray[0]; - int length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isAsciiLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - tmp = strArray[1]; - length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isAsciiLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - mInterpreter.propertyParamType(VCardConstants.PARAM_LANGUAGE); - mInterpreter.propertyParamValue(langval); - } - - private boolean isAsciiLetter(char ch) { - if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { - return true; - } - return false; - } - - /** - * Mainly for "X-" type. This accepts any kind of type without check. - */ - protected void handleAnyParam(String paramName, String paramValue) { - mInterpreter.propertyParamType(paramName); - mInterpreter.propertyParamValue(paramValue); - } - - protected void handlePropertyValue(String propertyName, String propertyValue) - throws IOException, VCardException { - final String upperEncoding = mCurrentEncoding.toUpperCase(); - if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) { - final long start = System.currentTimeMillis(); - final String result = getQuotedPrintable(propertyValue); - final ArrayList v = new ArrayList(); - v.add(result); - mInterpreter.propertyValues(v); - mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; - } else if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_BASE64) - || upperEncoding.equals(VCardConstants.PARAM_ENCODING_B)) { - final long start = System.currentTimeMillis(); - // It is very rare, but some BASE64 data may be so big that - // OutOfMemoryError occurs. To ignore such cases, use try-catch. - try { - final ArrayList arrayList = new ArrayList(); - arrayList.add(getBase64(propertyValue)); - mInterpreter.propertyValues(arrayList); - } catch (OutOfMemoryError error) { - Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); - mInterpreter.propertyValues(null); - } - mTimeHandleBase64 += System.currentTimeMillis() - start; - } else { - if (!(upperEncoding.equals("7BIT") || upperEncoding.equals("8BIT") || - upperEncoding.startsWith("X-"))) { - Log.w(LOG_TAG, - String.format("The encoding \"%s\" is unsupported by vCard %s", - mCurrentEncoding, getVersionString())); - } - - // Some device uses line folding defined in RFC 2425, which is not allowed - // in vCard 2.1 (while needed in vCard 3.0). - // - // e.g. - // BEGIN:VCARD - // VERSION:2.1 - // N:;Omega;;; - // EMAIL;INTERNET:"Omega" - // - // FN:Omega - // END:VCARD - // - // The vCard above assumes that email address should become: - // "Omega" - // - // But vCard 2.1 requires Quote-Printable when a line contains line break(s). - // - // For more information about line folding, - // see "5.8.1. Line delimiting and folding" in RFC 2425. - // - // We take care of this case more formally in vCard 3.0, so we only need to - // do this in vCard 2.1. - if (getVersion() == VCardConfig.VERSION_21) { - StringBuilder builder = null; - while (true) { - final String nextLine = peekLine(); - // We don't need to care too much about this exceptional case, - // but we should not wrongly eat up "END:VCARD", since it critically - // breaks this parser's state machine. - // Thus we roughly look over the next line and confirm it is at least not - // "END:VCARD". This extra fee is worth paying. This is exceptional - // anyway. - if (!TextUtils.isEmpty(nextLine) && - nextLine.charAt(0) == ' ' && - !"END:VCARD".contains(nextLine.toUpperCase())) { - getLine(); // Drop the next line. - - if (builder == null) { - builder = new StringBuilder(); - builder.append(propertyValue); - } - builder.append(nextLine.substring(1)); - } else { - break; - } - } - if (builder != null) { - propertyValue = builder.toString(); - } - } - - final long start = System.currentTimeMillis(); - ArrayList v = new ArrayList(); - v.add(maybeUnescapeText(propertyValue)); - mInterpreter.propertyValues(v); - mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; - } - } - - /** - *

    - * Parses and returns Quoted-Printable. - *

    - * - * @param firstString The string following a parameter name and attributes. - * Example: "string" in - * "ADR:ENCODING=QUOTED-PRINTABLE:string\n\r". - * @return whole Quoted-Printable string, including a given argument and - * following lines. Excludes the last empty line following to Quoted - * Printable lines. - * @throws IOException - * @throws VCardException - */ - private String getQuotedPrintable(String firstString) throws IOException, VCardException { - // Specifically, there may be some padding between = and CRLF. - // See the following: - // - // qp-line := *(qp-segment transport-padding CRLF) - // qp-part transport-padding - // qp-segment := qp-section *(SPACE / TAB) "=" - // ; Maximum length of 76 characters - // - // e.g. (from RFC 2045) - // Now's the time = - // for all folk to come= - // to the aid of their country. - if (firstString.trim().endsWith("=")) { - // remove "transport-padding" - int pos = firstString.length() - 1; - while (firstString.charAt(pos) != '=') { - } - StringBuilder builder = new StringBuilder(); - builder.append(firstString.substring(0, pos + 1)); - builder.append("\r\n"); - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("File ended during parsing a Quoted-Printable String"); - } - if (line.trim().endsWith("=")) { - // remove "transport-padding" - pos = line.length() - 1; - while (line.charAt(pos) != '=') { - } - builder.append(line.substring(0, pos + 1)); - builder.append("\r\n"); - } else { - builder.append(line); - break; - } - } - return builder.toString(); - } else { - return firstString; - } - } - - protected String getBase64(String firstString) throws IOException, VCardException { - StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - String line = getLine(); - if (line == null) { - throw new VCardException("File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - *

    - * Mainly for "ADR", "ORG", and "N" - *

    - */ - /* - * addressparts = 0*6(strnosemi ";") strnosemi ; PO Box, Extended Addr, - * Street, Locality, Region, Postal Code, Country Name orgparts = - * *(strnosemi ";") strnosemi ; First is Organization Name, remainder are - * Organization Units. nameparts = 0*4(strnosemi ";") strnosemi ; Family, - * Given, Middle, Prefix, Suffix. ; Example:Public;John;Q.;Reverend Dr.;III, - * Esq. strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi ; To include a - * semicolon in this string, it must be escaped ; with a "\" character. We - * do not care the number of "strnosemi" here. We are not sure whether we - * should add "\" CRLF to each value. We exclude them for now. - */ - 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 (mCurrentEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - propertyValue = getQuotedPrintable(propertyValue); - } - - mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue, - getVersion())); - } - - /* - * vCard 2.1 specifies AGENT allows one vcard entry. Currently we emit an - * error toward the AGENT property. - * // TODO: Support AGENT property. - * item = - * ... / [groups "."] "AGENT" [params] ":" vcard CRLF vcard = "BEGIN" [ws] - * ":" [ws] "VCARD" [ws] 1*CRLF items *CRLF "END" [ws] ":" [ws] "VCARD" - */ - protected void handleAgent(final String propertyValue) throws VCardException { - if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { - // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. - return; - } else { - throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); - } - } - - /** - * For vCard 3.0. - */ - protected String maybeUnescapeText(final String text) { - return text; - } - - /** - * 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 maybeUnescapeCharacter(final char ch) { - return unescapeCharacter(ch); - } - - /* package */ static String unescapeCharacter(final 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. - if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { - return String.valueOf(ch); - } else { - return null; - } - } - - private void showPerformanceInfo() { - Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); - Log.d(LOG_TAG, "Total readLine time: " + mReader.getTotalmillisecond() + " 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"); - } - - /** - * @return {@link VCardConfig#VERSION_21} - */ - protected int getVersion() { - return VCardConfig.VERSION_21; - } - - /** - * @return {@link VCardConfig#VERSION_30} - */ - protected String getVersionString() { - return VCardConstants.VERSION_V21; - } - - protected Set getKnownPropertyNameSet() { - return VCardParser_V21.sKnownPropertyNameSet; - } - - protected Set getKnownTypeSet() { - return VCardParser_V21.sKnownTypeSet; - } - - protected Set getKnownValueSet() { - return VCardParser_V21.sKnownValueSet; - } - - protected Set getAvailableEncodingSet() { - return VCardParser_V21.sAvailableEncoding; - } - - protected String getDefaultEncoding() { - return DEFAULT_ENCODING; - } - - - public void parse(InputStream is, VCardInterpreter interpreter) - throws IOException, VCardException { - if (is == null) { - throw new NullPointerException("InputStream must not be null."); - } - - final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset); - mReader = new CustomBufferedReader(tmpReader); - - mInterpreter = (interpreter != null ? interpreter : new EmptyInterpreter()); - - final long start = System.currentTimeMillis(); - if (mInterpreter != null) { - mInterpreter.start(); - } - parseVCardFile(); - if (mInterpreter != null) { - mInterpreter.end(); - } - mTimeTotal += System.currentTimeMillis() - start; - - if (VCardConfig.showPerformanceLog()) { - showPerformanceInfo(); - } - } - - public final void cancel() { - mCanceled = true; - } -} diff --git a/core/java/android/pim/vcard/VCardParserImpl_V30.java b/core/java/android/pim/vcard/VCardParserImpl_V30.java deleted file mode 100644 index 3fad9a08cc15e1a76ef17f9617498b9ffc578e16..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardParserImpl_V30.java +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.pim.vcard; - -import android.pim.vcard.exception.VCardException; -import android.util.Log; - -import java.io.IOException; -import java.util.Set; - -/** - *

    - * Basic implementation achieving vCard 3.0 parsing. - *

    - *

    - * This class inherits vCard 2.1 implementation since technically they are similar, - * while specifically there's logical no relevance between them. - * So that developers are not confused with the inheritance, - * {@link VCardParser_V30} does not inherit {@link VCardParser_V21}, while - * {@link VCardParserImpl_V30} inherits {@link VCardParserImpl_V21}. - *

    - * @hide - */ -/* package */ class VCardParserImpl_V30 extends VCardParserImpl_V21 { - private static final String LOG_TAG = "VCardParserImpl_V30"; - - private String mPreviousLine; - private boolean mEmittedAgentWarning = false; - - public VCardParserImpl_V30() { - super(); - } - - public VCardParserImpl_V30(int vcardType) { - super(vcardType); - } - - @Override - protected int getVersion() { - return VCardConfig.VERSION_30; - } - - @Override - protected String getVersionString() { - return VCardConstants.VERSION_V30; - } - - @Override - protected String getLine() throws IOException { - if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } else { - return mReader.readLine(); - } - } - - /** - * vCard 3.0 requires that the line with space at the beginning of the line - * must be combined with previous line. - */ - @Override - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - StringBuilder builder = null; - while (true) { - line = mReader.readLine(); - if (line == null) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - throw new VCardException("Reached end of buffer."); - } else if (line.length() == 0) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { - if (builder != null) { - // See Section 5.8.1 of RFC 2425 (MIME-DIR document). - // Following is the excerpts from it. - // - // DESCRIPTION:This is a long description that exists on a long line. - // - // Can be represented as: - // - // DESCRIPTION:This is a long description - // that exists on a long line. - // - // It could also be represented as: - // - // DESCRIPTION:This is a long descrip - // tion that exists o - // n a long line. - builder.append(line.substring(1)); - } else if (mPreviousLine != null) { - builder = new StringBuilder(); - builder.append(mPreviousLine); - mPreviousLine = null; - builder.append(line.substring(1)); - } else { - throw new VCardException("Space exists at the beginning of the line"); - } - } else { - if (mPreviousLine == null) { - mPreviousLine = line; - if (builder != null) { - return builder.toString(); - } - } else { - String ret = mPreviousLine; - mPreviousLine = line; - return ret; - } - } - } - } - - /* - * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF - * 1 * (contentline) - * ;A vCard object MUST include the VERSION, FN and N types. - * [group "."] "END" ":" "VCARD" 1 * CRLF - */ - @Override - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - return super.readBeginVCard(allowGarbage); - } - - @Override - protected void readEndVCard(boolean useCache, boolean allowGarbage) - throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - super.readEndVCard(useCache, allowGarbage); - } - - /** - * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. - */ - @Override - protected void handleParams(final String params) throws VCardException { - try { - super.handleParams(params); - } catch (VCardException e) { - // maybe IANA type - String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - handleAnyParam(strArray[0], strArray[1]); - } else { - // Must not come here in the current implementation. - throw new VCardException( - "Unknown params value: " + params); - } - } - } - - @Override - protected void handleAnyParam(final String paramName, final String paramValue) { - mInterpreter.propertyParamType(paramName); - splitAndPutParamValue(paramValue); - } - - @Override - protected void handleParamWithoutName(final String paramValue) { - handleType(paramValue); - } - - /* - * vCard 3.0 defines - * - * param = param-name "=" param-value *("," param-value) - * param-name = iana-token / x-name - * param-value = ptext / quoted-string - * quoted-string = DQUOTE QSAFE-CHAR DQUOTE - * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII - * ; Any character except CTLs, DQUOTE - * - * QSAFE-CHAR must not contain DQUOTE, including escaped one (\"). - */ - @Override - protected void handleType(final String paramValue) { - mInterpreter.propertyParamType("TYPE"); - splitAndPutParamValue(paramValue); - } - - /** - * Splits parameter values into pieces in accordance with vCard 3.0 specification and - * puts pieces into mInterpreter. - */ - /* - * param-value = ptext / quoted-string - * quoted-string = DQUOTE QSAFE-CHAR DQUOTE - * QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-ASCII - * ; Any character except CTLs, DQUOTE - * - * QSAFE-CHAR must not contain DQUOTE, including escaped one (\") - */ - private void splitAndPutParamValue(String paramValue) { - // "comma,separated:inside.dquote",pref - // --> - // - comma,separated:inside.dquote - // - pref - // - // Note: Though there's a code, we don't need to take much care of - // wrongly-added quotes like the example above, as they induce - // parse errors at the top level (when splitting a line into parts). - StringBuilder builder = null; // Delay initialization. - boolean insideDquote = false; - final int length = paramValue.length(); - for (int i = 0; i < length; i++) { - final char ch = paramValue.charAt(i); - if (ch == '"') { - if (insideDquote) { - // End of Dquote. - mInterpreter.propertyParamValue(builder.toString()); - builder = null; - insideDquote = false; - } else { - if (builder != null) { - if (builder.length() > 0) { - // e.g. - // pref"quoted" - Log.w(LOG_TAG, "Unexpected Dquote inside property."); - } else { - // e.g. - // pref,"quoted" - // "quoted",pref - mInterpreter.propertyParamValue(builder.toString()); - } - } - insideDquote = true; - } - } else if (ch == ',' && !insideDquote) { - if (builder == null) { - Log.w(LOG_TAG, "Comma is used before actual string comes. (" + - paramValue + ")"); - } else { - mInterpreter.propertyParamValue(builder.toString()); - builder = null; - } - } else { - // To stop creating empty StringBuffer at the end of parameter, - // we delay creating this object until this point. - if (builder == null) { - builder = new StringBuilder(); - } - builder.append(ch); - } - } - if (insideDquote) { - // e.g. - // "non-quote-at-end - Log.d(LOG_TAG, "Dangling Dquote."); - } - if (builder != null) { - if (builder.length() == 0) { - Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " + - "at the end of parameter value parsing."); - } else { - mInterpreter.propertyParamValue(builder.toString()); - } - } - } - - @Override - protected void handleAgent(final String propertyValue) { - // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. - // - // e.g. - // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n - // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n - // ET:jfriday@host.com\nEND:VCARD\n - // - // TODO: fix this. - // - // issue: - // vCard 3.0 also allows this as an example. - // - // AGENT;VALUE=uri: - // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com - // - // This is not vCard. Should we support this? - // - // Just ignore the line for now, since we cannot know how to handle it... - if (!mEmittedAgentWarning) { - Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); - mEmittedAgentWarning = true; - } - } - - /** - * vCard 3.0 does not require two CRLF at the last of BASE64 data. - * It only requires that data should be MIME-encoded. - */ - @Override - protected String getBase64(final String firstString) - throws IOException, VCardException { - final StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - final String line = getLine(); - if (line == null) { - throw new VCardException("File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } else if (!line.startsWith(" ") && !line.startsWith("\t")) { - mPreviousLine = line; - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") - * ; \\ encodes \, \n or \N encodes newline - * ; \; encodes ;, \, encodes , - * - * Note: Apple escapes ':' into '\:' while does not escape '\' - */ - @Override - protected String maybeUnescapeText(final String text) { - return unescapeText(text); - } - - public static String unescapeText(final String text) { - StringBuilder builder = new StringBuilder(); - final int length = text.length(); - for (int i = 0; i < length; i++) { - char ch = text.charAt(i); - if (ch == '\\' && i < length - 1) { - final char next_ch = text.charAt(++i); - if (next_ch == 'n' || next_ch == 'N') { - builder.append("\n"); - } else { - builder.append(next_ch); - } - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - @Override - protected String maybeUnescapeCharacter(final char ch) { - return unescapeCharacter(ch); - } - - public static String unescapeCharacter(final char ch) { - if (ch == 'n' || ch == 'N') { - return "\n"; - } else { - return String.valueOf(ch); - } - } - - @Override - protected Set getKnownPropertyNameSet() { - return VCardParser_V30.sKnownPropertyNameSet; - } -} diff --git a/core/java/android/pim/vcard/VCardParserImpl_V40.java b/core/java/android/pim/vcard/VCardParserImpl_V40.java deleted file mode 100644 index 0fe76bbd39fc1d1bcf03572a632a1569dfb400ae..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardParserImpl_V40.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.pim.vcard; - -import java.util.Set; - - -/** - *

    - * Basic implementation parsing vCard 4.0. - *

    - *

    - * vCard 4.0 is not published yet. Also this implementation is premature. - *

    - * @hide - */ -/* package */ class VCardParserImpl_V40 extends VCardParserImpl_V30 { - // private static final String LOG_TAG = "VCardParserImpl_V40"; - - public VCardParserImpl_V40() { - super(); - } - - public VCardParserImpl_V40(final int vcardType) { - super(vcardType); - } - - @Override - protected int getVersion() { - return VCardConfig.VERSION_40; - } - - @Override - protected String getVersionString() { - return VCardConstants.VERSION_V40; - } - - /** - * We escape "\N" into new line for safety. - */ - @Override - protected String maybeUnescapeText(final String text) { - return unescapeText(text); - } - - public static String unescapeText(final String text) { - // TODO: more strictly, vCard 4.0 requires different type of unescaping rule - // toward each property. - final StringBuilder builder = new StringBuilder(); - final int length = text.length(); - for (int i = 0; i < length; i++) { - char ch = text.charAt(i); - if (ch == '\\' && i < length - 1) { - final char next_ch = text.charAt(++i); - if (next_ch == 'n' || next_ch == 'N') { - builder.append("\n"); - } else { - builder.append(next_ch); - } - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - public static String unescapeCharacter(final char ch) { - if (ch == 'n' || ch == 'N') { - return "\n"; - } else { - return String.valueOf(ch); - } - } - - @Override - protected Set getKnownPropertyNameSet() { - return VCardParser_V40.sKnownPropertyNameSet; - } -} \ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java deleted file mode 100644 index 507a1763f377aad920416a0a0698ea19b52ba2f0..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ /dev/null @@ -1,109 +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.pim.vcard; - -import android.pim.vcard.exception.VCardException; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - *

    - * vCard parser for vCard 2.1. See the specification for more detail about the spec itself. - *

    - *

    - * The spec is written in 1996, and currently various types of "vCard 2.1" exist. - * To handle real the world vCard formats appropriately and effectively, this class does not - * obey with strict vCard 2.1. - * In stead, not only vCard spec but also real world vCard is considered. - *

    - * e.g. A lot of devices and softwares let vCard importer/exporter to use - * the PNG format to determine the type of image, while it is not allowed in - * the original specification. As of 2010, we can see even the FLV format - * (possible in Japanese mobile phones). - *

    - */ -public final class VCardParser_V21 implements VCardParser { - /** - * A unmodifiable Set storing the property names available in the vCard 2.1 specification. - */ - /* package */ static final Set sKnownPropertyNameSet = - Collections.unmodifiableSet(new HashSet( - Arrays.asList("BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", - "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", - "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER"))); - - /** - * A unmodifiable Set storing the types known in vCard 2.1. - */ - /* package */ static final Set sKnownTypeSet = - Collections.unmodifiableSet(new HashSet( - Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", - "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", - "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", - "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", - "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", - "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", - "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", - "WAVE", "AIFF", "PCM", "X509", "PGP"))); - - /** - * A unmodifiable Set storing the values for the type "VALUE", available in the vCard 2.1. - */ - /* package */ static final Set sKnownValueSet = - Collections.unmodifiableSet(new HashSet( - Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID"))); - - /** - *

    - * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 2.1. - *

    - *

    - * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. - * We allow it for safety. - *

    - */ - /* package */ static final Set sAvailableEncoding = - Collections.unmodifiableSet(new HashSet( - Arrays.asList(VCardConstants.PARAM_ENCODING_7BIT, - VCardConstants.PARAM_ENCODING_8BIT, - VCardConstants.PARAM_ENCODING_QP, - VCardConstants.PARAM_ENCODING_BASE64, - VCardConstants.PARAM_ENCODING_B))); - - private final VCardParserImpl_V21 mVCardParserImpl; - - public VCardParser_V21() { - mVCardParserImpl = new VCardParserImpl_V21(); - } - - public VCardParser_V21(int vcardType) { - mVCardParserImpl = new VCardParserImpl_V21(vcardType); - } - - public void parse(InputStream is, VCardInterpreter interepreter) - throws IOException, VCardException { - mVCardParserImpl.parse(is, interepreter); - } - - public void cancel() { - mVCardParserImpl.cancel(); - } -} diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java deleted file mode 100644 index 238d3a8a51de8806cd2b608be0c71eb2444c5169..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ /dev/null @@ -1,87 +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.pim.vcard; - -import android.pim.vcard.exception.VCardException; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - *

    - * vCard parser for vCard 3.0. See RFC 2426 for more detail. - *

    - *

    - * This parser allows vCard format which is not allowed in the RFC, since - * we have seen several vCard 3.0 files which don't comply with it. - *

    - *

    - * e.g. vCard 3.0 does not allow "CHARSET" attribute, but some actual files - * have it and they uses non UTF-8 charsets. UTF-8 is recommended in RFC 2426, - * but it is not a must. We silently allow "CHARSET". - *

    - */ -public class VCardParser_V30 implements VCardParser { - /* package */ static final Set sKnownPropertyNameSet = - Collections.unmodifiableSet(new HashSet(Arrays.asList( - "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", - "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", - "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 - "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", - "SORT-STRING", "CATEGORIES", "PRODID"))); // 3.0 - - /** - *

    - * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 3.0. - *

    - *

    - * Though vCard 2.1 specification does not allow "7BIT" or "BASE64", we allow them for safety. - *

    - *

    - * "QUOTED-PRINTABLE" is not allowed in vCard 3.0 and not in this parser either, - * because the encoding ambiguates how the vCard file to be parsed. - *

    - */ - /* package */ static final Set sAcceptableEncoding = - Collections.unmodifiableSet(new HashSet(Arrays.asList( - VCardConstants.PARAM_ENCODING_7BIT, - VCardConstants.PARAM_ENCODING_8BIT, - VCardConstants.PARAM_ENCODING_BASE64, - VCardConstants.PARAM_ENCODING_B))); - - private final VCardParserImpl_V30 mVCardParserImpl; - - public VCardParser_V30() { - mVCardParserImpl = new VCardParserImpl_V30(); - } - - public VCardParser_V30(int vcardType) { - mVCardParserImpl = new VCardParserImpl_V30(vcardType); - } - - public void parse(InputStream is, VCardInterpreter interepreter) - throws IOException, VCardException { - mVCardParserImpl.parse(is, interepreter); - } - - public void cancel() { - mVCardParserImpl.cancel(); - } -} diff --git a/core/java/android/pim/vcard/VCardParser_V40.java b/core/java/android/pim/vcard/VCardParser_V40.java deleted file mode 100644 index 65a2f6863e6fee255e1c590f3eaaabc8dbdafb13..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardParser_V40.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.pim.vcard; - -import android.pim.vcard.exception.VCardException; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - *

    - * vCard parser for vCard 4.0. - *

    - *

    - * Currently this parser is based on vCard 4.0 specification rev 11. - *

    - */ -public class VCardParser_V40 implements VCardParser { - /* package */ static final Set sKnownPropertyNameSet = - Collections.unmodifiableSet(new HashSet(Arrays.asList( - "BEGIN", "END", "SOURCE", "NAME", "KIND", "XML", - "FN", "N", "NICKNAME", "PHOTO", "BDAY", "DDAY", - "BIRTH", "DEATH", "ANNIVERSARY", "SEX", "ADR", - "LABEL", "TEL", "EMAIL", "IMPP", "LANG", "TZ", - "GEO", "TITLE", "ROLE", "LOGO", "ORG", "MEMBER", - "RELATED", "CATEGORIES", "NOTE", "PRODID", - "REV", "SOUND", "UID", "CLIENTPIDMAP", - "URL", "VERSION", "CLASS", "KEY", "FBURL", "CALENDRURI", - "CALURI"))); - - /** - *

    - * A unmodifiable Set storing the values for the type "ENCODING", available in vCard 4.0. - *

    - */ - /* package */ static final Set sAcceptableEncoding = - Collections.unmodifiableSet(new HashSet(Arrays.asList( - VCardConstants.PARAM_ENCODING_8BIT, - VCardConstants.PARAM_ENCODING_B))); - - private final VCardParserImpl_V30 mVCardParserImpl; - - public VCardParser_V40() { - mVCardParserImpl = new VCardParserImpl_V40(); - } - - public VCardParser_V40(int vcardType) { - mVCardParserImpl = new VCardParserImpl_V40(vcardType); - } - - @Override - public void parse(InputStream is, VCardInterpreter interepreter) - throws IOException, VCardException { - mVCardParserImpl.parse(is, interepreter); - } - - @Override - public void cancel() { - mVCardParserImpl.cancel(); - } -} \ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java deleted file mode 100644 index 4c6461e98ea9c3bb91821fa37adfd5503162eb79..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardSourceDetector.java +++ /dev/null @@ -1,205 +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.pim.vcard; - -import android.text.TextUtils; -import android.util.Log; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - *

    - * The class which tries to detects the source of a vCard file from its contents. - *

    - *

    - * The specification of vCard (including both 2.1 and 3.0) is not so strict as to - * guess its format just by reading beginning few lines (usually we can, but in - * some most pessimistic case, we cannot until at almost the end of the file). - * Also we cannot store all vCard entries in memory, while there's no specification - * how big the vCard entry would become after the parse. - *

    - *

    - * This class is usually used for the "first scan", in which we can understand which vCard - * version is used (and how many entries exist in a file). - *

    - */ -public class VCardSourceDetector implements VCardInterpreter { - private static final String LOG_TAG = "VCardSourceDetector"; - - private static Set APPLE_SIGNS = new HashSet(Arrays.asList( - "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", - "X-ABADR", "X-ABUID")); - - private static Set JAPANESE_MOBILE_PHONE_SIGNS = new HashSet(Arrays.asList( - "X-GNO", "X-GN", "X-REDUCTION")); - - private static Set WINDOWS_MOBILE_PHONE_SIGNS = new HashSet(Arrays.asList( - "X-MICROSOFT-ASST_TEL", "X-MICROSOFT-ASSISTANT", "X-MICROSOFT-OFFICELOC")); - - // Note: these signes appears before the signs of the other type (e.g. "X-GN"). - // In other words, Japanese FOMA mobile phones are detected as FOMA, not JAPANESE_MOBILE_PHONES. - private static Set FOMA_SIGNS = new HashSet(Arrays.asList( - "X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED", - "X-SD-DESCRIPTION")); - private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; - - /** - * Represents that no estimation is available. Users of this class is able to this - * constant when you don't want to let a vCard parser rely on estimation for parse type. - */ - public static final int PARSE_TYPE_UNKNOWN = 0; - - // For Apple's software, which does not mean this type is effective for all its products. - // We confirmed they usually use UTF-8, but not sure about vCard type. - private static final int PARSE_TYPE_APPLE = 1; - // For Japanese mobile phones, which are usually using Shift_JIS as a charset. - private static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; - // For some of mobile phones released from DoCoMo, which use nested vCard. - private static final int PARSE_TYPE_DOCOMO_TORELATE_NEST = 3; - // For Japanese Windows Mobel phones. It's version is supposed to be 6.5. - private static final int PARSE_TYPE_WINDOWS_MOBILE_V65_JP = 4; - - private int mParseType = 0; // Not sure. - - private boolean mNeedToParseVersion = false; - private int mVersion = -1; // -1 == unknown - - // Some mobile phones (like FOMA) tells us the charset of the data. - private boolean mNeedToParseCharset; - private String mSpecifiedCharset; - - public void start() { - } - - public void end() { - } - - public void startEntry() { - } - - public void startProperty() { - mNeedToParseCharset = false; - mNeedToParseVersion = false; - } - - public void endProperty() { - } - - public void endEntry() { - } - - public void propertyGroup(String group) { - } - - public void propertyName(String name) { - if (name.equalsIgnoreCase(VCardConstants.PROPERTY_VERSION)) { - mNeedToParseVersion = true; - return; - } else if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { - mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST; - // Probably Shift_JIS is used, but we should double confirm. - mNeedToParseCharset = true; - return; - } - if (mParseType != PARSE_TYPE_UNKNOWN) { - return; - } - if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { - mParseType = PARSE_TYPE_WINDOWS_MOBILE_V65_JP; - } else if (FOMA_SIGNS.contains(name)) { - mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST; - } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { - mParseType = PARSE_TYPE_MOBILE_PHONE_JP; - } else if (APPLE_SIGNS.contains(name)) { - mParseType = PARSE_TYPE_APPLE; - } - } - - public void propertyParamType(String type) { - } - - public void propertyParamValue(String value) { - } - - public void propertyValues(List values) { - if (mNeedToParseVersion && values.size() > 0) { - final String versionString = values.get(0); - if (versionString.equals(VCardConstants.VERSION_V21)) { - mVersion = VCardConfig.VERSION_21; - } else if (versionString.equals(VCardConstants.VERSION_V30)) { - mVersion = VCardConfig.VERSION_30; - } else if (versionString.equals(VCardConstants.VERSION_V40)) { - mVersion = VCardConfig.VERSION_40; - } else { - Log.w(LOG_TAG, "Invalid version string: " + versionString); - } - } else if (mNeedToParseCharset && values.size() > 0) { - mSpecifiedCharset = values.get(0); - } - } - - /** - * @return The available type can be used with vCard parser. You probably need to - * use {{@link #getEstimatedCharset()} to understand the charset to be used. - */ - public int getEstimatedType() { - switch (mParseType) { - case PARSE_TYPE_DOCOMO_TORELATE_NEST: - return VCardConfig.VCARD_TYPE_DOCOMO | VCardConfig.FLAG_TORELATE_NEST; - case PARSE_TYPE_MOBILE_PHONE_JP: - return VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE; - case PARSE_TYPE_APPLE: - case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: - default: { - if (mVersion == VCardConfig.VERSION_21) { - return VCardConfig.VCARD_TYPE_V21_GENERIC; - } else if (mVersion == VCardConfig.VERSION_30) { - return VCardConfig.VCARD_TYPE_V30_GENERIC; - } else if (mVersion == VCardConfig.VERSION_40) { - return VCardConfig.VCARD_TYPE_V40_GENERIC; - } else { - return VCardConfig.VCARD_TYPE_UNKNOWN; - } - } - } - } - - /** - *

    - * Returns charset String guessed from the source's properties. - * This method must be called after parsing target file(s). - *

    - * @return Charset String. Null is returned if guessing the source fails. - */ - public String getEstimatedCharset() { - if (TextUtils.isEmpty(mSpecifiedCharset)) { - return mSpecifiedCharset; - } - switch (mParseType) { - case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: - case PARSE_TYPE_DOCOMO_TORELATE_NEST: - case PARSE_TYPE_MOBILE_PHONE_JP: - return "SHIFT_JIS"; - case PARSE_TYPE_APPLE: - return "UTF-8"; - default: - return null; - } - } -} diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java deleted file mode 100644 index abceca0cb476c3ec8eccae7aca4247e41c503969..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/VCardUtils.java +++ /dev/null @@ -1,796 +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.pim.vcard; - -import android.content.ContentProviderOperation; -import android.pim.vcard.exception.VCardException; -import android.provider.ContactsContract.CommonDataKinds.Im; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; -import android.provider.ContactsContract.Data; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.util.Log; - -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.net.QuotedPrintableCodec; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Utilities for VCard handling codes. - */ -public class VCardUtils { - private static final String LOG_TAG = "VCardUtils"; - - // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is - // converted to two parameter 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 sPhoneTypesUnknownToContactsSet; - private static final Map sKnownPhoneTypeMap_StoI; - private static final Map sKnownImPropNameMap_ItoS; - private static final Set sMobilePhoneLabelSet; - - static { - sKnownPhoneTypesMap_ItoS = new HashMap(); - sKnownPhoneTypeMap_StoI = new HashMap(); - - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); - - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); - - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, - Phone.TYPE_CALLBACK); - sKnownPhoneTypeMap_StoI.put( - VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, - Phone.TYPE_TTY_TDD); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, - Phone.TYPE_ASSISTANT); - - sPhoneTypesUnknownToContactsSet = new HashSet(); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO); - - sKnownImPropNameMap_ItoS = new HashMap(); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, - VCardConstants.PROPERTY_X_GOOGLE_TALK); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING); - - // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone) - // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone) - // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone) - // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone) - sMobilePhoneLabelSet = new HashSet(Arrays.asList( - "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4", - "\uFF79\uFF72\uFF80\uFF72")); - } - - public static String getPhoneTypeString(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, - String number) { - if (number == null) { - number = ""; - } - int type = -1; - String label = null; - boolean isFax = false; - boolean hasPref = false; - - if (types != null) { - for (String typeString : types) { - if (typeString == null) { - continue; - } - typeString = typeString.toUpperCase(); - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - hasPref = true; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) { - isFax = true; - } else { - if (typeString.startsWith("X-") && type < 0) { - typeString = typeString.substring(2); - } - if (typeString.length() == 0) { - continue; - } - final Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString); - if (tmp != null) { - final int typeCandidate = tmp; - // TYPE_PAGER is prefered when the number contains @ surronded by - // a pager number and a domain name. - // e.g. - // o 1111@domain.com - // x @domain.com - // x 1111@ - final int indexOfAt = number.indexOf("@"); - if ((typeCandidate == Phone.TYPE_PAGER - && 0 < indexOfAt && indexOfAt < number.length() - 1) - || type < 0 - || type == Phone.TYPE_CUSTOM) { - 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; - } - } - - @SuppressWarnings("deprecation") - public static boolean isMobilePhoneLabel(final String label) { - // 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. - return ("_AUTO_CELL".equals(label) || sMobilePhoneLabelSet.contains(label)); - } - - public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) { - return sPhoneTypesUnknownToContactsSet.contains(label); - } - - public static String getPropertyNameForIm(final int protocol) { - return sKnownImPropNameMap_ItoS.get(protocol); - } - - public static String[] sortNameElements(final int vcardType, - final String familyName, final String middleName, final String givenName) { - final String[] list = new String[3]; - final int nameOrderType = VCardConfig.getNameOrderType(vcardType); - switch (nameOrderType) { - case VCardConfig.NAME_ORDER_JAPANESE: { - if (containsOnlyPrintableAscii(familyName) && - containsOnlyPrintableAscii(givenName)) { - list[0] = givenName; - list[1] = middleName; - list[2] = familyName; - } else { - 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. - *

    - */ - public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, - final ContentProviderOperation.Builder builder, - final VCardEntry.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); - } - - final String streetString; - if (TextUtils.isEmpty(postalData.street)) { - if (TextUtils.isEmpty(postalData.extendedAddress)) { - streetString = null; - } else { - streetString = postalData.extendedAddress; - } - } else { - if (TextUtils.isEmpty(postalData.extendedAddress)) { - streetString = postalData.street; - } else { - streetString = postalData.street + " " + postalData.extendedAddress; - } - } - builder.withValue(StructuredPostal.POBOX, postalData.pobox); - builder.withValue(StructuredPostal.STREET, streetString); - 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); - } - } - - public static String constructNameFromElements(final int vcardType, - final String familyName, final String middleName, final String givenName) { - return constructNameFromElements(vcardType, familyName, middleName, givenName, - null, null); - } - - public static String constructNameFromElements(final int vcardType, - final String familyName, final String middleName, final String givenName, - final String prefix, final String suffix) { - final StringBuilder builder = new StringBuilder(); - final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName); - boolean first = true; - if (!TextUtils.isEmpty(prefix)) { - first = false; - builder.append(prefix); - } - for (final 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(); - } - - /** - * Splits the given value into pieces using the delimiter ';' inside it. - * - * Escaped characters in those values are automatically unescaped into original form. - */ - public static List constructListFromValue(final String value, - final int vcardType) { - final List list = new ArrayList(); - StringBuilder builder = new StringBuilder(); - final int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch == '\\' && i < length - 1) { - char nextCh = value.charAt(i + 1); - final String unescapedString; - if (VCardConfig.isVersion40(vcardType)) { - unescapedString = VCardParserImpl_V40.unescapeCharacter(nextCh); - } else if (VCardConfig.isVersion30(vcardType)) { - unescapedString = VCardParserImpl_V30.unescapeCharacter(nextCh); - } else { - if (!VCardConfig.isVersion21(vcardType)) { - // Unknown vCard type - Log.w(LOG_TAG, "Unknown vCard type"); - } - unescapedString = VCardParserImpl_V21.unescapeCharacter(nextCh); - } - - if (unescapedString != null) { - builder.append(unescapedString); - i++; - } else { - builder.append(ch); - } - } else if (ch == ';') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else { - builder.append(ch); - } - } - list.add(builder.toString()); - return list; - } - - public static boolean containsOnlyPrintableAscii(final String...values) { - if (values == null) { - return true; - } - return containsOnlyPrintableAscii(Arrays.asList(values)); - } - - public static boolean containsOnlyPrintableAscii(final Collection values) { - if (values == null) { - return true; - } - for (final String value : values) { - if (TextUtils.isEmpty(value)) { - continue; - } - if (!TextUtils.isPrintableAsciiOnly(value)) { - 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(final String...values) { - if (values == null) { - return true; - } - return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); - } - - public static boolean containsOnlyNonCrLfPrintableAscii(final Collection values) { - if (values == null) { - return true; - } - final int asciiFirst = 0x20; - final int asciiLast = 0x7E; // included - for (final String value : values) { - if (TextUtils.isEmpty(value)) { - continue; - } - final int length = value.length(); - for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { - final int c = value.codePointAt(i); - if (!(asciiFirst <= c && c <= asciiLast)) { - return false; - } - } - } - return true; - } - - private static final Set sUnAcceptableAsciiInV21WordSet = - new HashSet(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); - - /** - *

    - * 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(final String...values) { - if (values == null) { - return true; - } - return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); - } - - public static boolean containsOnlyAlphaDigitHyphen(final Collection values) { - if (values == null) { - return true; - } - final int upperAlphabetFirst = 0x41; // A - final int upperAlphabetAfterLast = 0x5b; // [ - final int lowerAlphabetFirst = 0x61; // a - final int lowerAlphabetAfterLast = 0x7b; // { - final int digitFirst = 0x30; // 0 - final int digitAfterLast = 0x3A; // : - final int hyphen = '-'; - for (final String str : values) { - if (TextUtils.isEmpty(str)) { - continue; - } - 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 < lowerAlphabetAfterLast) || - (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) || - (digitFirst <= codepoint && codepoint < digitAfterLast) || - (codepoint == hyphen))) { - return false; - } - } - } - return true; - } - - public static boolean containsOnlyWhiteSpaces(final String...values) { - if (values == null) { - return true; - } - return containsOnlyWhiteSpaces(Arrays.asList(values)); - } - - public static boolean containsOnlyWhiteSpaces(final Collection values) { - if (values == null) { - return true; - } - for (final String str : values) { - if (TextUtils.isEmpty(str)) { - continue; - } - final int length = str.length(); - for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { - if (!Character.isWhitespace(str.codePointAt(i))) { - return false; - } - } - } - return true; - } - - /** - *

    - * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. - *

    - *

    - * vCard 2.1 specifies:
    - * word = <any printable 7bit us-ascii except []=:., > - *

    - */ - public static boolean isV21Word(final String value) { - if (TextUtils.isEmpty(value)) { - return true; - } - final int asciiFirst = 0x20; - final int asciiLast = 0x7E; // included - final int length = value.length(); - for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { - final int c = value.codePointAt(i); - if (!(asciiFirst <= c && c <= asciiLast) || - sUnAcceptableAsciiInV21WordSet.contains((char)c)) { - return false; - } - } - return true; - } - - private static final int[] sEscapeIndicatorsV30 = new int[]{ - ':', ';', ',', ' ' - }; - - private static final int[] sEscapeIndicatorsV40 = new int[]{ - ';', ':' - }; - - /** - *

    - * Returns String available as parameter value in vCard 3.0. - *

    - *

    - * RFC 2426 requires vCard composer to quote parameter values when it contains - * semi-colon, for example (See RFC 2426 for more information). - * This method checks whether the given String can be used without quotes. - *

    - *

    - * Note: We remove DQUOTE inside the given value silently for now. - *

    - */ - public static String toStringAsV30ParamValue(String value) { - return toStringAsParamValue(value, sEscapeIndicatorsV30); - } - - public static String toStringAsV40ParamValue(String value) { - return toStringAsParamValue(value, sEscapeIndicatorsV40); - } - - private static String toStringAsParamValue(String value, final int[] escapeIndicators) { - if (TextUtils.isEmpty(value)) { - value = ""; - } - final int asciiFirst = 0x20; - final int asciiLast = 0x7E; // included - final StringBuilder builder = new StringBuilder(); - final int length = value.length(); - boolean needQuote = false; - for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { - final int codePoint = value.codePointAt(i); - if (codePoint < asciiFirst || codePoint == '"') { - // CTL characters and DQUOTE are never accepted. Remove them. - continue; - } - builder.appendCodePoint(codePoint); - for (int indicator : escapeIndicators) { - if (codePoint == indicator) { - needQuote = true; - break; - } - } - } - - final String result = builder.toString(); - return ((result.isEmpty() || VCardUtils.containsOnlyWhiteSpaces(result)) - ? "" - : (needQuote ? ('"' + result + '"') - : result)); - } - - public static String toHalfWidthString(final String orgString) { - if (TextUtils.isEmpty(orgString)) { - return null; - } - final StringBuilder builder = new StringBuilder(); - final int length = orgString.length(); - for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) { - // All Japanese character is able to be expressed by char. - // Do not need to use String#codepPointAt(). - final char ch = orgString.charAt(i); - final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); - if (halfWidthText != null) { - builder.append(halfWidthText); - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - /** - * Guesses the format of input image. Currently just the first few bytes are used. - * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when - * the guess failed. - * @param input Image as byte array. - * @return The image type or null when the type cannot be determined. - */ - public static String guessImageType(final byte[] input) { - if (input == null) { - return null; - } - if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') { - return "GIF"; - } else if (input.length >= 4 && input[0] == (byte) 0x89 - && input[1] == 'P' && input[2] == 'N' && input[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... - return "PNG"; - } else if (input.length >= 2 && input[0] == (byte) 0xff - && input[1] == (byte) 0xd8) { - return "JPEG"; - } else { - return null; - } - } - - /** - * @return True when all the given values are null or empty Strings. - */ - public static boolean areAllEmpty(final String...values) { - if (values == null) { - return true; - } - - for (final String value : values) { - if (!TextUtils.isEmpty(value)) { - return false; - } - } - return true; - } - - //// The methods bellow may be used by unit test. - - /** - * Unquotes given Quoted-Printable value. value must not be null. - */ - public static String parseQuotedPrintable( - final String value, boolean strictLineBreaking, - String sourceCharset, String targetCharset) { - // "= " -> " ", "=\t" -> "\t". - // Previous code had done this replacement. Keep on the safe side. - final String quotedPrintable; - { - final StringBuilder builder = new StringBuilder(); - final int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch == '=' && i < length - 1) { - char nextCh = value.charAt(i + 1); - if (nextCh == ' ' || nextCh == '\t') { - builder.append(nextCh); - i++; - continue; - } - } - builder.append(ch); - } - quotedPrintable = builder.toString(); - } - - String[] lines; - if (strictLineBreaking) { - lines = quotedPrintable.split("\r\n"); - } else { - StringBuilder builder = new StringBuilder(); - final int length = quotedPrintable.length(); - ArrayList list = new ArrayList(); - for (int i = 0; i < length; i++) { - char ch = quotedPrintable.charAt(i); - if (ch == '\n') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else if (ch == '\r') { - list.add(builder.toString()); - builder = new StringBuilder(); - if (i < length - 1) { - char nextCh = quotedPrintable.charAt(i + 1); - if (nextCh == '\n') { - i++; - } - } - } else { - builder.append(ch); - } - } - final String lastLine = builder.toString(); - if (lastLine.length() > 0) { - list.add(lastLine); - } - lines = list.toArray(new String[0]); - } - - final StringBuilder builder = new StringBuilder(); - for (String line : lines) { - if (line.endsWith("=")) { - line = line.substring(0, line.length() - 1); - } - builder.append(line); - } - - final String rawString = builder.toString(); - if (TextUtils.isEmpty(rawString)) { - Log.w(LOG_TAG, "Given raw string is empty."); - } - - byte[] rawBytes = null; - try { - rawBytes = rawString.getBytes(sourceCharset); - } catch (UnsupportedEncodingException e) { - Log.w(LOG_TAG, "Failed to decode: " + sourceCharset); - rawBytes = rawString.getBytes(); - } - - byte[] decodedBytes = null; - try { - decodedBytes = QuotedPrintableCodec.decodeQuotedPrintable(rawBytes); - } catch (DecoderException e) { - Log.e(LOG_TAG, "DecoderException is thrown."); - decodedBytes = rawBytes; - } - - try { - return new String(decodedBytes, targetCharset); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); - return new String(decodedBytes); - } - } - - public static final VCardParser getAppropriateParser(int vcardType) - throws VCardException { - if (VCardConfig.isVersion21(vcardType)) { - return new VCardParser_V21(); - } else if (VCardConfig.isVersion30(vcardType)) { - return new VCardParser_V30(); - } else if (VCardConfig.isVersion40(vcardType)) { - return new VCardParser_V40(); - } else { - throw new VCardException("Version is not specified"); - } - } - - public static final String convertStringCharset( - String originalString, String sourceCharset, String targetCharset) { - if (sourceCharset.equalsIgnoreCase(targetCharset)) { - return originalString; - } - final Charset charset = Charset.forName(sourceCharset); - final ByteBuffer byteBuffer = charset.encode(originalString); - // byteBuffer.array() "may" return byte array which is larger than - // byteBuffer.remaining(). Here, we keep on the safe side. - final byte[] bytes = new byte[byteBuffer.remaining()]; - byteBuffer.get(bytes); - try { - return new String(bytes, targetCharset); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); - return null; - } - } - - // TODO: utilities for vCard 4.0: datetime, timestamp, integer, float, and boolean - - private VCardUtils() { - } -} diff --git a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java deleted file mode 100644 index 616aa7763b0e19f8c346b45d13958394245a86c3..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java +++ /dev/null @@ -1,33 +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.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/pim/vcard/exception/package.html b/core/java/android/pim/vcard/exception/package.html deleted file mode 100644 index 26b8a328b13212437892154b970ce092e6003af6..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/exception/package.html +++ /dev/null @@ -1,5 +0,0 @@ - - -{@hide} - - \ No newline at end of file diff --git a/core/java/android/pim/vcard/package.html b/core/java/android/pim/vcard/package.html deleted file mode 100644 index 26b8a328b13212437892154b970ce092e6003af6..0000000000000000000000000000000000000000 --- a/core/java/android/pim/vcard/package.html +++ /dev/null @@ -1,5 +0,0 @@ - - -{@hide} - - \ No newline at end of file diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java index f842d754cbb6e6cb5b9744e9a59437da572b11a6..f44cbe40f7ae371ec57fa9b73763023c1dea7113 100644 --- a/core/java/android/preference/ListPreference.java +++ b/core/java/android/preference/ListPreference.java @@ -39,6 +39,7 @@ public class ListPreference extends DialogPreference { private CharSequence[] mEntries; private CharSequence[] mEntryValues; private String mValue; + private String mSummary; private int mClickedDialogEntryIndex; public ListPreference(Context context, AttributeSet attrs) { @@ -49,8 +50,16 @@ public class ListPreference extends DialogPreference { mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries); mEntryValues = a.getTextArray(com.android.internal.R.styleable.ListPreference_entryValues); a.recycle(); + + /* Retrieve the Preference summary attribute since it's private + * in the Preference class. + */ + a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Preference, 0, 0); + mSummary = a.getString(com.android.internal.R.styleable.Preference_summary); + a.recycle(); } - + public ListPreference(Context context) { this(context, null); } @@ -126,6 +135,43 @@ public class ListPreference extends DialogPreference { persistString(value); } + /** + * Returns the summary of this ListPreference. If the summary + * has a {@linkplain java.lang.String#format String formatting} + * marker in it (i.e. "%s" or "%1$s"), then the current entry + * value will be substituted in its place. + * + * @return the summary with appropriate string substitution + */ + @Override + public CharSequence getSummary() { + final CharSequence entry = getEntry(); + if (mSummary == null || entry == null) { + return super.getSummary(); + } else { + return String.format(mSummary, entry); + } + } + + /** + * Sets the summary for this Preference with a CharSequence. + * If the summary has a + * {@linkplain java.lang.String#format String formatting} + * marker in it (i.e. "%s" or "%1$s"), then the current entry + * value will be substituted in its place when it's retrieved. + * + * @param summary The summary for the preference. + */ + @Override + public void setSummary(CharSequence summary) { + super.setSummary(summary); + if (summary == null && mSummary != null) { + mSummary = null; + } else if (summary != null && !summary.equals(mSummary)) { + mSummary = summary.toString(); + } + } + /** * Sets the value to the given index from the entry values. * diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java new file mode 100644 index 0000000000000000000000000000000000000000..42d555cff45c836261b99af7e49e53d4a3b5a425 --- /dev/null +++ b/core/java/android/preference/MultiSelectListPreference.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.preference; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; + +import java.util.HashSet; +import java.util.Set; + +/** + * A {@link Preference} that displays a list of entries as + * a dialog. + *

    + * This preference will store a set of strings into the SharedPreferences. + * This set will contain one or more values from the + * {@link #setEntryValues(CharSequence[])} array. + * + * @attr ref android.R.styleable#MultiSelectListPreference_entries + * @attr ref android.R.styleable#MultiSelectListPreference_entryValues + */ +public class MultiSelectListPreference extends DialogPreference { + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private Set mValues = new HashSet(); + private Set mNewValues = new HashSet(); + private boolean mPreferenceChanged; + + public MultiSelectListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MultiSelectListPreference, 0, 0); + mEntries = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entries); + mEntryValues = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entryValues); + a.recycle(); + } + + public MultiSelectListPreference(Context context) { + this(context, null); + } + + /** + * Sets the human-readable entries to be shown in the list. This will be + * shown in subsequent dialogs. + *

    + * Each entry must have a corresponding index in + * {@link #setEntryValues(CharSequence[])}. + * + * @param entries The entries. + * @see #setEntryValues(CharSequence[]) + */ + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + /** + * @see #setEntries(CharSequence[]) + * @param entriesResId The entries array as a resource. + */ + public void setEntries(int entriesResId) { + setEntries(getContext().getResources().getTextArray(entriesResId)); + } + + /** + * The list of entries to be shown in the list in subsequent dialogs. + * + * @return The list as an array. + */ + public CharSequence[] getEntries() { + return mEntries; + } + + /** + * The array to find the value to save for a preference when an entry from + * entries is selected. If a user clicks on the second item in entries, the + * second item in this array will be saved to the preference. + * + * @param entryValues The array to be used as values to save for the preference. + */ + public void setEntryValues(CharSequence[] entryValues) { + mEntryValues = entryValues; + } + + /** + * @see #setEntryValues(CharSequence[]) + * @param entryValuesResId The entry values array as a resource. + */ + public void setEntryValues(int entryValuesResId) { + setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); + } + + /** + * Returns the array of values to be saved for the preference. + * + * @return The array of values. + */ + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + /** + * Sets the value of the key. This should contain entries in + * {@link #getEntryValues()}. + * + * @param values The values to set for the key. + */ + public void setValues(Set values) { + mValues = values; + + persistStringSet(values); + } + + /** + * Retrieves the current value of the key. + */ + public Set getValues() { + return mValues; + } + + /** + * Returns the index of the given value (in the entry values array). + * + * @param value The value whose index should be returned. + * @return The index of the value, or -1 if not found. + */ + public int findIndexOfValue(String value) { + if (value != null && mEntryValues != null) { + for (int i = mEntryValues.length - 1; i >= 0; i--) { + if (mEntryValues[i].equals(value)) { + return i; + } + } + } + return -1; + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + + if (mEntries == null || mEntryValues == null) { + throw new IllegalStateException( + "MultiSelectListPreference requires an entries array and " + + "an entryValues array."); + } + + boolean[] checkedItems = getSelectedItems(); + builder.setMultiChoiceItems(mEntries, checkedItems, + new DialogInterface.OnMultiChoiceClickListener() { + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + mPreferenceChanged |= mNewValues.add(mEntries[which].toString()); + } else { + mPreferenceChanged |= mNewValues.remove(mEntries[which].toString()); + } + } + }); + mNewValues.clear(); + mNewValues.addAll(mValues); + } + + private boolean[] getSelectedItems() { + final CharSequence[] entries = mEntries; + final int entryCount = entries.length; + final Set values = mValues; + boolean[] result = new boolean[entryCount]; + + for (int i = 0; i < entryCount; i++) { + result[i] = values.contains(entries[i].toString()); + } + + return result; + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult && mPreferenceChanged) { + final Set values = mNewValues; + if (callChangeListener(values)) { + setValues(values); + } + } + mPreferenceChanged = false; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + final CharSequence[] defaultValues = a.getTextArray(index); + final int valueCount = defaultValues.length; + final Set result = new HashSet(); + + for (int i = 0; i < valueCount; i++) { + result.add(defaultValues[i].toString()); + } + + return result; + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValues(restoreValue ? getPersistedStringSet(mValues) : (Set) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.values = getValues(); + return myState; + } + + private static class SavedState extends BaseSavedState { + Set values; + + public SavedState(Parcel source) { + super(source); + values = new HashSet(); + String[] strings = source.readStringArray(); + + final int stringCount = strings.length; + for (int i = 0; i < stringCount; i++) { + values.add(strings[i]); + } + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeStringArray(values.toArray(new String[0])); + } + + 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]; + } + }; + } +} diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index dde6493fb3318896860aac8851dd8fda12fa28b8..17b2e82d09f9d8a4adae142b43bc0eb4237e73cc 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -16,8 +16,7 @@ package android.preference; -import java.util.ArrayList; -import java.util.List; +import com.android.internal.util.CharSequences; import android.content.Context; import android.content.Intent; @@ -28,7 +27,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; -import com.android.internal.util.CharSequences; import android.view.AbsSavedState; import android.view.LayoutInflater; import android.view.View; @@ -36,6 +34,10 @@ import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + /** * Represents the basic Preference UI building * block displayed by a {@link PreferenceActivity} in the form of a @@ -54,6 +56,7 @@ import android.widget.TextView; * @attr ref android.R.styleable#Preference_title * @attr ref android.R.styleable#Preference_summary * @attr ref android.R.styleable#Preference_order + * @attr ref android.R.styleable#Preference_fragment * @attr ref android.R.styleable#Preference_layout * @attr ref android.R.styleable#Preference_widgetLayout * @attr ref android.R.styleable#Preference_enabled @@ -86,6 +89,8 @@ public class Preference implements Comparable, OnDependencyChangeLis private CharSequence mSummary; private String mKey; private Intent mIntent; + private String mFragment; + private Bundle mExtras; private boolean mEnabled = true; private boolean mSelectable = true; private boolean mRequiresKey; @@ -208,6 +213,10 @@ public class Preference implements Comparable, OnDependencyChangeLis mOrder = a.getInt(attr, mOrder); break; + case com.android.internal.R.styleable.Preference_fragment: + mFragment = a.getString(attr); + break; + case com.android.internal.R.styleable.Preference_layout: mLayoutResId = a.getResourceId(attr, mLayoutResId); break; @@ -312,6 +321,44 @@ public class Preference implements Comparable, OnDependencyChangeLis return mIntent; } + /** + * Sets the class name of a fragment to be shown when this Preference is clicked. + * + * @param fragment The class name of the fragment associated with this Preference. + */ + public void setFragment(String fragment) { + mFragment = fragment; + } + + /** + * Return the fragment class name associated with this Preference. + * + * @return The fragment class name last set via {@link #setFragment} or XML. + */ + public String getFragment() { + return mFragment; + } + + /** + * Return the extras Bundle object associated with this preference, creating + * a new Bundle if there currently isn't one. You can use this to get and + * set individual extra key/value pairs. + */ + public Bundle getExtras() { + if (mExtras == null) { + mExtras = new Bundle(); + } + return mExtras; + } + + /** + * Return the extras Bundle object associated with this preference, + * returning null if there is not currently one. + */ + public Bundle peekExtras() { + return mExtras; + } + /** * Sets the layout resource that is inflated as the {@link View} to be shown * for this Preference. In most cases, the default layout is sufficient for @@ -1249,6 +1296,61 @@ public class Preference implements Comparable, OnDependencyChangeLis return mPreferenceManager.getSharedPreferences().getString(mKey, defaultReturnValue); } + /** + * Attempts to persist a set of Strings to the {@link android.content.SharedPreferences}. + *

    + * This will check if this Preference is persistent, get an editor from + * the {@link PreferenceManager}, put in the strings, and check if we should commit (and + * commit if so). + * + * @param values The values to persist. + * @return True if the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #getPersistedString(Set) + * + * @hide Pending API approval + */ + protected boolean persistStringSet(Set values) { + if (shouldPersist()) { + // Shouldn't store null + if (values.equals(getPersistedStringSet(null))) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putStringSet(mKey, values); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted set of Strings from the + * {@link android.content.SharedPreferences}. + *

    + * This will check if this Preference is persistent, get the SharedPreferences + * from the {@link PreferenceManager}, and get the value. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the SharedPreferences or the default return + * value. + * @see #persistStringSet(Set) + * + * @hide Pending API approval + */ + protected Set getPersistedStringSet(Set defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getStringSet(mKey, defaultReturnValue); + } + /** * Attempts to persist an int to the {@link android.content.SharedPreferences}. * diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 726793dcef98e8936fd8f59dd4b89d3744052acf..076ba10ee30ddc0893798c75b5c335d566520d57 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -16,135 +16,778 @@ package android.preference; -import android.app.Activity; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.app.Fragment; +import android.app.FragmentBreadCrumbs; +import android.app.FragmentTransaction; import android.app.ListActivity; +import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** - * Shows a hierarchy of {@link Preference} objects as - * lists, possibly spanning multiple screens. These preferences will - * automatically save to {@link SharedPreferences} as the user interacts with - * them. To retrieve an instance of {@link SharedPreferences} that the - * preference hierarchy in this activity will use, call - * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} - * with a context in the same package as this activity. - *

    - * Furthermore, the preferences shown will follow the visual style of system - * preferences. It is easy to create a hierarchy of preferences (that can be - * shown on multiple screens) via XML. For these reasons, it is recommended to - * use this activity (as a superclass) to deal with preferences in applications. - *

    - * A {@link PreferenceScreen} object should be at the top of the preference - * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy - * denote a screen break--that is the preferences contained within subsequent - * {@link PreferenceScreen} should be shown on another screen. The preference - * framework handles showing these other screens from the preference hierarchy. - *

    - * The preference hierarchy can be formed in multiple ways: - *

  • From an XML file specifying the hierarchy - *
  • From different {@link Activity Activities} that each specify its own - * preferences in an XML file via {@link Activity} meta-data - *
  • From an object hierarchy rooted with {@link PreferenceScreen} - *

    - * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The - * root element should be a {@link PreferenceScreen}. Subsequent elements can point - * to actual {@link Preference} subclasses. As mentioned above, subsequent - * {@link PreferenceScreen} in the hierarchy will result in the screen break. - *

    - * To specify an {@link Intent} to query {@link Activity Activities} that each - * have preferences, use {@link #addPreferencesFromIntent}. Each - * {@link Activity} can specify meta-data in the manifest (via the key - * {@link PreferenceManager#METADATA_KEY_PREFERENCES}) that points to an XML - * resource. These XML resources will be inflated into a single preference - * hierarchy and shown by this activity. - *

    - * To specify an object hierarchy rooted with {@link PreferenceScreen}, use - * {@link #setPreferenceScreen(PreferenceScreen)}. - *

    - * As a convenience, this activity implements a click listener for any - * preference in the current hierarchy, see - * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. - * - * @see Preference - * @see PreferenceScreen + * This is the base class for an activity to show a hierarchy of preferences + * to the user. Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB} + * this class only allowed the display of a single set of preference; this + * functionality should now be found in the new {@link PreferenceFragment} + * class. If you are using PreferenceActivity in its old mode, the documentation + * there applies to the deprecated APIs here. + * + *

    This activity shows one or more headers of preferences, each of with + * is associated with a {@link PreferenceFragment} to display the preferences + * of that header. The actual layout and display of these associations can + * however vary; currently there are two major approaches it may take: + * + *

      + *
    • On a small screen it may display only the headers as a single list + * when first launched. Selecting one of the header items will re-launch + * the activity with it only showing the PreferenceFragment of that header. + *
    • On a large screen in may display both the headers and current + * PreferenceFragment together as panes. Selecting a header item switches + * to showing the correct PreferenceFragment for that item. + *
    + * + *

    Subclasses of PreferenceActivity should implement + * {@link #onBuildHeaders} to populate the header list with the desired + * items. Doing this implicitly switches the class into its new "headers + * + fragments" mode rather than the old style of just showing a single + * preferences list. + * + * + *

    Sample Code

    + * + *

    The following sample code shows a simple preference activity that + * has two different sets of preferences. The implementation, consisting + * of the activity itself as well as its two preference fragments is:

    + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java + * activity} + * + *

    The preference_headers resource describes the headers to be displayed + * and the fragments associated with them. It is: + * + * {@sample development/samples/ApiDemos/res/xml/preference_headers.xml headers} + * + *

    The first header is shown by Prefs1Fragment, which populates itself + * from the following XML resource:

    + * + * {@sample development/samples/ApiDemos/res/xml/fragmented_preferences.xml preferences} + * + *

    Note that this XML resource contains a preference screen holding another + * fragment, the Prefs1FragmentInner implemented here. This allows the user + * to traverse down a hierarchy of preferences; pressing back will pop each + * fragment off the stack to return to the previous preferences. + * + *

    See {@link PreferenceFragment} for information on implementing the + * fragments themselves. */ public abstract class PreferenceActivity extends ListActivity implements - PreferenceManager.OnPreferenceTreeClickListener { - - private static final String PREFERENCES_TAG = "android:preferences"; - + PreferenceManager.OnPreferenceTreeClickListener, + PreferenceFragment.OnPreferenceStartFragmentCallback { + private static final String TAG = "PreferenceActivity"; + + // Constants for state save/restore + private static final String HEADERS_TAG = ":android:headers"; + private static final String CUR_HEADER_TAG = ":android:cur_header"; + private static final String PREFERENCES_TAG = ":android:preferences"; + + /** + * When starting this activity, the invoking Intent can contain this extra + * string to specify which fragment should be initially displayed. + */ + public static final String EXTRA_SHOW_FRAGMENT = ":android:show_fragment"; + + /** + * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, + * this extra can also be specify to supply a Bundle of arguments to pass + * to that fragment when it is instantiated during the initial creation + * of PreferenceActivity. + */ + public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args"; + + /** + * When starting this activity, the invoking Intent can contain this extra + * boolean that the header list should not be displayed. This is most often + * used in conjunction with {@link #EXTRA_SHOW_FRAGMENT} to launch + * the activity to display a specific fragment that the user has navigated + * to. + */ + public static final String EXTRA_NO_HEADERS = ":android:no_headers"; + + private static final String BACK_STACK_PREFS = ":android:prefs"; + + // extras that allow any preference activity to be launched as part of a wizard + + // show Back and Next buttons? takes boolean parameter + // Back will then return RESULT_CANCELED and Next RESULT_OK + private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; + + // add a Skip button? + private static final String EXTRA_PREFS_SHOW_SKIP = "extra_prefs_show_skip"; + + // specify custom text for the Back or Next buttons, or cause a button to not appear + // at all by setting it to null + private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; + private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; + + // --- State for new mode when showing a list of headers + prefs fragment + + private final ArrayList

    mHeaders = new ArrayList
    (); + + private HeaderAdapter mAdapter; + + private FrameLayout mListFooter; + + private View mPrefsContainer; + + private FragmentBreadCrumbs mFragmentBreadCrumbs; + + private boolean mSinglePane; + + private Header mCurHeader; + + // --- State for old mode when showing a single preference list + private PreferenceManager mPreferenceManager; - + private Bundle mSavedInstanceState; + // --- Common state + + private Button mNextButton; + /** * The starting request code given out to preference framework. */ private static final int FIRST_REQUEST_CODE = 100; - - private static final int MSG_BIND_PREFERENCES = 0; + + private static final int MSG_BIND_PREFERENCES = 1; + private static final int MSG_BUILD_HEADERS = 2; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { - - case MSG_BIND_PREFERENCES: + case MSG_BIND_PREFERENCES: { bindPreferences(); - break; + } break; + case MSG_BUILD_HEADERS: { + ArrayList
    oldHeaders = new ArrayList
    (mHeaders); + mHeaders.clear(); + onBuildHeaders(mHeaders); + mAdapter.notifyDataSetChanged(); + Header header = onGetNewHeader(); + if (header != null && header.fragment != null) { + Header mappedHeader = findBestMatchingHeader(header, oldHeaders); + if (mappedHeader == null || mCurHeader != mappedHeader) { + switchToHeader(header); + } + } else if (mCurHeader != null) { + Header mappedHeader = findBestMatchingHeader(mCurHeader, mHeaders); + if (mappedHeader != null) { + setSelectedHeader(mappedHeader); + } else { + switchToHeader(null); + } + } + } break; } } }; + private static class HeaderAdapter extends ArrayAdapter
    { + private static class HeaderViewHolder { + ImageView icon; + TextView title; + TextView summary; + } + + private LayoutInflater mInflater; + + public HeaderAdapter(Context context, List
    objects) { + super(context, 0, objects); + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + View view; + + if (convertView == null) { + view = mInflater.inflate(com.android.internal.R.layout.preference_header_item, + parent, false); + holder = new HeaderViewHolder(); + holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon); + holder.title = (TextView) view.findViewById(com.android.internal.R.id.title); + holder.summary = (TextView) view.findViewById(com.android.internal.R.id.summary); + view.setTag(holder); + } else { + view = convertView; + holder = (HeaderViewHolder) view.getTag(); + } + + // All view fields must be updated every time, because the view may be recycled + Header header = getItem(position); + holder.icon.setImageResource(header.iconRes); + holder.title.setText(header.title); + if (TextUtils.isEmpty(header.summary)) { + holder.summary.setVisibility(View.GONE); + } else { + holder.summary.setVisibility(View.VISIBLE); + holder.summary.setText(header.summary); + } + + return view; + } + } + + /** + * Default value for {@link Header#id Header.id} indicating that no + * identifier value is set. All other values (including those below -1) + * are valid. + */ + public static final long HEADER_ID_UNDEFINED = -1; + + /** + * Description of a single Header item that the user can select. + */ + public static final class Header implements Parcelable { + /** + * Identifier for this header, to correlate with a new list when + * it is updated. The default value is + * {@link PreferenceActivity#HEADER_ID_UNDEFINED}, meaning no id. + * @attr ref android.R.styleable#PreferenceHeader_id + */ + public long id = HEADER_ID_UNDEFINED; + + /** + * Title of the header that is shown to the user. + * @attr ref android.R.styleable#PreferenceHeader_title + */ + public CharSequence title; + + /** + * Optional summary describing what this header controls. + * @attr ref android.R.styleable#PreferenceHeader_summary + */ + public CharSequence summary; + + /** + * Optional text to show as the title in the bread crumb. + * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle + */ + public CharSequence breadCrumbTitle; + + /** + * Optional text to show as the short title in the bread crumb. + * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle + */ + public CharSequence breadCrumbShortTitle; + + /** + * Optional icon resource to show for this header. + * @attr ref android.R.styleable#PreferenceHeader_icon + */ + public int iconRes; + + /** + * Full class name of the fragment to display when this header is + * selected. + * @attr ref android.R.styleable#PreferenceHeader_fragment + */ + public String fragment; + + /** + * Optional arguments to supply to the fragment when it is + * instantiated. + */ + public Bundle fragmentArguments; + + /** + * Intent to launch when the preference is selected. + */ + public Intent intent; + + /** + * Optional additional data for use by subclasses of PreferenceActivity. + */ + public Bundle extras; + + public Header() { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(id); + TextUtils.writeToParcel(title, dest, flags); + TextUtils.writeToParcel(summary, dest, flags); + TextUtils.writeToParcel(breadCrumbTitle, dest, flags); + TextUtils.writeToParcel(breadCrumbShortTitle, dest, flags); + dest.writeInt(iconRes); + dest.writeString(fragment); + dest.writeBundle(fragmentArguments); + if (intent != null) { + dest.writeInt(1); + intent.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + dest.writeBundle(extras); + } + + public void readFromParcel(Parcel in) { + id = in.readLong(); + title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + breadCrumbTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + breadCrumbShortTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + iconRes = in.readInt(); + fragment = in.readString(); + fragmentArguments = in.readBundle(); + if (in.readInt() != 0) { + intent = Intent.CREATOR.createFromParcel(in); + } + extras = in.readBundle(); + } + + Header(Parcel in) { + readFromParcel(in); + } + + public static final Creator
    CREATOR = new Creator
    () { + public Header createFromParcel(Parcel source) { + return new Header(source); + } + public Header[] newArray(int size) { + return new Header[size]; + } + }; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(com.android.internal.R.layout.preference_list_content); - - mPreferenceManager = onCreatePreferenceManager(); + + mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer); + mPrefsContainer = findViewById(com.android.internal.R.id.prefs); + boolean hidingHeaders = onIsHidingHeaders(); + mSinglePane = hidingHeaders || !onIsMultiPane(); + String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT); + Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); + + if (savedInstanceState != null) { + // We are restarting from a previous saved state; used that to + // initialize, instead of starting fresh. + ArrayList
    headers = savedInstanceState.getParcelableArrayList(HEADERS_TAG); + if (headers != null) { + mHeaders.addAll(headers); + int curHeader = savedInstanceState.getInt(CUR_HEADER_TAG, + (int)HEADER_ID_UNDEFINED); + if (curHeader >= 0 && curHeader < mHeaders.size()) { + setSelectedHeader(mHeaders.get(curHeader)); + } + } + + } else { + if (initialFragment != null && mSinglePane) { + // If we are just showing a fragment, we want to run in + // new fragment mode, but don't need to compute and show + // the headers. + switchToHeader(initialFragment, initialArguments); + + } else { + // We need to try to build the headers. + onBuildHeaders(mHeaders); + + // If there are headers, then at this point we need to show + // them and, depending on the screen, we may also show in-line + // the currently selected preference fragment. + if (mHeaders.size() > 0) { + if (!mSinglePane) { + if (initialFragment == null) { + Header h = onGetInitialHeader(); + switchToHeader(h); + } else { + switchToHeader(initialFragment, initialArguments); + } + } + } + } + } + + // The default configuration is to only show the list view. Adjust + // visibility for other configurations. + if (initialFragment != null && mSinglePane) { + // Single pane, showing just a prefs fragment. + getListView().setVisibility(View.GONE); + mPrefsContainer.setVisibility(View.VISIBLE); + } else if (mHeaders.size() > 0) { + mAdapter = new HeaderAdapter(this, mHeaders); + setListAdapter(mAdapter); + if (!mSinglePane) { + // Multi-pane. + getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); + if (mCurHeader != null) { + setSelectedHeader(mCurHeader); + } + mPrefsContainer.setVisibility(View.VISIBLE); + } + } else { + // If there are no headers, we are in the old "just show a screen + // of preferences" mode. + mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); + mPreferenceManager.setOnPreferenceTreeClickListener(this); + } + getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); + + // see if we should show Back/Next buttons + Intent intent = getIntent(); + if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { + + findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE); + + Button backButton = (Button)findViewById(com.android.internal.R.id.back_button); + backButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + Button skipButton = (Button)findViewById(com.android.internal.R.id.skip_button); + skipButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_OK); + finish(); + } + }); + mNextButton = (Button)findViewById(com.android.internal.R.id.next_button); + mNextButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_OK); + finish(); + } + }); + + // set our various button parameters + if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); + if (TextUtils.isEmpty(buttonText)) { + mNextButton.setVisibility(View.GONE); + } + else { + mNextButton.setText(buttonText); + } + } + if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); + if (TextUtils.isEmpty(buttonText)) { + backButton.setVisibility(View.GONE); + } + else { + backButton.setText(buttonText); + } + } + if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_SKIP, false)) { + skipButton.setVisibility(View.VISIBLE); + } + } + } + + /** + * Returns true if this activity is currently showing the header list. + */ + public boolean hasHeaders() { + return getListView().getVisibility() == View.VISIBLE + && mPreferenceManager == null; + } + + /** + * Returns true if this activity is showing multiple panes -- the headers + * and a preference fragment. + */ + public boolean isMultiPane() { + return hasHeaders() && mPrefsContainer.getVisibility() == View.VISIBLE; + } + + /** + * Called to determine if the activity should run in multi-pane mode. + * The default implementation returns true if the screen is large + * enough. + */ + public boolean onIsMultiPane() { + Configuration config = getResources().getConfiguration(); + if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_XLARGE) { + return true; + } + if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_LARGE + && config.orientation == Configuration.ORIENTATION_LANDSCAPE) { + return true; + } + return false; + } + + /** + * Called to determine whether the header list should be hidden. + * The default implementation returns the + * value given in {@link #EXTRA_NO_HEADERS} or false if it is not supplied. + * This is set to false, for example, when the activity is being re-launched + * to show a particular preference activity. + */ + public boolean onIsHidingHeaders() { + return getIntent().getBooleanExtra(EXTRA_NO_HEADERS, false); + } + + /** + * Called to determine the initial header to be shown. The default + * implementation simply returns the fragment of the first header. Note + * that the returned Header object does not actually need to exist in + * your header list -- whatever its fragment is will simply be used to + * show for the initial UI. + */ + public Header onGetInitialHeader() { + return mHeaders.get(0); + } + + /** + * Called after the header list has been updated ({@link #onBuildHeaders} + * has been called and returned due to {@link #invalidateHeaders()}) to + * specify the header that should now be selected. The default implementation + * returns null to keep whatever header is currently selected. + */ + public Header onGetNewHeader() { + return null; + } + + /** + * Called when the activity needs its list of headers build. By + * implementing this and adding at least one item to the list, you + * will cause the activity to run in its modern fragment mode. Note + * that this function may not always be called; for example, if the + * activity has been asked to display a particular fragment without + * the header list, there is no need to build the headers. + * + *

    Typical implementations will use {@link #loadHeadersFromResource} + * to fill in the list from a resource. + * + * @param target The list in which to place the headers. + */ + public void onBuildHeaders(List

    target) { + } + + /** + * Call when you need to change the headers being displayed. Will result + * in onBuildHeaders() later being called to retrieve the new list. + */ + public void invalidateHeaders() { + if (!mHandler.hasMessages(MSG_BUILD_HEADERS)) { + mHandler.sendEmptyMessage(MSG_BUILD_HEADERS); + } + } + + /** + * Parse the given XML file as a header description, adding each + * parsed Header into the target list. + * + * @param resid The XML resource to load and parse. + * @param target The list in which the parsed headers should be placed. + */ + public void loadHeadersFromResource(int resid, List
    target) { + XmlResourceParser parser = null; + try { + parser = getResources().getXml(resid); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!"preference-headers".equals(nodeName)) { + throw new RuntimeException( + "XML document must start with tag; found" + + nodeName + " at " + parser.getPositionDescription()); + } + + Bundle curBundle = null; + + final int outerDepth = parser.getDepth(); + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + nodeName = parser.getName(); + if ("header".equals(nodeName)) { + Header header = new Header(); + + TypedArray sa = getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.PreferenceHeader); + header.id = sa.getResourceId( + com.android.internal.R.styleable.PreferenceHeader_id, + (int)HEADER_ID_UNDEFINED); + header.title = sa.getText( + com.android.internal.R.styleable.PreferenceHeader_title); + header.summary = sa.getText( + com.android.internal.R.styleable.PreferenceHeader_summary); + header.breadCrumbTitle = sa.getText( + com.android.internal.R.styleable.PreferenceHeader_breadCrumbTitle); + header.breadCrumbShortTitle = sa.getText( + com.android.internal.R.styleable.PreferenceHeader_breadCrumbShortTitle); + header.iconRes = sa.getResourceId( + com.android.internal.R.styleable.PreferenceHeader_icon, 0); + header.fragment = sa.getString( + com.android.internal.R.styleable.PreferenceHeader_fragment); + sa.recycle(); + + if (curBundle == null) { + curBundle = new Bundle(); + } + + final int innerDepth = parser.getDepth(); + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String innerNodeName = parser.getName(); + if (innerNodeName.equals("extra")) { + getResources().parseBundleExtra("extra", attrs, curBundle); + XmlUtils.skipCurrentTag(parser); + + } else if (innerNodeName.equals("intent")) { + header.intent = Intent.parseIntent(getResources(), parser, attrs); + + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + if (curBundle.size() > 0) { + header.fragmentArguments = curBundle; + curBundle = null; + } + + target.add(header); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + } catch (XmlPullParserException e) { + throw new RuntimeException("Error parsing headers", e); + } catch (IOException e) { + throw new RuntimeException("Error parsing headers", e); + } finally { + if (parser != null) parser.close(); + } + + } + + /** + * Set a footer that should be shown at the bottom of the header list. + */ + public void setListFooter(View view) { + mListFooter.removeAllViews(); + mListFooter.addView(view, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT)); } @Override protected void onStop() { super.onStop(); - - mPreferenceManager.dispatchActivityStop(); + + if (mPreferenceManager != null) { + mPreferenceManager.dispatchActivityStop(); + } } @Override protected void onDestroy() { super.onDestroy(); - - mPreferenceManager.dispatchActivityDestroy(); + + if (mPreferenceManager != null) { + mPreferenceManager.dispatchActivityDestroy(); + } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - Bundle container = new Bundle(); - preferenceScreen.saveHierarchyState(container); - outState.putBundle(PREFERENCES_TAG, container); + if (mHeaders.size() > 0) { + outState.putParcelableArrayList(HEADERS_TAG, mHeaders); + if (mCurHeader != null) { + int index = mHeaders.indexOf(mCurHeader); + if (index >= 0) { + outState.putInt(CUR_HEADER_TAG, index); + } + } + } + + if (mPreferenceManager != null) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + Bundle container = new Bundle(); + preferenceScreen.saveHierarchyState(container); + outState.putBundle(PREFERENCES_TAG, container); + } } } @Override protected void onRestoreInstanceState(Bundle state) { - Bundle container = state.getBundle(PREFERENCES_TAG); - if (container != null) { - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - preferenceScreen.restoreHierarchyState(container); - mSavedInstanceState = state; - return; + if (mPreferenceManager != null) { + Bundle container = state.getBundle(PREFERENCES_TAG); + if (container != null) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.restoreHierarchyState(container); + mSavedInstanceState = state; + return; + } } } @@ -156,14 +799,211 @@ public abstract class PreferenceActivity extends ListActivity implements @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - - mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); + + if (mPreferenceManager != null) { + mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); + } } @Override public void onContentChanged() { super.onContentChanged(); - postBindPreferences(); + + if (mPreferenceManager != null) { + postBindPreferences(); + } + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + + if (mAdapter != null) { + onHeaderClick(mHeaders.get(position), position); + } + } + + /** + * Called when the user selects an item in the header list. The default + * implementation will call either {@link #startWithFragment(String, Bundle)} + * or {@link #switchToHeader(Header)} as appropriate. + * + * @param header The header that was selected. + * @param position The header's position in the list. + */ + public void onHeaderClick(Header header, int position) { + if (header.fragment != null) { + if (mSinglePane) { + startWithFragment(header.fragment, header.fragmentArguments); + } else { + switchToHeader(header); + } + } else if (header.intent != null) { + startActivity(header.intent); + } + } + + /** + * Start a new instance of this activity, showing only the given + * preference fragment. When launched in this mode, the header list + * will be hidden and the given preference fragment will be instantiated + * and fill the entire activity. + * + * @param fragmentName The name of the fragment to display. + * @param args Optional arguments to supply to the fragment. + */ + public void startWithFragment(String fragmentName, Bundle args) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(this, getClass()); + intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); + intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + intent.putExtra(EXTRA_NO_HEADERS, true); + startActivity(intent); + } + + /** + * Change the base title of the bread crumbs for the current preferences. + * This will normally be called for you. See + * {@link android.app.FragmentBreadCrumbs} for more information. + */ + public void showBreadCrumbs(CharSequence title, CharSequence shortTitle) { + if (mFragmentBreadCrumbs == null) { + mFragmentBreadCrumbs = new FragmentBreadCrumbs(this); + mFragmentBreadCrumbs.setActivity(this); + getActionBar().setCustomNavigationMode(mFragmentBreadCrumbs); + } + mFragmentBreadCrumbs.setTitle(title, shortTitle); + } + + void setSelectedHeader(Header header) { + mCurHeader = header; + int index = mHeaders.indexOf(header); + if (index >= 0) { + getListView().setItemChecked(index, true); + } else { + getListView().clearChoices(); + } + if (header != null) { + CharSequence title = header.breadCrumbTitle; + if (title == null) title = header.title; + if (title == null) title = getTitle(); + showBreadCrumbs(title, header.breadCrumbShortTitle); + } else { + showBreadCrumbs(getTitle(), null); + } + } + + public void switchToHeaderInner(String fragmentName, Bundle args) { + getFragmentManager().popBackStack(BACK_STACK_PREFS, POP_BACK_STACK_INCLUSIVE); + Fragment f = Fragment.instantiate(this, fragmentName, args); + getFragmentManager().openTransaction().replace( + com.android.internal.R.id.prefs, f).commit(); + } + + /** + * When in two-pane mode, switch the fragment pane to show the given + * preference fragment. + * + * @param fragmentName The name of the fragment to display. + * @param args Optional arguments to supply to the fragment. + */ + public void switchToHeader(String fragmentName, Bundle args) { + setSelectedHeader(null); + switchToHeaderInner(fragmentName, args); + } + + /** + * When in two-pane mode, switch to the fragment pane to show the given + * preference fragment. + * + * @param header The new header to display. + */ + public void switchToHeader(Header header) { + switchToHeaderInner(header.fragment, header.fragmentArguments); + setSelectedHeader(header); + } + + Header findBestMatchingHeader(Header cur, ArrayList
    from) { + ArrayList
    matches = new ArrayList
    (); + for (int j=0; j 1) { + for (int j=0; j + * Furthermore, the preferences shown will follow the visual style of system + * preferences. It is easy to create a hierarchy of preferences (that can be + * shown on multiple screens) via XML. For these reasons, it is recommended to + * use this fragment (as a superclass) to deal with preferences in applications. + *

    + * A {@link PreferenceScreen} object should be at the top of the preference + * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy + * denote a screen break--that is the preferences contained within subsequent + * {@link PreferenceScreen} should be shown on another screen. The preference + * framework handles showing these other screens from the preference hierarchy. + *

    + * The preference hierarchy can be formed in multiple ways: + *

  • From an XML file specifying the hierarchy + *
  • From different {@link Activity Activities} that each specify its own + * preferences in an XML file via {@link Activity} meta-data + *
  • From an object hierarchy rooted with {@link PreferenceScreen} + *

    + * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The + * root element should be a {@link PreferenceScreen}. Subsequent elements can point + * to actual {@link Preference} subclasses. As mentioned above, subsequent + * {@link PreferenceScreen} in the hierarchy will result in the screen break. + *

    + * To specify an {@link Intent} to query {@link Activity Activities} that each + * have preferences, use {@link #addPreferencesFromIntent}. Each + * {@link Activity} can specify meta-data in the manifest (via the key + * {@link PreferenceManager#METADATA_KEY_PREFERENCES}) that points to an XML + * resource. These XML resources will be inflated into a single preference + * hierarchy and shown by this fragment. + *

    + * To specify an object hierarchy rooted with {@link PreferenceScreen}, use + * {@link #setPreferenceScreen(PreferenceScreen)}. + *

    + * As a convenience, this fragment implements a click listener for any + * preference in the current hierarchy, see + * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. + * + * + *

    Sample Code

    + * + *

    The following sample code shows a simple preference fragment that is + * populated from a resource. The resource it loads is:

    + * + * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences} + * + *

    The fragment implementation itself simply populates the preferences + * when created. Note that the preferences framework takes care of loading + * the current values out of the app preferences and writing them when changed:

    + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java + * fragment} + * + * @see Preference + * @see PreferenceScreen + */ +public abstract class PreferenceFragment extends Fragment implements + PreferenceManager.OnPreferenceTreeClickListener { + + private static final String PREFERENCES_TAG = "android:preferences"; + + private PreferenceManager mPreferenceManager; + private ListView mList; + private boolean mHavePrefs; + private boolean mInitDone; + + /** + * The starting request code given out to preference framework. + */ + private static final int FIRST_REQUEST_CODE = 100; + + private static final int MSG_BIND_PREFERENCES = 1; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case MSG_BIND_PREFERENCES: + bindPreferences(); + break; + } + } + }; + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + /** + * Interface that PreferenceFragment's containing activity should + * implement to be able to process preference items that wish to + * switch to a new fragment. + */ + public interface OnPreferenceStartFragmentCallback { + /** + * Called when the user has clicked on a Preference that has + * a fragment class name associated with it. The implementation + * to should instantiate and switch to an instance of the given + * fragment. + */ + boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mPreferenceManager = new PreferenceManager(getActivity(), FIRST_REQUEST_CODE); + mPreferenceManager.setOnPreferenceTreeClickListener(this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(com.android.internal.R.layout.preference_list_content, + container, false); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); + + if (mHavePrefs) { + bindPreferences(); + } + + mInitDone = true; + + if (savedInstanceState != null) { + Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG); + if (container != null) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.restoreHierarchyState(container); + } + } + } + } + + @Override + public void onStop() { + super.onStop(); + mPreferenceManager.dispatchActivityStop(); + } + + @Override + public void onDestroyView() { + mList = null; + mHandler.removeCallbacks(mRequestFocus); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mPreferenceManager.dispatchActivityDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + Bundle container = new Bundle(); + preferenceScreen.saveHierarchyState(container); + outState.putBundle(PREFERENCES_TAG, container); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); + } + + /** + * Returns the {@link PreferenceManager} used by this fragment. + * @return The {@link PreferenceManager}. + */ + public PreferenceManager getPreferenceManager() { + return mPreferenceManager; + } + + /** + * Sets the root of the preference hierarchy that this fragment is showing. + * + * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. + */ + public void setPreferenceScreen(PreferenceScreen preferenceScreen) { + if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { + mHavePrefs = true; + if (mInitDone) { + postBindPreferences(); + } + } + } + + /** + * Gets the root of the preference hierarchy that this fragment is showing. + * + * @return The {@link PreferenceScreen} that is the root of the preference + * hierarchy. + */ + public PreferenceScreen getPreferenceScreen() { + return mPreferenceManager.getPreferenceScreen(); + } + + /** + * Adds preferences from activities that match the given {@link Intent}. + * + * @param intent The {@link Intent} to query activities. + */ + public void addPreferencesFromIntent(Intent intent) { + requirePreferenceManager(); + + setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); + } + + /** + * Inflates the given XML resource and adds the preference hierarchy to the current + * preference hierarchy. + * + * @param preferencesResId The XML resource ID to inflate. + */ + public void addPreferencesFromResource(int preferencesResId) { + requirePreferenceManager(); + + setPreferenceScreen(mPreferenceManager.inflateFromResource(getActivity(), + preferencesResId, getPreferenceScreen())); + } + + /** + * {@inheritDoc} + */ + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + if (preference.getFragment() != null && + getActivity() instanceof OnPreferenceStartFragmentCallback) { + return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment( + this, preference); + } + return false; + } + + /** + * Finds a {@link Preference} based on its key. + * + * @param key The key of the preference to retrieve. + * @return The {@link Preference} with the key, or null. + * @see PreferenceGroup#findPreference(CharSequence) + */ + public Preference findPreference(CharSequence key) { + if (mPreferenceManager == null) { + return null; + } + return mPreferenceManager.findPreference(key); + } + + private void requirePreferenceManager() { + if (mPreferenceManager == null) { + throw new RuntimeException("This should be called after super.onCreate."); + } + } + + private void postBindPreferences() { + if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; + mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); + } + + private void bindPreferences() { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.bind(getListView()); + } + } + + /** @hide */ + public ListView getListView() { + ensureList(); + return mList; + } + + private void ensureList() { + if (mList != null) { + return; + } + View root = getView(); + if (root == null) { + throw new IllegalStateException("Content view not yet created"); + } + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mList = (ListView)rawListView; + if (mList == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + mHandler.post(mRequestFocus); + } +} diff --git a/core/java/android/preference/PreferenceInflater.java b/core/java/android/preference/PreferenceInflater.java index 779e746dabd8ea67b59044b017c0fb91df7c0ad4..c21aa185a7a2e998bbc72fa86fadf1a65eb06025 100644 --- a/core/java/android/preference/PreferenceInflater.java +++ b/core/java/android/preference/PreferenceInflater.java @@ -16,6 +16,8 @@ package android.preference; +import com.android.internal.util.XmlUtils; + import java.io.IOException; import java.util.Map; @@ -39,6 +41,7 @@ import android.util.Log; class PreferenceInflater extends GenericInflater { private static final String TAG = "PreferenceInflater"; private static final String INTENT_TAG_NAME = "intent"; + private static final String EXTRA_TAG_NAME = "extra"; private PreferenceManager mPreferenceManager; @@ -73,14 +76,28 @@ class PreferenceInflater extends GenericInflater { try { intent = Intent.parseIntent(getContext().getResources(), parser, attrs); } catch (IOException e) { - Log.w(TAG, "Could not parse Intent."); - Log.w(TAG, e); + XmlPullParserException ex = new XmlPullParserException( + "Error parsing preference"); + ex.initCause(e); + throw ex; } if (intent != null) { parentPreference.setIntent(intent); } + return true; + } else if (tag.equals(EXTRA_TAG_NAME)) { + getContext().getResources().parseBundleExtra(EXTRA_TAG_NAME, attrs, + parentPreference.getExtras()); + try { + XmlUtils.skipCurrentTag(parser); + } catch (IOException e) { + XmlPullParserException ex = new XmlPullParserException( + "Error parsing preference"); + ex.initCause(e); + throw ex; + } return true; } diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java index 95e54324f4458debc0c51bd3e35f1b8ac44985f2..c7f8ab2e5e262b86248e557c80366861d71b564c 100644 --- a/core/java/android/preference/PreferenceScreen.java +++ b/core/java/android/preference/PreferenceScreen.java @@ -91,7 +91,8 @@ public final class PreferenceScreen extends PreferenceGroup implements AdapterVi /** * Returns an adapter that can be attached to a {@link PreferenceActivity} - * to show the preferences contained in this {@link PreferenceScreen}. + * or {@link PreferenceFragment} to show the preferences contained in this + * {@link PreferenceScreen}. *

    * This {@link PreferenceScreen} will NOT appear in the returned adapter, instead * it appears in the hierarchy above this {@link PreferenceScreen}. @@ -136,7 +137,7 @@ public final class PreferenceScreen extends PreferenceGroup implements AdapterVi @Override protected void onClick() { - if (getIntent() != null || getPreferenceCount() == 0) { + if (getIntent() != null || getFragment() != null || getPreferenceCount() == 0) { return; } diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java index 2fba1d7917cd8f6faae60263261883dc65a2b06c..14d648558de0b782d9ca930f40e338875f0b2afc 100644 --- a/core/java/android/provider/Browser.java +++ b/core/java/android/provider/Browser.java @@ -17,22 +17,30 @@ package android.provider; 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.database.DatabaseUtils; +import android.graphics.BitmapFactory; import android.net.Uri; -import android.os.AsyncTask; +import android.provider.BrowserContract.Bookmarks; +import android.provider.BrowserContract.History; +import android.provider.BrowserContract.Searches; import android.util.Log; import android.webkit.WebIconDatabase; -import java.util.Date; - public class Browser { private static final String LOGTAG = "browser"; - public static final Uri BOOKMARKS_URI = - Uri.parse("content://browser/bookmarks"); + + /** + * A table containing both bookmarks and history items. The columns of the table are defined in + * {@link BookmarkColumns}. Reading this table requires the + * {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} permission and writing to it + * requires the {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} permission. + */ + public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks"); /** * The name of extra data when starting Browser with ACTION_VIEW or @@ -49,12 +57,11 @@ public class Browser { * application. *

    * The value is a unique identification string that will be used to - * indentify the calling application. The Browser will attempt to reuse the + * identify the calling application. The Browser will attempt to reuse the * same window each time the application launches the Browser with the same * identifier. */ - public static final String EXTRA_APPLICATION_ID = - "com.android.browser.application_id"; + public static final String EXTRA_APPLICATION_ID = "com.android.browser.application_id"; /** * The name of the extra data in the VIEW intent. The data are key/value @@ -68,10 +75,17 @@ public class Browser { /* if you change column order you must also change indices below */ public static final String[] HISTORY_PROJECTION = new String[] { - BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS, - BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE, - BookmarkColumns.FAVICON, BookmarkColumns.THUMBNAIL, - BookmarkColumns.TOUCH_ICON, BookmarkColumns.USER_ENTERED }; + BookmarkColumns._ID, // 0 + BookmarkColumns.URL, // 1 + BookmarkColumns.VISITS, // 2 + BookmarkColumns.DATE, // 3 + BookmarkColumns.BOOKMARK, // 4 + BookmarkColumns.TITLE, // 5 + BookmarkColumns.FAVICON, // 6 + BookmarkColumns.THUMBNAIL, // 7 + BookmarkColumns.TOUCH_ICON, // 8 + BookmarkColumns.USER_ENTERED, // 9 + }; /* these indices dependent on HISTORY_PROJECTION */ public static final int HISTORY_PROJECTION_ID_INDEX = 0; @@ -92,19 +106,33 @@ public class Browser { /* columns needed to determine whether to truncate history */ public static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] { - BookmarkColumns._ID, BookmarkColumns.DATE, }; + BookmarkColumns._ID, + BookmarkColumns.DATE, + }; + public static final int TRUNCATE_HISTORY_PROJECTION_ID_INDEX = 0; /* truncate this many history items at a time */ public static final int TRUNCATE_N_OLDEST = 5; - public static final Uri SEARCHES_URI = - Uri.parse("content://browser/searches"); + /** + * A table containing a log of browser searches. The columns of the table are defined in + * {@link SearchColumns}. Reading this table requires the + * {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} permission and writing to it + * requires the {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} permission. + */ + public static final Uri SEARCHES_URI = Uri.parse("content://browser/searches"); - /* if you change column order you must also change indices - below */ + /** + * A projection of {@link #SEARCHES_URI} that contains {@link SearchColumns#_ID}, + * {@link SearchColumns#SEARCH}, and {@link SearchColumns#DATE}. + */ public static final String[] SEARCHES_PROJECTION = new String[] { - SearchColumns._ID, SearchColumns.SEARCH, SearchColumns.DATE }; + // if you change column order you must also change indices below + SearchColumns._ID, // 0 + SearchColumns.SEARCH, // 1 + SearchColumns.DATE, // 2 + }; /* these indices dependent on SEARCHES_PROJECTION */ public static final int SEARCHES_PROJECTION_SEARCH_INDEX = 1; @@ -121,9 +149,10 @@ public class Browser { private static final int MAX_HISTORY_COUNT = 250; /** - * Open the AddBookmark activity to save a bookmark. Launch with - * and/or url, which can be edited by the user before saving. - * @param c Context used to launch the AddBookmark activity. + * Open an activity to save a bookmark. Launch with a title + * and/or a url, both of which can be edited by the user before saving. + * + * @param c Context used to launch the activity to add a bookmark. * @param title Title for the bookmark. Can be null or empty string. * @param url Url for the bookmark. Can be null or empty string. */ @@ -152,8 +181,15 @@ public class Browser { */ public final static String EXTRA_SHARE_FAVICON = "share_favicon"; - public static final void sendString(Context c, String s) { - sendString(c, s, c.getString(com.android.internal.R.string.sendText)); + /** + * Sends the given string using an Intent with {@link Intent#ACTION_SEND} and a mime type + * of text/plain. The string is put into {@link Intent#EXTRA_TEXT}. + * + * @param context the context used to start the activity + * @param string the string to send + */ + public static final void sendString(Context context, String string) { + sendString(context, string, context.getString(com.android.internal.R.string.sendText)); } /** @@ -174,48 +210,49 @@ public class Browser { send.putExtra(Intent.EXTRA_TEXT, stringToSend); try { - c.startActivity(Intent.createChooser(send, chooserDialogTitle)); + Intent i = Intent.createChooser(send, chooserDialogTitle); + // In case this is called from outside an Activity + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + c.startActivity(i); } catch(android.content.ActivityNotFoundException ex) { // if no app handles it, do nothing } } /** - * Return a cursor pointing to a list of all the bookmarks. + * Return a cursor pointing to a list of all the bookmarks. The cursor will have a single + * column, {@link BookmarkColumns#URL}. + *

    * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * * @param cr The ContentResolver used to access the database. */ public static final Cursor getAllBookmarks(ContentResolver cr) throws IllegalStateException { - return cr.query(BOOKMARKS_URI, - new String[] { BookmarkColumns.URL }, - "bookmark = 1", null, null); + return cr.query(Bookmarks.CONTENT_URI, + new String[] { Bookmarks.URL }, + Bookmarks.IS_FOLDER + " = 0", null, null); } /** - * Return a cursor pointing to a list of all visited site urls. + * Return a cursor pointing to a list of all visited site urls. The cursor will + * have a single column, {@link BookmarkColumns#URL}. + *

    * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * * @param cr The ContentResolver used to access the database. */ public static final Cursor getAllVisitedUrls(ContentResolver cr) throws IllegalStateException { - return cr.query(BOOKMARKS_URI, - new String[] { BookmarkColumns.URL }, null, null, null); + return cr.query(History.CONTENT_URI, + new String[] { History.URL }, null, null, null); } private static final void addOrUrlEquals(StringBuilder sb) { sb.append(" OR " + BookmarkColumns.URL + " = "); } - /** - * Return a Cursor with all history/bookmarks that are similar to url, - * where similar means 'http(s)://' and 'www.' are optional, but the rest - * of the url is the same. - * @param cr The ContentResolver used to access the database. - * @param url The url to compare to. - * @hide - */ - public static final Cursor getVisitedLike(ContentResolver cr, String url) { + private static final Cursor getVisitedLike(ContentResolver cr, String url) { boolean secure = false; String compareString = url; if (compareString.startsWith("http://")) { @@ -229,14 +266,14 @@ public class Browser { } StringBuilder whereClause = null; if (secure) { - whereClause = new StringBuilder(BookmarkColumns.URL + " = "); + whereClause = new StringBuilder(Bookmarks.URL + " = "); DatabaseUtils.appendEscapedSQLString(whereClause, "https://" + compareString); addOrUrlEquals(whereClause); DatabaseUtils.appendEscapedSQLString(whereClause, "https://www." + compareString); } else { - whereClause = new StringBuilder(BookmarkColumns.URL + " = "); + whereClause = new StringBuilder(Bookmarks.URL + " = "); DatabaseUtils.appendEscapedSQLString(whereClause, compareString); addOrUrlEquals(whereClause); @@ -250,7 +287,7 @@ public class Browser { DatabaseUtils.appendEscapedSQLString(whereClause, "http://" + wwwString); } - return cr.query(BOOKMARKS_URI, HISTORY_PROJECTION, + return cr.query(History.CONTENT_URI, new String[] { History._ID, History.VISITS }, whereClause.toString(), null, null); } @@ -266,26 +303,24 @@ public class Browser { */ public static final void updateVisitedHistory(ContentResolver cr, String url, boolean real) { - long now = new Date().getTime(); + long now = System.currentTimeMillis(); Cursor c = null; try { c = getVisitedLike(cr, url); /* We should only get one answer that is exactly the same. */ if (c.moveToFirst()) { - ContentValues map = new ContentValues(); + ContentValues values = new ContentValues(); if (real) { - map.put(BookmarkColumns.VISITS, c - .getInt(HISTORY_PROJECTION_VISITS_INDEX) + 1); + values.put(History.VISITS, c.getInt(1) + 1); } else { - map.put(BookmarkColumns.USER_ENTERED, 1); + values.put(History.USER_ENTERED, 1); } - map.put(BookmarkColumns.DATE, now); - String[] projection = new String[] - { Integer.valueOf(c.getInt(0)).toString() }; - cr.update(BOOKMARKS_URI, map, "_id = ?", projection); + values.put(History.DATE_LAST_VISITED, now); + cr.update(ContentUris.withAppendedId(History.CONTENT_URI, c.getLong(0)), + values, null, null); } else { truncateHistory(cr); - ContentValues map = new ContentValues(); + ContentValues values = new ContentValues(); int visits; int user_entered; if (real) { @@ -295,14 +330,13 @@ public class Browser { visits = 0; user_entered = 1; } - map.put(BookmarkColumns.URL, url); - map.put(BookmarkColumns.VISITS, visits); - map.put(BookmarkColumns.DATE, now); - map.put(BookmarkColumns.BOOKMARK, 0); - map.put(BookmarkColumns.TITLE, url); - map.put(BookmarkColumns.CREATED, 0); - map.put(BookmarkColumns.USER_ENTERED, user_entered); - cr.insert(BOOKMARKS_URI, map); + values.put(History.URL, url); + values.put(History.VISITS, visits); + values.put(History.DATE_LAST_VISITED, now); + values.put(History.TITLE, url); + values.put(History.DATE_CREATED, 0); + values.put(History.USER_ENTERED, user_entered); + cr.insert(History.CONTENT_URI, values); } } catch (IllegalStateException e) { Log.e(LOGTAG, "updateVisitedHistory", e); @@ -322,10 +356,10 @@ public class Browser { String[] str = null; try { String[] projection = new String[] { - "url" + History.URL, }; - c = cr.query(BOOKMARKS_URI, projection, "visits > 0", null, - null); + c = cr.query(History.CONTENT_URI, projection, History.VISITS + " > 0", null, null); + if (c == null) return new String[0]; str = new String[c.getCount()]; int i = 0; while (c.moveToNext()) { @@ -353,31 +387,29 @@ public class Browser { * @param cr The ContentResolver used to access the database. */ public static final void truncateHistory(ContentResolver cr) { - Cursor c = null; + // TODO make a single request to the provider to do this in a single transaction + Cursor cursor = null; try { + // Select non-bookmark history, ordered by date - c = cr.query( - BOOKMARKS_URI, - TRUNCATE_HISTORY_PROJECTION, - "bookmark = 0", - null, - BookmarkColumns.DATE); - // Log.v(LOGTAG, "history count " + c.count()); - if (c.moveToFirst() && c.getCount() >= MAX_HISTORY_COUNT) { + cursor = cr.query(History.CONTENT_URI, + new String[] { History._ID, History.URL, History.DATE_LAST_VISITED }, + null, null, History.DATE_LAST_VISITED + " ASC"); + + if (cursor.moveToFirst() && cursor.getCount() >= MAX_HISTORY_COUNT) { + final WebIconDatabase iconDb = WebIconDatabase.getInstance(); /* eliminate oldest history items */ for (int i = 0; i < TRUNCATE_N_OLDEST; i++) { - // Log.v(LOGTAG, "truncate history " + - // c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX)); - cr.delete(BOOKMARKS_URI, "_id = " + - c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX), - null); - if (!c.moveToNext()) break; + cr.delete(ContentUris.withAppendedId(History.CONTENT_URI, cursor.getLong(0)), + null, null); + iconDb.releaseIconForPageUrl(cursor.getString(1)); + if (!cursor.moveToNext()) break; } } } catch (IllegalStateException e) { Log.e(LOGTAG, "truncateHistory", e); } finally { - if (c != null) c.close(); + if (cursor != null) cursor.close(); } } @@ -388,23 +420,17 @@ public class Browser { * @return boolean True if the history can be cleared. */ public static final boolean canClearHistory(ContentResolver cr) { - Cursor c = null; + Cursor cursor = null; boolean ret = false; try { - c = cr.query( - BOOKMARKS_URI, - new String [] { BookmarkColumns._ID, - BookmarkColumns.BOOKMARK, - BookmarkColumns.VISITS }, - "bookmark = 0 OR visits > 0", - null, - null - ); - ret = c.moveToFirst(); + cursor = cr.query(History.CONTENT_URI, + new String [] { History._ID, History.VISITS }, + null, null, null); + ret = cursor.getCount() > 0; } catch (IllegalStateException e) { Log.e(LOGTAG, "canClearHistory", e); } finally { - if (c != null) c.close(); + if (cursor != null) cursor.close(); } return ret; } @@ -420,67 +446,36 @@ public class Browser { } /** - * Helper function to delete all history items and revert all - * bookmarks to zero visits which meet the criteria provided. - * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} - * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} + * Helper function to delete all history items and release the icons for them in the + * {@link WebIconDatabase}. + * + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} + * * @param cr The ContentResolver used to access the database. * @param whereClause String to limit the items affected. * null means all items. */ - private static final void deleteHistoryWhere(ContentResolver cr, - String whereClause) { - Cursor c = null; + private static final void deleteHistoryWhere(ContentResolver cr, String whereClause) { + Cursor cursor = null; try { - c = cr.query(BOOKMARKS_URI, - HISTORY_PROJECTION, - whereClause, - null, - null); - if (c.moveToFirst()) { + cursor = cr.query(History.CONTENT_URI, new String[] { History.URL }, whereClause, + null, null); + if (cursor.moveToFirst()) { final WebIconDatabase iconDb = WebIconDatabase.getInstance(); - /* Delete favicons, and revert bookmarks which have been visited - * to simply bookmarks. - */ - StringBuffer sb = new StringBuffer(); - boolean firstTime = true; do { - String url = c.getString(HISTORY_PROJECTION_URL_INDEX); - boolean isBookmark = - c.getInt(HISTORY_PROJECTION_BOOKMARK_INDEX) == 1; - if (isBookmark) { - if (firstTime) { - firstTime = false; - } else { - sb.append(" OR "); - } - sb.append("( _id = "); - sb.append(c.getInt(0)); - sb.append(" )"); - } else { - iconDb.releaseIconForPageUrl(url); - } - } while (c.moveToNext()); - - if (!firstTime) { - ContentValues map = new ContentValues(); - map.put(BookmarkColumns.VISITS, 0); - map.put(BookmarkColumns.DATE, 0); - /* FIXME: Should I also remove the title? */ - cr.update(BOOKMARKS_URI, map, sb.toString(), null); - } + // Delete favicons + // TODO don't release if the URL is bookmarked + iconDb.releaseIconForPageUrl(cursor.getString(0)); + } while (cursor.moveToNext()); - String deleteWhereClause = BookmarkColumns.BOOKMARK + " = 0"; - if (whereClause != null) { - deleteWhereClause += " AND " + whereClause; - } - cr.delete(BOOKMARKS_URI, deleteWhereClause, null); + cr.delete(History.CONTENT_URI, whereClause, null); } } catch (IllegalStateException e) { Log.e(LOGTAG, "deleteHistoryWhere", e); return; } finally { - if (c != null) c.close(); + if (cursor != null) cursor.close(); } } @@ -520,10 +515,7 @@ public class Browser { */ public static final void deleteFromHistory(ContentResolver cr, String url) { - StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = "); - DatabaseUtils.appendEscapedSQLString(sb, url); - String matchesUrl = sb.toString(); - deleteHistoryWhere(cr, matchesUrl); + cr.delete(History.CONTENT_URI, History.URL + "=?", new String[] { url }); } /** @@ -534,30 +526,13 @@ public class Browser { * @param search The string to add to the searches database. */ public static final void addSearchUrl(ContentResolver cr, String search) { - long now = new Date().getTime(); - Cursor c = null; - try { - c = cr.query( - SEARCHES_URI, - SEARCHES_PROJECTION, - SEARCHES_WHERE_CLAUSE, - new String [] { search }, - null); - ContentValues map = new ContentValues(); - map.put(SearchColumns.SEARCH, search); - map.put(SearchColumns.DATE, now); - /* We should only get one answer that is exactly the same. */ - if (c.moveToFirst()) { - cr.update(SEARCHES_URI, map, "_id = " + c.getInt(0), null); - } else { - cr.insert(SEARCHES_URI, map); - } - } catch (IllegalStateException e) { - Log.e(LOGTAG, "addSearchUrl", e); - } finally { - if (c != null) c.close(); - } + // The content provider will take care of updating existing searches instead of duplicating + ContentValues values = new ContentValues(); + values.put(Searches.SEARCH, search); + values.put(Searches.DATE, System.currentTimeMillis()); + cr.insert(Searches.CONTENT_URI, values); } + /** * Remove all searches from the search database. * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} @@ -567,7 +542,7 @@ public class Browser { // FIXME: Should this clear the urls to which these searches lead? // (i.e. remove google.com/query= blah blah blah) try { - cr.delete(SEARCHES_URI, null, null); + cr.delete(Searches.CONTENT_URI, null, null); } catch (IllegalStateException e) { Log.e(LOGTAG, "clearSearches", e); } @@ -586,35 +561,92 @@ public class Browser { */ public static final void requestAllIcons(ContentResolver cr, String where, WebIconDatabase.IconListener listener) { - WebIconDatabase.getInstance() - .bulkRequestIconForPageUrl(cr, where, listener); + WebIconDatabase.getInstance().bulkRequestIconForPageUrl(cr, where, listener); } + /** + * Column definitions for the mixed bookmark and history items available + * at {@link #BOOKMARKS_URI}. + */ public static class BookmarkColumns implements BaseColumns { + /** + * The URL of the bookmark or history item. + *

    Type: TEXT (URL)

    + */ public static final String URL = "url"; + + /** + * The number of time the item has been visited. + *

    Type: NUMBER

    + */ public static final String VISITS = "visits"; + + /** + * The date the item was last visited, in milliseconds since the epoch. + *

    Type: NUMBER (date in milliseconds since January 1, 1970)

    + */ public static final String DATE = "date"; + + /** + * Flag indicating that an item is a bookmark. A value of 1 indicates a bookmark, a value + * of 0 indicates a history item. + *

    Type: INTEGER (boolean)

    + */ public static final String BOOKMARK = "bookmark"; + + /** + * The user visible title of the bookmark or history item. + *

    Type: TEXT

    + */ public static final String TITLE = "title"; + + /** + * The date the item created, in milliseconds since the epoch. + *

    Type: NUMBER (date in milliseconds since January 1, 1970)

    + */ public static final String CREATED = "created"; + + /** + * The favicon of the bookmark. Must decode via {@link BitmapFactory#decodeByteArray}. + *

    Type: BLOB (image)

    + */ public static final String FAVICON = "favicon"; + /** * @hide */ public static final String THUMBNAIL = "thumbnail"; + /** * @hide */ public static final String TOUCH_ICON = "touch_icon"; + /** * @hide */ public static final String USER_ENTERED = "user_entered"; } + /** + * Column definitions for the search history table, available at {@link #SEARCHES_URI}. + */ public static class SearchColumns implements BaseColumns { + /** + * @deprecated Not used. + */ + @Deprecated public static final String URL = "url"; + + /** + * The user entered search term. + */ public static final String SEARCH = "search"; + + /** + * The date the search was performed, in milliseconds since the epoch. + *

    Type: NUMBER (date in milliseconds since January 1, 1970)

    + */ public static final String DATE = "date"; } } diff --git a/core/java/android/provider/BrowserContract.java b/core/java/android/provider/BrowserContract.java new file mode 100644 index 0000000000000000000000000000000000000000..276bddc29e2367a03933aa48a96b41f769f0756c --- /dev/null +++ b/core/java/android/provider/BrowserContract.java @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.provider; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.SyncStateContract; +import android.util.Pair; + +/** + * @hide + */ +public class BrowserContract { + /** The authority for the browser provider */ + public static final String AUTHORITY = "com.android.browser"; + + /** A content:// style uri to the authority for the browser 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 parameter for use when querying any table that allows specifying a limit on the number + * of rows returned. + */ + public static final String PARAM_LIMIT = "limit"; + + /** + * 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 these columns. + */ + 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"; + /** Generic column for use by sync adapters. */ + public static final String SYNC5 = "sync5"; + } + + /** + * Convenience definitions for use in implementing chrome bookmarks sync in the Bookmarks table. + */ + public static final class ChromeSyncColumns { + private ChromeSyncColumns() {} + + /** The server unique ID for an item */ + public static final String SERVER_UNIQUE = BaseSyncColumns.SYNC3; + + public static final String FOLDER_NAME_ROOT = "google_chrome"; + public static final String FOLDER_NAME_BOOKMARKS = "google_chrome_bookmarks"; + public static final String FOLDER_NAME_BOOKMARKS_BAR = "bookmark_bar"; + public static final String FOLDER_NAME_OTHER_BOOKMARKS = "other_bookmarks"; + + /** The client unique ID for an item */ + public static final String CLIENT_UNIQUE = BaseSyncColumns.SYNC4; + } + + /** + * Columns that appear when each row of a table belongs to a specific + * account, including sync information that an account may need. + */ + 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"; + + /** + * The time that this row was last modified by a client (msecs since the epoch). + *

    Type: INTEGER

    + */ + public static final String DATE_MODIFIED = "modified"; + } + + interface CommonColumns { + /** + * The unique ID for a row. + *

    Type: INTEGER (long)

    + */ + public static final String _ID = "_id"; + + /** + * The URL of the bookmark. + *

    Type: TEXT (URL)

    + */ + public static final String URL = "url"; + + /** + * The user visible title of the bookmark. + *

    Type: TEXT

    + */ + public static final String TITLE = "title"; + + /** + * The time that this row was created on its originating client (msecs + * since the epoch). + *

    Type: INTEGER

    + */ + public static final String DATE_CREATED = "created"; + } + + interface ImageColumns { + /** + * The favicon of the bookmark, may be NULL. + * Must decode via {@link BitmapFactory#decodeByteArray}. + *

    Type: BLOB (image)

    + */ + public static final String FAVICON = "favicon"; + + /** + * A thumbnail of the page,may be NULL. + * Must decode via {@link BitmapFactory#decodeByteArray}. + *

    Type: BLOB (image)

    + */ + public static final String THUMBNAIL = "thumbnail"; + + /** + * The touch icon for the web page, may be NULL. + * Must decode via {@link BitmapFactory#decodeByteArray}. + *

    Type: BLOB (image)

    + * @hide + */ + public static final String TOUCH_ICON = "touch_icon"; + } + + interface HistoryColumns { + /** + * The date the item was last visited, in milliseconds since the epoch. + *

    Type: INTEGER (date in milliseconds since January 1, 1970)

    + */ + public static final String DATE_LAST_VISITED = "date"; + + /** + * The number of times the item has been visited. + *

    Type: INTEGER

    + */ + public static final String VISITS = "visits"; + + public static final String USER_ENTERED = "user_entered"; + } + + /** + * The bookmarks table, which holds the user's browser bookmarks. + */ + public static final class Bookmarks implements CommonColumns, ImageColumns, SyncColumns { + /** + * This utility class cannot be instantiated. + */ + private Bookmarks() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks"); + + /** + * The content:// style URI for the default folder + */ + public static final Uri CONTENT_URI_DEFAULT_FOLDER = + Uri.withAppendedPath(CONTENT_URI, "folder"); + + /** + * Query parameter used to specify an account name + */ + public static final String PARAM_ACCOUNT_NAME = "acct_name"; + + /** + * Query parameter used to specify an account type + */ + public static final String PARAM_ACCOUNT_TYPE = "acct_type"; + + /** + * Builds a URI that points to a specific folder. + * @param folderId the ID of the folder to point to + */ + public static final Uri buildFolderUri(long folderId) { + return ContentUris.withAppendedId(CONTENT_URI_DEFAULT_FOLDER, folderId); + } + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of bookmarks. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark"; + + /** + * The MIME type of a {@link #CONTENT_URI} of a single bookmark. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark"; + + /** + * Query parameter to use if you want to see deleted bookmarks that are still + * around on the device and haven't been synced yet. + * @see #IS_DELETED + */ + public static final String QUERY_PARAMETER_SHOW_DELETED = "show_deleted"; + + /** + * Flag indicating if an item is a folder or bookmark. Non-zero values indicate + * a folder and zero indicates a bookmark. + *

    Type: INTEGER (boolean)

    + */ + public static final String IS_FOLDER = "folder"; + + /** + * The ID of the parent folder. ID 0 is the root folder. + *

    Type: INTEGER (reference to item in the same table)

    + */ + public static final String PARENT = "parent"; + + /** + * The source ID for an item's parent. Read-only. + * @see #PARENT + */ + public static final String PARENT_SOURCE_ID = "parent_source"; + + /** + * The position of the bookmark in relation to it's siblings that share the same + * {@link #PARENT}. May be negative. + *

    Type: INTEGER

    + */ + public static final String POSITION = "position"; + + /** + * The item that the bookmark should be inserted after. + * May be negative. + *

    Type: INTEGER

    + */ + public static final String INSERT_AFTER = "insert_after"; + + /** + * The source ID for the item that the bookmark should be inserted after. Read-only. + * May be negative. + *

    Type: INTEGER

    + * @see #INSERT_AFTER + */ + public static final String INSERT_AFTER_SOURCE_ID = "insert_after_source"; + + /** + * A flag to indicate if an item has been deleted. Queries will not return deleted + * entries unless you add the {@link #QUERY_PARAMETER_SHOW_DELETED} query paramter + * to the URI when performing your query. + *

    Type: INTEGER (non-zero if the item has been deleted, zero if it hasn't) + * @see #QUERY_PARAMETER_SHOW_DELETED + */ + public static final String IS_DELETED = "deleted"; + } + + /** + * Read-only table that lists all the accounts that are used to provide bookmarks. + */ + public static final class Accounts { + /** + * Directory under {@link Bookmarks#CONTENT_URI} + */ + public static final Uri CONTENT_URI = + AUTHORITY_URI.buildUpon().appendPath("accounts").build(); + + /** + * 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"; + } + + /** + * The history table, which holds the browsing history. + */ + public static final class History implements CommonColumns, HistoryColumns, ImageColumns { + /** + * This utility class cannot be instantiated. + */ + private History() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of browser history items. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history"; + + /** + * The MIME type of a {@link #CONTENT_URI} of a single browser history item. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history"; + } + + /** + * The search history table. + * @hide + */ + public static final class Searches { + private Searches() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "searches"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of browser search items. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searches"; + + /** + * The MIME type of a {@link #CONTENT_URI} of a single browser search item. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/searches"; + + /** + * The unique ID for a row. + *

    Type: INTEGER (long)

    + */ + public static final String _ID = "_id"; + + /** + * The user entered search term. + */ + public static final String SEARCH = "search"; + + /** + * The date the search was performed, in milliseconds since the epoch. + *

    Type: NUMBER (date in milliseconds since January 1, 1970)

    + */ + public static final String DATE = "date"; + } + + /** + * 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); + } + } + + /** + * Stores images for URLs. Only support query() and update(). + * @hide + */ + public static final class Images implements ImageColumns { + /** + * This utility class cannot be instantiated + */ + private Images() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "images"); + + /** + * The URL the images came from. + *

    Type: TEXT (URL)

    + */ + public static final String URL = "url_key"; + } + + /** + * A combined view of bookmarks and history. All bookmarks in all folders are included and + * no folders are included. + */ + public static final class Combined implements CommonColumns, HistoryColumns, ImageColumns { + /** + * This utility class cannot be instantiated + */ + private Combined() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined"); + + /** + * Flag indicating that an item is a bookmark. A value of 1 indicates a bookmark, a value + * of 0 indicates a history item. + *

    Type: INTEGER (boolean)

    + */ + public static final String IS_BOOKMARK = "bookmark"; + } +} diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index 6355a9c5f9c5f1a872997c24b92fffc83ea4ae2a..88ce0f07958340c33e931401da228e62c5e43304 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -76,64 +76,30 @@ public final class Calendar { */ public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + /** - * Columns from the Calendars table that other tables join into themselves. + * 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. */ - public interface CalendarsColumns - { - /** - * The color of the calendar - *

    Type: INTEGER (color value)

    - */ - public static final String COLOR = "color"; - - /** - * The level of access that the user has for the calendar - *

    Type: INTEGER (one of the values below)

    - */ - public static final String ACCESS_LEVEL = "access_level"; - - /** Cannot access the calendar */ - public static final int NO_ACCESS = 0; - /** Can only see free/busy information about the calendar */ - public static final int FREEBUSY_ACCESS = 100; - /** Can read all event details */ - public static final int READ_ACCESS = 200; - public static final int RESPOND_ACCESS = 300; - public static final int OVERRIDE_ACCESS = 400; - /** Full access to modify the calendar, but not the access control settings */ - public static final int CONTRIBUTOR_ACCESS = 500; - 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; - - /** - * Is the calendar selected to be displayed? - *

    Type: INTEGER (boolean)

    - */ - public static final String SELECTED = "selected"; - - /** - * The timezone the calendar's events occurs in - *

    Type: TEXT

    - */ - public static final String TIMEZONE = "timezone"; - - /** - * If this calendar is in the list of calendars that are selected for - * syncing then "sync_events" is 1, otherwise 0. - *

    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"; + 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"; + /** Generic column for use by sync adapters. */ + public static final String SYNC5 = "sync5"; + } + /** + * Columns for Sync information used by Calendars and Events tables. + */ + public interface SyncColumns extends BaseSyncColumns { /** * The account that was used to sync the entry to the device. *

    Type: TEXT

    @@ -185,6 +151,12 @@ public final class Calendar { */ public static final String _SYNC_DIRTY = "_sync_dirty"; + } + + /** + * Columns from the Account information used by Calendars and Events tables. + */ + public interface AccountColumns { /** * The name of the account instance to which this row belongs, which when paired with * {@link #ACCOUNT_TYPE} identifies a specific account. @@ -200,10 +172,161 @@ public final class Calendar { public static final String ACCOUNT_TYPE = "account_type"; } + /** + * Columns from the Calendars table that other tables join into themselves. + */ + public interface CalendarsColumns { + /** + * The color of the calendar + *

    Type: INTEGER (color value)

    + */ + public static final String COLOR = "color"; + + /** + * The level of access that the user has for the calendar + *

    Type: INTEGER (one of the values below)

    + */ + public static final String ACCESS_LEVEL = "access_level"; + + /** Cannot access the calendar */ + public static final int NO_ACCESS = 0; + /** Can only see free/busy information about the calendar */ + public static final int FREEBUSY_ACCESS = 100; + /** Can read all event details */ + public static final int READ_ACCESS = 200; + public static final int RESPOND_ACCESS = 300; + public static final int OVERRIDE_ACCESS = 400; + /** Full access to modify the calendar, but not the access control settings */ + public static final int CONTRIBUTOR_ACCESS = 500; + 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; + + /** + * Is the calendar selected to be displayed? + *

    Type: INTEGER (boolean)

    + */ + public static final String SELECTED = "selected"; + + /** + * The timezone the calendar's events occurs in + *

    Type: TEXT

    + */ + public static final String TIMEZONE = "timezone"; + + /** + * If this calendar is in the list of calendars that are selected for + * syncing then "sync_events" is 1, otherwise 0. + *

    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"; + + /** + * Whether the row has been deleted. A deleted row should be ignored. + *

    Type: INTEGER (boolean)

    + */ + public static final String DELETED = "deleted"; + } + + /** + * Class that represents a Calendar Entity. There is one entry per calendar. + */ + public static class CalendarsEntity implements BaseColumns, SyncColumns, CalendarsColumns { + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + + "/calendar_entities"); + + public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) { + return new EntityIteratorImpl(cursor, resolver); + } + + public static EntityIterator newEntityIterator(Cursor cursor, + ContentProviderClient provider) { + return new EntityIteratorImpl(cursor, provider); + } + + private static class EntityIteratorImpl extends CursorEntityIterator { + private final ContentResolver mResolver; + private final ContentProviderClient mProvider; + + public EntityIteratorImpl(Cursor cursor, ContentResolver resolver) { + super(cursor); + mResolver = resolver; + mProvider = null; + } + + public EntityIteratorImpl(Cursor cursor, ContentProviderClient provider) { + super(cursor); + mResolver = null; + mProvider = provider; + } + + @Override + public Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException { + // we expect the cursor is already at the row we need to read from + final long calendarId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID)); + + // Create the content value + ContentValues cv = new ContentValues(); + cv.put(_ID, calendarId); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT_TYPE); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_TIME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_MARK); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC2); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC3); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC4); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC5); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.NAME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.DISPLAY_NAME); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.COLOR); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ACCESS_LEVEL); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELECTED); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SYNC_EVENTS); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.LOCATION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TIMEZONE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.OWNER_ACCOUNT); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, + Calendars.ORGANIZER_CAN_RESPOND); + + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); + + // Create the Entity from the ContentValue + Entity entity = new Entity(cv); + + // Set cursor to next row + cursor.moveToNext(); + + // Return the created Entity + return entity; + } + } + } + /** * Contains a list of available calendars. */ - public static class Calendars implements BaseColumns, CalendarsColumns + public static class Calendars implements BaseColumns, SyncColumns, AccountColumns, + CalendarsColumns { private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?" + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?"; @@ -257,6 +380,24 @@ public final class Calendar { */ public static final String URL = "url"; + /** + * The URL for the calendar itself + *

    Type: TEXT (URL)

    + */ + public static final String SELF_URL = "selfUrl"; + + /** + * The URL for the calendar to be edited + *

    Type: TEXT (URL)

    + */ + public static final String EDIT_URL = "editUrl"; + + /** + * The URL for the calendar events + *

    Type: TEXT (URL)

    + */ + public static final String EVENTS_URL = "eventsUrl"; + /** * The name of the calendar *

    Type: TEXT

    @@ -275,12 +416,6 @@ public final class Calendar { */ public static final String LOCATION = "location"; - /** - * Should the calendar be hidden in the calendar selection panel? - *

    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. @@ -296,6 +431,9 @@ public final class Calendar { public static final String ORGANIZER_CAN_RESPOND = "organizerCanRespond"; } + /** + * Columns from the Attendees table that other tables join into themselves. + */ public interface AttendeesColumns { /** @@ -361,8 +499,7 @@ public final class Calendar { /** * Columns from the Events table that other tables join into themselves. */ - public interface EventsColumns - { + public interface EventsColumns { /** * The calendar the event belongs to *

    Type: INTEGER (foreign key to the Calendars table)

    @@ -437,6 +574,18 @@ public final class Calendar { */ public static final String DTEND = "dtend"; + /** + * The time the event starts with allDay events in a local tz + *

    Type: INTEGER (long; millis since epoch)

    + */ + public static final String DTSTART2 = "dtstart2"; + + /** + * The time the event ends with allDay events in a local tz + *

    Type: INTEGER (long; millis since epoch)

    + */ + public static final String DTEND2 = "dtend2"; + /** * The duration of the event *

    Type: TEXT (duration in RFC2445 format)

    @@ -449,6 +598,12 @@ public final class Calendar { */ public static final String EVENT_TIMEZONE = "eventTimezone"; + /** + * The timezone for the event, allDay events will have a local tz instead of UTC + *

    Type: TEXT + */ + public static final String EVENT_TIMEZONE2 = "eventTimezone2"; + /** * Whether the event lasts all day or not *

    Type: INTEGER (boolean)

    @@ -598,7 +753,8 @@ public final class Calendar { /** * Contains one entry per calendar event. Recurring events show up as a single entry. */ - public static final class EventsEntity implements BaseColumns, EventsColumns, CalendarsColumns { + public static final class EventsEntity implements BaseColumns, SyncColumns, AccountColumns, + EventsColumns { /** * The content:// style URL for this table */ @@ -703,8 +859,8 @@ public final class Calendar { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.URL); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, EventsColumns.DELETED); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1); Entity entity = new Entity(cv); Cursor subCursor; @@ -795,7 +951,8 @@ public final class Calendar { /** * Contains one entry per calendar event. Recurring events show up as a single entry. */ - public static final class Events implements BaseColumns, EventsColumns, CalendarsColumns { + public static final class Events implements BaseColumns, SyncColumns, AccountColumns, + EventsColumns { private static final String[] FETCH_ENTRY_COLUMNS = new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID }; @@ -859,6 +1016,15 @@ public final class Calendar { null, DEFAULT_SORT_ORDER); } + public static final Cursor query(ContentResolver cr, String[] projection, + long begin, long end, String searchQuery) { + Uri.Builder builder = CONTENT_SEARCH_URI.buildUpon(); + ContentUris.appendId(builder, begin); + ContentUris.appendId(builder, end); + return cr.query(builder.build(), projection, WHERE_CALENDARS_SELECTED, + new String[] { searchQuery }, DEFAULT_SORT_ORDER); + } + public static final Cursor query(ContentResolver cr, String[] projection, long begin, long end, String where, String orderBy) { Uri.Builder builder = CONTENT_URI.buildUpon(); @@ -873,6 +1039,21 @@ public final class Calendar { null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } + public static final Cursor query(ContentResolver cr, String[] projection, long begin, + long end, String searchQuery, String where, String orderBy) { + Uri.Builder builder = CONTENT_SEARCH_URI.buildUpon(); + ContentUris.appendId(builder, begin); + ContentUris.appendId(builder, end); + builder = builder.appendPath(searchQuery); + if (TextUtils.isEmpty(where)) { + where = WHERE_CALENDARS_SELECTED; + } else { + where = "(" + where + ") AND " + WHERE_CALENDARS_SELECTED; + } + return cr.query(builder.build(), projection, where, null, + orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + /** * The content:// style URL for this table */ @@ -880,6 +1061,10 @@ public final class Calendar { "/instances/when"); public static final Uri CONTENT_BY_DAY_URI = Uri.parse("content://" + AUTHORITY + "/instances/whenbyday"); + public static final Uri CONTENT_SEARCH_URI = Uri.parse("content://" + AUTHORITY + + "/instances/search"); + public static final Uri CONTENT_SEARCH_BY_DAY_URI = + Uri.parse("content://" + AUTHORITY + "/instances/searchbyday"); /** * The default sort order for this table. @@ -896,7 +1081,6 @@ public final class Calendar { * required for correctness, it just adds a nice touch. */ public static final String SORT_CALENDAR_VIEW = "begin ASC, end DESC, title ASC"; - /** * The beginning time of the instance, in UTC milliseconds *

    Type: INTEGER (long; millis since epoch)

    @@ -1055,7 +1239,7 @@ public final class Calendar { public static final String MAX_EVENTDAYS = "maxEventDays"; } - public static final class CalendarMetaData implements CalendarMetaDataColumns { + public static final class CalendarMetaData implements CalendarMetaDataColumns, BaseColumns { } public interface EventDaysColumns { @@ -1453,4 +1637,43 @@ public final class Calendar { public static final Uri CONTENT_URI = Uri.withAppendedPath(Calendar.CONTENT_URI, CONTENT_DIRECTORY); } + + /** + * Columns from the EventsRawTimes table + */ + public interface EventsRawTimesColumns { + /** + * The corresponding event id + *

    Type: INTEGER (long)

    + */ + public static final String EVENT_ID = "event_id"; + + /** + * The RFC2445 compliant time the event starts + *

    Type: TEXT

    + */ + public static final String DTSTART_2445 = "dtstart2445"; + + /** + * The RFC2445 compliant time the event ends + *

    Type: TEXT

    + */ + public static final String DTEND_2445 = "dtend2445"; + + /** + * The RFC2445 compliant original instance time of the recurring event for which this + * event is an exception. + *

    Type: TEXT

    + */ + public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445"; + + /** + * The RFC2445 compliant last date this event repeats on, or NULL if it never ends + *

    Type: TEXT

    + */ + public static final String LAST_DATE_2445 = "lastDate2445"; + } + + public static final class EventsRawTimes implements BaseColumns, EventsRawTimesColumns { + } } diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index d52632b985420f0d4b5595e047eeb87961c1e1af..bf051f58a2868a263e96ec91408115d8dec5fb59 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -87,6 +87,17 @@ public class CallLog { */ public static final String NUMBER = "number"; + /** + * The ISO 3166-1 two letters country code of the country where the + * user received or made the call. + *

    + * Type: TEXT + *

    + * + * @hide + */ + public static final String COUNTRY_ISO = "countryiso"; + /** * The date the call occured, in milliseconds since the epoch *

    Type: INTEGER (long)

    diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 09cc3e0660f57bbf13f18fe601da56bc9947ee8a..afbc8ada04052da15879bc1e4d214f266bab653d 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -30,9 +30,9 @@ import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteException; import android.graphics.Rect; import android.net.Uri; +import android.net.Uri.Builder; import android.os.RemoteException; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -41,6 +41,7 @@ import android.view.View; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.util.ArrayList; /** *

    @@ -129,6 +130,20 @@ public final class ContactsContract { */ public static final String REQUESTING_PACKAGE_PARAM_KEY = "requesting_package"; + /** + * Query parameter that should be used by the client to access a specific + * {@link Directory}. The parameter value should be the _ID of the corresponding + * directory, e.g. + * {@code content://com.android.contacts/data/emails/filter/acme?directory=3} + */ + public static final String DIRECTORY_PARAM_KEY = "directory"; + + /** + * A query parameter that limits the number of results returned. The + * parameter value should be an integer. + */ + public static final String LIMIT_PARAM_KEY = "limit"; + /** * @hide */ @@ -180,6 +195,295 @@ public final class ContactsContract { public static final int DISPLAY_ORDER_ALTERNATIVE = 2; } + /** + * A Directory represents a contacts corpus, e.g. Local contacts, + * Google Apps Global Address List or Corporate Global Address List. + *

    + * A Directory is implemented as a content provider with its unique authority and + * the same API as the main Contacts Provider. However, there is no expectation that + * every directory provider will implement this Contract in its entirety. If a + * directory provider does not have an implementation for a specific request, it + * should throw an UnsupportedOperationException. + *

    + *

    + * The most important use case for Directories is search. A Directory provider is + * expected to support at least {@link ContactsContract.Contacts#CONTENT_FILTER_URI + * Contacts.CONTENT_FILTER_URI}. If a Directory provider wants to participate + * in email and phone lookup functionalities, it should also implement + * {@link CommonDataKinds.Email#CONTENT_FILTER_URI CommonDataKinds.Email.CONTENT_FILTER_URI} + * and + * {@link CommonDataKinds.Phone#CONTENT_FILTER_URI CommonDataKinds.Phone.CONTENT_FILTER_URI}. + *

    + *

    + * A directory provider should return NULL for every projection field it does not + * recognize, rather than throwing an exception. This way it will not be broken + * if ContactsContract is extended with new fields in the future. + *

    + *

    + * The client interacts with a directory via Contacts Provider by supplying an + * optional {@code directory=} query parameter. + *

    + *

    + * When the Contacts Provider receives the request, it transforms the URI and forwards + * the request to the corresponding directory content provider. + * The URI is transformed in the following fashion: + *

      + *
    • The URI authority is replaced with the corresponding {@link #DIRECTORY_AUTHORITY}.
    • + *
    • The {@code accountName=} and {@code accountType=} parameters are added or + * replaced using the corresponding {@link #ACCOUNT_TYPE} and {@link #ACCOUNT_NAME} values.
    • + *
    • If the URI is missing a ContactsContract.REQUESTING_PACKAGE_PARAM_KEY + * parameter, this parameter is added.
    • + *
    + *

    + *

    + * Clients should send directory requests to Contacts Provider and let it + * forward them to the respective providers rather than constructing + * directory provider URIs by themselves. This level of indirection allows + * Contacts Provider to implement additional system-level features and + * optimizations. Access to Contacts Provider is protected by the + * READ_CONTACTS permission, but access to the directory provider is not. + * Therefore directory providers must reject requests coming from clients + * other than the Contacts Provider itself. An easy way to prevent such + * unauthorized access is to check the name of the calling package: + *

    +     * private boolean isCallerAllowed() {
    +     *   PackageManager pm = getContext().getPackageManager();
    +     *   for (String packageName: pm.getPackagesForUid(Binder.getCallingUid())) {
    +     *     if (packageName.equals("com.android.providers.contacts")) {
    +     *       return true;
    +     *     }
    +     *   }
    +     *   return false;
    +     * }
    +     * 
    + *

    + *

    + * The Directory table is read-only and is maintained by the Contacts Provider + * automatically. + *

    + *

    It always has at least these two rows: + *

      + *
    • + * The local directory. It has {@link Directory#_ID Directory._ID} = + * {@link Directory#DEFAULT Directory.DEFAULT}. This directory can be used to access locally + * stored contacts. The same can be achieved by omitting the {@code directory=} + * parameter altogether. + *
    • + *
    • + * The local invisible contacts. The corresponding directory ID is + * {@link Directory#LOCAL_INVISIBLE Directory.LOCAL_INVISIBLE}. + *
    • + *
    + *

    + *

    Custom Directories are discovered by the Contacts Provider following this procedure: + *

      + *
    • It finds all installed content providers with meta data identifying them + * as directory providers in AndroidManifest.xml: + * + * <meta-data android:name="android.content.ContactDirectory" + * android:value="true" /> + * + *

      + * This tag should be placed inside the corresponding content provider declaration. + *

      + *
    • + *
    • + * Then Contacts Provider sends a {@link Directory#CONTENT_URI Directory.CONTENT_URI} + * query to each of the directory authorities. A directory provider must implement + * this query and return a list of directories. Each directory returned by + * the provider must have a unique combination for the {@link #ACCOUNT_NAME} and + * {@link #ACCOUNT_TYPE} columns (nulls are allowed). Since directory IDs are assigned + * automatically, the _ID field will not be part of the query projection. + *
    • + *
    • Contacts Provider compiles directory lists received from all directory + * providers into one, assigns each individual directory a globally unique ID and + * stores all directory records in the Directory table. + *
    • + *
    + *

    + *

    Contacts Provider automatically interrogates newly installed or replaced packages. + * Thus simply installing a package containing a directory provider is sufficient + * to have that provider registered. A package supplying a directory provider does + * not have to contain launchable activities. + *

    + *

    + * Every row in the Directory table is automatically associated with the corresponding package + * (apk). If the package is later uninstalled, all corresponding directory rows + * are automatically removed from the Contacts Provider. + *

    + *

    + * When the list of directories handled by a directory provider changes + * (for instance when the user adds a new Directory account), the directory provider + * should call {@link #notifyDirectoryChange} to notify the Contacts Provider of the change. + * In response, the Contacts Provider will requery the directory provider to obtain the + * new list of directories. + *

    + *

    + * A directory row can be optionally associated with an existing account + * (see {@link android.accounts.AccountManager}). If the account is later removed, + * the corresponding directory rows are automatically removed from the Contacts Provider. + *

    + */ + public static final class Directory implements BaseColumns { + + /** + * Not instantiable. + */ + private Directory() { + } + + /** + * The content:// style URI for this table. Requests to this URI can be + * performed on the UI thread because they are always unblocking. + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "directories"); + + /** + * The MIME-type of {@link #CONTENT_URI} providing a directory of + * contact directories. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/contact_directories"; + + /** + * The MIME type of a {@link #CONTENT_URI} item. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/contact_directory"; + + /** + * _ID of the default directory, which represents locally stored contacts. + */ + public static final long DEFAULT = 0; + + /** + * _ID of the directory that represents locally stored invisible contacts. + */ + public static final long LOCAL_INVISIBLE = 1; + + /** + * The name of the package that owns this directory. Contacts Provider + * fill it in with the name of the package containing the directory provider. + * If the package is later uninstalled, the directories it owns are + * automatically removed from this table. + * + *

    TYPE: TEXT

    + */ + public static final String PACKAGE_NAME = "packageName"; + + /** + * The type of directory captured as a resource ID in the context of the + * package {@link #PACKAGE_NAME}, e.g. "Corporate Directory" + * + *

    TYPE: INTEGER

    + */ + public static final String TYPE_RESOURCE_ID = "typeResourceId"; + + /** + * An optional name that can be used in the UI to represent this directory, + * e.g. "Acme Corp" + *

    TYPE: text

    + */ + public static final String DISPLAY_NAME = "displayName"; + + /** + *

    + * The authority of the Directory Provider. Contacts Provider will + * use this authority to forward requests to the directory provider. + * A directory provider can leave this column empty - Contacts Provider will fill it in. + *

    + *

    + * Clients of this API should not send requests directly to this authority. + * All directory requests must be routed through Contacts Provider. + *

    + * + *

    TYPE: text

    + */ + public static final String DIRECTORY_AUTHORITY = "authority"; + + /** + * The account type which this directory is associated. + * + *

    TYPE: text

    + */ + public static final String ACCOUNT_TYPE = "accountType"; + + /** + * The account with which this directory is associated. If the account is later + * removed, the directories it owns are automatically removed from this table. + * + *

    TYPE: text

    + */ + public static final String ACCOUNT_NAME = "accountName"; + + /** + * One of {@link #EXPORT_SUPPORT_NONE}, {@link #EXPORT_SUPPORT_ANY_ACCOUNT}, + * {@link #EXPORT_SUPPORT_SAME_ACCOUNT_ONLY}. This is the expectation the + * directory has for data exported from it. Clients must obey this setting. + */ + public static final String EXPORT_SUPPORT = "exportSupport"; + + /** + * An {@link #EXPORT_SUPPORT} setting that indicates that the directory + * does not allow any data to be copied out of it. + */ + public static final int EXPORT_SUPPORT_NONE = 0; + + /** + * An {@link #EXPORT_SUPPORT} setting that indicates that the directory + * allow its data copied only to the account specified by + * {@link #ACCOUNT_TYPE}/{@link #ACCOUNT_NAME}. + */ + public static final int EXPORT_SUPPORT_SAME_ACCOUNT_ONLY = 1; + + /** + * An {@link #EXPORT_SUPPORT} setting that indicates that the directory + * allow its data copied to any contacts account. + */ + public static final int EXPORT_SUPPORT_ANY_ACCOUNT = 2; + + /** + * One of {@link #SHORTCUT_SUPPORT_NONE}, {@link #SHORTCUT_SUPPORT_DATA_ITEMS_ONLY}, + * {@link #SHORTCUT_SUPPORT_FULL}, This is the expectation the directory + * has for shortcuts created for its elements. Clients must obey this setting. + */ + public static final String SHORTCUT_SUPPORT = "shortcutSupport"; + + /** + * An {@link #SHORTCUT_SUPPORT} setting that indicates that the directory + * does not allow any shortcuts created for its contacts. + */ + public static final int SHORTCUT_SUPPORT_NONE = 0; + + /** + * An {@link #SHORTCUT_SUPPORT} setting that indicates that the directory + * allow creation of shortcuts for data items like email, phone or postal address, + * but not the entire contact. + */ + public static final int SHORTCUT_SUPPORT_DATA_ITEMS_ONLY = 1; + + /** + * An {@link #SHORTCUT_SUPPORT} setting that indicates that the directory + * allow creation of shortcuts for contact as well as their constituent elements. + */ + public static final int SHORTCUT_SUPPORT_FULL = 2; + + /** + * Notifies the system of a change in the list of directories handled by + * a particular directory provider. The Contacts provider will turn around + * and send a query to the directory provider for the full list of directories, + * which will replace the previous list. + */ + public static void notifyDirectoryChange(ContentResolver resolver) { + // This is done to trigger a query by Contacts Provider back to the directory provider. + // No data needs to be sent back, because the provider can infer the calling + // package from binder. + ContentValues contentValues = new ContentValues(); + resolver.update(Directory.CONTENT_URI, contentValues, null, null); + } + } + /** * @hide should be removed when users are updated to refer to SyncState * @deprecated use SyncState instead @@ -412,6 +716,7 @@ public final class ContactsContract { * Contact Chat Capabilities. See {@link StatusUpdates} for individual * definitions. *

    Type: NUMBER

    + * @hide */ public static final String CONTACT_CHAT_CAPABILITY = "contact_chat_capability"; @@ -457,7 +762,6 @@ public final class ContactsContract { * 'family name', 'given name' 'middle name'. The CJK tradition is * 'family name' 'middle name' 'given name', with Japanese favoring a space between * the names and Chinese omitting the space. - * @hide */ public interface FullNameStyle { public static final int UNDEFINED = 0; @@ -476,7 +780,6 @@ public final class ContactsContract { /** * Constants for various styles of capturing the pronunciation of a person's name. - * @hide */ public interface PhoneticNameStyle { public static final int UNDEFINED = 0; @@ -502,8 +805,6 @@ public final class ContactsContract { /** * Types of data used to produce the display name for a contact. Listed in the order * of increasing priority. - * - * @hide */ public interface DisplayNameSources { public static final int UNDEFINED = 0; @@ -519,15 +820,12 @@ public final class ContactsContract { * * @see Contacts * @see RawContacts - * @hide */ protected interface ContactNameColumns { /** * The kind of data that is used as the display name for the contact, such as - * structured name or email address. See DisplayNameSources. - * - * TODO: convert DisplayNameSources to a link after it is un-hidden + * structured name or email address. See {@link DisplayNameSources}. */ public static final String DISPLAY_NAME_SOURCE = "display_name_source"; @@ -575,9 +873,7 @@ public final class ContactsContract { /** * The phonetic alphabet used to represent the {@link #PHONETIC_NAME}. See - * PhoneticNameStyle. - * - * TODO: convert PhoneticNameStyle to a link after it is un-hidden + * {@link PhoneticNameStyle}. */ public static final String PHONETIC_NAME_STYLE = "phonetic_name_style"; @@ -587,13 +883,10 @@ public final class ContactsContract { * {@link #PHONETIC_NAME_STYLE}. *

    *

    - * The value may be set manually by the user. - * This capability is is of interest only in countries - * with commonly used phonetic - * alphabets, such as Japan and Korea. See PhoneticNameStyle. + * The value may be set manually by the user. This capability is of + * interest only in countries with commonly used phonetic alphabets, + * such as Japan and Korea. See {@link PhoneticNameStyle}. *

    - * - * TODO: convert PhoneticNameStyle to a link after it is un-hidden */ public static final String PHONETIC_NAME = "phonetic_name"; @@ -975,10 +1268,10 @@ public final class ContactsContract { } /** - * Mark a contact as having been contacted. This updates the - * {@link #TIMES_CONTACTED} and {@link #LAST_TIME_CONTACTED} for the - * contact, plus the corresponding values of any associated raw - * contacts. + * Mark a contact as having been contacted. Updates two fields: + * {@link #TIMES_CONTACTED} and {@link #LAST_TIME_CONTACTED}. The + * TIMES_CONTACTED field is incremented by 1 and the LAST_TIME_CONTACTED + * field is populated with the current system time. * * @param resolver the ContentResolver to use * @param contactId the person who was contacted @@ -1040,7 +1333,8 @@ public final class ContactsContract { /** * A sub-directory of a single contact that contains all of the constituent raw contact - * {@link ContactsContract.Data} rows. + * {@link ContactsContract.Data} rows. This directory can be used either + * with a {@link #CONTENT_URI} or {@link #CONTENT_LOOKUP_URI}. */ public static final class Data implements BaseColumns, DataColumns { /** @@ -1054,6 +1348,57 @@ public final class ContactsContract { public static final String CONTENT_DIRECTORY = "data"; } + /** + *

    + * A sub-directory of a contact that contains all of its + * {@link ContactsContract.RawContacts} as well as + * {@link ContactsContract.Data} rows. To access this directory append + * {@link #CONTENT_DIRECTORY} to the contact URI. + *

    + *

    + * Entity has three ID fields: {@link #CONTACT_ID} for the contact, + * {@link #RAW_CONTACT_ID} for the raw contact and {@link #DATA_ID} for + * the data rows. Entity always contains at least one row per + * constituent raw contact, even if there are no actual data rows. In + * this case the {@link #DATA_ID} field will be null. + *

    + *

    + * Entity reads all data for the entire contact in one transaction, to + * guarantee consistency. There is significant data duplication + * in the Entity (each row repeats all Contact columns and all RawContact + * columns), so the benefits of transactional consistency should be weighed + * against the cost of transferring large amounts of denormalized data + * from the Provider. + *

    + */ + public static final class Entity implements BaseColumns, ContactsColumns, + ContactNameColumns, RawContactsColumns, BaseSyncColumns, SyncColumns, DataColumns, + StatusColumns, ContactOptionsColumns, ContactStatusColumns { + /** + * no public constructor since this is a utility class + */ + private Entity() { + } + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "entities"; + + /** + * The ID of the raw contact row. + *

    Type: INTEGER

    + */ + public static final String RAW_CONTACT_ID = "raw_contact_id"; + + /** + * The ID of the data row. The value will be null if this raw contact has no + * data rows. + *

    Type: INTEGER

    + */ + public static final String DATA_ID = "data_id"; + } + /** *

    * A read-only sub-directory of a single contact aggregate that @@ -1080,9 +1425,13 @@ public final class ContactsContract { * * *

    + *

    + * This directory can be used either with a {@link #CONTENT_URI} or + * {@link #CONTENT_LOOKUP_URI}. + *

    */ - // TODO: add ContactOptionsColumns, ContactStatusColumns - public static final class AggregationSuggestions implements BaseColumns, ContactsColumns { + public static final class AggregationSuggestions implements BaseColumns, ContactsColumns, + ContactOptionsColumns, ContactStatusColumns { /** * No public constructor since this is a utility class */ @@ -1094,6 +1443,106 @@ public final class ContactsContract { * {@link android.provider.ContactsContract.Contacts#CONTENT_FILTER_URI}. */ public static final String CONTENT_DIRECTORY = "suggestions"; + + /** + * Used with {@link Builder#addParameter} to specify what kind of data is + * supplied for the suggestion query. + * + * @hide + */ + public static final String PARAMETER_MATCH_NAME = "name"; + + /** + * Used with {@link Builder#addParameter} to specify what kind of data is + * supplied for the suggestion query. + * + * @hide + */ + public static final String PARAMETER_MATCH_EMAIL = "email"; + + /** + * Used with {@link Builder#addParameter} to specify what kind of data is + * supplied for the suggestion query. + * + * @hide + */ + public static final String PARAMETER_MATCH_PHONE = "phone"; + + /** + * Used with {@link Builder#addParameter} to specify what kind of data is + * supplied for the suggestion query. + * + * @hide + */ + public static final String PARAMETER_MATCH_NICKNAME = "nickname"; + + /** + * A convenience builder for aggregation suggestion content URIs. + * + * TODO: change documentation for this class to use the builder. + * @hide + */ + public static final class Builder { + private long mContactId; + private ArrayList mKinds = new ArrayList(); + private ArrayList mValues = new ArrayList(); + private int mLimit; + + /** + * Optional existing contact ID. If it is not provided, the search + * will be based exclusively on the values supplied with {@link #addParameter}. + */ + public Builder setContactId(long contactId) { + this.mContactId = contactId; + return this; + } + + /** + * A value that can be used when searching for an aggregation + * suggestion. + * + * @param kind can be one of + * {@link AggregationSuggestions#PARAMETER_MATCH_NAME}, + * {@link AggregationSuggestions#PARAMETER_MATCH_EMAIL}, + * {@link AggregationSuggestions#PARAMETER_MATCH_NICKNAME}, + * {@link AggregationSuggestions#PARAMETER_MATCH_PHONE} + */ + public Builder addParameter(String kind, String value) { + if (!TextUtils.isEmpty(value)) { + mKinds.add(kind); + mValues.add(value); + } + return this; + } + + public Builder setLimit(int limit) { + mLimit = limit; + return this; + } + + public Uri build() { + android.net.Uri.Builder builder = Contacts.CONTENT_URI.buildUpon(); + builder.appendEncodedPath(String.valueOf(mContactId)); + builder.appendPath(Contacts.AggregationSuggestions.CONTENT_DIRECTORY); + if (mLimit != 0) { + builder.appendQueryParameter("limit", String.valueOf(mLimit)); + } + + int count = mKinds.size(); + for (int i = 0; i < count; i++) { + builder.appendQueryParameter("query", mKinds.get(i) + ":" + mValues.get(i)); + } + + return builder.build(); + } + } + + /** + * @hide + */ + public static final Builder builder() { + return new Builder(); + } } /** @@ -1129,9 +1578,12 @@ public final class ContactsContract { *

    You should also consider using the convenience method * {@link ContactsContract.Contacts#openContactPhotoInputStream(ContentResolver, Uri)} *

    + *

    + * This directory can be used either with a {@link #CONTENT_URI} or + * {@link #CONTENT_LOOKUP_URI}. + *

    */ - // TODO: change DataColumns to DataColumnsWithJoins - public static final class Photo implements BaseColumns, DataColumns { + public static final class Photo implements BaseColumns, DataColumnsWithJoins { /** * no public constructor since this is a utility class */ @@ -1147,7 +1599,6 @@ public final class ContactsContract { * that could be inflated using {@link android.graphics.BitmapFactory}. *

    * Type: BLOB - * @hide TODO: Unhide in a separate CL */ public static final String PHOTO = DATA15; } @@ -1156,7 +1607,10 @@ public final class ContactsContract { * 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 + * @param contactUri the contact whose photo should be used. This can be used with + * either a {@link #CONTENT_URI} or a {@link #CONTENT_LOOKUP_URI} URI. + *

    + * @return an InputStream of the photo, or null if no photo is present */ public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri contactUri) { @@ -1244,6 +1698,13 @@ public final class ContactsContract { * @hide */ public static final String NAME_VERIFIED = "name_verified"; + + /** + * The "read-only" flag: "0" by default, "1" if the row cannot be modified or + * deleted except by a sync adapter. See {@link ContactsContract#CALLER_IS_SYNCADAPTER}. + *

    Type: INTEGER

    + */ + public static final String RAW_CONTACT_IS_READ_ONLY = "raw_contact_is_read_only"; } /** @@ -1629,10 +2090,10 @@ public final class ContactsContract { public static final int AGGREGATION_MODE_DEFAULT = 0; /** - * Do not use. - * - * TODO: deprecate in favor of {@link #AGGREGATION_MODE_DEFAULT} + * Aggregation mode: aggregate at the time the raw contact is inserted/updated. + * @deprecated Aggregation is synchronous, this historic value is a no-op */ + @Deprecated public static final int AGGREGATION_MODE_IMMEDIATE = 1; /** @@ -1700,9 +2161,7 @@ public final class ContactsContract { /** * A sub-directory of a single raw contact that contains all of its * {@link ContactsContract.Data} rows. To access this directory - * append {@link Data#CONTENT_DIRECTORY} to the contact URI. - * - * TODO: deprecate in favor of {@link RawContacts.Entity}. + * append {@link Data#CONTENT_DIRECTORY} to the raw contact URI. */ public static final class Data implements BaseColumns, DataColumns { /** @@ -1721,7 +2180,7 @@ public final class ContactsContract { *

    * A sub-directory of a single raw contact that contains all of its * {@link ContactsContract.Data} rows. To access this directory append - * {@link #CONTENT_DIRECTORY} to the contact URI. See + * {@link RawContacts.Entity#CONTENT_DIRECTORY} to the raw contact URI. See * {@link RawContactsEntity} for a stand-alone table containing the same * data. *

    @@ -1733,10 +2192,10 @@ public final class ContactsContract { * null. *

    *

    - * Entity reads all - * data for a raw contact in one transaction, to guarantee - * consistency. - *

    + * Using Entity should be preferred to using two separate queries: + * RawContacts followed by Data. The reason is that Entity reads all + * data for a raw contact in one transaction, so there is no possibility + * of the data changing between the two queries. */ public static final class Entity implements BaseColumns, DataColumns { /** @@ -1751,7 +2210,7 @@ public final class ContactsContract { public static final String CONTENT_DIRECTORY = "entity"; /** - * The ID of the data column. The value will be null if this raw contact has no + * The ID of the data row. The value will be null if this raw contact has no * data rows. *

    Type: INTEGER

    */ @@ -1839,28 +2298,21 @@ public final class ContactsContract { Data.DATA_VERSION); for (String key : DATA_KEYS) { final int columnIndex = cursor.getColumnIndexOrThrow(key); - if (cursor.isNull(columnIndex)) { - // don't put anything - } else { - try { + switch (cursor.getType(columnIndex)) { + case Cursor.FIELD_TYPE_NULL: + // don't put anything + break; + case Cursor.FIELD_TYPE_INTEGER: + case Cursor.FIELD_TYPE_FLOAT: + case Cursor.FIELD_TYPE_STRING: cv.put(key, cursor.getString(columnIndex)); - } catch (SQLiteException e) { + break; + case Cursor.FIELD_TYPE_BLOB: cv.put(key, cursor.getBlob(columnIndex)); - } + break; + default: + throw new IllegalStateException("Invalid or unhandled data type"); } - // TODO: go back to this version of the code when bug - // http://b/issue?id=2306370 is fixed. -// if (cursor.isNull(columnIndex)) { -// // don't put anything -// } else if (cursor.isLong(columnIndex)) { -// values.put(key, cursor.getLong(columnIndex)); -// } else if (cursor.isFloat(columnIndex)) { -// values.put(key, cursor.getFloat(columnIndex)); -// } else if (cursor.isString(columnIndex)) { -// values.put(key, cursor.getString(columnIndex)); -// } else if (cursor.isBlob(columnIndex)) { -// values.put(key, cursor.getBlob(columnIndex)); -// } } contact.addSubValue(ContactsContract.Data.CONTENT_URI, cv); } while (cursor.moveToNext()); @@ -1961,23 +2413,28 @@ public final class ContactsContract { /** * Contact's audio/video chat capability level. *

    Type: INTEGER (one of the values below)

    + * @hide */ public static final String CHAT_CAPABILITY = "chat_capability"; /** - * An allowed value of {@link #CHAT_CAPABILITY}. Indicates that the contact's device can - * display a video feed. + * An allowed flag of {@link #CHAT_CAPABILITY}. Indicates audio-chat capability (microphone + * and speaker) + * @hide */ - public static final int CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY = 1; + public static final int CAPABILITY_HAS_VOICE = 1; /** - * An allowed value of {@link #CHAT_CAPABILITY}. Indicates audio-chat capability. + * An allowed flag of {@link #CHAT_CAPABILITY}. Indicates that the contact's device can + * display a video feed. + * @hide */ - public static final int CAPABILITY_HAS_VOICE = 2; + public static final int CAPABILITY_HAS_VIDEO = 2; /** - * An allowed value of {@link #CHAT_CAPABILITY}. Indicates that the contact's device has a + * An allowed flag of {@link #CHAT_CAPABILITY}. Indicates that the contact's device has a * camera that can be used for video chat (e.g. a front-facing camera on a phone). + * @hide */ public static final int CAPABILITY_HAS_CAMERA = 4; } @@ -2022,6 +2479,13 @@ public final class ContactsContract { */ public static final String IS_SUPER_PRIMARY = "is_super_primary"; + /** + * The "read-only" flag: "0" by default, "1" if the row cannot be modified or + * deleted except by a sync adapter. See {@link ContactsContract#CALLER_IS_SYNCADAPTER}. + *

    Type: INTEGER

    + */ + public static final String IS_READ_ONLY = "is_read_only"; + /** * 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 @@ -2857,6 +3321,14 @@ public final class ContactsContract { *

    Type: TEXT

    */ public static final String LABEL = "label"; + + /** + * The phone number's E164 representation. + *

    Type: TEXT

    + * + * @hide + */ + public static final String NORMALIZED_NUMBER = "normalized_number"; } /** @@ -3126,25 +3598,6 @@ public final class ContactsContract { * * * - * int - * {@link #CHAT_CAPABILITY} - * read/write - * Contact IM chat compatibility value. The allowed values are: - *

    - *

      - *
    • {@link #CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY}
    • - *
    • {@link #CAPABILITY_HAS_VOICE}
    • - *
    • {@link #CAPABILITY_HAS_CAMERA}
    • - *
    - *

    - *

    - * Since chat compatibility is inherently volatile as the contact's availability moves from - * one device to another, the content provider may choose not to store this field in long-term - * storage. - *

    - * - * - * * String * {@link #STATUS} * read/write @@ -3710,6 +4163,14 @@ public final class ContactsContract { */ public static final String NUMBER = DATA; + /** + * The phone number's E164 representation. + *

    Type: TEXT

    + * + * @hide + */ + public static final String NORMALIZED_NUMBER = DATA4; + /** * @deprecated use {@link #getTypeLabel(Resources, int, CharSequence)} instead. * @hide @@ -3792,7 +4253,7 @@ public final class ContactsContract { * * * String - * {@link #DATA} + * {@link #ADDRESS} * {@link #DATA1} * Email address itself. * @@ -3883,7 +4344,6 @@ public final class ContactsContract { /** * The email address. *

    Type: TEXT

    - * @hide TODO: Unhide in a separate CL */ public static final String ADDRESS = DATA1; @@ -5047,6 +5507,23 @@ public final class ContactsContract { * Type: INTEGER (boolean) */ public static final String SHOULD_SYNC = "should_sync"; + + /** + * Any newly created contacts will automatically be added to groups that have this + * flag set to true. + *

    + * Type: INTEGER (boolean) + */ + public static final String AUTO_ADD = "auto_add"; + + /** + * When a contacts is marked as a favorites it will be automatically added + * to the groups that have this flag set, and when it is removed from favorites + * it will be removed from these groups. + *

    + * Type: INTEGER (boolean) + */ + public static final String FAVORITES = "favorites"; } /** @@ -5187,6 +5664,8 @@ public final class ContactsContract { DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, DELETED); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, NOTES); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SHOULD_SYNC); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, FAVORITES); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, AUTO_ADD); cursor.moveToNext(); return new Entity(values); } @@ -5702,6 +6181,28 @@ public final class ContactsContract { public static final String SHOW_OR_CREATE_CONTACT = "com.android.contacts.action.SHOW_OR_CREATE_CONTACT"; + /** + * Starts an Activity that lets the user select the multiple phones from a + * list of phone numbers which come from the contacts or + * {@link #EXTRA_PHONE_URIS}. + *

    + * The phone numbers being passed in through {@link #EXTRA_PHONE_URIS} + * could belong to the contacts or not, and will be selected by default. + *

    + * The user's selection will be returned from + * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)} + * if the resultCode is + * {@link android.app.Activity#RESULT_OK}, the array of picked phone + * numbers are in the Intent's + * {@link #EXTRA_PHONE_URIS}; otherwise, the + * {@link android.app.Activity#RESULT_CANCELED} is returned if the user + * left the Activity without changing the selection. + * + * @hide + */ + public static final String ACTION_GET_MULTIPLE_PHONES = + "com.android.contacts.action.GET_MULTIPLE_PHONES"; + /** * Used with {@link #SHOW_OR_CREATE_CONTACT} to force creating a new * contact if no matching contact found. Otherwise, default behavior is @@ -5722,6 +6223,23 @@ public final class ContactsContract { public static final String EXTRA_CREATE_DESCRIPTION = "com.android.contacts.action.CREATE_DESCRIPTION"; + /** + * Used with {@link #ACTION_GET_MULTIPLE_PHONES} as the input or output value. + *

    + * The phone numbers want to be picked by default should be passed in as + * input value. These phone numbers could belong to the contacts or not. + *

    + * The phone numbers which were picked by the user are returned as output + * value. + *

    + * Type: array of URIs, the tel URI is used for the phone numbers which don't + * belong to any contact, the content URI is used for phone id in contacts. + * + * @hide + */ + public static final String EXTRA_PHONE_URIS = + "com.android.contacts.extra.PHONE_URIS"; + /** * Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to specify a * dialog location using screen coordinates. When not specified, the diff --git a/core/java/android/provider/DrmStore.java b/core/java/android/provider/DrmStore.java index c438ac4aa5990215d16abf72cf7f20ef8d62b092..34f2f0d254321d4cb724eda2249eb7c5418ae182 100644 --- a/core/java/android/provider/DrmStore.java +++ b/core/java/android/provider/DrmStore.java @@ -131,7 +131,7 @@ public final class DrmStore * 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 fis The FileInputStream to insert * @param title The title for the content (or null) * @return uri to the DRM record or null */ @@ -143,11 +143,11 @@ public final class DrmStore DrmRawContent content = new DrmRawContent(fis, (int) fis.available(), DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING); String mimeType = content.getContentType(); + long size = fis.getChannel().size(); DrmRightsManager manager = manager = DrmRightsManager.getInstance(); DrmRights rights = manager.queryRights(content); InputStream stream = content.getContentInputStream(rights); - long size = stream.available(); Uri contentUri = null; if (mimeType.startsWith("audio/")) { diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 075da338cb391ffc33daf12b5414c5f5caa550cc..1417ef5503ca8bc06fb19cab7ab2e8ed3fcaef4b 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -253,8 +253,106 @@ public final class MediaStore { *

    Type: TEXT

    */ public static final String MIME_TYPE = "mime_type"; + + /** + * The row ID in the MTP object table corresponding to this media file. + *

    Type: INTEGER

    + * @hide + */ + public static final String MTP_OBJECT_ID = "object_id"; + + /** + * The MTP object handle of a newly transfered file. + * Used to pass the new file's object handle through the media scanner + * from MTP to the media provider + * For internal use only by MTP, media scanner and media provider. + *

    Type: INTEGER

    + * @hide + */ + public static final String MEDIA_SCANNER_NEW_OBJECT_ID = "media_scanner_new_object_id"; } + /** + * Media provider table containing an index of all files in the storage. + * This can be used by applications to find all documents of a particular type + * and is also used internally by the device side MTP implementation. + * @hide + */ + public static final class Files { + + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/file"); + } + + public static final Uri getContentUri(String volumeName, + long fileId) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/file/" + fileId); + } + + public static Uri getMtpObjectsUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/object"); + } + + public static final Uri getMtpObjectsUri(String volumeName, + long fileId) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/object/" + fileId); + } + + // Used to implement the MTP GetObjectReferences and SetObjectReferences commands. + public static final Uri getMtpReferencesUri(String volumeName, + long fileId) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/object/" + fileId + "/references"); + } + + /** + * Fields for master table for all media files. + * Table also contains MediaColumns._ID, DATA, SIZE and DATE_MODIFIED. + */ + public interface FileColumns extends MediaColumns { + /** + * The MTP format code of the file + *

    Type: INTEGER

    + */ + public static final String FORMAT = "format"; + + /** + * The index of the parent directory of the file + *

    Type: INTEGER

    + */ + public static final String PARENT = "parent"; + + /** + * Identifier for the media table containing the file. + * Used internally by MediaProvider + *

    Type: INTEGER

    + */ + public static final String MEDIA_TABLE = "media_table"; + + /** + * The ID of the file in its media table. + *

    Type: INTEGER

    + */ + public static final String MEDIA_ID = "media_id"; + } + + /** + * The MIME type of the file + *

    Type: TEXT

    + */ + public static final String MIME_TYPE = "mime_type"; + + /** + * The title of the content + *

    Type: TEXT

    + */ + public static final String TITLE = "title"; + } + /** * This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended * to be accessed elsewhere. @@ -335,26 +433,27 @@ public final class MediaStore { // Log.v(TAG, "getThumbnail: origId="+origId+", kind="+kind+", isVideo="+isVideo); // If the magic is non-zero, we simply return thumbnail if it does exist. // querying MediaProvider and simply return thumbnail. - MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); - long magic = thumbFile.getMagic(origId); - if (magic != 0) { - if (kind == MICRO_KIND) { - synchronized (sThumbBufLock) { - if (sThumbBuf == null) { - sThumbBuf = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; - } - if (thumbFile.getMiniThumbFromFile(origId, sThumbBuf) != null) { - bitmap = BitmapFactory.decodeByteArray(sThumbBuf, 0, sThumbBuf.length); - if (bitmap == null) { - Log.w(TAG, "couldn't decode byte array."); + MiniThumbFile thumbFile = new MiniThumbFile(isVideo ? Video.Media.EXTERNAL_CONTENT_URI + : Images.Media.EXTERNAL_CONTENT_URI); + Cursor c = null; + try { + long magic = thumbFile.getMagic(origId); + if (magic != 0) { + if (kind == MICRO_KIND) { + synchronized (sThumbBufLock) { + if (sThumbBuf == null) { + sThumbBuf = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; + } + if (thumbFile.getMiniThumbFromFile(origId, sThumbBuf) != null) { + bitmap = BitmapFactory.decodeByteArray(sThumbBuf, 0, sThumbBuf.length); + if (bitmap == null) { + Log.w(TAG, "couldn't decode byte array."); + } } } - } - return bitmap; - } else if (kind == MINI_KIND) { - String column = isVideo ? "video_id=" : "image_id="; - Cursor c = null; - try { + return bitmap; + } else if (kind == MINI_KIND) { + String column = isVideo ? "video_id=" : "image_id="; c = cr.query(baseUri, PROJECTION, column + origId, null, null); if (c != null && c.moveToFirst()) { bitmap = getMiniThumbFromFile(c, baseUri, cr, options); @@ -362,17 +461,13 @@ public final class MediaStore { return bitmap; } } - } finally { - if (c != null) c.close(); } } - } - Cursor c = null; - try { Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1") .appendQueryParameter("orig_id", String.valueOf(origId)) .appendQueryParameter("group_id", String.valueOf(groupId)).build(); + if (c != null) c.close(); c = cr.query(blockingUri, PROJECTION, null, null, null); // This happens when original image/video doesn't exist. if (c == null) return null; @@ -423,6 +518,9 @@ public final class MediaStore { Log.w(TAG, ex); } finally { if (c != null) c.close(); + // To avoid file descriptor leak in application process. + thumbFile.deactivate(); + thumbFile = null; } return bitmap; } diff --git a/core/java/android/provider/Mtp.java b/core/java/android/provider/Mtp.java new file mode 100644 index 0000000000000000000000000000000000000000..bba54e195eda00eccf06b16c2c3001a93f64fdfb --- /dev/null +++ b/core/java/android/provider/Mtp.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.provider; + +import android.content.ContentUris; +import android.net.Uri; +import android.util.Log; + + +/** + * The MTP provider supports accessing content on MTP and PTP devices. + * @hide + */ +public final class Mtp +{ + private final static String TAG = "Mtp"; + + public static final String AUTHORITY = "mtp"; + + private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/"; + private static final String CONTENT_AUTHORITY_DEVICE_SLASH = "content://" + AUTHORITY + "/device/"; + + + /** + * Broadcast Action: A broadcast to indicate the end of an MTP session with the host. + * This broadcast is only sent if MTP activity has modified the media database during the + * most recent MTP session + */ + public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END"; + + /** + * Contains list of all MTP/PTP devices + */ + public static final class Device implements BaseColumns { + + public static final Uri CONTENT_URI = Uri.parse(CONTENT_AUTHORITY_SLASH + "device"); + + public static Uri getContentUri(int deviceID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID); + } + + /** + * The manufacturer of the device + *

    Type: TEXT

    + */ + public static final String MANUFACTURER = "manufacturer"; + + /** + * The model name of the device + *

    Type: TEXT

    + */ + public static final String MODEL = "model"; + } + + /** + * Contains list of storage units for an MTP/PTP device + */ + public static final class Storage implements BaseColumns { + + public static Uri getContentUri(int deviceID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + "/storage"); + } + + public static Uri getContentUri(int deviceID, int storageID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + "/storage/" + storageID); + } + + /** + * Storage unit identifier + *

    Type: TEXT

    + */ + public static final String IDENTIFIER = "identifier"; + + /** + * Storage unit description + *

    Type: TEXT

    + */ + public static final String DESCRIPTION = "description"; + } + + /** + * Contains list of objects on an MTP/PTP device + */ + public static final class Object implements BaseColumns { + + public static Uri getContentUri(int deviceID, int objectID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + + "/object/" + objectID); + } + + public static Uri getContentUriForObjectChildren(int deviceID, int objectID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + + "/object/" + objectID + "/child"); + } + + public static Uri getContentUriForStorageChildren(int deviceID, int storageID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + + "/storage/" + storageID + "/child"); + } + + /** + * The following columns correspond to the fields in the ObjectInfo dataset + * as described in the MTP specification. + */ + + /** + * The ID of the storage unit containing the object. + *

    Type: INTEGER

    + */ + public static final String STORAGE_ID = "storage_id"; + + /** + * The object's format. Can be one of the FORMAT_* symbols below, + * or any of the valid MTP object formats as defined in the MTP specification. + *

    Type: INTEGER

    + */ + public static final String FORMAT = "format"; + + /** + * The protection status of the object. See the PROTECTION_STATUS_*symbols below. + *

    Type: INTEGER

    + */ + public static final String PROTECTION_STATUS = "protection_status"; + + /** + * The size of the object in bytes. + *

    Type: INTEGER

    + */ + public static final String SIZE = "size"; + + /** + * The object's thumbnail format. Can be one of the FORMAT_* symbols below, + * or any of the valid MTP object formats as defined in the MTP specification. + *

    Type: INTEGER

    + */ + public static final String THUMB_FORMAT = "thumb_format"; + + /** + * The size of the object's thumbnail in bytes. + *

    Type: INTEGER

    + */ + public static final String THUMB_SIZE = "thumb_size"; + + /** + * The width of the object's thumbnail in pixels. + *

    Type: INTEGER

    + */ + public static final String THUMB_WIDTH = "thumb_width"; + + /** + * The height of the object's thumbnail in pixels. + *

    Type: INTEGER

    + */ + public static final String THUMB_HEIGHT = "thumb_height"; + + /** + * The object's thumbnail. + *

    Type: BLOB

    + */ + public static final String THUMB = "thumb"; + + /** + * The width of the object in pixels. + *

    Type: INTEGER

    + */ + public static final String IMAGE_WIDTH = "image_width"; + + /** + * The height of the object in pixels. + *

    Type: INTEGER

    + */ + public static final String IMAGE_HEIGHT = "image_height"; + + /** + * The depth of the object in bits per pixel. + *

    Type: INTEGER

    + */ + public static final String IMAGE_DEPTH = "image_depth"; + + /** + * The ID of the object's parent, or zero if the object + * is in the root of its storage unit. + *

    Type: INTEGER

    + */ + public static final String PARENT = "parent"; + + /** + * The association type for a container object. + * For folders this is typically {@link #ASSOCIATION_TYPE_GENERIC_FOLDER} + *

    Type: INTEGER

    + */ + public static final String ASSOCIATION_TYPE = "association_type"; + + /** + * Contains additional information about container objects. + *

    Type: INTEGER

    + */ + public static final String ASSOCIATION_DESC = "association_desc"; + + /** + * The sequence number of the object, typically used for an association + * containing images taken in sequence. + *

    Type: INTEGER

    + */ + public static final String SEQUENCE_NUMBER = "sequence_number"; + + /** + * The name of the object. + *

    Type: TEXT

    + */ + public static final String NAME = "name"; + + /** + * The date the object was created, in seconds since January 1, 1970. + *

    Type: INTEGER

    + */ + public static final String DATE_CREATED = "date_created"; + + /** + * The date the object was last modified, in seconds since January 1, 1970. + *

    Type: INTEGER

    + */ + public static final String DATE_MODIFIED = "date_modified"; + + /** + * A list of keywords associated with an object, separated by spaces. + *

    Type: TEXT

    + */ + public static final String KEYWORDS = "keywords"; + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 69151dff34d0a6c753b59f330eaca09853a4bcdd..4d87b79717f9991d37773cde60c3a26b61131518 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -16,14 +16,11 @@ package android.provider; -import com.google.android.collect.Maps; -import org.apache.commons.codec.binary.Base64; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; -import android.content.ContentQueryMap; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -37,20 +34,18 @@ import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; -import android.os.*; -import android.telephony.TelephonyManager; +import android.os.BatteryManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.SystemProperties; import android.text.TextUtils; import android.util.AndroidException; import android.util.Config; import android.util.Log; import java.net.URISyntaxException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Map; /** @@ -1009,7 +1004,7 @@ public final class Settings { public static boolean hasInterestingConfigurationChanges(int changes) { return (changes&ActivityInfo.CONFIG_FONT_SCALE) != 0; } - + public static boolean getShowGTalkServiceStatus(ContentResolver cr) { return getInt(cr, SHOW_GTALK_SERVICE_STATUS, 0) != 0; } @@ -1133,6 +1128,7 @@ public final class Settings { */ public static final int WIFI_SLEEP_POLICY_NEVER = 2; + //TODO: deprecate static IP constants /** * Whether to use static IP and other static network attributes. *

    @@ -1216,7 +1212,7 @@ public final class Settings { public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern"; /** - * @deprecated Use + * @deprecated Use * {@link android.provider.Settings.Secure#LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED} * instead */ @@ -1693,6 +1689,12 @@ public final class Settings { */ public static final String UNLOCK_SOUND = "unlock_sound"; + /** + * True if we should appear as a PTP device instead of MTP. + * @hide + */ + public static final String USE_PTP_INTERFACE = "use_ptp_interface"; + /** * Receive incoming SIP calls? * 0 = no @@ -1789,6 +1791,7 @@ public final class Settings { LOCKSCREEN_SOUNDS_ENABLED, SHOW_WEB_SUGGESTIONS, NOTIFICATION_LIGHT_PULSE, + USE_PTP_INTERFACE, SIP_CALL_OPTIONS, SIP_RECEIVE_CALLS }; @@ -2326,6 +2329,14 @@ public final class Settings { return ("bluetooth_a2dp_sink_priority_" + address.toUpperCase()); } + /** + * Get the key that retrieves a bluetooth Input Device's priority. + * @hide + */ + public static final String getBluetoothInputDevicePriorityKey(String address) { + return ("bluetooth_input_device_priority_" + address.toUpperCase()); + } + /** * Whether or not data roaming is enabled. (0 = false, 1 = true) */ @@ -2358,10 +2369,31 @@ public final class Settings { public static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods"; /** - * Host name and port for a user-selected proxy. + * Host name and port for global proxy. */ public static final String HTTP_PROXY = "http_proxy"; + /** + * Exclusion list for global proxy. This string contains a list of comma-separated + * domains where the global proxy does not apply. Domains should be listed in a comma- + * separated list. Example of acceptable formats: ".domain1.com,my.domain2.com" + * @hide + */ + public static final String HTTP_PROXY_EXCLUSION_LIST = "http_proxy_exclusion_list"; + + /** + * Enables the UI setting to allow the user to specify the global HTTP proxy + * and associated exclusion list. + * @hide + */ + public static final String SET_GLOBAL_HTTP_PROXY = "set_global_http_proxy"; + + /** + * Setting for default DNS in case nobody suggests one + * @hide + */ + public static final String DEFAULT_DNS_SERVER = "default_dns_server"; + /** * Whether the package installer should allow installation of apps downloaded from * sources other than the Android Market (vending machine). @@ -2392,6 +2424,14 @@ public final class Settings { public static final String LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED = "lock_pattern_tactile_feedback_enabled"; + /** + * This preference allows the device to be locked given time after screen goes off, + * subject to current DeviceAdmin policy limits. + * @hide + */ + public static final String LOCK_SCREEN_LOCK_AFTER_TIMEOUT = "lock_screen_lock_after_timeout"; + + /** * Whether assisted GPS should be enabled or not. * @hide @@ -2452,6 +2492,14 @@ public final class Settings { */ public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url"; + /** + * A positive value indicates the frequency of SamplingProfiler + * taking snapshots in hertz. Zero value means SamplingProfiler is disabled. + * + * @hide + */ + public static final String SAMPLING_PROFILER_HZ = "sampling_profiler_hz"; + /** * Settings classname to launch when Settings is clicked from All * Applications. Needed because of user testing between the old @@ -2482,6 +2530,60 @@ public final class Settings { public static final String ENABLED_ACCESSIBILITY_SERVICES = "enabled_accessibility_services"; + /** + * If injection of accessibility enhancing JavaScript scripts + * is enabled. + *

    + * Note: Accessibility injecting scripts are served by the + * Google infrastructure and enable users with disabilities to + * efficiantly navigate in and explore web content. + *

    + *

    + * This property represents a boolean value. + *

    + * @hide + */ + public static final String ACCESSIBILITY_SCRIPT_INJECTION = + "accessibility_script_injection"; + + /** + * Key bindings for navigation in built-in accessibility support for web content. + *

    + * Note: These key bindings are for the built-in accessibility navigation for + * web content which is used as a fall back solution if JavaScript in a WebView + * is not enabled or the user has not opted-in script injection from Google. + *

    + *

    + * The bindings are separated by semi-colon. A binding is a mapping from + * a key to a sequence of actions (for more details look at + * android.webkit.AccessibilityInjector). A key is represented as the hexademical + * string representation of an integer obtained from a meta state (optional) shifted + * sixteen times left and bitwise ored with a key code. An action is represented + * as a hexademical string representation of an integer where the first two digits + * are navigation action index, the second, the third, and the fourth digit pairs + * represent the action arguments. The separate actions in a binding are colon + * separated. The key and the action sequence it maps to are separated by equals. + *

    + *

    + * For example, the binding below maps the DPAD right button to traverse the + * current navigation axis once without firing an accessibility event and to + * perform the same traversal again but to fire an event: + * + * 0x16=0x01000100:0x01000101; + * + *

    + *

    + * The goal of this binding is to enable dynamic rebinding of keys to + * navigation actions for web content without requiring a framework change. + *

    + *

    + * This property represents a string value. + *

    + * @hide + */ + public static final String ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS = + "accessibility_web_content_key_bindings"; + /** * Setting to always use the default text-to-speech settings regardless * of the application settings. @@ -3462,6 +3564,7 @@ public final class Settings { PARENTAL_CONTROL_REDIRECT_URL, USB_MASS_STORAGE_ENABLED, ACCESSIBILITY_ENABLED, + ACCESSIBILITY_SCRIPT_INJECTION, BACKUP_AUTO_RESTORE, ENABLED_ACCESSIBILITY_SERVICES, TTS_USE_DEFAULTS, @@ -3606,7 +3709,7 @@ public final class Settings { while (intent == null && c.moveToNext()) { try { String intentURI = c.getString(c.getColumnIndexOrThrow(INTENT)); - intent = Intent.getIntent(intentURI); + intent = Intent.parseUri(intentURI, 0); } catch (java.net.URISyntaxException e) { // The stored URL is bad... ignore it. } catch (IllegalArgumentException e) { @@ -3645,26 +3748,14 @@ public final class Settings { // If a shortcut is supplied, and it is already defined for // another bookmark, then remove the old definition. if (shortcut != 0) { - Cursor c = cr.query(CONTENT_URI, - sShortcutProjection, sShortcutSelection, - new String[] { String.valueOf((int) shortcut) }, null); - try { - if (c.moveToFirst()) { - while (c.getCount() > 0) { - if (!c.deleteRow()) { - Log.w(TAG, "Could not delete existing shortcut row"); - } - } - } - } finally { - if (c != null) c.close(); - } + cr.delete(CONTENT_URI, sShortcutSelection, + new String[] { String.valueOf((int) shortcut) }); } ContentValues values = new ContentValues(); if (title != null) values.put(TITLE, title); if (folder != null) values.put(FOLDER, folder); - values.put(INTENT, intent.toURI()); + values.put(INTENT, intent.toUri(0)); if (shortcut != 0) values.put(SHORTCUT, (int) shortcut); values.put(ORDERING, ordering); return cr.insert(CONTENT_URI, values); @@ -3716,7 +3807,7 @@ public final class Settings { Intent intent; try { - intent = Intent.getIntent(intentUri); + intent = Intent.parseUri(intentUri, 0); } catch (URISyntaxException e) { return ""; } diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index bf9e8549aaaf406230cff15346eb1ff2add862e1..fa5cd8b6aee51718c1bffb0fff6260477dd64b01 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SqliteWrapper; import android.net.Uri; +import android.os.Environment; import android.telephony.SmsMessage; import android.text.TextUtils; import android.util.Config; @@ -561,15 +562,24 @@ public final class Telephony { * values:

    * *
      - *
    • transactionId (Integer) - The WAP transaction - * ID
    • + *
    • transactionId (Integer) - The WAP transaction ID
    • *
    • pduType (Integer) - The WAP PDU type
    • *
    • header (byte[]) - The header of the message
    • *
    • data (byte[]) - The data payload of the message
    • + *
    • contentTypeParameters (HashMap<String,String>) + * - Any parameters associated with the content type + * (decoded from the WSP Content-Type header)
    • *
    * *

    If a BroadcastReceiver encounters an error while processing * this intent it should set the result code appropriately.

    + * + *

    The contentTypeParameters extra value is map of content parameters keyed by + * their names.

    + * + *

    If any unassigned well-known parameters are encountered, the key of the map will + * be 'unassigned/0x...', where '...' is the hex value of the unassigned parameter. If + * a parameter has No-Value the value in the map will be null.

    */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String WAP_PUSH_RECEIVED_ACTION = @@ -582,7 +592,7 @@ public final class Telephony { */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String SIM_FULL_ACTION = - "android.provider.Telephony.SIM_FULL"; + "android.provider.Telephony.SIM_FULL"; /** * Broadcast Action: An incoming SMS has been rejected by the @@ -1526,7 +1536,8 @@ public final class Telephony { * 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 String SCRAP_FILE_PATH = + Environment.getExternalStorageDirectory().getPath() + "/mms/scrapSpace/.temp.jpg"; } public static final class Intents { diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index e05fe7b8ce9f273af922caefc85cb4a72509a6cc..ab79aaf645e828c0570d31a7464158df4ad05a9e 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -20,6 +20,8 @@ import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothInputDevice; +import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothUuid; import android.content.Context; import android.content.Intent; @@ -30,6 +32,8 @@ import android.util.Log; import java.util.HashMap; import java.util.Set; +import android.os.PowerManager; + /** * TODO: Move this to @@ -51,6 +55,9 @@ class BluetoothEventLoop { private final BluetoothService mBluetoothService; private final BluetoothAdapter mAdapter; private final Context mContext; + // The WakeLock is used for bringing up the LCD during a pairing request + // from remote device when Android is in Suspend state. + private PowerManager.WakeLock mWakeLock; private static final int EVENT_RESTART_BLUETOOTH = 1; private static final int EVENT_PAIRING_CONSENT_DELAYED_ACCEPT = 2; @@ -105,6 +112,11 @@ class BluetoothEventLoop { mContext = context; mPasskeyAgentRequestData = new HashMap(); mAdapter = adapter; + //WakeLock instantiation in BluetoothEventLoop class + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP + | PowerManager.ON_AFTER_RELEASE, TAG); + mWakeLock.setReferenceCounted(false); initializeNativeDataNative(); } @@ -290,7 +302,8 @@ class BluetoothEventLoop { return; } if (DBG) { - log("Device property changed:" + address + "property:" + name); + log("Device property changed: " + address + " property: " + + name + " value: " + propValues[1]); } BluetoothDevice device = mAdapter.getRemoteDevice(address); if (name.equals("Name")) { @@ -357,6 +370,44 @@ class BluetoothEventLoop { } } + private void onInputDevicePropertyChanged(String path, String[] propValues) { + String address = mBluetoothService.getAddressFromObjectPath(path); + if (address == null) { + Log.e(TAG, "onInputDevicePropertyChanged: Address of the remote device in null"); + return; + } + log(" Input Device : Name of Property is:" + propValues[0]); + boolean state = false; + if (propValues[1].equals("true")) { + state = true; + } + mBluetoothService.handleInputDevicePropertyChange(address, state); + } + + private void onPanDevicePropertyChanged(String deviceObjectPath, String[] propValues) { + String name = propValues[0]; + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (address == null) { + Log.e(TAG, "onPanDevicePropertyChanged: Address of the remote device in null"); + return; + } + if (DBG) { + log("Pan Device property changed: " + address + " property: " + + name + " value: "+ propValues[1]); + } + BluetoothDevice device = mAdapter.getRemoteDevice(address); + if (name.equals("Connected")) { + if (propValues[1].equals("false")) { + mBluetoothService.handlePanDeviceStateChange(device, + BluetoothInputDevice.STATE_DISCONNECTED); + } + } else if (name.equals("Interface")) { + String iface = propValues[1]; + mBluetoothService.handlePanDeviceStateChange(device, iface, + BluetoothInputDevice.STATE_CONNECTED); + } + } + private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) { String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { @@ -398,37 +449,46 @@ class BluetoothEventLoop { mHandler.sendMessageDelayed(message, 1500); return; } - + // Acquire wakelock during PIN code request to bring up LCD display + mWakeLock.acquire(); 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); + // Release wakelock to allow the LCD to go off after the PIN popup notifcation. + mWakeLock.release(); return; } private void onRequestPasskeyConfirmation(String objectPath, int passkey, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; - + // Acquire wakelock during PIN code request to bring up LCD display + mWakeLock.acquire(); 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); + // Release wakelock to allow the LCD to go off after the PIN popup notifcation. + mWakeLock.release(); return; } private void onRequestPasskey(String objectPath, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; - + // Acquire wakelock during PIN code request to bring up LCD display + mWakeLock.acquire(); 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); + // Release wakelock to allow the LCD to go off after the PIN popup notifcation. + mWakeLock.release(); return; } @@ -461,10 +521,14 @@ class BluetoothEventLoop { if (mBluetoothService.attemptAutoPair(address)) return; } } + // Acquire wakelock during PIN code request to bring up LCD display + mWakeLock.acquire(); 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); + // Release wakelock to allow the LCD to go off after the PIN popup notifcation. + mWakeLock.release(); return; } @@ -472,12 +536,16 @@ class BluetoothEventLoop { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; + // Acquire wakelock during PIN code request to bring up LCD display + mWakeLock.acquire(); 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); + //Release wakelock to allow the LCD to go off after the PIN popup notifcation. + mWakeLock.release(); } private void onRequestOobData(String objectPath , int nativeData) { @@ -492,6 +560,8 @@ class BluetoothEventLoop { } private boolean onAgentAuthorize(String objectPath, String deviceUuid) { + if (!mBluetoothService.isEnabled()) return false; + String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize"); @@ -500,15 +570,15 @@ class BluetoothEventLoop { boolean authorized = false; ParcelUuid uuid = ParcelUuid.fromString(deviceUuid); - BluetoothA2dp a2dp = new BluetoothA2dp(mContext); + BluetoothDevice device = mAdapter.getRemoteDevice(address); // 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)) && - !isOtherSinkInNonDisconnectingState(address)) { - BluetoothDevice device = mAdapter.getRemoteDevice(address); + if ((BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid) + || BluetoothUuid.isAdvAudioDist(uuid)) && + !isOtherSinkInNonDisconnectingState(address)) { + BluetoothA2dp a2dp = new BluetoothA2dp(mContext); + authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF; if (authorized) { Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address); @@ -516,6 +586,18 @@ class BluetoothEventLoop { } else { Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address); } + } else if (BluetoothUuid.isInputDevice(uuid) && !isOtherInputDeviceConnected(address)) { + BluetoothInputDevice inputDevice = new BluetoothInputDevice(mContext); + authorized = inputDevice.getInputDevicePriority(device) > + BluetoothInputDevice.PRIORITY_OFF; + if (authorized) { + Log.i(TAG, "Allowing incoming HID connection from " + address); + } else { + Log.i(TAG, "Rejecting incoming HID connection from " + address); + } + } else if (BluetoothUuid.isBnep(uuid) || BluetoothUuid.isNap(uuid) && + mBluetoothService.allowIncomingTethering()){ + authorized = true; } else { Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address); } @@ -523,6 +605,18 @@ class BluetoothEventLoop { return authorized; } + private boolean isOtherInputDeviceConnected(String address) { + Set devices = + mBluetoothService.lookupInputDevicesMatchingStates(new int[] { + BluetoothInputDevice.STATE_CONNECTING, + BluetoothInputDevice.STATE_CONNECTED}); + + for (BluetoothDevice device : devices) { + if (!device.getAddress().equals(address)) return true; + } + return false; + } + private boolean onAgentOutOfBandDataAvailable(String objectPath) { if (!mBluetoothService.isEnabled()) return false; @@ -534,7 +628,6 @@ class BluetoothEventLoop { return true; } return false; - } private boolean isOtherSinkInNonDisconnectingState(String address) { @@ -588,6 +681,60 @@ class BluetoothEventLoop { } } + private void onInputDeviceConnectionResult(String path, boolean result) { + // Success case gets handled by Property Change signal + if (!result) { + String address = mBluetoothService.getAddressFromObjectPath(path); + if (address == null) return; + + boolean connected = false; + BluetoothDevice device = mAdapter.getRemoteDevice(address); + int state = mBluetoothService.getInputDeviceState(device); + if (state == BluetoothInputDevice.STATE_CONNECTING) { + connected = false; + } else if (state == BluetoothInputDevice.STATE_DISCONNECTING) { + connected = true; + } else { + Log.e(TAG, "Error onInputDeviceConnectionResult. State is:" + state); + } + mBluetoothService.handleInputDevicePropertyChange(address, connected); + } + } + + private void onPanDeviceConnectionResult(String path, boolean result) { + log ("onPanDeviceConnectionResult " + path + " " + result); + // Success case gets handled by Property Change signal + if (!result) { + String address = mBluetoothService.getAddressFromObjectPath(path); + if (address == null) return; + + boolean connected = false; + BluetoothDevice device = mAdapter.getRemoteDevice(address); + int state = mBluetoothService.getPanDeviceState(device); + if (state == BluetoothPan.STATE_CONNECTING) { + connected = false; + } else if (state == BluetoothPan.STATE_DISCONNECTING) { + connected = true; + } else { + Log.e(TAG, "Error onPanDeviceConnectionResult. State is: " + + state + " result: "+ result); + } + int newState = connected? BluetoothPan.STATE_CONNECTED : + BluetoothPan.STATE_DISCONNECTED; + mBluetoothService.handlePanDeviceStateChange(device, newState); + } + } + + private void onNetworkDeviceDisconnected(String address) { + BluetoothDevice device = mAdapter.getRemoteDevice(address); + mBluetoothService.handlePanDeviceStateChange(device, BluetoothPan.STATE_DISCONNECTED); + } + + private void onNetworkDeviceConnected(String address, String iface, int destUuid) { + BluetoothDevice device = mAdapter.getRemoteDevice(address); + mBluetoothService.handlePanDeviceStateChange(device, iface, BluetoothPan.STATE_CONNECTED); + } + private void onRestartRequired() { if (mBluetoothService.isEnabled()) { Log.e(TAG, "*** A serious error occured (did bluetoothd crash?) - " + diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index dfe3a25fbc15cffd73932d7dc7801da199af6cd0..7f160c4a20012e72adbef5e4f6e92f1e37b7edef 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -29,7 +29,9 @@ import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothDeviceProfileState; +import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothProfileState; +import android.bluetooth.BluetoothInputDevice; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetooth; @@ -40,9 +42,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.res.Resources.NotFoundException; +import android.net.ConnectivityManager; +import android.net.InterfaceConfiguration; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.INetworkManagementService; import android.os.Message; import android.os.ParcelUuid; import android.os.RemoteException; @@ -71,8 +77,10 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import java.util.Set; public class BluetoothService extends IBluetooth.Stub { private static final String TAG = "BluetoothService"; @@ -85,6 +93,7 @@ public class BluetoothService extends IBluetooth.Stub { private int mBluetoothState; private boolean mRestart = false; // need to call enable() after disable() private boolean mIsDiscovering; + private boolean mTetheringOn; private BluetoothAdapter mAdapter; // constant after init() private final BondState mBondState = new BondState(); // local cache of bondings @@ -113,6 +122,13 @@ public class BluetoothService extends IBluetooth.Stub { private static final long INIT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY = 3000; private static final long MAX_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY = 12000; + private ArrayList mBluetoothIfaceAddresses; + private int mMaxPanDevices; + + private static final String BLUETOOTH_IFACE_ADDR_START= "192.168.44.1"; + private static final int BLUETOOTH_MAX_PAN_CONNECTIONS = 5; + private static final String BLUETOOTH_NETMASK = "255.255.255.0"; + // 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; @@ -136,10 +152,14 @@ public class BluetoothService extends IBluetooth.Stub { private final HashMap mDeviceProfileState; private final BluetoothProfileState mA2dpProfileState; private final BluetoothProfileState mHfpProfileState; + private final BluetoothProfileState mHidProfileState; private BluetoothA2dpService mA2dpService; + private final HashMap mInputDevices; + private final HashMap> mPanDevices; private final HashMap> mDeviceOobData; + private static String mDockAddress; private String mDockPin; @@ -190,6 +210,7 @@ public class BluetoothService extends IBluetooth.Stub { mBluetoothState = BluetoothAdapter.STATE_OFF; mIsDiscovering = false; + mTetheringOn = false; mAdapterProperties = new HashMap(); mDeviceProperties = new HashMap>(); @@ -201,15 +222,27 @@ public class BluetoothService extends IBluetooth.Stub { mDeviceProfileState = new HashMap(); mA2dpProfileState = new BluetoothProfileState(mContext, BluetoothProfileState.A2DP); mHfpProfileState = new BluetoothProfileState(mContext, BluetoothProfileState.HFP); + mHidProfileState = new BluetoothProfileState(mContext, BluetoothProfileState.HID); + + mBluetoothIfaceAddresses = new ArrayList(); + try { + mMaxPanDevices = context.getResources().getInteger( + com.android.internal.R.integer.config_max_pan_devices); + } catch (NotFoundException e) { + mMaxPanDevices = BLUETOOTH_MAX_PAN_CONNECTIONS; + } mHfpProfileState.start(); mA2dpProfileState.start(); + mHidProfileState.start(); IntentFilter filter = new IntentFilter(); registerForAirplaneMode(filter); filter.addAction(Intent.ACTION_DOCK_EVENT); mContext.registerReceiver(mReceiver, filter); + mInputDevices = new HashMap(); + mPanDevices = new HashMap>(); } public static synchronized String readDockBluetoothAddress() { @@ -337,6 +370,7 @@ public class BluetoothService extends IBluetooth.Stub { } setBluetoothState(BluetoothAdapter.STATE_TURNING_OFF); mHandler.removeMessages(MESSAGE_REGISTER_SDP_RECORDS); + setBluetoothTetheringNative(false, BluetoothPan.NAP_ROLE, BluetoothPan.NAP_BRIDGE); // Allow 3 seconds for profiles to gracefully disconnect // TODO: Introduce a callback mechanism so that each profile can notify @@ -567,8 +601,12 @@ public class BluetoothService extends IBluetooth.Stub { mBondState.readAutoPairingData(); mBondState.loadBondState(); initProfileState(); + + //Register SDP records. mHandler.sendMessageDelayed( mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 1, -1), 3000); + setBluetoothTetheringNative(true, BluetoothPan.NAP_ROLE, BluetoothPan.NAP_BRIDGE); + // Log bluetooth on to battery stats. long ident = Binder.clearCallingIdentity(); @@ -747,6 +785,10 @@ public class BluetoothService extends IBluetooth.Stub { removeProfileState(address); } + // HID is handled by BluetoothService, other profiles + // will be handled by their respective services. + setInitialInputDevicePriority(mAdapter.getRemoteDevice(address), state); + if (DBG) log(address + " bond state " + oldState + " -> " + state + " (" + reason + ")"); Intent intent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED); @@ -1362,6 +1404,409 @@ public class BluetoothService extends IBluetooth.Stub { return sp.contains(SHARED_PREFERENCE_DOCK_ADDRESS + address); } + public synchronized boolean isTetheringOn() { + return mTetheringOn; + } + + /*package*/ synchronized boolean allowIncomingTethering() { + if (isTetheringOn() && getConnectedPanDevices().length < mMaxPanDevices) + return true; + return false; + } + + private BroadcastReceiver mTetheringReceiver = null; + + public synchronized void setBluetoothTethering(boolean value) { + if (!value) { + disconnectPan(); + } + + if (getBluetoothState() != BluetoothAdapter.STATE_ON && value) { + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + mTetheringReceiver = new BroadcastReceiver() { + @Override + public synchronized void onReceive(Context context, Intent intent) { + if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF) + == BluetoothAdapter.STATE_ON) { + mTetheringOn = true; + mContext.unregisterReceiver(mTetheringReceiver); + } + } + }; + mContext.registerReceiver(mTetheringReceiver, filter); + } else { + mTetheringOn = value; + } + } + + public synchronized int getPanDeviceState(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + + Pair panDevice = mPanDevices.get(device); + if (panDevice == null) { + return BluetoothPan.STATE_DISCONNECTED; + } + return panDevice.first; + } + + public synchronized boolean connectPanDevice(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + + String objectPath = getObjectPathFromAddress(device.getAddress()); + if (DBG) log("connect PAN(" + objectPath + ")"); + if (getPanDeviceState(device) != BluetoothPan.STATE_DISCONNECTED) { + log (device + " already connected to PAN"); + } + + int connectedCount = 0; + for (BluetoothDevice panDevice: mPanDevices.keySet()) { + if (getPanDeviceState(panDevice) == BluetoothPan.STATE_CONNECTED) { + connectedCount ++; + } + } + if (connectedCount > 8) { + log (device + " could not connect to PAN because 8 other devices are already connected"); + return false; + } + + handlePanDeviceStateChange(device, BluetoothPan.STATE_CONNECTING); + if (connectPanDeviceNative(objectPath, "nap", "panu")) { + log ("connecting to PAN"); + return true; + } else { + handlePanDeviceStateChange(device, BluetoothPan.STATE_DISCONNECTED); + log ("could not connect to PAN"); + return false; + } + } + + private synchronized boolean disconnectPan() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (DBG) log("disconnect all PAN devices"); + + for (BluetoothDevice device: mPanDevices.keySet()) { + if (getPanDeviceState(device) == BluetoothPan.STATE_CONNECTED) { + if (!disconnectPanDevice(device)) { + log ("could not disconnect Pan Device "+device.getAddress()); + return false; + } + } + } + return true; + } + + public synchronized BluetoothDevice[] getConnectedPanDevices() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + + Set devices = new HashSet(); + for (BluetoothDevice device: mPanDevices.keySet()) { + if (getPanDeviceState(device) == BluetoothPan.STATE_CONNECTED) { + devices.add(device); + } + } + return devices.toArray(new BluetoothDevice[devices.size()]); + } + + public synchronized boolean disconnectPanDevice(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + String objectPath = getObjectPathFromAddress(device.getAddress()); + if (DBG) log("disconnect PAN(" + objectPath + ")"); + if (getPanDeviceState(device) != BluetoothPan.STATE_CONNECTED) { + log (device + " already disconnected from PAN"); + } + handlePanDeviceStateChange(device, BluetoothPan.STATE_DISCONNECTING); + return disconnectPanDeviceNative(objectPath); + } + + /*package*/ synchronized void handlePanDeviceStateChange(BluetoothDevice device, + String iface, + int state) { + int prevState; + String ifaceAddr = null; + + if (mPanDevices.get(device) == null) { + prevState = BluetoothPan.STATE_DISCONNECTED; + } else { + prevState = mPanDevices.get(device).first; + ifaceAddr = mPanDevices.get(device).second; + } + if (prevState == state) return; + + if (state == BluetoothPan.STATE_CONNECTED) { + ifaceAddr = enableTethering(iface); + if (ifaceAddr == null) Log.e(TAG, "Error seting up tether interface"); + } else if (state == BluetoothPan.STATE_DISCONNECTED) { + if (ifaceAddr != null) { + mBluetoothIfaceAddresses.remove(ifaceAddr); + ifaceAddr = null; + } + } + + Pair value = new Pair(state, ifaceAddr); + mPanDevices.put(device, value); + + Intent intent = new Intent(BluetoothPan.ACTION_PAN_STATE_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothPan.EXTRA_PREVIOUS_PAN_STATE, prevState); + intent.putExtra(BluetoothPan.EXTRA_PAN_STATE, state); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + if (DBG) log("Pan Device state : device: " + device + " State:" + prevState + "->" + state); + } + + /*package*/ synchronized void handlePanDeviceStateChange(BluetoothDevice device, + int state) { + handlePanDeviceStateChange(device, null, state); + } + + private String createNewTetheringAddressLocked() { + if (getConnectedPanDevices().length == mMaxPanDevices) { + log("Max PAN device connections reached"); + return null; + } + String address = BLUETOOTH_IFACE_ADDR_START; + while (true) { + if (mBluetoothIfaceAddresses.contains(address)) { + String[] addr = address.split("\\."); + Integer newIp = Integer.parseInt(addr[2]) + 1; + address = address.replace(addr[2], newIp.toString()); + } else { + break; + } + } + mBluetoothIfaceAddresses.add(address); + return address; + } + + // configured when we start tethering + private synchronized String enableTethering(String iface) { + log("updateTetherState:" + iface); + + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + INetworkManagementService service = INetworkManagementService.Stub.asInterface(b); + ConnectivityManager cm = + (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + String[] bluetoothRegexs = cm.getTetherableBluetoothRegexs(); + + // bring toggle the interfaces + String[] currentIfaces = new String[0]; + try { + currentIfaces = service.listInterfaces(); + } catch (Exception e) { + Log.e(TAG, "Error listing Interfaces :" + e); + return null; + } + + boolean found = false; + for (String currIface: currentIfaces) { + if (currIface.equals(iface)) { + found = true; + break; + } + } + + if (!found) return null; + + String address = createNewTetheringAddressLocked(); + if (address == null) return null; + + InterfaceConfiguration ifcg = null; + try { + ifcg = service.getInterfaceConfig(iface); + if (ifcg != null) { + String[] addr = BLUETOOTH_NETMASK.split("\\."); + ifcg.netmask = (Integer.parseInt(addr[0]) << 24) + + (Integer.parseInt(addr[1]) << 16) + + (Integer.parseInt(addr[2]) << 8) + + (Integer.parseInt(addr[3])); + if (ifcg.ipAddr == 0) { + addr = address.split("\\."); + + ifcg.ipAddr = (Integer.parseInt(addr[0]) << 24) + + (Integer.parseInt(addr[1]) << 16) + + (Integer.parseInt(addr[2]) << 8) + + (Integer.parseInt(addr[3])); + ifcg.interfaceFlags = + ifcg.interfaceFlags.replace("down", "up"); + } + ifcg.interfaceFlags = ifcg.interfaceFlags.replace("running", ""); + ifcg.interfaceFlags = ifcg.interfaceFlags.replace(" "," "); + service.setInterfaceConfig(iface, ifcg); + if (cm.tether(iface) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { + Log.e(TAG, "Error tethering "+iface); + } + } + } catch (Exception e) { + Log.e(TAG, "Error configuring interface " + iface + ", :" + e); + return null; + } + return address; + } + + public synchronized boolean connectInputDevice(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + + String objectPath = getObjectPathFromAddress(device.getAddress()); + if (objectPath == null || + getInputDeviceState(device) != BluetoothInputDevice.STATE_DISCONNECTED || + getInputDevicePriority(device) == BluetoothInputDevice.PRIORITY_OFF) { + return false; + } + BluetoothDeviceProfileState state = mDeviceProfileState.get(device.getAddress()); + if (state != null) { + Message msg = new Message(); + msg.arg1 = BluetoothDeviceProfileState.CONNECT_HID_OUTGOING; + msg.obj = state; + mHidProfileState.sendMessage(msg); + return true; + } + return false; + } + + public synchronized boolean connectInputDeviceInternal(BluetoothDevice device) { + String objectPath = getObjectPathFromAddress(device.getAddress()); + handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_CONNECTING); + if (!connectInputDeviceNative(objectPath)) { + handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_DISCONNECTED); + return false; + } + return true; + } + + public synchronized boolean disconnectInputDevice(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + + String objectPath = getObjectPathFromAddress(device.getAddress()); + if (objectPath == null || getConnectedInputDevices().length == 0) { + return false; + } + BluetoothDeviceProfileState state = mDeviceProfileState.get(device.getAddress()); + if (state != null) { + Message msg = new Message(); + msg.arg1 = BluetoothDeviceProfileState.DISCONNECT_HID_OUTGOING; + msg.obj = state; + mHidProfileState.sendMessage(msg); + return true; + } + return false; + } + + public synchronized boolean disconnectInputDeviceInternal(BluetoothDevice device) { + String objectPath = getObjectPathFromAddress(device.getAddress()); + handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_DISCONNECTING); + if (!disconnectInputDeviceNative(objectPath)) { + handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_CONNECTED); + return false; + } + return true; + } + + public synchronized int getInputDeviceState(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + + if (mInputDevices.get(device) == null) { + return BluetoothInputDevice.STATE_DISCONNECTED; + } + return mInputDevices.get(device); + } + + public synchronized BluetoothDevice[] getConnectedInputDevices() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + Set devices = lookupInputDevicesMatchingStates( + new int[] {BluetoothInputDevice.STATE_CONNECTED}); + return devices.toArray(new BluetoothDevice[devices.size()]); + } + + public synchronized int getInputDevicePriority(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()), + BluetoothInputDevice.PRIORITY_UNDEFINED); + } + + public synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { + return false; + } + return Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()), + priority); + } + + /*package*/synchronized Set lookupInputDevicesMatchingStates(int[] states) { + Set inputDevices = new HashSet(); + if (mInputDevices.isEmpty()) { + return inputDevices; + } + for (BluetoothDevice device: mInputDevices.keySet()) { + int inputDeviceState = getInputDeviceState(device); + for (int state : states) { + if (state == inputDeviceState) { + inputDevices.add(device); + break; + } + } + } + return inputDevices; + } + + private synchronized void handleInputDeviceStateChange(BluetoothDevice device, int state) { + int prevState; + if (mInputDevices.get(device) == null) { + prevState = BluetoothInputDevice.STATE_DISCONNECTED; + } else { + prevState = mInputDevices.get(device); + } + if (prevState == state) return; + + mInputDevices.put(device, state); + + if (getInputDevicePriority(device) > + BluetoothInputDevice.PRIORITY_OFF && + state == BluetoothInputDevice.STATE_CONNECTING || + state == BluetoothInputDevice.STATE_CONNECTED) { + // We have connected or attempting to connect. + // Bump priority + setInputDevicePriority(device, BluetoothInputDevice.PRIORITY_AUTO_CONNECT); + } + + Intent intent = new Intent(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothInputDevice.EXTRA_PREVIOUS_INPUT_DEVICE_STATE, prevState); + intent.putExtra(BluetoothInputDevice.EXTRA_INPUT_DEVICE_STATE, state); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + if (DBG) log("InputDevice state : device: " + device + " State:" + prevState + "->" + state); + + } + + /*package*/ void handleInputDevicePropertyChange(String address, boolean connected) { + int state = connected ? BluetoothInputDevice.STATE_CONNECTED : + BluetoothInputDevice.STATE_DISCONNECTED; + BluetoothDevice device = mAdapter.getRemoteDevice(address); + handleInputDeviceStateChange(device, state); + } + + private void setInitialInputDevicePriority(BluetoothDevice device, int state) { + switch (state) { + case BluetoothDevice.BOND_BONDED: + if (getInputDevicePriority(device) == BluetoothInputDevice.PRIORITY_UNDEFINED) { + setInputDevicePriority(device, BluetoothInputDevice.PRIORITY_ON); + } + break; + case BluetoothDevice.BOND_NONE: + setInputDevicePriority(device, BluetoothInputDevice.PRIORITY_UNDEFINED); + break; + } + } + /*package*/ boolean isRemoteDeviceInCache(String address) { return (mDeviceProperties.get(address) != null); } @@ -1384,7 +1829,7 @@ public class BluetoothService extends IBluetooth.Stub { if (updateRemoteDevicePropertiesCache(address)) return getRemoteDeviceProperty(address, property); } - Log.e(TAG, "getRemoteDeviceProperty: " + property + "not present:" + address); + Log.e(TAG, "getRemoteDeviceProperty: " + property + " not present: " + address); return null; } @@ -2277,4 +2722,10 @@ public class BluetoothService extends IBluetooth.Stub { short channel); private native boolean removeServiceRecordNative(int handle); private native boolean setLinkTimeoutNative(String path, int num_slots); + private native boolean connectInputDeviceNative(String path); + private native boolean disconnectInputDeviceNative(String path); + + private native boolean setBluetoothTetheringNative(boolean value, String nap, String bridge); + private native boolean connectPanDeviceNative(String path, String srcRole, String dstRole); + private native boolean disconnectPanDeviceNative(String path); } diff --git a/core/java/android/speech/SpeechRecognizer.java b/core/java/android/speech/SpeechRecognizer.java index 8fa0d593d87b5cc158c114fe2b95a72053091898..cd73ba85063183334f0d5f1dcdbe9e6f68d3d19e 100644 --- a/core/java/android/speech/SpeechRecognizer.java +++ b/core/java/android/speech/SpeechRecognizer.java @@ -50,7 +50,7 @@ public class SpeechRecognizer { private static final String TAG = "SpeechRecognizer"; /** - * Used to retrieve an {@code ArrayList<String>} from the {@link Bundle} passed to the + * Used to retrieve an {@code ArrayList} from the {@link Bundle} passed to the * {@link RecognitionListener#onResults(Bundle)} and * {@link RecognitionListener#onPartialResults(Bundle)} methods. These strings are the possible * recognition results, where the first element is the most likely candidate. diff --git a/core/java/android/text/AndroidBidi.java b/core/java/android/text/AndroidBidi.java index e4f934e7217326485753100ef18ed3121db72447..eacd40d0db29e353eb9252bf629361c2a05c5cd6 100644 --- a/core/java/android/text/AndroidBidi.java +++ b/core/java/android/text/AndroidBidi.java @@ -16,6 +16,8 @@ package android.text; +import android.text.Layout.Directions; + /** * Access the ICU bidi implementation. * @hide @@ -44,5 +46,132 @@ package android.text; return result; } + /** + * Returns run direction information for a line within a paragraph. + * + * @param dir base line direction, either Layout.DIR_LEFT_TO_RIGHT or + * Layout.DIR_RIGHT_TO_LEFT + * @param levels levels as returned from {@link #bidi} + * @param lstart start of the line in the levels array + * @param chars the character array (used to determine whitespace) + * @param cstart the start of the line in the chars array + * @param len the length of the line + * @return the directions + */ + public static Directions directions(int dir, byte[] levels, int lstart, + char[] chars, int cstart, int len) { + + int baseLevel = dir == Layout.DIR_LEFT_TO_RIGHT ? 0 : 1; + int curLevel = levels[lstart]; + int minLevel = curLevel; + int runCount = 1; + for (int i = lstart + 1, e = lstart + len; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + ++runCount; + } + } + + // add final run for trailing counter-directional whitespace + int visLen = len; + if ((curLevel & 1) != (baseLevel & 1)) { + // look for visible end + while (--visLen >= 0) { + char ch = chars[cstart + visLen]; + + if (ch == '\n') { + --visLen; + break; + } + + if (ch != ' ' && ch != '\t') { + break; + } + } + ++visLen; + if (visLen != len) { + ++runCount; + } + } + + if (runCount == 1 && minLevel == baseLevel) { + // we're done, only one run on this line + if ((minLevel & 1) != 0) { + return Layout.DIRS_ALL_RIGHT_TO_LEFT; + } + return Layout.DIRS_ALL_LEFT_TO_RIGHT; + } + + int[] ld = new int[runCount * 2]; + int maxLevel = minLevel; + int levelBits = minLevel << Layout.RUN_LEVEL_SHIFT; + { + // Start of first pair is always 0, we write + // length then start at each new run, and the + // last run length after we're done. + int n = 1; + int prev = lstart; + curLevel = minLevel; + for (int i = lstart, e = lstart + visLen; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + if (level > maxLevel) { + maxLevel = level; + } else if (level < minLevel) { + minLevel = level; + } + // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT + ld[n++] = (i - prev) | levelBits; + ld[n++] = i - lstart; + levelBits = curLevel << Layout.RUN_LEVEL_SHIFT; + prev = i; + } + } + ld[n] = (lstart + visLen - prev) | levelBits; + if (visLen < len) { + ld[++n] = visLen; + ld[++n] = (len - visLen) | (baseLevel << Layout.RUN_LEVEL_SHIFT); + } + } + + // See if we need to swap any runs. + // If the min level run direction doesn't match the base + // direction, we always need to swap (at this point + // we have more than one run). + // Otherwise, we don't need to swap the lowest level. + // Since there are no logically adjacent runs at the same + // level, if the max level is the same as the (new) min + // level, we have a series of alternating levels that + // is already in order, so there's no more to do. + // + boolean swap; + if ((minLevel & 1) == baseLevel) { + minLevel += 1; + swap = maxLevel > minLevel; + } else { + swap = runCount > 1; + } + if (swap) { + for (int level = maxLevel - 1; level >= minLevel; --level) { + for (int i = 0; i < ld.length; i += 2) { + if (levels[ld[i]] >= level) { + int e = i + 2; + while (e < ld.length && levels[ld[e]] >= level) { + e += 2; + } + for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) { + int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x; + x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x; + } + i = e + 2; + } + } + } + } + return new Directions(ld); + } + private native static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo); } \ No newline at end of file diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 944f7354fdb2cd326976fc6ae7d14c6b961095cd..9309b05c6fdaacf0a31a8f91d07fe6a4c2c0746a 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -208,11 +208,11 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * width because the width that was passed in was for the * full text, not the ellipsized form. */ - synchronized (sTemp) { - mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - source, 0, source.length(), - null))); - } + TextLine line = TextLine.obtain(); + line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + mMax = (int) FloatMath.ceil(line.metrics(null)); + TextLine.recycle(line); } if (includepad) { @@ -276,14 +276,13 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback if (fm == null) { fm = new Metrics(); } - - int wid; - synchronized (sTemp) { - wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - text, 0, text.length(), fm))); - } - fm.width = wid; + TextLine line = TextLine.obtain(); + line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + fm.width = (int) FloatMath.ceil(line.metrics(fm)); + TextLine.recycle(line); + return fm; } else { return null; @@ -389,7 +388,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback public static class Metrics extends Paint.FontMetricsInt { public int width; - + @Override public String toString() { return super.toString() + " width=" + width; } diff --git a/core/java/android/text/ClipboardManager.java b/core/java/android/text/ClipboardManager.java index 52039afe474283d41e9a1c85ceb46694daff26fb..0b239cfe7a294e1586cbba00a51356fd311d0833 100644 --- a/core/java/android/text/ClipboardManager.java +++ b/core/java/android/text/ClipboardManager.java @@ -16,73 +16,26 @@ package android.text; -import android.content.Context; -import android.os.RemoteException; -import android.os.Handler; -import android.os.IBinder; -import android.os.ServiceManager; -import android.util.Log; - /** - * Interface to the clipboard service, for placing and retrieving text in - * the global clipboard. - * - *

    - * You do not instantiate this class directly; instead, retrieve it through - * {@link android.content.Context#getSystemService}. - * - * @see android.content.Context#getSystemService + * @deprecated Old text-only interace to the clipboard. See + * {@link android.content.ClipboardManager} for the modern API. */ -public class ClipboardManager { - private static IClipboard sService; - - private Context mContext; - - static private IClipboard getService() { - if (sService != null) { - return sService; - } - IBinder b = ServiceManager.getService("clipboard"); - sService = IClipboard.Stub.asInterface(b); - return sService; - } - - /** {@hide} */ - public ClipboardManager(Context context, Handler handler) { - mContext = context; - } - +@Deprecated +public abstract class ClipboardManager { /** * Returns the text on the clipboard. It will eventually be possible * to store types other than text too, in which case this will return * null if the type cannot be coerced to text. */ - public CharSequence getText() { - try { - return getService().getClipboardText(); - } catch (RemoteException e) { - return null; - } - } + public abstract CharSequence getText(); /** * Sets the contents of the clipboard to the specified text. */ - public void setText(CharSequence text) { - try { - getService().setClipboardText(text); - } catch (RemoteException e) { - } - } + public abstract void setText(CharSequence text); /** * Returns true if the clipboard contains text; false otherwise. */ - public boolean hasText() { - try { - return getService().hasClipboardText(); - } catch (RemoteException e) { - return false; - } - } + public abstract boolean hasText(); } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 14e5655bb448ba2f7cfbe0b1cd4ff34a57425df7..b6aa03ab7124c8245a5dc9f1c4c1feb9bb24bd83 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -310,7 +310,6 @@ extends Layout Directions[] objects = new Directions[1]; - for (int i = 0; i < n; i++) { ints[START] = reflowed.getLineStart(i) | (reflowed.getParagraphDirection(i) << DIR_SHIFT) | diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java index c3bd0aeb8c975582d192a52b1f56133f3b0cc96a..d426d1247c7bb900df3096c9817d500144d608bb 100644 --- a/core/java/android/text/GraphicsOperations.java +++ b/core/java/android/text/GraphicsOperations.java @@ -34,13 +34,33 @@ extends CharSequence float x, float y, Paint p); /** + * Just like {@link Canvas#drawTextRun}. + * {@hide} + */ + void drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd, + float x, float y, int flags, Paint p); + + /** * Just like {@link Paint#measureText}. */ float measureText(int start, int end, Paint p); - /** * Just like {@link Paint#getTextWidths}. */ public int getTextWidths(int start, int end, float[] widths, Paint p); + + /** + * Just like {@link Paint#getTextRunAdvances}. + * @hide + */ + float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, + int flags, float[] advances, int advancesIndex, Paint paint); + + /** + * Just like {@link Paint#getTextRunCursor}. + * @hide + */ + int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, + int cursorOpt, Paint p); } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 38ac9b7322796fa507081b806054b4ca7c4676be..54ac906c327795fcba149c396cace69f39e42ae8 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,29 +16,33 @@ package android.text; +import com.android.internal.util.ArrayUtils; + import android.emoji.EmojiFactory; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Path; -import com.android.internal.util.ArrayUtils; - -import junit.framework.Assert; -import android.text.style.*; +import android.graphics.Rect; import android.text.method.TextKeyListener; +import android.text.style.AlignmentSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.LineBackgroundSpan; +import android.text.style.ParagraphStyle; +import android.text.style.ReplacementSpan; +import android.text.style.TabStopSpan; +import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; import android.view.KeyEvent; +import java.util.Arrays; + /** - * A base class that manages text layout in visual elements on - * the screen. - *

    For text that will be edited, use a {@link DynamicLayout}, - * which will be updated as the text changes. + * A base class that manages text layout in visual elements on + * the screen. + *

    For text that will be edited, use a {@link DynamicLayout}, + * which will be updated as the text changes. * For text that will not change, use a {@link StaticLayout}. */ public abstract class Layout { - private static final boolean DEBUG = false; private static final ParagraphStyle[] NO_PARA_SPANS = ArrayUtils.emptyArray(ParagraphStyle.class); @@ -54,9 +58,7 @@ public abstract class Layout { MIN_EMOJI = -1; MAX_EMOJI = -1; } - }; - - private RectF mEmojiRect; + } /** * Return how wide a layout must be in order to display the @@ -66,7 +68,7 @@ public abstract class Layout { TextPaint paint) { return getDesiredWidth(source, 0, source.length(), paint); } - + /** * Return how wide a layout must be in order to display the * specified text slice with one line per paragraph. @@ -85,8 +87,7 @@ public abstract class Layout { next = end; // note, omits trailing paragraph char - float w = measureText(paint, workPaint, - source, i, next, null, true, null); + float w = measurePara(paint, workPaint, source, i, next); if (w > need) need = w; @@ -116,6 +117,15 @@ public abstract class Layout { if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); + // Ensure paint doesn't have baselineShift set. + // While normally we don't modify the paint the user passed in, + // we were already doing this in Styled.drawUniformRun with both + // baselineShift and bgColor. We probably should reevaluate bgColor. + if (paint != null) { + paint.bgColor = 0; + paint.baselineShift = 0; + } + mText = text; mPaint = paint; mWorkPaint = new TextPaint(); @@ -175,7 +185,6 @@ public abstract class Layout { dbottom = sTempRect.bottom; } - int top = 0; int bottom = getLineTop(getLineCount()); @@ -185,26 +194,28 @@ public abstract class Layout { if (dbottom < bottom) { bottom = dbottom; } - - int first = getLineForVertical(top); + + int first = getLineForVertical(top); int last = getLineForVertical(bottom); - + int previousLineBottom = getLineTop(first); int previousLineEnd = getLineStart(first); - + TextPaint paint = mPaint; CharSequence buf = mText; int width = mWidth; boolean spannedText = mSpannedText; ParagraphStyle[] spans = NO_PARA_SPANS; - int spanend = 0; + int spanEnd = 0; int textLength = 0; // First, draw LineBackgroundSpans. - // LineBackgroundSpans know nothing about the alignment or direction of - // the layout or line. XXX: Should they? + // LineBackgroundSpans know nothing about the alignment, margins, or + // direction of the layout or line. XXX: Should they? + // They are evaluated at each line. if (spannedText) { + Spanned sp = (Spanned) buf; textLength = buf.length(); for (int i = first; i <= last; i++) { int start = previousLineEnd; @@ -216,12 +227,14 @@ public abstract class Layout { previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(i); - if (start >= spanend) { - Spanned sp = (Spanned) buf; - spanend = sp.nextSpanTransition(start, textLength, - LineBackgroundSpan.class); - spans = sp.getSpans(start, spanend, - LineBackgroundSpan.class); + if (start >= spanEnd) { + // These should be infrequent, so we'll use this so that + // we don't have to check as often. + spanEnd = sp.nextSpanTransition(start, textLength, + LineBackgroundSpan.class); + // All LineBackgroundSpans on a line contribute to its + // background. + spans = getParagraphSpans(sp, start, end, LineBackgroundSpan.class); } for (int n = 0; n < spans.length; n++) { @@ -234,11 +247,11 @@ public abstract class Layout { } } // reset to their original values - spanend = 0; + spanEnd = 0; previousLineBottom = getLineTop(first); previousLineEnd = getLineStart(first); spans = NO_PARA_SPANS; - } + } // There can be a highlight even without spans if we are drawing // a non-spanned transformation of a spanned editing buffer. @@ -255,7 +268,11 @@ public abstract class Layout { } Alignment align = mAlignment; - + TabStops tabStops = null; + boolean tabStopsIsInitialized = false; + + TextLine tl = TextLine.obtain(); + // Next draw the lines, one at a time. // the baseline is the top of the following line minus the current // line's descent. @@ -270,19 +287,30 @@ public abstract class Layout { previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(i); - boolean isFirstParaLine = false; - if (spannedText) { - if (start == 0 || buf.charAt(start - 1) == '\n') { - isFirstParaLine = true; - } - // New batch of paragraph styles, compute the alignment. - // Last alignment style wins. - if (start >= spanend) { - Spanned sp = (Spanned) buf; - spanend = sp.nextSpanTransition(start, textLength, + int dir = getParagraphDirection(i); + int left = 0; + int right = mWidth; + + if (spannedText) { + Spanned sp = (Spanned) buf; + boolean isFirstParaLine = (start == 0 || + buf.charAt(start - 1) == '\n'); + + // New batch of paragraph styles, collect into spans array. + // Compute the alignment, last alignment style wins. + // Reset tabStops, we'll rebuild if we encounter a line with + // tabs. + // We expect paragraph spans to be relatively infrequent, use + // spanEnd so that we can check less frequently. Since + // paragraph styles ought to apply to entire paragraphs, we can + // just collect the ones present at the start of the paragraph. + // If spanEnd is before the end of the paragraph, that's not + // our problem. + if (start >= spanEnd && (i == first || isFirstParaLine)) { + spanEnd = sp.nextSpanTransition(start, textLength, ParagraphStyle.class); - spans = sp.getSpans(start, spanend, ParagraphStyle.class); - + spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); + align = mAlignment; for (int n = spans.length-1; n >= 0; n--) { if (spans[n] instanceof AlignmentSpan) { @@ -290,45 +318,49 @@ public abstract class Layout { break; } } + + tabStopsIsInitialized = false; } - } - - int dir = getParagraphDirection(i); - int left = 0; - int right = mWidth; - // Draw all leading margin spans. Adjust left or right according - // to the paragraph direction of the line. - if (spannedText) { + // Draw all leading margin spans. Adjust left or right according + // to the paragraph direction of the line. final int length = spans.length; for (int n = 0; n < length; n++) { if (spans[n] instanceof LeadingMarginSpan) { LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; + boolean useFirstLineMargin = isFirstParaLine; + if (margin instanceof LeadingMarginSpan2) { + int count = ((LeadingMarginSpan2) margin).getLeadingMarginLineCount(); + int startLine = getLineForOffset(sp.getSpanStart(margin)); + useFirstLineMargin = i < startLine + count; + } if (dir == DIR_RIGHT_TO_LEFT) { margin.drawLeadingMargin(c, paint, right, dir, ltop, lbaseline, lbottom, buf, start, end, isFirstParaLine, this); - - right -= margin.getLeadingMargin(isFirstParaLine); + right -= margin.getLeadingMargin(useFirstLineMargin); } else { margin.drawLeadingMargin(c, paint, left, dir, ltop, lbaseline, lbottom, buf, start, end, isFirstParaLine, this); - - boolean useMargin = isFirstParaLine; - if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) { - int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount(); - useMargin = count > i; - } - left += margin.getLeadingMargin(useMargin); + left += margin.getLeadingMargin(useFirstLineMargin); } } } } - // Adjust the point at which to start rendering depending on the - // alignment of the paragraph. + boolean hasTabOrEmoji = getLineContainsTab(i); + // Can't tell if we have tabs for sure, currently + if (hasTabOrEmoji && !tabStopsIsInitialized) { + if (tabStops == null) { + tabStops = new TabStops(TAB_INCREMENT, spans); + } else { + tabStops.reset(TAB_INCREMENT, spans); + } + tabStopsIsInitialized = true; + } + int x; if (align == Alignment.ALIGN_NORMAL) { if (dir == DIR_LEFT_TO_RIGHT) { @@ -337,41 +369,80 @@ public abstract class Layout { x = right; } } else { - int max = (int)getLineMax(i, spans, false); + int max = (int)getLineExtent(i, tabStops, false); if (align == Alignment.ALIGN_OPPOSITE) { - if (dir == DIR_RIGHT_TO_LEFT) { - x = left + max; - } else { + if (dir == DIR_LEFT_TO_RIGHT) { x = right - max; - } - } else { - // Alignment.ALIGN_CENTER - max = max & ~1; - int half = (right - left - max) >> 1; - if (dir == DIR_RIGHT_TO_LEFT) { - x = right - half; } else { - x = left + half; + x = left - max; } + } else { // Alignment.ALIGN_CENTER + max = max & ~1; + x = (right + left - max) >> 1; } } Directions directions = getLineDirections(i); - boolean hasTab = getLineContainsTab(i); if (directions == DIRS_ALL_LEFT_TO_RIGHT && - !spannedText && !hasTab) { - if (DEBUG) { - Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT); - Assert.assertNotNull(c); - } + !spannedText && !hasTabOrEmoji) { // XXX: assumes there's nothing additional to be done c.drawText(buf, start, end, x, lbaseline, paint); } else { - drawText(c, buf, start, end, dir, directions, - x, ltop, lbaseline, lbottom, paint, mWorkPaint, - hasTab, spans); + tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops); + tl.draw(c, x, ltop, lbaseline, lbottom); + } + } + + TextLine.recycle(tl); + } + + /** + * Return the start position of the line, given the left and right bounds + * of the margins. + * + * @param line the line index + * @param left the left bounds (0, or leading margin if ltr para) + * @param right the right bounds (width, minus leading margin if rtl para) + * @return the start position of the line (to right of line if rtl para) + */ + private int getLineStartPos(int line, int left, int right) { + // Adjust the point at which to start rendering depending on the + // alignment of the paragraph. + Alignment align = getParagraphAlignment(line); + int dir = getParagraphDirection(line); + + int x; + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_LEFT_TO_RIGHT) { + x = left; + } else { + x = right; + } + } else { + TabStops tabStops = null; + if (mSpannedText && getLineContainsTab(line)) { + Spanned spanned = (Spanned) mText; + int start = getLineStart(line); + int spanEnd = spanned.nextSpanTransition(start, spanned.length(), + TabStopSpan.class); + TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd, TabStopSpan.class); + if (tabSpans.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, tabSpans); + } + } + int max = (int)getLineExtent(line, tabStops, false); + if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_LEFT_TO_RIGHT) { + x = right - max; + } else { + x = left - max; + } + } else { // Alignment.ALIGN_CENTER + max = max & ~1; + x = (left + right - max) >> 1; } } + return x; } /** @@ -417,7 +488,7 @@ public abstract class Layout { mWidth = wid; } - + /** * Return the total height of this layout. */ @@ -450,7 +521,7 @@ public abstract class Layout { * Return the number of lines of text in this layout. */ public abstract int getLineCount(); - + /** * Return the baseline for the specified line (0…getLineCount() - 1) * If bounds is not null, return the top, left, right, bottom extents @@ -524,13 +595,95 @@ public abstract class Layout { */ public abstract int getBottomPadding(); + + /** + * Returns true if the character at offset and the preceding character + * are at different run levels (and thus there's a split caret). + * @param offset the offset + * @return true if at a level boundary + */ + private boolean isLevelBoundary(int offset) { + int line = getLineForOffset(offset); + Directions dirs = getLineDirections(line); + if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { + return false; + } + + int[] runs = dirs.mDirections; + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + if (offset == lineStart || offset == lineEnd) { + int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1; + int runIndex = offset == lineStart ? 0 : runs.length - 2; + return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel; + } + + offset -= lineStart; + for (int i = 0; i < runs.length; i += 2) { + if (offset == runs[i]) { + return true; + } + } + return false; + } + + private boolean primaryIsTrailingPrevious(int offset) { + int line = getLineForOffset(offset); + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int[] runs = getLineDirections(line).mDirections; + + int levelAt = -1; + for (int i = 0; i < runs.length; i += 2) { + int start = lineStart + runs[i]; + int limit = start + (runs[i+1] & RUN_LENGTH_MASK); + if (limit > lineEnd) { + limit = lineEnd; + } + if (offset >= start && offset < limit) { + if (offset > start) { + // Previous character is at same level, so don't use trailing. + return false; + } + levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; + break; + } + } + if (levelAt == -1) { + // Offset was limit of line. + levelAt = getParagraphDirection(line) == 1 ? 0 : 1; + } + + // At level boundary, check previous level. + int levelBefore = -1; + if (offset == lineStart) { + levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; + } else { + offset -= 1; + for (int i = 0; i < runs.length; i += 2) { + int start = lineStart + runs[i]; + int limit = start + (runs[i+1] & RUN_LENGTH_MASK); + if (limit > lineEnd) { + limit = lineEnd; + } + if (offset >= start && offset < limit) { + levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; + break; + } + } + } + + return levelBefore < levelAt; + } + /** * Get the primary horizontal position for the specified text offset. * This is the location where a new character would be inserted in * the paragraph's primary direction. */ public float getPrimaryHorizontal(int offset) { - return getHorizontal(offset, false, true); + boolean trailing = primaryIsTrailingPrevious(offset); + return getHorizontal(offset, trailing); } /** @@ -539,66 +692,42 @@ public abstract class Layout { * the direction other than the paragraph's primary direction. */ public float getSecondaryHorizontal(int offset) { - return getHorizontal(offset, true, true); + boolean trailing = primaryIsTrailingPrevious(offset); + return getHorizontal(offset, !trailing); } - private float getHorizontal(int offset, boolean trailing, boolean alt) { + private float getHorizontal(int offset, boolean trailing) { int line = getLineForOffset(offset); - return getHorizontal(offset, trailing, alt, line); + return getHorizontal(offset, trailing, line); } - private float getHorizontal(int offset, boolean trailing, boolean alt, - int line) { + private float getHorizontal(int offset, boolean trailing, int line) { int start = getLineStart(line); - int end = getLineVisibleEnd(line); + int end = getLineEnd(line); int dir = getParagraphDirection(line); - boolean tab = getLineContainsTab(line); + boolean hasTabOrEmoji = getLineContainsTab(line); Directions directions = getLineDirections(line); - TabStopSpan[] tabs = null; - if (tab && mText instanceof Spanned) { - tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + TabStops tabStops = null; + if (hasTabOrEmoji && mText instanceof Spanned) { + // Just checking this line should be good enough, tabs should be + // consistent across all lines in a paragraph. + TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); + if (tabs.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse + } } - float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, - dir, directions, trailing, alt, tab, tabs); - - if (offset > end) { - if (dir == DIR_RIGHT_TO_LEFT) - wid -= measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - else - wid += measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - } + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops); + float wid = tl.measure(offset - start, trailing, null); + TextLine.recycle(tl); - Alignment align = getParagraphAlignment(line); int left = getParagraphLeft(line); int right = getParagraphRight(line); - if (align == Alignment.ALIGN_NORMAL) { - if (dir == DIR_RIGHT_TO_LEFT) - return right + wid; - else - return left + wid; - } - - float max = getLineMax(line); - - if (align == Alignment.ALIGN_OPPOSITE) { - if (dir == DIR_RIGHT_TO_LEFT) - return left + max + wid; - else - return right - max + wid; - } else { /* align == Alignment.ALIGN_CENTER */ - int imax = ((int) max) & ~1; - - if (dir == DIR_RIGHT_TO_LEFT) - return right - (((right - left) - imax) / 2) + wid; - else - return left + ((right - left) - imax) / 2 + wid; - } + return getLineStartPos(line, left, right) + wid; } /** @@ -656,38 +785,76 @@ public abstract class Layout { } /** - * Gets the horizontal extent of the specified line, excluding - * trailing whitespace. + * Gets the unsigned horizontal extent of the specified line, including + * leading margin indent, but excluding trailing whitespace. */ public float getLineMax(int line) { - return getLineMax(line, null, false); + float margin = getParagraphLeadingMargin(line); + float signedExtent = getLineExtent(line, false); + return margin + signedExtent >= 0 ? signedExtent : -signedExtent; } /** - * Gets the horizontal extent of the specified line, including - * trailing whitespace. + * Gets the unsigned horizontal extent of the specified line, including + * leading margin indent and trailing whitespace. */ public float getLineWidth(int line) { - return getLineMax(line, null, true); + float margin = getParagraphLeadingMargin(line); + float signedExtent = getLineExtent(line, true); + return margin + signedExtent >= 0 ? signedExtent : -signedExtent; } - private float getLineMax(int line, Object[] tabs, boolean full) { + /** + * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the + * tab stops instead of using the ones passed in. + * @param line the index of the line + * @param full whether to include trailing whitespace + * @return the extent of the line + */ + private float getLineExtent(int line, boolean full) { int start = getLineStart(line); - int end; + int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + + boolean hasTabsOrEmoji = getLineContainsTab(line); + TabStops tabStops = null; + if (hasTabsOrEmoji && mText instanceof Spanned) { + // Just checking this line should be good enough, tabs should be + // consistent across all lines in a paragraph. + TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); + if (tabs.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse + } + } + Directions directions = getLineDirections(line); + int dir = getParagraphDirection(line); - if (full) { - end = getLineEnd(line); - } else { - end = getLineVisibleEnd(line); - } - boolean tab = getLineContainsTab(line); + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); + float width = tl.metrics(null); + TextLine.recycle(tl); + return width; + } - if (tabs == null && tab && mText instanceof Spanned) { - tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); - } + /** + * Returns the signed horizontal extent of the specified line, excluding + * leading margin. If full is false, excludes trailing whitespace. + * @param line the index of the line + * @param tabStops the tab stops, can be null if we know they're not used. + * @param full whether to include trailing whitespace + * @return the extent of the text on this line + */ + private float getLineExtent(int line, TabStops tabStops, boolean full) { + int start = getLineStart(line); + int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + boolean hasTabsOrEmoji = getLineContainsTab(line); + Directions directions = getLineDirections(line); + int dir = getParagraphDirection(line); - return measureText(mPaint, mWorkPaint, - mText, start, end, null, tab, tabs); + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); + float width = tl.metrics(null); + TextLine.recycle(tl); + return width; } /** @@ -738,7 +905,7 @@ public abstract class Layout { } /** - * Get the character offset on the specfied line whose position is + * Get the character offset on the specified line whose position is * closest to the specified horizontal position. */ public int getOffsetForHorizontal(int line, float horiz) { @@ -752,14 +919,13 @@ public abstract class Layout { int best = min; float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); - int here = min; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - int swap = ((i & 1) == 0) ? 1 : -1; + for (int i = 0; i < dirs.mDirections.length; i += 2) { + int here = min + dirs.mDirections[i]; + int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); + int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1; if (there > max) there = max; - int high = there - 1 + 1, low = here + 1 - 1, guess; while (high - low > 1) { @@ -792,7 +958,7 @@ public abstract class Layout { if (dist < bestdist) { bestdist = dist; - best = low; + best = low; } } @@ -802,8 +968,6 @@ public abstract class Layout { bestdist = dist; best = here; } - - here = there; } float dist = Math.abs(getPrimaryHorizontal(max) - horiz); @@ -823,19 +987,15 @@ public abstract class Layout { return getLineStart(line + 1); } - /** + /** * Return the text offset after the last visible character (so whitespace * is not counted) on the specified line. */ public int getLineVisibleEnd(int line) { return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); } - - private int getLineVisibleEnd(int line, int start, int end) { - if (DEBUG) { - Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end); - } + private int getLineVisibleEnd(int line, int start, int end) { CharSequence text = mText; char ch; if (line == getLineCount() - 1) { @@ -882,207 +1042,62 @@ public abstract class Layout { return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); } - /** - * Return the text offset that would be reached by moving left - * (possibly onto another line) from the specified offset. - */ public int getOffsetToLeftOf(int offset) { - int line = getLineForOffset(offset); - int start = getLineStart(line); - int end = getLineEnd(line); - Directions dirs = getLineDirections(line); - - if (line != getLineCount() - 1) - end--; - - float horiz = getPrimaryHorizontal(offset); - - int best = offset; - float besth = Integer.MIN_VALUE; - int candidate; - - candidate = TextUtils.getOffsetBefore(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetAfter(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - int here = start; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - if (there > end) - there = end; - - float h = getPrimaryHorizontal(here); - - if (h < horiz && h > besth) { - best = here; - besth = h; - } - - candidate = TextUtils.getOffsetAfter(mText, here); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetBefore(mText, there); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - here = there; - } - - float h = getPrimaryHorizontal(end); - - if (h < horiz && h > besth) { - best = end; - besth = h; - } - - if (best != offset) - return best; - - int dir = getParagraphDirection(line); - - if (dir > 0) { - if (line == 0) - return best; - else - return getOffsetForHorizontal(line - 1, 10000); - } else { - if (line == getLineCount() - 1) - return best; - else - return getOffsetForHorizontal(line + 1, 10000); - } + return getOffsetToLeftRightOf(offset, true); } - /** - * Return the text offset that would be reached by moving right - * (possibly onto another line) from the specified offset. - */ public int getOffsetToRightOf(int offset) { - int line = getLineForOffset(offset); - int start = getLineStart(line); - int end = getLineEnd(line); - Directions dirs = getLineDirections(line); - - if (line != getLineCount() - 1) - end--; - - float horiz = getPrimaryHorizontal(offset); - - int best = offset; - float besth = Integer.MAX_VALUE; - int candidate; - - candidate = TextUtils.getOffsetBefore(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetAfter(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; - } - } - - int here = start; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - if (there > end) - there = end; - - float h = getPrimaryHorizontal(here); - - if (h > horiz && h < besth) { - best = here; - besth = h; - } - - candidate = TextUtils.getOffsetAfter(mText, here); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); + return getOffsetToLeftRightOf(offset, false); + } - if (h > horiz && h < besth) { - best = candidate; - besth = h; + private int getOffsetToLeftRightOf(int caret, boolean toLeft) { + int line = getLineForOffset(caret); + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int lineDir = getParagraphDirection(line); + + boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); + if (caret == (advance ? lineEnd : lineStart)) { + // walking off line, so look at the line we're headed to + if (caret == lineStart) { + if (line > 0) { + --line; + } else { + return caret; // at very start, don't move } - } - - candidate = TextUtils.getOffsetBefore(mText, there); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; + } else { + if (line < getLineCount() - 1) { + ++line; + } else { + return caret; // at very end, don't move } } - here = there; - } - - float h = getPrimaryHorizontal(end); - - if (h > horiz && h < besth) { - best = end; - besth = h; + lineStart = getLineStart(line); + lineEnd = getLineEnd(line); + int newDir = getParagraphDirection(line); + if (newDir != lineDir) { + // unusual case. we want to walk onto the line, but it runs + // in a different direction than this one, so we fake movement + // in the opposite direction. + toLeft = !toLeft; + lineDir = newDir; + } } - if (best != offset) - return best; - - int dir = getParagraphDirection(line); + Directions directions = getLineDirections(line); - if (dir > 0) { - if (line == getLineCount() - 1) - return best; - else - return getOffsetForHorizontal(line + 1, -10000); - } else { - if (line == 0) - return best; - else - return getOffsetForHorizontal(line - 1, -10000); - } + TextLine tl = TextLine.obtain(); + // XXX: we don't care about tabs + tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); + caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); + tl = TextLine.recycle(tl); + return caret; } private int getOffsetAtStartOf(int offset) { + // XXX this probably should skip local reorderings and + // zero-width characters, look at callers if (offset == 0) return 0; @@ -1115,7 +1130,7 @@ public abstract class Layout { /** * Fills in the specified Path with a representation of a cursor * at the specified offset. This will often be a vertical line - * but can be multiple discontinous lines in text with multiple + * but can be multiple discontinuous lines in text with multiple * directionalities. */ public void getCursorPath(int point, Path dest, @@ -1127,7 +1142,8 @@ public abstract class Layout { int bottom = getLineTop(line+1); float h1 = getPrimaryHorizontal(point) - 0.5f; - float h2 = getSecondaryHorizontal(point) - 0.5f; + float h2 = isLevelBoundary(point) ? + getSecondaryHorizontal(point) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, KeyEvent.META_SHIFT_ON) | @@ -1204,9 +1220,10 @@ public abstract class Layout { if (lineend > linestart && mText.charAt(lineend - 1) == '\n') lineend--; - int here = linestart; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; + for (int i = 0; i < dirs.mDirections.length; i += 2) { + int here = linestart + dirs.mDirections[i]; + int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); + if (there > lineend) there = lineend; @@ -1215,14 +1232,12 @@ public abstract class Layout { int en = Math.min(end, there); if (st != en) { - float h1 = getHorizontal(st, false, false, line); - float h2 = getHorizontal(en, true, false, line); + float h1 = getHorizontal(st, false, line); + float h2 = getHorizontal(en, true, line); dest.addRect(h1, top, h2, bottom, Path.Direction.CW); } } - - here = there; } } @@ -1257,7 +1272,7 @@ public abstract class Layout { addSelection(startline, start, getLineEnd(startline), top, getLineBottom(startline), dest); - + if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) dest.addRect(getLineLeft(startline), top, 0, getLineBottom(startline), Path.Direction.CW); @@ -1293,7 +1308,7 @@ public abstract class Layout { if (mSpannedText) { Spanned sp = (Spanned) mText; - AlignmentSpan[] spans = sp.getSpans(getLineStart(line), + AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), getLineEnd(line), AlignmentSpan.class); @@ -1310,422 +1325,173 @@ public abstract class Layout { * Get the left edge of the specified paragraph, inset by left margins. */ public final int getParagraphLeft(int line) { - int dir = getParagraphDirection(line); - int left = 0; - - boolean par = false; - int off = getLineStart(line); - if (off == 0 || mText.charAt(off - 1) == '\n') - par = true; - - if (dir == DIR_LEFT_TO_RIGHT) { - if (mSpannedText) { - Spanned sp = (Spanned) mText; - LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), - getLineEnd(line), - LeadingMarginSpan.class); - - for (int i = 0; i < spans.length; i++) { - boolean margin = par; - LeadingMarginSpan span = spans[i]; - if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) { - int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount(); - margin = count >= line; - } - left += span.getLeadingMargin(margin); - } - } + int dir = getParagraphDirection(line); + if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { + return left; // leading margin has no impact, or no styles } - - return left; + return getParagraphLeadingMargin(line); } /** * Get the right edge of the specified paragraph, inset by right margins. */ public final int getParagraphRight(int line) { - int dir = getParagraphDirection(line); - int right = mWidth; - - boolean par = false; - int off = getLineStart(line); - if (off == 0 || mText.charAt(off - 1) == '\n') - par = true; - - - if (dir == DIR_RIGHT_TO_LEFT) { - if (mSpannedText) { - Spanned sp = (Spanned) mText; - LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), - getLineEnd(line), - LeadingMarginSpan.class); - - for (int i = 0; i < spans.length; i++) { - right -= spans[i].getLeadingMargin(par); - } - } + int dir = getParagraphDirection(line); + if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { + return right; // leading margin has no impact, or no styles } - - return right; + return right - getParagraphLeadingMargin(line); } - private void drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, Directions directions, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean hasTabs, Object[] parspans) { - char[] buf; - if (!hasTabs) { - if (directions == DIRS_ALL_LEFT_TO_RIGHT) { - if (DEBUG) { - Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); - } - Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); - return; - } - buf = null; - } else { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - float h = 0; - - int here = 0; - for (int i = 0; i < directions.mDirections.length; i++) { - int there = here + directions.mDirections[i]; - if (there > end - start) - there = end - start; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - if (j == there || buf[j] == '\t') { - h += Styled.drawText(canvas, text, - start + segstart, start + j, - dir, (i & 1) != 0, x + h, - top, y, bottom, paint, workPaint, - start + j != end); - - if (j != there && buf[j] == '\t') - h = dir * nextTab(text, start, end, h * dir, parspans); - - segstart = j + 1; - } else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) { - int emoji = Character.codePointAt(buf, j); - - if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { - Bitmap bm = EMOJI_FACTORY. - getBitmapFromAndroidPua(emoji); - - if (bm != null) { - h += Styled.drawText(canvas, text, - start + segstart, start + j, - dir, (i & 1) != 0, x + h, - top, y, bottom, paint, workPaint, - start + j != end); - - if (mEmojiRect == null) { - mEmojiRect = new RectF(); - } + /** + * Returns the effective leading margin (unsigned) for this line, + * taking into account LeadingMarginSpan and LeadingMarginSpan2. + * @param line the line index + * @return the leading margin of this line + */ + private int getParagraphLeadingMargin(int line) { + if (!mSpannedText) { + return 0; + } + Spanned spanned = (Spanned) mText; - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - start + j, start + j + 1, - null); - - float bitmapHeight = bm.getHeight(); - float textHeight = -workPaint.ascent(); - float scale = textHeight / bitmapHeight; - float width = bm.getWidth() * scale; + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, + LeadingMarginSpan.class); + LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, + LeadingMarginSpan.class); + if (spans.length == 0) { + return 0; // no leading margin span; + } - mEmojiRect.set(x + h, y - textHeight, - x + h + width, y); + int margin = 0; - canvas.drawBitmap(bm, null, mEmojiRect, paint); - h += width; + boolean isFirstParaLine = lineStart == 0 || + spanned.charAt(lineStart - 1) == '\n'; - j++; - segstart = j + 1; - } - } - } + for (int i = 0; i < spans.length; i++) { + LeadingMarginSpan span = spans[i]; + boolean useFirstLineMargin = isFirstParaLine; + if (span instanceof LeadingMarginSpan2) { + int spStart = spanned.getSpanStart(span); + int spanLine = getLineForOffset(spStart); + int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount(); + useFirstLineMargin = line < spanLine + count; } - - here = there; + margin += span.getLeadingMargin(useFirstLineMargin); } - if (hasTabs) - TextUtils.recycle(buf); + return margin; } - private static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, - int start, int offset, int end, - int dir, Directions directions, - boolean trailing, boolean alt, - boolean hasTabs, Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - float h = 0; - - if (alt) { - if (dir == DIR_RIGHT_TO_LEFT) - trailing = !trailing; - } - - int here = 0; - for (int i = 0; i < directions.mDirections.length; i++) { - if (alt) - trailing = !trailing; - - int there = here + directions.mDirections[i]; - if (there > end - start) - there = end - start; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && j < there) { - codept = buf[j]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) { - codept = Character.codePointAt(buf, j); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (j == there || codept == '\t' || bm != null) { - float segw; - - if (offset < start + j || - (trailing && offset <= start + j)) { - if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) { - h += Styled.measureText(paint, workPaint, text, - start + segstart, offset, - null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) { - h -= Styled.measureText(paint, workPaint, text, - start + segstart, offset, - null); - return h; - } - } - - segw = Styled.measureText(paint, workPaint, text, - start + segstart, start + j, - null); - - if (offset < start + j || - (trailing && offset <= start + j)) { - if (dir == DIR_LEFT_TO_RIGHT) { - h += segw - Styled.measureText(paint, workPaint, - text, - start + segstart, - offset, null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= segw - Styled.measureText(paint, workPaint, - text, - start + segstart, - offset, null); - return h; - } - } - - if (dir == DIR_RIGHT_TO_LEFT) - h -= segw; - else - h += segw; - - if (j != there && buf[j] == '\t') { - if (offset == start + j) - return h; - - h = dir * nextTab(text, start, end, h * dir, tabs); - } - - if (bm != null) { - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - j, j + 2, null); - - float wid = (float) bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= wid; - } else { - h += wid; + /* package */ + static float measurePara(TextPaint paint, TextPaint workPaint, + CharSequence text, int start, int end) { + + MeasuredText mt = MeasuredText.obtain(); + TextLine tl = TextLine.obtain(); + try { + mt.setPara(text, start, end, DIR_REQUEST_LTR); + Directions directions; + int dir; + if (mt.mEasy) { + directions = DIRS_ALL_LEFT_TO_RIGHT; + dir = Layout.DIR_LEFT_TO_RIGHT; + } else { + directions = AndroidBidi.directions(mt.mDir, mt.mLevels, + 0, mt.mChars, 0, mt.mLen); + dir = mt.mDir; + } + char[] chars = mt.mChars; + int len = mt.mLen; + boolean hasTabs = false; + TabStops tabStops = null; + for (int i = 0; i < len; ++i) { + if (chars[i] == '\t') { + hasTabs = true; + if (text instanceof Spanned) { + Spanned spanned = (Spanned) text; + int spanEnd = spanned.nextSpanTransition(start, end, + TabStopSpan.class); + TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, + TabStopSpan.class); + if (spans.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, spans); } - - j++; } - - segstart = j + 1; + break; } } - - here = there; + tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops); + return tl.metrics(null); + } finally { + TextLine.recycle(tl); + MeasuredText.recycle(mt); } - - if (hasTabs) - TextUtils.recycle(buf); - - return h; } /** - * Measure width of a run of text on a single line that is known to all be - * in the same direction as the paragraph base direction. Returns the width, - * and the line metrics in fm if fm is not null. - * - * @param paint the paint for the text; will not be modified - * @param workPaint paint available for modification - * @param text text - * @param start start of the line - * @param end limit of the line - * @param fm object to return integer metrics in, can be null - * @param hasTabs true if it is known that the line has tabs - * @param tabs tab position information - * @return the width of the text from start to end + * @hide */ - /* package */ static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, - int start, int end, - Paint.FontMetricsInt fm, - boolean hasTabs, Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - int len = end - start; - - int lastPos = 0; - float width = 0; - int ascent = 0, descent = 0, top = 0, bottom = 0; - - if (fm != null) { - fm.ascent = 0; - fm.descent = 0; - } - - for (int pos = hasTabs ? 0 : len; pos <= len; pos++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && pos < len) { - codept = buf[pos]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) { - codept = Character.codePointAt(buf, pos); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (pos == len || codept == '\t' || bm != null) { - workPaint.baselineShift = 0; - - width += Styled.measureText(paint, workPaint, text, - start + lastPos, start + pos, - fm); - - if (fm != null) { - if (workPaint.baselineShift < 0) { - fm.ascent += workPaint.baselineShift; - fm.top += workPaint.baselineShift; - } else { - fm.descent += workPaint.baselineShift; - fm.bottom += workPaint.baselineShift; + /* package */ static class TabStops { + private int[] mStops; + private int mNumStops; + private int mIncrement; + + TabStops(int increment, Object[] spans) { + reset(increment, spans); + } + + void reset(int increment, Object[] spans) { + this.mIncrement = increment; + + int ns = 0; + if (spans != null) { + int[] stops = this.mStops; + for (Object o : spans) { + if (o instanceof TabStopSpan) { + if (stops == null) { + stops = new int[10]; + } else if (ns == stops.length) { + int[] nstops = new int[ns * 2]; + for (int i = 0; i < ns; ++i) { + nstops[i] = stops[i]; + } + stops = nstops; + } + stops[ns++] = ((TabStopSpan) o).getTabStop(); } } - - if (pos != len) { - if (bm == null) { - // no emoji, must have hit a tab - width = nextTab(text, start, end, width, tabs); - } else { - // This sets up workPaint with the font on the emoji - // text, so that we can extract the ascent and scale. - - // We can't use the result of the previous call to - // measureText because the emoji might have its own style. - // We have to initialize workPaint here because if the - // text is unstyled measureText might not use workPaint - // at all. - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - start + pos, start + pos + 1, null); - - width += (float) bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - // Since we had an emoji, we bump past the second half - // of the surrogate pair. - pos++; - } + if (ns > 1) { + Arrays.sort(stops, 0, ns); } + if (stops != this.mStops) { + this.mStops = stops; + } + } + this.mNumStops = ns; + } - if (fm != null) { - if (fm.ascent < ascent) { - ascent = fm.ascent; - } - if (fm.descent > descent) { - descent = fm.descent; - } - - if (fm.top < top) { - top = fm.top; - } - if (fm.bottom > bottom) { - bottom = fm.bottom; + float nextTab(float h) { + int ns = this.mNumStops; + if (ns > 0) { + int[] stops = this.mStops; + for (int i = 0; i < ns; ++i) { + int stop = stops[i]; + if (stop > h) { + return stop; } - - // No need to take bitmap height into account here, - // since it is scaled to match the text height. } - - lastPos = pos + 1; } + return nextDefaultStop(h, mIncrement); } - if (fm != null) { - fm.ascent = ascent; - fm.descent = descent; - fm.top = top; - fm.bottom = bottom; + public static float nextDefaultStop(float h, int inc) { + return ((int) ((h + inc) / inc)) * inc; } - - if (hasTabs) - TextUtils.recycle(buf); - - return width; } /** @@ -1747,7 +1513,7 @@ public abstract class Layout { if (text instanceof Spanned) { if (tabs == null) { - tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class); + tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); alltabs = true; } @@ -1774,6 +1540,38 @@ public abstract class Layout { return mSpannedText; } + /** + * Returns the same as text.getSpans(), except where + * start and end are the same and are not + * at the very beginning of the text, in which case an empty array + * is returned instead. + *

    + * This is needed because of the special case that getSpans() + * on an empty range returns the spans adjacent to that range, which is + * primarily for the sake of TextWatchers so they will get + * notifications when text goes from empty to non-empty. But it also + * has the unfortunate side effect that if the text ends with an empty + * paragraph, that paragraph accidentally picks up the styles of the + * preceding paragraph (even though those styles will not be picked up + * by new text that is inserted into the empty paragraph). + *

    + * The reason it just checks whether start and end + * is the same is that the only time a line can contain 0 characters + * is if it is the final paragraph of the Layout; otherwise any line will + * contain at least one printing or newline character. The reason for the + * additional check if start is greater than 0 is that + * if the empty paragraph is the entire content of the buffer, paragraph + * styles that are already applied to the buffer will apply to text that + * is inserted into it. + */ + /* package */ static T[] getParagraphSpans(Spanned text, int start, int end, Class type) { + if (start == end && start > 0) { + return (T[]) ArrayUtils.emptyArray(type); + } + + return text.getSpans(start, end, type); + } + private void ellipsize(int start, int end, int line, char[] dest, int destoff) { int ellipsisCount = getEllipsisCount(line); @@ -1804,23 +1602,22 @@ public abstract class Layout { /** * Stores information about bidirectional (left-to-right or right-to-left) - * text within the layout of a line. TODO: This work is not complete - * or correct and will be fleshed out in a later revision. + * text within the layout of a line. */ public static class Directions { - private short[] mDirections; - - // The values in mDirections are the offsets from the first character - // in the line to the next flip in direction. Runs at even indices - // are left-to-right, the others are right-to-left. So, for example, - // a line that starts with a right-to-left run has 0 at mDirections[0], - // since the 'first' (ltr) run is zero length. - // - // The code currently assumes that each run is adjacent to the previous - // one, progressing in the base line direction. This isn't sufficient - // to handle nested runs, for example numeric text in an rtl context - // in an ltr paragraph. - /* package */ Directions(short[] dirs) { + // Directions represents directional runs within a line of text. + // Runs are pairs of ints listed in visual order, starting from the + // leading margin. The first int of each pair is the offset from + // the first character of the line to the start of the run. The + // second int represents both the length and level of the run. + // The length is in the lower bits, accessed by masking with + // DIR_LENGTH_MASK. The level is in the higher bits, accessed + // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. + // To simply test for an RTL direction, test the bit using + // DIR_RTL_FLAG, if set then the direction is rtl. + + /* package */ int[] mDirections; + /* package */ Directions(int[] dirs) { mDirections = dirs; } } @@ -1831,6 +1628,7 @@ public abstract class Layout { * line is ellipsized, not getLineStart().) */ public abstract int getEllipsisStart(int line); + /** * Returns the number of characters to be ellipsized away, or 0 if * no ellipsis is to take place. @@ -1870,7 +1668,7 @@ public abstract class Layout { public int length() { return mText.length(); } - + public CharSequence subSequence(int start, int end) { char[] s = new char[end - start]; getChars(start, end, s, 0); @@ -1931,17 +1729,22 @@ public abstract class Layout { private Alignment mAlignment = Alignment.ALIGN_NORMAL; private float mSpacingMult; private float mSpacingAdd; - private static Rect sTempRect = new Rect(); + private static final Rect sTempRect = new Rect(); private boolean mSpannedText; public static final int DIR_LEFT_TO_RIGHT = 1; public static final int DIR_RIGHT_TO_LEFT = -1; - + /* package */ static final int DIR_REQUEST_LTR = 1; /* package */ static final int DIR_REQUEST_RTL = -1; /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; + /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; + /* package */ static final int RUN_LEVEL_SHIFT = 26; + /* package */ static final int RUN_LEVEL_MASK = 0x3f; + /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; + public enum Alignment { ALIGN_NORMAL, ALIGN_OPPOSITE, @@ -1953,9 +1756,7 @@ public abstract class Layout { private static final int TAB_INCREMENT = 20; /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = - new Directions(new short[] { 32767 }); + new Directions(new int[] { 0, RUN_LENGTH_MASK }); /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = - new Directions(new short[] { 0, 32767 }); - + new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); } - diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java new file mode 100644 index 0000000000000000000000000000000000000000..d5699f1de1bbbbb4b2f33b3948735918235d8e32 --- /dev/null +++ b/core/java/android/text/MeasuredText.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import com.android.internal.util.ArrayUtils; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.util.Log; + +/** + * @hide + */ +class MeasuredText { + /* package */ CharSequence mText; + /* package */ int mTextStart; + /* package */ float[] mWidths; + /* package */ char[] mChars; + /* package */ byte[] mLevels; + /* package */ int mDir; + /* package */ boolean mEasy; + /* package */ int mLen; + private int mPos; + private TextPaint mWorkPaint; + + private MeasuredText() { + mWorkPaint = new TextPaint(); + } + + private static MeasuredText[] cached = new MeasuredText[3]; + + /* package */ + static MeasuredText obtain() { + MeasuredText mt; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + mt = cached[i]; + cached[i] = null; + return mt; + } + } + } + mt = new MeasuredText(); + Log.e("MEAS", "new: " + mt); + return mt; + } + + /* package */ + static MeasuredText recycle(MeasuredText mt) { + mt.mText = null; + if (mt.mLen < 1000) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = mt; + break; + } + } + } + } + return null; + } + + /** + * Analyzes text for bidirectional runs. Allocates working buffers. + */ + /* package */ + void setPara(CharSequence text, int start, int end, int bidiRequest) { + mText = text; + mTextStart = start; + + int len = end - start; + mLen = len; + mPos = 0; + + if (mWidths == null || mWidths.length < len) { + mWidths = new float[ArrayUtils.idealFloatArraySize(len)]; + } + if (mChars == null || mChars.length < len) { + mChars = new char[ArrayUtils.idealCharArraySize(len)]; + } + TextUtils.getChars(text, start, end, mChars, 0); + + if (text instanceof Spanned) { + Spanned spanned = (Spanned) text; + ReplacementSpan[] spans = spanned.getSpans(start, end, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int startInPara = spanned.getSpanStart(spans[i]) - start; + int endInPara = spanned.getSpanEnd(spans[i]) - start; + for (int j = startInPara; j < endInPara; j++) { + mChars[j] = '\uFFFC'; + } + } + } + + if (TextUtils.doesNotNeedBidi(mChars, 0, len)) { + mDir = Layout.DIR_LEFT_TO_RIGHT; + mEasy = true; + } else { + if (mLevels == null || mLevels.length < len) { + mLevels = new byte[ArrayUtils.idealByteArraySize(len)]; + } + mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); + mEasy = false; + } + } + + float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { + if (fm != null) { + paint.getFontMetricsInt(fm); + } + + int p = mPos; + mPos = p + len; + + if (mEasy) { + int flags = mDir == Layout.DIR_LEFT_TO_RIGHT + ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL; + return paint.getTextRunAdvances(mChars, p, len, p, len, flags, mWidths, p); + } + + float totalAdvance = 0; + int level = mLevels[p]; + for (int q = p, i = p + 1, e = p + len;; ++i) { + if (i == e || mLevels[i] != level) { + int flags = (level & 0x1) == 0 ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL; + totalAdvance += + paint.getTextRunAdvances(mChars, q, i - q, q, i - q, flags, mWidths, q); + if (i == e) { + break; + } + q = i; + level = mLevels[i]; + } + } + return totalAdvance; + } + + float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, + Paint.FontMetricsInt fm) { + + TextPaint workPaint = mWorkPaint; + workPaint.set(paint); + // XXX paint should not have a baseline shift, but... + workPaint.baselineShift = 0; + + ReplacementSpan replacement = null; + for (int i = 0; i < spans.length; i++) { + MetricAffectingSpan span = spans[i]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + span.updateMeasureState(workPaint); + } + } + + float wid; + if (replacement == null) { + wid = addStyleRun(workPaint, len, fm); + } else { + // Use original text. Shouldn't matter. + wid = replacement.getSize(workPaint, mText, mTextStart + mPos, + mTextStart + mPos + len, fm); + float[] w = mWidths; + w[mPos] = wid; + for (int i = mPos + 1, e = mPos + len; i < e; i++) + w[i] = 0; + } + + if (fm != null) { + if (workPaint.baselineShift < 0) { + fm.ascent += workPaint.baselineShift; + fm.top += workPaint.baselineShift; + } else { + fm.descent += workPaint.baselineShift; + fm.bottom += workPaint.baselineShift; + } + } + + return wid; + } + + int breakText(int start, int limit, boolean forwards, float width) { + float[] w = mWidths; + if (forwards) { + for (int i = start; i < limit; ++i) { + if ((width -= w[i]) < 0) { + return i - start; + } + } + } else { + for (int i = limit; --i >= start;) { + if ((width -= w[i]) < 0) { + return limit - i -1; + } + } + } + + return limit - start; + } + + float measure(int start, int limit) { + float width = 0; + float[] w = mWidths; + for (int i = start; i < limit; ++i) { + width += w[i]; + } + return width; + } +} \ No newline at end of file diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index caaafa1473976ea153e614e36df24da26b9b81a2..fc01ef2ba52f24af17736b2828f642c17344d3a3 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -17,8 +17,9 @@ package android.text; import com.android.internal.util.ArrayUtils; -import android.graphics.Paint; + import android.graphics.Canvas; +import android.graphics.Paint; import java.lang.reflect.Array; @@ -312,12 +313,15 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, moveGapTo(end); - if (tbend - tbstart >= mGapLength + (end - start)) - resizeFor(mText.length - mGapLength + - tbend - tbstart - (end - start)); + // Can be negative + final int nbNewChars = (tbend - tbstart) - (end - start); - mGapStart += tbend - tbstart - (end - start); - mGapLength -= tbend - tbstart - (end - start); + if (nbNewChars >= mGapLength) { + resizeFor(mText.length + nbNewChars - mGapLength); + } + + mGapStart += nbNewChars; + mGapLength -= nbNewChars; if (mGapLength < 1) new Exception("mGapLength < 1").printStackTrace(); @@ -707,6 +711,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, * the specified range of the buffer. The kind may be Object.class to get * a list of all the spans regardless of type. */ + @SuppressWarnings("unchecked") public T[] getSpans(int queryStart, int queryEnd, Class kind) { int spanCount = mSpanCount; Object[] spans = mSpans; @@ -717,8 +722,8 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, int gaplen = mGapLength; int count = 0; - Object[] ret = null; - Object ret1 = null; + T[] ret = null; + T ret1 = null; for (int i = 0; i < spanCount; i++) { int spanStart = starts[i]; @@ -750,11 +755,13 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } if (count == 0) { - ret1 = spans[i]; + // Safe conversion thanks to the isInstance test above + ret1 = (T) spans[i]; count++; } else { if (count == 1) { - ret = (Object[]) Array.newInstance(kind, spanCount - i + 1); + // Safe conversion, but requires a suppressWarning + ret = (T[]) Array.newInstance(kind, spanCount - i + 1); ret[0] = ret1; } @@ -771,29 +778,33 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } System.arraycopy(ret, j, ret, j + 1, count - j); - ret[j] = spans[i]; + // Safe conversion thanks to the isInstance test above + ret[j] = (T) spans[i]; count++; } else { - ret[count++] = spans[i]; + // Safe conversion thanks to the isInstance test above + ret[count++] = (T) spans[i]; } } } if (count == 0) { - return (T[]) ArrayUtils.emptyArray(kind); + return ArrayUtils.emptyArray(kind); } if (count == 1) { - ret = (Object[]) Array.newInstance(kind, 1); + // Safe conversion, but requires a suppressWarning + ret = (T[]) Array.newInstance(kind, 1); ret[0] = ret1; - return (T[]) ret; + return ret; } if (count == ret.length) { - return (T[]) ret; + return ret; } - Object[] nret = (Object[]) Array.newInstance(kind, count); + // Safe conversion, but requires a suppressWarning + T[] nret = (T[]) Array.newInstance(kind, count); System.arraycopy(ret, 0, nret, 0, count); - return (T[]) nret; + return nret; } /** @@ -862,6 +873,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, /** * Return a String containing a copy of the chars in this buffer. */ + @Override public String toString() { int len = length(); char[] buf = new char[len]; @@ -952,6 +964,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } } +/* private boolean isprint(char c) { // XXX if (c >= ' ' && c <= '~') return true; @@ -959,7 +972,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return false; } -/* private static final int startFlag(int flag) { return (flag >> 4) & 0x0F; } @@ -1054,7 +1066,32 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } } + /** + * Don't call this yourself -- exists for Canvas to use internally. + * {@hide} + */ + public void drawTextRun(Canvas c, int start, int end, + int contextStart, int contextEnd, + float x, float y, int flags, Paint p) { + checkRange("drawTextRun", start, end); + + int contextLen = contextEnd - contextStart; + int len = end - start; + if (contextEnd <= mGapStart) { + c.drawTextRun(mText, start, len, contextStart, contextLen, x, y, flags, p); + } else if (contextStart >= mGapStart) { + c.drawTextRun(mText, start + mGapLength, len, contextStart + mGapLength, + contextLen, x, y, flags, p); + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + c.drawTextRun(buf, start - contextStart, len, 0, contextLen, x, y, flags, p); + TextUtils.recycle(buf); + } + } + + /** * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ @@ -1103,6 +1140,58 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return ret; } + /** + * Don't call this yourself -- exists for Paint to use internally. + * {@hide} + */ + public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, + float[] advances, int advancesPos, Paint p) { + + float ret; + + int contextLen = contextEnd - contextStart; + int len = end - start; + + if (end <= mGapStart) { + ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen, + flags, advances, advancesPos); + } else if (start >= mGapStart) { + ret = p.getTextRunAdvances(mText, start + mGapLength, len, + contextStart + mGapLength, contextLen, flags, advances, advancesPos); + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + ret = p.getTextRunAdvances(buf, start - contextStart, len, + 0, contextLen, flags, advances, advancesPos); + TextUtils.recycle(buf); + } + + return ret; + } + + public int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, + int cursorOpt, Paint p) { + + int ret; + + int contextLen = contextEnd - contextStart; + if (contextEnd <= mGapStart) { + ret = p.getTextRunCursor(mText, contextStart, contextLen, + flags, offset, cursorOpt); + } else if (contextStart >= mGapStart) { + ret = p.getTextRunCursor(mText, contextStart + mGapLength, contextLen, + flags, offset + mGapLength, cursorOpt) - mGapLength; + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + ret = p.getTextRunCursor(buf, 0, contextLen, + flags, offset - contextStart, cursorOpt) + contextStart; + TextUtils.recycle(buf); + } + + return ret; + } + // Documentation from interface public void setFilters(InputFilter[] filters) { if (filters == null) { diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java index 154497dc37301100a6aebe338e91f44747d46631..d14fcbc4668fe42c4b5d8f09d96d59067cfb360b 100644 --- a/core/java/android/text/Spanned.java +++ b/core/java/android/text/Spanned.java @@ -91,7 +91,7 @@ extends CharSequence public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK; /** - * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand + * Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand * to include text inserted at their ending point but not at their * starting point. When 0-length, they behave like points. */ diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index f02ad2a401ff8d01d7902d5a29320c0f4ba1ef56..cc969cb9f50c72e9f7b5783ed9a73b549227f486 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,14 +16,15 @@ package android.text; +import com.android.internal.util.ArrayUtils; + import android.graphics.Bitmap; import android.graphics.Paint; -import com.android.internal.util.ArrayUtils; -import android.util.Log; import android.text.style.LeadingMarginSpan; import android.text.style.LineHeightSpan; import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; +import android.text.style.TabStopSpan; +import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; /** * StaticLayout is a Layout for text that will not be edited after it @@ -31,8 +32,9 @@ import android.text.style.ReplacementSpan; *

    This is used by widgets to control text layout. You should not need * to use this class directly unless you are implementing your own widget * or custom display object, or would be tempted to call - * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) - * Canvas.drawText()} directly.

    + * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, + * float, float, android.graphics.Paint) + * Canvas.drawText()} directly.

    */ public class StaticLayout @@ -62,7 +64,7 @@ extends Layout boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { super((ellipsize == null) - ? source + ? source : (source instanceof Spanned) ? new SpannedEllipsizer(source) : new Ellipsizer(source), @@ -72,7 +74,7 @@ extends Layout * This is annoying, but we can't refer to the layout until * superclass construction is finished, and the superclass * constructor wants the reference to the display text. - * + * * This will break if the superclass constructor ever actually * cares about the content instead of just holding the reference. */ @@ -94,13 +96,13 @@ extends Layout mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); + generate(source, bufstart, bufend, paint, outerwidth, align, spacingmult, spacingadd, includepad, includepad, ellipsize != null, ellipsizedWidth, ellipsize); - mChdirs = null; - mChs = null; - mWidths = null; + mMeasured = MeasuredText.recycle(mMeasured); mFontMetricsInt = null; } @@ -111,6 +113,7 @@ extends Layout mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); } /* package */ void generate(CharSequence source, int bufstart, int bufend, @@ -128,59 +131,50 @@ extends Layout Paint.FontMetricsInt fm = mFontMetricsInt; int[] choosehtv = null; - int end = TextUtils.indexOf(source, '\n', bufstart, bufend); - int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart; - boolean first = true; - - if (mChdirs == null) { - mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)]; - mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)]; - mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)]; - } - - byte[] chdirs = mChdirs; - char[] chs = mChs; - float[] widths = mWidths; + MeasuredText measured = mMeasured; - AlteredCharSequence alter = null; Spanned spanned = null; - if (source instanceof Spanned) spanned = (Spanned) source; int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX - for (int start = bufstart; start <= bufend; start = end) { - if (first) - first = false; - else - end = TextUtils.indexOf(source, '\n', start, bufend); - - if (end < 0) - end = bufend; + int paraEnd; + for (int paraStart = bufstart; paraStart <= bufend; paraStart = paraEnd) { + paraEnd = TextUtils.indexOf(source, '\n', paraStart, bufend); + if (paraEnd < 0) + paraEnd = bufend; else - end++; + paraEnd++; + int paraLen = paraEnd - paraStart; - int firstWidthLineCount = 1; + int firstWidthLineLimit = mLineCount + 1; int firstwidth = outerwidth; int restwidth = outerwidth; LineHeightSpan[] chooseht = null; if (spanned != null) { - LeadingMarginSpan[] sp; - - sp = spanned.getSpans(start, end, LeadingMarginSpan.class); + LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, + LeadingMarginSpan.class); for (int i = 0; i < sp.length; i++) { LeadingMarginSpan lms = sp[i]; firstwidth -= sp[i].getLeadingMargin(true); restwidth -= sp[i].getLeadingMargin(false); - if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) { - firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount(); + + // LeadingMarginSpan2 is odd. The count affects all + // leading margin spans, not just this particular one, + // and start from the top of the span, not the top of the + // paragraph. + if (lms instanceof LeadingMarginSpan2) { + LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; + int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2)); + firstWidthLineLimit = lmsFirstLine + + lms2.getLeadingMarginLineCount(); } } - chooseht = spanned.getSpans(start, end, LineHeightSpan.class); + chooseht = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class); if (chooseht.length != 0) { if (choosehtv == null || @@ -192,11 +186,11 @@ extends Layout for (int i = 0; i < chooseht.length; i++) { int o = spanned.getSpanStart(chooseht[i]); - if (o < start) { + if (o < paraStart) { // starts in this layout, before the // current paragraph - choosehtv[i] = getLineTop(getLineForOffset(o)); + choosehtv[i] = getLineTop(getLineForOffset(o)); } else { // starts in this paragraph @@ -206,162 +200,87 @@ extends Layout } } - if (end - start > chdirs.length) { - chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)]; - mChdirs = chdirs; - } - if (end - start > chs.length) { - chs = new char[ArrayUtils.idealCharArraySize(end - start)]; - mChs = chs; - } - if ((end - start) * 2 > widths.length) { - widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)]; - mWidths = widths; - } - - TextUtils.getChars(source, start, end, chs, 0); - final int n = end - start; - - boolean easy = true; - boolean altered = false; - int dir = DEFAULT_DIR; // XXX - - for (int i = 0; i < n; i++) { - if (chs[i] >= FIRST_RIGHT_TO_LEFT) { - easy = false; - break; - } - } + measured.setPara(source, paraStart, paraEnd, DIR_REQUEST_DEFAULT_LTR); + char[] chs = measured.mChars; + float[] widths = measured.mWidths; + byte[] chdirs = measured.mLevels; + int dir = measured.mDir; + boolean easy = measured.mEasy; - // Ensure that none of the underlying characters are treated - // as viable breakpoints, and that the entire run gets the - // same bidi direction. - - if (source instanceof Spanned) { - Spanned sp = (Spanned) source; - ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class); - - for (int y = 0; y < spans.length; y++) { - int a = sp.getSpanStart(spans[y]); - int b = sp.getSpanEnd(spans[y]); - - for (int x = a; x < b; x++) { - chs[x - start] = '\uFFFC'; - } - } - } - - if (!easy) { - // XXX put override flags, etc. into chdirs - dir = bidi(dir, chs, chdirs, n, false); - - // Do mirroring for right-to-left segments - - for (int i = 0; i < n; i++) { - if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - int j; - - for (j = i; j < n; j++) { - if (chdirs[j] != - Character.DIRECTIONALITY_RIGHT_TO_LEFT) - break; - } - - if (AndroidCharacter.mirror(chs, i, j - i)) - altered = true; - - i = j - 1; - } - } - } - - CharSequence sub; - - if (altered) { - if (alter == null) - alter = AlteredCharSequence.make(source, chs, start, end); - else - alter.update(chs, start, end); - - sub = alter; - } else { - sub = source; - } + CharSequence sub = source; int width = firstwidth; float w = 0; - int here = start; + int here = paraStart; - int ok = start; + int ok = paraStart; float okwidth = w; int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0; - int fit = start; + int fit = paraStart; float fitwidth = w; int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0; - boolean tab = false; - - int next; - for (int i = start; i < end; i = next) { - if (spanned == null) - next = end; - else - next = spanned.nextSpanTransition(i, end, - MetricAffectingSpan. - class); - - if (spanned == null) { - paint.getTextWidths(sub, i, next, widths); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); - - paint.getFontMetricsInt(fm); - } else { - mWorkPaint.baselineShift = 0; + boolean hasTabOrEmoji = false; + boolean hasTab = false; + TabStops tabStops = null; + + for (int spanStart = paraStart, spanEnd = spanStart, nextSpanStart; + spanStart < paraEnd; spanStart = nextSpanStart) { - Styled.getTextWidths(paint, mWorkPaint, - spanned, i, next, - widths, fm); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); + if (spanStart == spanEnd) { + if (spanned == null) + spanEnd = paraEnd; + else + spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, + MetricAffectingSpan.class); - if (mWorkPaint.baselineShift < 0) { - fm.ascent += mWorkPaint.baselineShift; - fm.top += mWorkPaint.baselineShift; + int spanLen = spanEnd - spanStart; + if (spanned == null) { + measured.addStyleRun(paint, spanLen, fm); } else { - fm.descent += mWorkPaint.baselineShift; - fm.bottom += mWorkPaint.baselineShift; + MetricAffectingSpan[] spans = + spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); + measured.addStyleRun(paint, spans, spanLen, fm); } } + nextSpanStart = spanEnd; + int startInPara = spanStart - paraStart; + int endInPara = spanEnd - paraStart; + int fmtop = fm.top; int fmbottom = fm.bottom; int fmascent = fm.ascent; int fmdescent = fm.descent; - if (false) { - StringBuilder sb = new StringBuilder(); - for (int j = i; j < next; j++) { - sb.append(widths[j - start + (end - start)]); - sb.append(' '); - } - - Log.e("text", sb.toString()); - } - - for (int j = i; j < next; j++) { - char c = chs[j - start]; + for (int j = spanStart; j < spanEnd; j++) { + char c = chs[j - paraStart]; float before = w; if (c == '\n') { ; } else if (c == '\t') { - w = Layout.nextTab(sub, start, end, w, null); - tab = true; - } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < next) { - int emoji = Character.codePointAt(chs, j - start); + if (hasTab == false) { + hasTab = true; + hasTabOrEmoji = true; + if (spanned != null) { + // First tab this para, check for tabstops + TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, + paraEnd, TabStopSpan.class); + if (spans.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, spans); + } + } + } + if (tabStops != null) { + w = tabStops.nextTab(w); + } else { + w = TabStops.nextDefaultStop(w, TAB_INCREMENT); + } + } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < spanEnd) { + int emoji = Character.codePointAt(chs, j - paraStart); if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { Bitmap bm = EMOJI_FACTORY. @@ -376,21 +295,21 @@ extends Layout whichPaint = mWorkPaint; } - float wid = (float) bm.getWidth() * + float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight(); w += wid; - tab = true; + hasTabOrEmoji = true; j++; } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); @@ -411,7 +330,7 @@ extends Layout /* * From the Unicode Line Breaking Algorithm: * (at least approximately) - * + * * .,:; are class IS: breakpoints * except when adjacent to digits * / is class SY: a breakpoint @@ -426,12 +345,12 @@ extends Layout if (c == ' ' || c == '\t' || ((c == '.' || c == ',' || c == ':' || c == ';') && - (j - 1 < here || !Character.isDigit(chs[j - 1 - start])) && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) && + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || ((c == '/' || c == '-') && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || (c >= FIRST_CJK && isIdeographic(c, true) && - j + 1 < next && isIdeographic(chs[j + 1 - start], false))) { + j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) { okwidth = w; ok = j + 1; @@ -448,7 +367,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -457,10 +376,10 @@ extends Layout okascent, okdescent, oktop, okbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -484,7 +403,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -493,10 +412,10 @@ extends Layout okascent, okdescent, oktop, okbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -509,19 +428,20 @@ extends Layout fittop, fitbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, fit == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, fitwidth, paint); here = fit; } else { // Log.e("text", "output one " + here + " to " +(here + 1)); - measureText(paint, mWorkPaint, - source, here, here + 1, fm, tab, - null); + // XXX not sure why the existing fm wasn't ok. + // measureText(paint, mWorkPaint, + // source, here, here + 1, fm, tab, + // null); v = out(source, here, here+1, @@ -529,19 +449,22 @@ extends Layout fm.top, fm.bottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, here + 1 == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, - widths[here - start], paint); + widths[here - paraStart], paint); here = here + 1; } - if (here < i) { - j = next = here; // must remeasure + if (here < spanStart) { + // didn't output all the text for this span + // we've measured the raw widths, though, so + // just reset the start point + j = nextSpanStart = here; } else { j = here - 1; // continue looping } @@ -551,14 +474,14 @@ extends Layout fitascent = fitdescent = fittop = fitbottom = 0; okascent = okdescent = oktop = okbottom = 0; - if (--firstWidthLineCount <= 0) { + if (--firstWidthLineLimit <= 0) { width = restwidth; } } } } - if (end != here) { + if (paraEnd != here) { if ((fittop | fitbottom | fitdescent | fitascent) == 0) { paint.getFontMetricsInt(fm); @@ -571,20 +494,20 @@ extends Layout // Log.e("text", "output rest " + here + " to " + end); v = out(source, - here, end, fitascent, fitdescent, + here, paraEnd, fitascent, fitdescent, fittop, fitbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, - end == bufend, includepad, trackpad, - widths, start, end - start, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, + paraEnd == bufend, includepad, trackpad, + chs, widths, here - paraStart, where, ellipsizedWidth, w, paint); } - start = end; + paraStart = paraEnd; - if (end == bufend) + if (paraEnd == bufend) break; } @@ -599,246 +522,13 @@ extends Layout v, spacingmult, spacingadd, null, null, fm, false, - needMultiply, bufend, chdirs, DEFAULT_DIR, true, + needMultiply, bufend, null, DEFAULT_DIR, true, true, includepad, trackpad, - widths, bufstart, 0, + null, null, bufstart, where, ellipsizedWidth, 0, paint); } } - /** - * Runs the unicode bidi algorithm on the first n chars in chs, returning - * the char dirs in chInfo and the base line direction of the first - * paragraph. - * - * XXX change result from dirs to levels - * - * @param dir the direction flag, either DIR_REQUEST_LTR, - * DIR_REQUEST_RTL, DIR_REQUEST_DEFAULT_LTR, or DIR_REQUEST_DEFAULT_RTL. - * @param chs the text to examine - * @param chInfo on input, if hasInfo is true, override and other flags - * representing out-of-band embedding information. On output, the generated - * dirs of the text. - * @param n the length of the text/information in chs and chInfo - * @param hasInfo true if chInfo has input information, otherwise the - * input data in chInfo is ignored. - * @return the resolved direction level of the first paragraph, either - * DIR_LEFT_TO_RIGHT or DIR_RIGHT_TO_LEFT. - */ - /* package */ static int bidi(int dir, char[] chs, byte[] chInfo, int n, - boolean hasInfo) { - - AndroidCharacter.getDirectionalities(chs, chInfo, n); - - /* - * Determine primary paragraph direction if not specified - */ - if (dir != DIR_REQUEST_LTR && dir != DIR_REQUEST_RTL) { - // set up default - dir = dir >= 0 ? DIR_LEFT_TO_RIGHT : DIR_RIGHT_TO_LEFT; - for (int j = 0; j < n; j++) { - int d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) { - dir = DIR_LEFT_TO_RIGHT; - break; - } - if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - dir = DIR_RIGHT_TO_LEFT; - break; - } - } - } - - final byte SOR = dir == DIR_LEFT_TO_RIGHT ? - Character.DIRECTIONALITY_LEFT_TO_RIGHT : - Character.DIRECTIONALITY_RIGHT_TO_LEFT; - - /* - * XXX Explicit overrides should go here - */ - - /* - * Weak type resolution - */ - - // dump(chdirs, n, "initial"); - - // W1 non spacing marks - for (int j = 0; j < n; j++) { - if (chInfo[j] == Character.NON_SPACING_MARK) { - if (j == 0) - chInfo[j] = SOR; - else - chInfo[j] = chInfo[j - 1]; - } - } - - // dump(chdirs, n, "W1"); - - // W2 european numbers - byte cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - cur = d; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) { - if (cur == - Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; - } - } - - // dump(chdirs, n, "W2"); - - // W3 arabic letters - for (int j = 0; j < n; j++) { - if (chInfo[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - chInfo[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - } - - // dump(chdirs, n, "W3"); - - // W4 single separator between numbers - for (int j = 1; j < n - 1; j++) { - byte d = chInfo[j]; - byte prev = chInfo[j - 1]; - byte next = chInfo[j + 1]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) { - if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && - next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) { - if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && - next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER && - next == Character.DIRECTIONALITY_ARABIC_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; - } - } - - // dump(chdirs, n, "W4"); - - // W5 european number terminators - boolean adjacent = false; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - adjacent = true; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - else - adjacent = false; - } - - //dump(chdirs, n, "W5"); - - // W5 european number terminators part 2, - // W6 separators and terminators - adjacent = false; - for (int j = n - 1; j >= 0; j--) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - adjacent = true; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) { - if (adjacent) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - else - chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; - } - else { - adjacent = false; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR || - d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR || - d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR || - d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR) - chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; - } - } - - // dump(chdirs, n, "W6"); - - // W7 strong direction of european numbers - cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == SOR || - d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) - cur = d; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = cur; - } - - // dump(chdirs, n, "W7"); - - // N1, N2 neutrals - cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - cur = d; - } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER || - d == Character.DIRECTIONALITY_ARABIC_NUMBER) { - cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - } else { - byte dd = SOR; - int k; - - for (k = j + 1; k < n; k++) { - dd = chInfo[k]; - - if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - break; - } - if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER || - dd == Character.DIRECTIONALITY_ARABIC_NUMBER) { - dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - break; - } - } - - for (int y = j; y < k; y++) { - if (dd == cur) - chInfo[y] = cur; - else - chInfo[y] = SOR; - } - - j = k - 1; - } - } - - // dump(chdirs, n, "final"); - - // extra: enforce that all tabs and surrogate characters go the - // primary direction - // TODO: actually do directions right for surrogates - - for (int j = 0; j < n; j++) { - char c = chs[j]; - - if (c == '\t' || (c >= 0xD800 && c <= 0xDFFF)) { - chInfo[j] = SOR; - } - } - - return dir; - } - private static final char FIRST_CJK = '\u2E80'; /** * Returns true if the specified character is one of those specified @@ -944,37 +634,15 @@ extends Layout } */ - private static int getFit(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - float wid) { - int high = end + 1, low = start - 1, guess; - - while (high - low > 1) { - guess = (high + low) / 2; - - if (measureText(paint, workPaint, - text, start, guess, null, true, null) > wid) - high = guess; - else - low = guess; - } - - if (low < start) - return start; - else - return low; - } - private int out(CharSequence text, int start, int end, int above, int below, int top, int bottom, int v, float spacingmult, float spacingadd, LineHeightSpan[] chooseht, int[] choosehtv, - Paint.FontMetricsInt fm, boolean tab, + Paint.FontMetricsInt fm, boolean hasTabOrEmoji, boolean needMultiply, int pstart, byte[] chdirs, int dir, boolean easy, boolean last, boolean includepad, boolean trackpad, - float[] widths, int widstart, int widoff, + char[] chs, float[] widths, int widstart, TextUtils.TruncateAt ellipsize, float ellipsiswidth, float textwidth, TextPaint paint) { int j = mLineCount; @@ -982,8 +650,6 @@ extends Layout int want = off + mColumns + TOP; int[] lines = mLines; - // Log.e("text", "line " + start + " to " + end + (last ? "===" : "")); - if (want >= lines.length) { int nlen = ArrayUtils.idealIntArraySize(want + 1); int[] grow = new int[nlen]; @@ -1059,59 +725,23 @@ extends Layout lines[off + mColumns + START] = end; lines[off + mColumns + TOP] = v; - if (tab) + if (hasTabOrEmoji) lines[off + TAB] |= TAB_MASK; - { - lines[off + DIR] |= dir << DIR_SHIFT; - - int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; - int count = 0; - - if (!easy) { - for (int k = start; k < end; k++) { - if (chdirs[k - pstart] != cur) { - count++; - cur = chdirs[k - pstart]; - } - } - } - - Directions linedirs; - - if (count == 0) { - linedirs = DIRS_ALL_LEFT_TO_RIGHT; - } else { - short[] ld = new short[count + 1]; - - cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; - count = 0; - int here = start; - - for (int k = start; k < end; k++) { - if (chdirs[k - pstart] != cur) { - // XXX check to make sure we don't - // overflow short - ld[count++] = (short) (k - here); - cur = chdirs[k - pstart]; - here = k; - } - } - - ld[count] = (short) (end - here); - - if (count == 1 && ld[0] == 0) { - linedirs = DIRS_ALL_RIGHT_TO_LEFT; - } else { - linedirs = new Directions(ld); - } - } - + lines[off + DIR] |= dir << DIR_SHIFT; + Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; + // easy means all chars < the first RTL, so no emoji, no nothing + // XXX a run with no text or all spaces is easy but might be an empty + // RTL paragraph. Make sure easy is false if this is the case. + if (easy) { mLineDirections[j] = linedirs; + } else { + mLineDirections[j] = AndroidBidi.directions(dir, chdirs, widstart, chs, + widstart, end - start); // If ellipsize is in marquee mode, do not apply ellipsis on the first line if (ellipsize != null && (ellipsize != TextUtils.TruncateAt.MARQUEE || j != 0)) { - calculateEllipsis(start, end, widths, widstart, widoff, + calculateEllipsis(start, end, widths, widstart, ellipsiswidth, ellipsize, j, textwidth, paint); } @@ -1122,7 +752,7 @@ extends Layout } private void calculateEllipsis(int linestart, int lineend, - float[] widths, int widstart, int widoff, + float[] widths, int widstart, float avail, TextUtils.TruncateAt where, int line, float textwidth, TextPaint paint) { int len = lineend - linestart; @@ -1142,7 +772,7 @@ extends Layout int i; for (i = len; i >= 0; i--) { - float w = widths[i - 1 + linestart - widstart + widoff]; + float w = widths[i - 1 + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1158,7 +788,7 @@ extends Layout int i; for (i = 0; i < len; i++) { - float w = widths[i + linestart - widstart + widoff]; + float w = widths[i + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1175,7 +805,7 @@ extends Layout float ravail = (avail - ellipsiswid) / 2; for (right = len; right >= 0; right--) { - float w = widths[right - 1 + linestart - widstart + widoff]; + float w = widths[right - 1 + linestart - widstart]; if (w + rsum > ravail) { break; @@ -1186,7 +816,7 @@ extends Layout float lavail = avail - ellipsiswid - rsum; for (left = 0; left < right; left++) { - float w = widths[left + linestart - widstart + widoff]; + float w = widths[left + linestart - widstart]; if (w + lsum > lavail) { break; @@ -1203,7 +833,7 @@ extends Layout mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; } - // Override the baseclass so we can directly access our members, + // Override the base class so we can directly access our members, // rather than relying on member functions. // The logic mirrors that of Layout.getLineForVertical // FIXME: It may be faster to do a linear search for layouts without many lines. @@ -1232,11 +862,11 @@ extends Layout } public int getLineTop(int line) { - return mLines[mColumns * line + TOP]; + return mLines[mColumns * line + TOP]; } public int getLineDescent(int line) { - return mLines[mColumns * line + DESCENT]; + return mLines[mColumns * line + DESCENT]; } public int getLineStart(int line) { @@ -1309,13 +939,11 @@ extends Layout private static final int DIR_SHIFT = 30; private static final int TAB_MASK = 0x20000000; - private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; + private static final int TAB_INCREMENT = 20; // same as Layout, but that's private /* - * These are reused across calls to generate() + * This is reused across calls to generate() */ - private byte[] mChdirs; - private char[] mChs; - private float[] mWidths; + private MeasuredText mMeasured; private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); } diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java deleted file mode 100644 index 513b2cd444373845387a0d96119de41f69630ecc..0000000000000000000000000000000000000000 --- a/core/java/android/text/Styled.java +++ /dev/null @@ -1,434 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.text; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.text.style.CharacterStyle; -import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; - -/** - * This class provides static methods for drawing and measuring styled text, - * like {@link android.text.Spanned} object with - * {@link android.text.style.ReplacementSpan}. - * - * @hide - */ -public class Styled -{ - /** - * Draws and/or measures a uniform run of text on a single line. No span of - * interest should start or end in the middle of this run (if not - * drawing, character spans that don't affect metrics can be ignored). - * Neither should the run direction change in the middle of the run. - * - *

    The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - *

    On return, workPaint will reflect the original paint plus any - * modifications made by character styles on the run. - * - *

    The returned width is signed and will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawUniformRun(Canvas canvas, - Spanned text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - boolean haveWidth = false; - float ret = 0; - CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); - - ReplacementSpan replacement = null; - - // XXX: This shouldn't be modifying paint, only workPaint. - // However, the members belonging to TextPaint should have default - // values anyway. Better to ensure this in the Layout constructor. - paint.bgColor = 0; - paint.baselineShift = 0; - workPaint.set(paint); - - if (spans.length > 0) { - for (int i = 0; i < spans.length; i++) { - CharacterStyle span = spans[i]; - - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateDrawState(workPaint); - } - } - } - - if (replacement == null) { - CharSequence tmp; - int tmpstart, tmpend; - - if (runIsRtl) { - tmp = TextUtils.getReverse(text, start, end); - tmpstart = 0; - // XXX: assumes getReverse doesn't change the length of the text - tmpend = end - start; - } else { - tmp = text; - tmpstart = start; - tmpend = end; - } - - if (fmi != null) { - workPaint.getFontMetricsInt(fmi); - } - - if (canvas != null) { - if (workPaint.bgColor != 0) { - int c = workPaint.getColor(); - Paint.Style s = workPaint.getStyle(); - workPaint.setColor(workPaint.bgColor); - workPaint.setStyle(Paint.Style.FILL); - - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - canvas.drawRect(x - ret, top, x, bottom, workPaint); - else - canvas.drawRect(x, top, x + ret, bottom, workPaint); - - workPaint.setStyle(s); - workPaint.setColor(c); - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - canvas.drawText(tmp, tmpstart, tmpend, - x - ret, y + workPaint.baselineShift, workPaint); - } else { - if (needWidth) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - - canvas.drawText(tmp, tmpstart, tmpend, - x, y + workPaint.baselineShift, workPaint); - } - } else { - if (needWidth && !haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - } else { - ret = replacement.getSize(workPaint, text, start, end, fmi); - - if (canvas != null) { - if (dir == Layout.DIR_RIGHT_TO_LEFT) - replacement.draw(canvas, text, start, end, - x - ret, top, y, bottom, workPaint); - else - replacement.draw(canvas, text, start, end, - x, top, y, bottom, workPaint); - } - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - return -ret; - else - return ret; - } - - /** - * Returns the advance widths for a uniform left-to-right run of text with - * no style changes in the middle of the run. If any style is replacement - * text, the first character will get the width of the replacement and the - * remaining characters will get a width of 0. - * - * @param paint the paint, will not be modified - * @param workPaint a paint to modify; on return will reflect the original - * paint plus the effect of all spans on the run - * @param text the text - * @param start the start of the run - * @param end the limit of the run - * @param widths array to receive the advance widths of the characters. Must - * be at least a large as (end - start). - * @param fmi FontMetrics information; can be null - * @return the actual number of widths returned - */ - public static int getTextWidths(TextPaint paint, - TextPaint workPaint, - Spanned text, int start, int end, - float[] widths, Paint.FontMetricsInt fmi) { - MetricAffectingSpan[] spans = - text.getSpans(start, end, MetricAffectingSpan.class); - - ReplacementSpan replacement = null; - workPaint.set(paint); - - for (int i = 0; i < spans.length; i++) { - MetricAffectingSpan span = spans[i]; - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateMeasureState(workPaint); - } - } - - if (replacement == null) { - workPaint.getFontMetricsInt(fmi); - workPaint.getTextWidths(text, start, end, widths); - } else { - int wid = replacement.getSize(workPaint, text, start, end, fmi); - - if (end > start) { - widths[0] = wid; - for (int i = start + 1; i < end; i++) - widths[i - start] = 0; - } - } - return end - start; - } - - /** - * Renders and/or measures a directional run of text on a single line. - * Unlike {@link #drawUniformRun}, this can render runs that cross style - * boundaries. Returns the signed advance width, if requested. - * - *

    The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - *

    This optimizes for unstyled text and so workPaint might not be - * modified by this call. - * - *

    The returned advance width will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawDirectionalRun(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - // XXX: It looks like all calls to this API match dir and runIsRtl, so - // having both parameters is redundant and confusing. - - // fast path for unstyled text - if (!(text instanceof Spanned)) { - float ret = 0; - - if (runIsRtl) { - CharSequence tmp = TextUtils.getReverse(text, start, end); - // XXX: this assumes getReverse doesn't tweak the length of - // the text - int tmpend = end - start; - - if (canvas != null || needWidth) - ret = paint.measureText(tmp, 0, tmpend); - - if (canvas != null) - canvas.drawText(tmp, 0, tmpend, - x - ret, y, paint); - } else { - if (needWidth) - ret = paint.measureText(text, start, end); - - if (canvas != null) - canvas.drawText(text, start, end, x, y, paint); - } - - if (fmi != null) { - paint.getFontMetricsInt(fmi); - } - - return ret * dir; // Layout.DIR_RIGHT_TO_LEFT == -1 - } - - float ox = x; - int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0; - - Spanned sp = (Spanned) text; - Class division; - - if (canvas == null) - division = MetricAffectingSpan.class; - else - division = CharacterStyle.class; - - int next; - for (int i = start; i < end; i = next) { - next = sp.nextSpanTransition(i, end, division); - - // XXX: if dir and runIsRtl were not the same, this would draw - // spans in the wrong order, but no one appears to call it this - // way. - x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl, - x, top, y, bottom, fmi, paint, workPaint, - needWidth || next != end); - - if (fmi != null) { - if (fmi.ascent < minAscent) - minAscent = fmi.ascent; - if (fmi.descent > maxDescent) - maxDescent = fmi.descent; - - if (fmi.top < minTop) - minTop = fmi.top; - if (fmi.bottom > maxBottom) - maxBottom = fmi.bottom; - } - } - - if (fmi != null) { - if (start == end) { - paint.getFontMetricsInt(fmi); - } else { - fmi.ascent = minAscent; - fmi.descent = maxDescent; - fmi.top = minTop; - fmi.bottom = maxBottom; - } - } - - return x - ox; - } - - /** - * Draws a unidirectional run of text on a single line, and optionally - * returns the signed advance. Unlike drawDirectionalRun, the paragraph - * direction and run direction can be different. - */ - /* package */ static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl - if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) || - (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) { - // TODO: this needs the real direction - float ch = drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint, - workPaint, true); - - ch *= dir; // DIR_RIGHT_TO_LEFT == -1 - drawDirectionalRun(canvas, text, start, end, -dir, - runIsRtl, x + ch, top, y, bottom, null, paint, - workPaint, true); - - return ch; - } - - return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl, - x, top, y, bottom, null, paint, workPaint, - needWidth); - } - - /** - * Draws a run of text on a single line, with its - * origin at (x,y), in the specified Paint. The origin is interpreted based - * on the Align setting in the Paint. - * - * This method considers style information in the text (e.g. even when text - * is an instance of {@link android.text.Spanned}, this method correctly - * draws the text). See also - * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, - * float, Paint)} and - * {@link android.graphics.Canvas#drawRect(float, float, float, float, - * Paint)}. - * - * @param canvas The target canvas - * @param text The text to be drawn - * @param start The index of the first character in text to draw - * @param end (end - 1) is the index of the last character in text to draw - * @param direction The direction of the text. This must be - * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or - * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. - * @param x The x-coordinate of origin for where to draw the text - * @param top The top side of the rectangle to be drawn - * @param y The y-coordinate of origin for where to draw the text - * @param bottom The bottom side of the rectangle to be drawn - * @param paint The main {@link TextPaint} object. - * @param workPaint The {@link TextPaint} object used for temporal - * workspace. - * @param needWidth If true, this method returns the width of drawn text - * @return Width of the drawn text if needWidth is true - */ - public static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int direction, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // For safety. - direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT - : Layout.DIR_RIGHT_TO_LEFT; - - // Hide runIsRtl parameter since it is meaningless for external - // developers. - // XXX: the runIsRtl probably ought to be the same as direction, then - // this could draw rtl text. - return drawText(canvas, text, start, end, direction, false, - x, top, y, bottom, paint, workPaint, needWidth); - } - - /** - * Returns the width of a run of left-to-right text on a single line, - * considering style information in the text (e.g. even when text is an - * instance of {@link android.text.Spanned}, this method correctly measures - * the width of the text). - * - * @param paint the main {@link TextPaint} object; will not be modified - * @param workPaint the {@link TextPaint} object available for modification; - * will not necessarily be used - * @param text the text to measure - * @param start the index of the first character to start measuring - * @param end 1 beyond the index of the last character to measure - * @param fmi FontMetrics information; can be null - * @return The width of the text - */ - public static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - Paint.FontMetricsInt fmi) { - return drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, - 0, 0, 0, 0, fmi, paint, workPaint, true); - } -} diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java new file mode 100644 index 0000000000000000000000000000000000000000..2f7482c2f15d4e338c9850159aefc452d42eeeb5 --- /dev/null +++ b/core/java/android/text/TextLine.java @@ -0,0 +1,940 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import com.android.internal.util.ArrayUtils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.RectF; +import android.text.Layout.Directions; +import android.text.Layout.TabStops; +import android.text.style.CharacterStyle; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.util.Log; + +/** + * Represents a line of styled text, for measuring in visual order and + * for rendering. + * + *

    Get a new instance using obtain(), and when finished with it, return it + * to the pool using recycle(). + * + *

    Call set to prepare the instance for use, then either draw, measure, + * metrics, or caretToLeftRightOf. + * + * @hide + */ +class TextLine { + private TextPaint mPaint; + private CharSequence mText; + private int mStart; + private int mLen; + private int mDir; + private Directions mDirections; + private boolean mHasTabs; + private TabStops mTabs; + private char[] mChars; + private boolean mCharsValid; + private Spanned mSpanned; + private final TextPaint mWorkPaint = new TextPaint(); + + private static TextLine[] cached = new TextLine[3]; + + /** + * Returns a new TextLine from the shared pool. + * + * @return an uninitialized TextLine + */ + static TextLine obtain() { + TextLine tl; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + tl = cached[i]; + cached[i] = null; + return tl; + } + } + } + tl = new TextLine(); + Log.v("TLINE", "new: " + tl); + return tl; + } + + /** + * Puts a TextLine back into the shared pool. Do not use this TextLine once + * it has been returned. + * @param tl the textLine + * @return null, as a convenience from clearing references to the provided + * TextLine + */ + static TextLine recycle(TextLine tl) { + tl.mText = null; + tl.mPaint = null; + tl.mDirections = null; + if (tl.mLen < 250) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = tl; + break; + } + } + } + } + return null; + } + + /** + * Initializes a TextLine and prepares it for use. + * + * @param paint the base paint for the line + * @param text the text, can be Styled + * @param start the start of the line relative to the text + * @param limit the limit of the line relative to the text + * @param dir the paragraph direction of this line + * @param directions the directions information of this line + * @param hasTabs true if the line might contain tabs or emoji + * @param tabStops the tabStops. Can be null. + */ + void set(TextPaint paint, CharSequence text, int start, int limit, int dir, + Directions directions, boolean hasTabs, TabStops tabStops) { + mPaint = paint; + mText = text; + mStart = start; + mLen = limit - start; + mDir = dir; + mDirections = directions; + mHasTabs = hasTabs; + mSpanned = null; + + boolean hasReplacement = false; + if (text instanceof Spanned) { + mSpanned = (Spanned) text; + hasReplacement = mSpanned.getSpans(start, limit, + ReplacementSpan.class).length > 0; + } + + mCharsValid = hasReplacement || hasTabs || + directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; + + if (mCharsValid) { + if (mChars == null || mChars.length < mLen) { + mChars = new char[ArrayUtils.idealCharArraySize(mLen)]; + } + TextUtils.getChars(text, start, limit, mChars, 0); + if (hasReplacement) { + // Handle these all at once so we don't have to do it as we go. + // Replace the first character of each replacement run with the + // object-replacement character and the remainder with zero width + // non-break space aka BOM. Cursor movement code skips these + // zero-width characters. + char[] chars = mChars; + for (int i = start, inext; i < limit; i = inext) { + inext = mSpanned.nextSpanTransition(i, limit, + ReplacementSpan.class); + if (mSpanned.getSpans(i, inext, ReplacementSpan.class) + .length > 0) { // transition into a span + chars[i - start] = '\ufffc'; + for (int j = i - start + 1, e = inext - start; j < e; ++j) { + chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip + } + } + } + } + } + mTabs = tabStops; + } + + /** + * Renders the TextLine. + * + * @param c the canvas to render on + * @param x the leading margin position + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + */ + void draw(Canvas c, float x, int top, int y, int bottom) { + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + drawRun(c, 0, 0, mLen, false, x, top, y, bottom, false); + return; + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + drawRun(c, 0, 0, mLen, true, x, top, y, bottom, false); + return; + } + } + + float h = 0; + int[] runs = mDirections.mDirections; + RectF emojiRect = null; + + int lastRunIndex = runs.length - 2; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + char[] chars = mChars; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = mChars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(mChars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + h += drawRun(c, i, segstart, j, runIsRtl, x+h, top, y, bottom, + i != lastRunIndex || j != mLen); + + if (codept == '\t') { + h = mDir * nextTab(h * mDir); + } else if (bm != null) { + float bmAscent = ascent(j); + float bitmapHeight = bm.getHeight(); + float scale = -bmAscent / bitmapHeight; + float width = bm.getWidth() * scale; + + if (emojiRect == null) { + emojiRect = new RectF(); + } + emojiRect.set(x + h, y + bmAscent, + x + h + width, y); + c.drawBitmap(bm, null, emojiRect, mPaint); + h += width; + j++; + } + segstart = j + 1; + } + } + } + } + + /** + * Returns metrics information for the entire line. + * + * @param fmi receives font metrics information, can be null + * @return the signed width of the line + */ + float metrics(FontMetricsInt fmi) { + return measure(mLen, false, fmi); + } + + /** + * Returns information about a position on the line. + * + * @param offset the line-relative character offset, between 0 and the + * line length, inclusive + * @param trailing true to measure the trailing edge of the character + * before offset, false to measure the leading edge of the character + * at offset. + * @param fmi receives metrics information about the requested + * character, can be null. + * @return the signed offset from the leading margin to the requested + * character edge. + */ + float measure(int offset, boolean trailing, FontMetricsInt fmi) { + int target = trailing ? offset - 1 : offset; + if (target < 0) { + return 0; + } + + float h = 0; + + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + return measureRun(0, 0, offset, mLen, false, fmi); + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + return measureRun(0, 0, offset, mLen, true, fmi); + } + } + + char[] chars = mChars; + int[] runs = mDirections.mDirections; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = chars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(chars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + boolean inSegment = target >= segstart && target < j; + + boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; + if (inSegment && advance) { + return h += measureRun(i, segstart, offset, j, runIsRtl, fmi); + } + + float w = measureRun(i, segstart, j, j, runIsRtl, fmi); + h += advance ? w : -w; + + if (inSegment) { + return h += measureRun(i, segstart, offset, j, runIsRtl, null); + } + + if (codept == '\t') { + if (offset == j) { + return h; + } + h = mDir * nextTab(h * mDir); + if (target == j) { + return h; + } + } + + if (bm != null) { + float bmAscent = ascent(j); + float wid = bm.getWidth() * -bmAscent / bm.getHeight(); + h += mDir * wid; + j++; + } + + segstart = j + 1; + } + } + } + + return h; + } + + /** + * Draws a unidirectional (but possibly multi-styled) run of text. + * + * @param c the canvas to draw on + * @param runIndex the index of this directional run + * @param start the line-relative start + * @param limit the line-relative limit + * @param runIsRtl true if the run is right-to-left + * @param x the position of the run that is closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param needWidth true if the width value is required. + * @return the signed width of the run, based on the paragraph direction. + * Only valid if needWidth is true. + */ + private float drawRun(Canvas c, int runIndex, int start, + int limit, boolean runIsRtl, float x, int top, int y, int bottom, + boolean needWidth) { + + if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { + float w = -measureRun(runIndex, start, limit, limit, runIsRtl, null); + handleRun(runIndex, start, limit, limit, runIsRtl, c, x + w, top, + y, bottom, null, false); + return w; + } + + return handleRun(runIndex, start, limit, limit, runIsRtl, c, x, top, + y, bottom, null, needWidth); + } + + /** + * Measures a unidirectional (but possibly multi-styled) run of text. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param offset the offset to measure to, between start and limit inclusive + * @param limit the line-relative limit of the run + * @param runIsRtl true if the run is right-to-left + * @param fmi receives metrics information about the requested + * run, can be null. + * @return the signed width from the start of the run to the leading edge + * of the character at offset, based on the run (not paragraph) direction + */ + private float measureRun(int runIndex, int start, + int offset, int limit, boolean runIsRtl, FontMetricsInt fmi) { + return handleRun(runIndex, start, offset, limit, runIsRtl, null, + 0, 0, 0, 0, fmi, true); + } + + /** + * Walk the cursor through this line, skipping conjuncts and + * zero-width characters. + * + *

    This function cannot properly walk the cursor off the ends of the line + * since it does not know about any shaping on the previous/following line + * that might affect the cursor position. Callers must either avoid these + * situations or handle the result specially. + * + * @param cursor the starting position of the cursor, between 0 and the + * length of the line, inclusive + * @param toLeft true if the caret is moving to the left. + * @return the new offset. If it is less than 0 or greater than the length + * of the line, the previous/following line should be examined to get the + * actual offset. + */ + int getOffsetToLeftRightOf(int cursor, boolean toLeft) { + // 1) The caret marks the leading edge of a character. The character + // logically before it might be on a different level, and the active caret + // position is on the character at the lower level. If that character + // was the previous character, the caret is on its trailing edge. + // 2) Take this character/edge and move it in the indicated direction. + // This gives you a new character and a new edge. + // 3) This position is between two visually adjacent characters. One of + // these might be at a lower level. The active position is on the + // character at the lower level. + // 4) If the active position is on the trailing edge of the character, + // the new caret position is the following logical character, else it + // is the character. + + int lineStart = 0; + int lineEnd = mLen; + boolean paraIsRtl = mDir == -1; + int[] runs = mDirections.mDirections; + + int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; + boolean trailing = false; + + if (cursor == lineStart) { + runIndex = -2; + } else if (cursor == lineEnd) { + runIndex = runs.length; + } else { + // First, get information about the run containing the character with + // the active caret. + for (runIndex = 0; runIndex < runs.length; runIndex += 2) { + runStart = lineStart + runs[runIndex]; + if (cursor >= runStart) { + runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > lineEnd) { + runLimit = lineEnd; + } + if (cursor < runLimit) { + runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & + Layout.RUN_LEVEL_MASK; + if (cursor == runStart) { + // The caret is on a run boundary, see if we should + // use the position on the trailing edge of the previous + // logical character instead. + int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; + int pos = cursor - 1; + for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { + prevRunStart = lineStart + runs[prevRunIndex]; + if (pos >= prevRunStart) { + prevRunLimit = prevRunStart + + (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK); + if (prevRunLimit > lineEnd) { + prevRunLimit = lineEnd; + } + if (pos < prevRunLimit) { + prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) + & Layout.RUN_LEVEL_MASK; + if (prevRunLevel < runLevel) { + // Start from logically previous character. + runIndex = prevRunIndex; + runLevel = prevRunLevel; + runStart = prevRunStart; + runLimit = prevRunLimit; + trailing = true; + break; + } + } + } + } + } + break; + } + } + } + + // caret might be == lineEnd. This is generally a space or paragraph + // separator and has an associated run, but might be the end of + // text, in which case it doesn't. If that happens, we ran off the + // end of the run list, and runIndex == runs.length. In this case, + // we are at a run boundary so we skip the below test. + if (runIndex != runs.length) { + boolean runIsRtl = (runLevel & 0x1) != 0; + boolean advance = toLeft == runIsRtl; + if (cursor != (advance ? runLimit : runStart) || advance != trailing) { + // Moving within or into the run, so we can move logically. + newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit, + runIsRtl, cursor, advance); + // If the new position is internal to the run, we're at the strong + // position already so we're finished. + if (newCaret != (advance ? runLimit : runStart)) { + return newCaret; + } + } + } + } + + // If newCaret is -1, we're starting at a run boundary and crossing + // into another run. Otherwise we've arrived at a run boundary, and + // need to figure out which character to attach to. Note we might + // need to run this twice, if we cross a run boundary and end up at + // another run boundary. + while (true) { + boolean advance = toLeft == paraIsRtl; + int otherRunIndex = runIndex + (advance ? 2 : -2); + if (otherRunIndex >= 0 && otherRunIndex < runs.length) { + int otherRunStart = lineStart + runs[otherRunIndex]; + int otherRunLimit = otherRunStart + + (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK); + if (otherRunLimit > lineEnd) { + otherRunLimit = lineEnd; + } + int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & + Layout.RUN_LEVEL_MASK; + boolean otherRunIsRtl = (otherRunLevel & 1) != 0; + + advance = toLeft == otherRunIsRtl; + if (newCaret == -1) { + newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart, + otherRunLimit, otherRunIsRtl, + advance ? otherRunStart : otherRunLimit, advance); + if (newCaret == (advance ? otherRunLimit : otherRunStart)) { + // Crossed and ended up at a new boundary, + // repeat a second and final time. + runIndex = otherRunIndex; + runLevel = otherRunLevel; + continue; + } + break; + } + + // The new caret is at a boundary. + if (otherRunLevel < runLevel) { + // The strong character is in the other run. + newCaret = advance ? otherRunStart : otherRunLimit; + } + break; + } + + if (newCaret == -1) { + // We're walking off the end of the line. The paragraph + // level is always equal to or lower than any internal level, so + // the boundaries get the strong caret. + newCaret = advance ? mLen + 1 : -1; + break; + } + + // Else we've arrived at the end of the line. That's a strong position. + // We might have arrived here by crossing over a run with no internal + // breaks and dropping out of the above loop before advancing one final + // time, so reset the caret. + // Note, we use '<=' below to handle a situation where the only run + // on the line is a counter-directional run. If we're not advancing, + // we can end up at the 'lineEnd' position but the caret we want is at + // the lineStart. + if (newCaret <= lineEnd) { + newCaret = advance ? lineEnd : lineStart; + } + break; + } + + return newCaret; + } + + /** + * Returns the next valid offset within this directional run, skipping + * conjuncts and zero-width characters. This should not be called to walk + * off the end of the line, since the returned values might not be valid + * on neighboring lines. If the returned offset is less than zero or + * greater than the line length, the offset should be recomputed on the + * preceding or following line, respectively. + * + * @param runIndex the run index + * @param runStart the start of the run + * @param runLimit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param offset the offset + * @param after true if the new offset should logically follow the provided + * offset + * @return the new offset + */ + private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, + boolean runIsRtl, int offset, boolean after) { + + if (runIndex < 0 || offset == (after ? mLen : 0)) { + // Walking off end of line. Since we don't know + // what cursor positions are available on other lines, we can't + // return accurate values. These are a guess. + if (after) { + return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; + } + return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; + } + + TextPaint wp = mWorkPaint; + wp.set(mPaint); + + int spanStart = runStart; + int spanLimit; + if (mSpanned == null) { + spanLimit = runLimit; + } else { + int target = after ? offset + 1 : offset; + int limit = mStart + runLimit; + while (true) { + spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit, + MetricAffectingSpan.class) - mStart; + if (spanLimit >= target) { + break; + } + spanStart = spanLimit; + } + + MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, + mStart + spanLimit, MetricAffectingSpan.class); + + if (spans.length > 0) { + ReplacementSpan replacement = null; + for (int j = 0; j < spans.length; j++) { + MetricAffectingSpan span = spans[j]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + span.updateMeasureState(wp); + } + } + + if (replacement != null) { + // If we have a replacement span, we're moving either to + // the start or end of this span. + return after ? spanLimit : spanStart; + } + } + } + + int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; + int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; + if (mCharsValid) { + return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, + flags, offset, cursorOpt); + } else { + return wp.getTextRunCursor(mText, mStart + spanStart, + mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart; + } + } + + /** + * Utility function for measuring and rendering text. The text must + * not include a tab or emoji. + * + * @param wp the working paint + * @param start the start of the text + * @param end the end of the text + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null if rendering is not needed + * @param x the edge of the run closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width of the run is needed + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleText(TextPaint wp, int start, int end, + int contextStart, int contextEnd, boolean runIsRtl, + Canvas c, float x, int top, int y, int bottom, + FontMetricsInt fmi, boolean needWidth) { + + float ret = 0; + + int runLen = end - start; + int contextLen = contextEnd - contextStart; + if (needWidth || (c != null && (wp.bgColor != 0 || runIsRtl))) { + int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; + if (mCharsValid) { + ret = wp.getTextRunAdvances(mChars, start, runLen, + contextStart, contextLen, flags, null, 0); + } else { + int delta = mStart; + ret = wp.getTextRunAdvances(mText, delta + start, + delta + end, delta + contextStart, delta + contextEnd, + flags, null, 0); + } + } + + if (fmi != null) { + wp.getFontMetricsInt(fmi); + } + + if (c != null) { + if (runIsRtl) { + x -= ret; + } + + if (wp.bgColor != 0) { + int color = wp.getColor(); + Paint.Style s = wp.getStyle(); + wp.setColor(wp.bgColor); + wp.setStyle(Paint.Style.FILL); + + c.drawRect(x, top, x + ret, bottom, wp); + + wp.setStyle(s); + wp.setColor(color); + } + + drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, + x, y + wp.baselineShift); + } + + return runIsRtl ? -ret : ret; + } + + /** + * Utility function for measuring and rendering a replacement. + * + * @param replacement the replacement + * @param wp the work paint + * @param runIndex the run index + * @param start the start of the run + * @param limit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null if not rendering + * @param x the edge of the replacement closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width of the replacement is needed + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleReplacement(ReplacementSpan replacement, TextPaint wp, + int runIndex, int start, int limit, boolean runIsRtl, Canvas c, + float x, int top, int y, int bottom, FontMetricsInt fmi, + boolean needWidth) { + + float ret = 0; + + int textStart = mStart + start; + int textLimit = mStart + limit; + + if (needWidth || (c != null && runIsRtl)) { + ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); + } + + if (c != null) { + if (runIsRtl) { + x -= ret; + } + replacement.draw(c, mText, textStart, textLimit, + x, top, y, bottom, wp); + } + + return runIsRtl ? -ret : ret; + } + + /** + * Utility function for handling a unidirectional run. The run must not + * contain tabs or emoji but can contain styles. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param measureLimit the offset to measure to, between start and limit inclusive + * @param limit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null + * @param x the end of the run closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width is required + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleRun(int runIndex, int start, int measureLimit, + int limit, boolean runIsRtl, Canvas c, float x, int top, int y, + int bottom, FontMetricsInt fmi, boolean needWidth) { + + // Shaping needs to take into account context up to metric boundaries, + // but rendering needs to take into account character style boundaries. + // So we iterate through metric runs to get metric bounds, + // then within each metric run iterate through character style runs + // for the run bounds. + float ox = x; + for (int i = start, inext; i < measureLimit; i = inext) { + TextPaint wp = mWorkPaint; + wp.set(mPaint); + + int mlimit; + if (mSpanned == null) { + inext = limit; + mlimit = measureLimit; + } else { + inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit, + MetricAffectingSpan.class) - mStart; + + mlimit = inext < measureLimit ? inext : measureLimit; + MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i, + mStart + mlimit, MetricAffectingSpan.class); + + if (spans.length > 0) { + ReplacementSpan replacement = null; + for (int j = 0; j < spans.length; j++) { + MetricAffectingSpan span = spans[j]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + // We might have a replacement that uses the draw + // state, otherwise measure state would suffice. + span.updateDrawState(wp); + } + } + + if (replacement != null) { + x += handleReplacement(replacement, wp, runIndex, i, + mlimit, runIsRtl, c, x, top, y, bottom, fmi, + needWidth || mlimit < measureLimit); + continue; + } + } + } + + if (mSpanned == null || c == null) { + x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top, + y, bottom, fmi, needWidth || mlimit < measureLimit); + } else { + for (int j = i, jnext; j < mlimit; j = jnext) { + jnext = mSpanned.nextSpanTransition(mStart + j, + mStart + mlimit, CharacterStyle.class) - mStart; + + CharacterStyle[] spans = mSpanned.getSpans(mStart + j, + mStart + jnext, CharacterStyle.class); + + wp.set(mPaint); + for (int k = 0; k < spans.length; k++) { + CharacterStyle span = spans[k]; + span.updateDrawState(wp); + } + + x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, + top, y, bottom, fmi, needWidth || jnext < measureLimit); + } + } + } + + return x - ox; + } + + /** + * Render a text run with the set-up paint. + * + * @param c the canvas + * @param wp the paint used to render the text + * @param start the start of the run + * @param end the end of the run + * @param contextStart the start of context for the run + * @param contextEnd the end of the context for the run + * @param runIsRtl true if the run is right-to-left + * @param x the x position of the left edge of the run + * @param y the baseline of the run + */ + private void drawTextRun(Canvas c, TextPaint wp, int start, int end, + int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { + + int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR; + if (mCharsValid) { + int count = end - start; + int contextCount = contextEnd - contextStart; + c.drawTextRun(mChars, start, count, contextStart, contextCount, + x, y, flags, wp); + } else { + int delta = mStart; + c.drawTextRun(mText, delta + start, delta + end, + delta + contextStart, delta + contextEnd, x, y, flags, wp); + } + } + + /** + * Returns the ascent of the text at start. This is used for scaling + * emoji. + * + * @param pos the line-relative position + * @return the ascent of the text at start + */ + float ascent(int pos) { + if (mSpanned == null) { + return mPaint.ascent(); + } + + pos += mStart; + MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, + MetricAffectingSpan.class); + if (spans.length == 0) { + return mPaint.ascent(); + } + + TextPaint wp = mWorkPaint; + wp.set(mPaint); + for (MetricAffectingSpan span : spans) { + span.updateMeasureState(wp); + } + return wp.ascent(); + } + + /** + * Returns the next tab position. + * + * @param h the (unsigned) offset from the leading margin + * @return the (unsigned) tab position after this offset + */ + float nextTab(float h) { + if (mTabs != null) { + return mTabs.nextTab(h); + } + return TabStops.nextDefaultStop(h, TAB_INCREMENT); + } + + private static final int TAB_INCREMENT = 20; +} diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 8675d05f3116625823ed9c918f11d9a9bb2ca5b6..774826510ba7708fcd6822cb45dc6ad4aaf10eee 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -17,12 +17,11 @@ package android.text; import com.android.internal.R; +import com.android.internal.util.ArrayUtils; -import android.content.res.ColorStateList; import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; -import android.text.method.TextKeyListener.Capitalize; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -45,10 +44,8 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Printer; -import com.android.internal.util.ArrayUtils; - -import java.util.regex.Pattern; import java.util.Iterator; +import java.util.regex.Pattern; public class TextUtils { private TextUtils() { /* cannot be instantiated */ } @@ -983,7 +980,7 @@ public class TextUtils { /** * Returns the original text if it fits in the specified width * given the properties of the specified Paint, - * or, if it does not fit, a copy with ellipsis character added + * or, if it does not fit, a copy with ellipsis character added * at the specified edge or center. * If preserveLength is specified, the returned copy * will be padded with zero-width spaces to preserve the original @@ -992,7 +989,7 @@ public class TextUtils { * report the start and end of the ellipsized range. */ public static CharSequence ellipsize(CharSequence text, - TextPaint p, + TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback) { @@ -1003,13 +1000,12 @@ public class TextUtils { int len = text.length(); - // Use Paint.breakText() for the non-Spanned case to avoid having - // to allocate memory and accumulate the character widths ourselves. - - if (!(text instanceof Spanned)) { - float wid = p.measureText(text, 0, len); + MeasuredText mt = MeasuredText.obtain(); + try { + float width = setPara(mt, paint, text, 0, text.length(), + Layout.DIR_REQUEST_DEFAULT_LTR); - if (wid <= avail) { + if (width <= avail) { if (callback != null) { callback.ellipsized(0, 0); } @@ -1017,252 +1013,71 @@ public class TextUtils { return text; } - float ellipsiswid = p.measureText(sEllipsis); - - if (ellipsiswid > avail) { - if (callback != null) { - callback.ellipsized(0, len); - } - - if (preserveLength) { - char[] buf = obtain(len); - for (int i = 0; i < len; i++) { - buf[i] = '\uFEFF'; - } - String ret = new String(buf, 0, len); - recycle(buf); - return ret; - } else { - return ""; - } - } - - if (where == TruncateAt.START) { - int fit = p.breakText(text, 0, len, false, - avail - ellipsiswid, null); - - if (callback != null) { - callback.ellipsized(0, len - fit); - } - - if (preserveLength) { - return blank(text, 0, len - fit); - } else { - return sEllipsis + text.toString().substring(len - fit, len); - } + // XXX assumes ellipsis string does not require shaping and + // is unaffected by style + float ellipsiswid = paint.measureText(sEllipsis); + avail -= ellipsiswid; + + int left = 0; + int right = len; + if (avail < 0) { + // it all goes + } else if (where == TruncateAt.START) { + right = len - mt.breakText(0, len, false, avail); } else if (where == TruncateAt.END) { - int fit = p.breakText(text, 0, len, true, - avail - ellipsiswid, null); - - if (callback != null) { - callback.ellipsized(fit, len); - } - - if (preserveLength) { - return blank(text, fit, len); - } else { - return text.toString().substring(0, fit) + sEllipsis; - } - } else /* where == TruncateAt.MIDDLE */ { - int right = p.breakText(text, 0, len, false, - (avail - ellipsiswid) / 2, null); - float used = p.measureText(text, len - right, len); - int left = p.breakText(text, 0, len - right, true, - avail - ellipsiswid - used, null); - - if (callback != null) { - callback.ellipsized(left, len - right); - } - - if (preserveLength) { - return blank(text, left, len - right); - } else { - String s = text.toString(); - return s.substring(0, left) + sEllipsis + - s.substring(len - right, len); - } + left = mt.breakText(0, len, true, avail); + } else { + right = len - mt.breakText(0, len, false, avail / 2); + avail -= mt.measure(right, len); + left = mt.breakText(0, right, true, avail); } - } - - // But do the Spanned cases by hand, because it's such a pain - // to iterate the span transitions backwards and getTextWidths() - // will give us the information we need. - - // getTextWidths() always writes into the start of the array, - // so measure each span into the first half and then copy the - // results into the second half to use later. - - float[] wid = new float[len * 2]; - TextPaint temppaint = new TextPaint(); - Spanned sp = (Spanned) text; - - int next; - for (int i = 0; i < len; i = next) { - next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); - - Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); - System.arraycopy(wid, 0, wid, len + i, next - i); - } - - float sum = 0; - for (int i = 0; i < len; i++) { - sum += wid[len + i]; - } - if (sum <= avail) { if (callback != null) { - callback.ellipsized(0, 0); + callback.ellipsized(left, right); } - return text; - } - - float ellipsiswid = p.measureText(sEllipsis); - - if (ellipsiswid > avail) { - if (callback != null) { - callback.ellipsized(0, len); - } + char[] buf = mt.mChars; + Spanned sp = text instanceof Spanned ? (Spanned) text : null; + int remaining = len - (right - left); if (preserveLength) { - char[] buf = obtain(len); - for (int i = 0; i < len; i++) { + if (remaining > 0) { // else eliminate the ellipsis too + buf[left++] = '\u2026'; + } + for (int i = left; i < right; i++) { buf[i] = '\uFEFF'; } - SpannableString ss = new SpannableString(new String(buf, 0, len)); - recycle(buf); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - return ""; - } - } - - if (where == TruncateAt.START) { - sum = 0; - int i; - - for (i = len; i >= 0; i--) { - float w = wid[len + i - 1]; - - if (w + sum + ellipsiswid > avail) { - break; + String s = new String(buf, 0, len); + if (sp == null) { + return s; } - - sum += w; - } - - if (callback != null) { - callback.ellipsized(0, i); - } - - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, 0, i)); + SpannableString ss = new SpannableString(s); copySpansFrom(sp, 0, len, Object.class, ss, 0); return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(1, text, i, len); - - return out; } - } else if (where == TruncateAt.END) { - sum = 0; - int i; - for (i = 0; i < len; i++) { - float w = wid[len + i]; - - if (w + sum + ellipsiswid > avail) { - break; - } - - sum += w; - } - - if (callback != null) { - callback.ellipsized(i, len); - } - - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, i, len)); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(0, text, 0, i); - - return out; - } - } else /* where = TruncateAt.MIDDLE */ { - float lsum = 0, rsum = 0; - int left = 0, right = len; - - float ravail = (avail - ellipsiswid) / 2; - for (right = len; right >= 0; right--) { - float w = wid[len + right - 1]; - - if (w + rsum > ravail) { - break; - } - - rsum += w; - } - - float lavail = avail - ellipsiswid - rsum; - for (left = 0; left < right; left++) { - float w = wid[len + left]; - - if (w + lsum > lavail) { - break; - } - - lsum += w; + if (remaining == 0) { + return ""; } - if (callback != null) { - callback.ellipsized(left, right); + if (sp == null) { + StringBuilder sb = new StringBuilder(remaining + sEllipsis.length()); + sb.append(buf, 0, left); + sb.append(sEllipsis); + sb.append(buf, right, len - right); + return sb.toString(); } - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, left, right)); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(0, text, 0, left); - out.insert(out.length(), text, right, len); - - return out; - } + SpannableStringBuilder ssb = new SpannableStringBuilder(); + ssb.append(text, 0, left); + ssb.append(sEllipsis); + ssb.append(text, right, len); + return ssb; + } finally { + MeasuredText.recycle(mt); } } - private static String blank(CharSequence source, int start, int end) { - int len = source.length(); - char[] buf = obtain(len); - - if (start != 0) { - getChars(source, 0, start, buf, 0); - } - if (end != len) { - getChars(source, end, len, buf, end); - } - - if (start != end) { - buf[start] = '\u2026'; - - for (int i = start + 1; i < end; i++) { - buf[i] = '\uFEFF'; - } - } - - String ret = new String(buf, 0, len); - recycle(buf); - - return ret; - } - /** * Converts a CharSequence of the comma-separated form "Andy, Bob, * Charles, David" that is too wide to fit into the specified width @@ -1278,78 +1093,119 @@ public class TextUtils { TextPaint p, float avail, String oneMore, String more) { - int len = text.length(); - char[] buf = new char[len]; - TextUtils.getChars(text, 0, len, buf, 0); - int commaCount = 0; - for (int i = 0; i < len; i++) { - if (buf[i] == ',') { - commaCount++; + MeasuredText mt = MeasuredText.obtain(); + try { + int len = text.length(); + float width = setPara(mt, p, text, 0, len, Layout.DIR_REQUEST_DEFAULT_LTR); + if (width <= avail) { + return text; } - } - - float[] wid; - if (text instanceof Spanned) { - Spanned sp = (Spanned) text; - TextPaint temppaint = new TextPaint(); - wid = new float[len * 2]; + char[] buf = mt.mChars; - int next; - for (int i = 0; i < len; i = next) { - next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); - - Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); - System.arraycopy(wid, 0, wid, len + i, next - i); + int commaCount = 0; + for (int i = 0; i < len; i++) { + if (buf[i] == ',') { + commaCount++; + } } - System.arraycopy(wid, len, wid, 0, len); - } else { - wid = new float[len]; - p.getTextWidths(text, 0, len, wid); - } + int remaining = commaCount + 1; - int ok = 0; - int okRemaining = commaCount + 1; - String okFormat = ""; + int ok = 0; + int okRemaining = remaining; + String okFormat = ""; - int w = 0; - int count = 0; + int w = 0; + int count = 0; + float[] widths = mt.mWidths; - for (int i = 0; i < len; i++) { - w += wid[i]; + int request = mt.mDir == 1 ? Layout.DIR_REQUEST_LTR : + Layout.DIR_REQUEST_RTL; - if (buf[i] == ',') { - count++; + MeasuredText tempMt = MeasuredText.obtain(); + for (int i = 0; i < len; i++) { + w += widths[i]; - int remaining = commaCount - count + 1; - float moreWid; - String format; + if (buf[i] == ',') { + count++; - if (remaining == 1) { - format = " " + oneMore; - } else { - format = " " + String.format(more, remaining); - } + String format; + // XXX should not insert spaces, should be part of string + // XXX should use plural rules and not assume English plurals + if (--remaining == 1) { + format = " " + oneMore; + } else { + format = " " + String.format(more, remaining); + } - moreWid = p.measureText(format); + // XXX this is probably ok, but need to look at it more + tempMt.setPara(format, 0, format.length(), request); + float moreWid = mt.addStyleRun(p, mt.mLen, null); - if (w + moreWid <= avail) { - ok = i + 1; - okRemaining = remaining; - okFormat = format; + if (w + moreWid <= avail) { + ok = i + 1; + okRemaining = remaining; + okFormat = format; + } } } - } + MeasuredText.recycle(tempMt); - if (w <= avail) { - return text; - } else { SpannableStringBuilder out = new SpannableStringBuilder(okFormat); out.insert(0, text, 0, ok); return out; + } finally { + MeasuredText.recycle(mt); + } + } + + private static float setPara(MeasuredText mt, TextPaint paint, + CharSequence text, int start, int end, int bidiRequest) { + + mt.setPara(text, start, end, bidiRequest); + + float width; + Spanned sp = text instanceof Spanned ? (Spanned) text : null; + int len = end - start; + if (sp == null) { + width = mt.addStyleRun(paint, len, null); + } else { + width = 0; + int spanEnd; + for (int spanStart = 0; spanStart < len; spanStart = spanEnd) { + spanEnd = sp.nextSpanTransition(spanStart, len, + MetricAffectingSpan.class); + MetricAffectingSpan[] spans = sp.getSpans( + spanStart, spanEnd, MetricAffectingSpan.class); + width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); + } + } + + return width; + } + + private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; + + /* package */ + static boolean doesNotNeedBidi(CharSequence s, int start, int end) { + for (int i = start; i < end; i++) { + if (s.charAt(i) >= FIRST_RIGHT_TO_LEFT) { + return false; + } } + return true; + } + + /* package */ + static boolean doesNotNeedBidi(char[] text, int start, int len) { + for (int i = start, e = i + len; i < e; i++) { + if (text[i] >= FIRST_RIGHT_TO_LEFT) { + return false; + } + } + return true; } /* package */ static char[] obtain(int len) { @@ -1529,7 +1385,7 @@ public class TextUtils { */ public static final int CAP_MODE_CHARACTERS = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; - + /** * Capitalization mode for {@link #getCapsMode}: capitalize the first * character of all words. This value is explicitly defined to be the same as @@ -1537,7 +1393,7 @@ public class TextUtils { */ public static final int CAP_MODE_WORDS = InputType.TYPE_TEXT_FLAG_CAP_WORDS; - + /** * Capitalization mode for {@link #getCapsMode}: capitalize the first * character of each sentence. This value is explicitly defined to be the same as @@ -1545,13 +1401,13 @@ public class TextUtils { */ public static final int CAP_MODE_SENTENCES = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; - + /** * Determine what caps mode should be in effect at the current offset in * the text. Only the mode bits set in reqModes will be * checked. Note that the caps mode flags here are explicitly defined * to match those in {@link InputType}. - * + * * @param cs The text that should be checked for caps modes. * @param off Location in the text at which to check. * @param reqModes The modes to be checked: may be any combination of diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 4e2c3c395ce759f02bd28bdf7c9329e749b34f9c..a95dad79b112c4818479671f2b762391baf60dfd 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -502,7 +502,7 @@ public class DateUtils } } } else if (duration < WEEK_IN_MILLIS && minResolution < WEEK_IN_MILLIS) { - count = duration / DAY_IN_MILLIS; + count = getNumberOfDaysPassed(time, now); if (past) { if (abbrevRelative) { resId = com.android.internal.R.plurals.abbrev_num_days_ago; @@ -526,6 +526,24 @@ public class DateUtils return String.format(format, count); } + /** + * Returns the number of days passed between two dates. + * + * @param date1 first date + * @param date2 second date + * @return number of days passed between to dates. + */ + private synchronized static long getNumberOfDaysPassed(long date1, long date2) { + if (sThenTime == null) { + sThenTime = new Time(); + } + sThenTime.set(date1); + int day1 = Time.getJulianDay(date1, sThenTime.gmtoff); + sThenTime.set(date2); + int day2 = Time.getJulianDay(date2, sThenTime.gmtoff); + return Math.abs(day2 - day1); + } + /** * Return string describing the elapsed time since startTime formatted like * "[relative time/date], [time]". @@ -1595,40 +1613,45 @@ public class DateUtils public static CharSequence getRelativeTimeSpanString(Context c, long millis, boolean withPreposition) { + String result; long now = System.currentTimeMillis(); long span = now - millis; - if (sNowTime == null) { - sNowTime = new Time(); - sThenTime = new Time(); - } + synchronized (DateUtils.class) { + if (sNowTime == null) { + sNowTime = new Time(); + } - sNowTime.set(now); - sThenTime.set(millis); + if (sThenTime == null) { + sThenTime = new Time(); + } - String result; - int prepositionId; - if (span < DAY_IN_MILLIS && sNowTime.weekDay == sThenTime.weekDay) { - // Same day - int flags = FORMAT_SHOW_TIME; - result = formatDateRange(c, millis, millis, flags); - prepositionId = R.string.preposition_for_time; - } else if (sNowTime.year != sThenTime.year) { - // Different years - int flags = FORMAT_SHOW_DATE | FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE; - result = formatDateRange(c, millis, millis, flags); - - // This is a date (like "10/31/2008" so use the date preposition) - prepositionId = R.string.preposition_for_date; - } else { - // Default - int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_MONTH; - result = formatDateRange(c, millis, millis, flags); - prepositionId = R.string.preposition_for_date; - } - if (withPreposition) { - Resources res = c.getResources(); - result = res.getString(prepositionId, result); + sNowTime.set(now); + sThenTime.set(millis); + + int prepositionId; + if (span < DAY_IN_MILLIS && sNowTime.weekDay == sThenTime.weekDay) { + // Same day + int flags = FORMAT_SHOW_TIME; + result = formatDateRange(c, millis, millis, flags); + prepositionId = R.string.preposition_for_time; + } else if (sNowTime.year != sThenTime.year) { + // Different years + int flags = FORMAT_SHOW_DATE | FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE; + result = formatDateRange(c, millis, millis, flags); + + // This is a date (like "10/31/2008" so use the date preposition) + prepositionId = R.string.preposition_for_date; + } else { + // Default + int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_MONTH; + result = formatDateRange(c, millis, millis, flags); + prepositionId = R.string.preposition_for_date; + } + if (withPreposition) { + Resources res = c.getResources(); + result = res.getString(prepositionId, result); + } } return result; } diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java index 8eae111ee5d354909b4728e1604c6b2b0c8ef9ee..c05a8fefd6a4782e786d0b732b225e07450c0693 100644 --- a/core/java/android/text/format/Time.java +++ b/core/java/android/text/format/Time.java @@ -32,7 +32,7 @@ public class Time { private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000"; private static final String Y_M_D_T_H_M_S_000_Z = "%Y-%m-%dT%H:%M:%S.000Z"; private static final String Y_M_D = "%Y-%m-%d"; - + public static final String TIMEZONE_UTC = "UTC"; /** @@ -170,11 +170,11 @@ public class Time { public Time() { this(TimeZone.getDefault().getID()); } - + /** * A copy constructor. Construct a Time object by copying the given * Time object. No normalization occurs. - * + * * @param other */ public Time(Time other) { @@ -185,17 +185,17 @@ public class Time { * Ensures the values in each field are in range. For example if the * current value of this calendar is March 32, normalize() will convert it * to April 1. It also fills in weekDay, yearDay, isDst and gmtoff. - * + * *

    * If "ignoreDst" is true, then this method sets the "isDst" field to -1 * (the "unknown" value) before normalizing. It then computes the * correct value for "isDst". - * + * *

    * See {@link #toMillis(boolean)} for more information about when to * use true or false for "ignoreDst". - * - * @return the UTC milliseconds since the epoch + * + * @return the UTC milliseconds since the epoch */ native public long normalize(boolean ignoreDst); @@ -379,13 +379,13 @@ public class Time { * Parses a date-time string in either the RFC 2445 format or an abbreviated * format that does not include the "time" field. For example, all of the * following strings are valid: - * + * *

      *
    • "20081013T160000Z"
    • *
    • "20081013T160000"
    • *
    • "20081013"
    • *
    - * + * * Returns whether or not the time is in UTC (ends with Z). If the string * ends with "Z" then the timezone is set to UTC. If the date-time string * included only a date and no time field, then the allDay @@ -396,10 +396,10 @@ public class Time { * yearDay, and gmtoff are always set to zero, * and the field isDst is set to -1 (unknown). To set those * fields, call {@link #normalize(boolean)} after parsing. - * + * * To parse a date-time string and convert it to UTC milliseconds, do * something like this: - * + * *
          *   Time time = new Time();
          *   String date = "20081013T160000Z";
    @@ -428,25 +428,25 @@ public class Time {
          * Parse a time in RFC 3339 format.  This method also parses simple dates
          * (that is, strings that contain no time or time offset).  For example,
          * all of the following strings are valid:
    -     * 
    +     *
          * 
      *
    • "2008-10-13T16:00:00.000Z"
    • *
    • "2008-10-13T16:00:00.000+07:00"
    • *
    • "2008-10-13T16:00:00.000-07:00"
    • *
    • "2008-10-13"
    • *
    - * + * *

    * If the string contains a time and time offset, then the time offset will * be used to convert the time value to UTC. *

    - * + * *

    * If the given string contains just a date (with no time field), then * the {@link #allDay} field is set to true and the {@link #hour}, * {@link #minute}, and {@link #second} fields are set to zero. *

    - * + * *

    * Returns true if the resulting time value is in UTC time. *

    @@ -462,7 +462,7 @@ public class Time { } return false; } - + native private boolean nativeParse3339(String s); /** @@ -484,13 +484,13 @@ public class Time { * not change any of the fields in this Time object. If you want * to normalize the fields in this Time object and also get the milliseconds * then use {@link #normalize(boolean)}. - * + * *

    * If "ignoreDst" is false, then this method uses the current setting of the * "isDst" field and will adjust the returned time if the "isDst" field is * wrong for the given time. See the sample code below for an example of * this. - * + * *

    * If "ignoreDst" is true, then this method ignores the current setting of * the "isDst" field in this Time object and will instead figure out the @@ -499,27 +499,27 @@ public class Time { * correct value of the "isDst" field is when the time is inherently * ambiguous because it falls in the hour that is repeated when switching * from Daylight-Saving Time to Standard Time. - * + * *

    * Here is an example where toMillis(true) adjusts the time, * assuming that DST changes at 2am on Sunday, Nov 4, 2007. - * + * *

          * Time time = new Time();
    -     * time.set(2007, 10, 4);  // set the date to Nov 4, 2007, 12am
    +     * time.set(4, 10, 2007);  // set the date to Nov 4, 2007, 12am
          * time.normalize();       // this sets isDst = 1
          * time.monthDay += 1;     // changes the date to Nov 5, 2007, 12am
          * millis = time.toMillis(false);   // millis is Nov 4, 2007, 11pm
          * millis = time.toMillis(true);    // millis is Nov 5, 2007, 12am
          * 
    - * + * *

    * To avoid this problem, use toMillis(true) * after adding or subtracting days or explicitly setting the "monthDay" * field. On the other hand, if you are adding * or subtracting hours or minutes, then you should use * toMillis(false). - * + * *

    * You should also use toMillis(false) if you want * to read back the same milliseconds that you set with {@link #set(long)} @@ -531,14 +531,14 @@ public class Time { * Sets the fields in this Time object given the UTC milliseconds. After * this method returns, all the fields are normalized. * This also sets the "isDst" field to the correct value. - * + * * @param millis the time in UTC milliseconds since the epoch. */ native public void set(long millis); /** * Format according to RFC 2445 DATETIME type. - * + * *

    * The same as format("%Y%m%dT%H%M%S"). */ @@ -584,7 +584,7 @@ public class Time { * Sets the date from the given fields. Also sets allDay to true. * Sets weekDay, yearDay and gmtoff to 0, and isDst to -1. * Call {@link #normalize(boolean)} if you need those. - * + * * @param monthDay the day of the month (in the range [1,31]) * @param month the zero-based month number (in the range [0,11]) * @param year the year @@ -606,7 +606,7 @@ public class Time { /** * Returns true if the time represented by this Time object occurs before * the given time. - * + * * @param that a given Time object to compare against * @return true if this time is less than the given time */ @@ -618,7 +618,7 @@ public class Time { /** * Returns true if the time represented by this Time object occurs after * the given time. - * + * * @param that a given Time object to compare against * @return true if this time is greater than the given time */ @@ -632,12 +632,12 @@ public class Time { * closest Thursday yearDay. */ private static final int[] sThursdayOffset = { -3, 3, 2, 1, 0, -1, -2 }; - + /** * Computes the week number according to ISO 8601. The current Time * object must already be normalized because this method uses the * yearDay and weekDay fields. - * + * *

    * In IS0 8601, weeks start on Monday. * The first week of the year (week 1) is defined by ISO 8601 as the @@ -645,12 +645,12 @@ public class Time { * Or equivalently, the week containing January 4. Or equivalently, * the week with the year's first Thursday in it. *

    - * + * *

    * The week number can be calculated by counting Thursdays. Week N * contains the Nth Thursday of the year. *

    - * + * * @return the ISO week number. */ public int getWeekNumber() { @@ -661,7 +661,7 @@ public class Time { if (closestThursday >= 0 && closestThursday <= 364) { return closestThursday / 7 + 1; } - + // The week crosses a year boundary. Time temp = new Time(this); temp.monthDay += sThursdayOffset[weekDay]; @@ -670,7 +670,7 @@ public class Time { } /** - * Return a string in the RFC 3339 format. + * Return a string in the RFC 3339 format. *

    * If allDay is true, expresses the time as Y-M-D

    *

    @@ -691,13 +691,13 @@ public class Time { int offset = (int)Math.abs(gmtoff); int minutes = (offset % 3600) / 60; int hours = offset / 3600; - + return String.format("%s%s%02d:%02d", base, sign, hours, minutes); } } - + /** - * Returns true if the day of the given time is the epoch on the Julian Calendar + * Returns true if the day of the given time is the epoch on the Julian Calendar * (January 1, 1970 on the Gregorian calendar). * * @param time the time to test @@ -707,7 +707,7 @@ public class Time { long millis = time.toMillis(true); return getJulianDay(millis, 0) == EPOCH_JULIAN_DAY; } - + /** * Computes the Julian day number, given the UTC milliseconds * and the offset (in seconds) from UTC. The Julian day for a given @@ -716,10 +716,10 @@ public class Time { * what timezone is being used. The Julian day is useful for testing * if two events occur on the same day and for determining the relative * time of an event from the present ("yesterday", "3 days ago", etc.). - * + * *

    * Use {@link #toMillis(boolean)} to get the milliseconds. - * + * * @param millis the time in UTC milliseconds * @param gmtoff the offset from UTC in seconds * @return the Julian day @@ -729,7 +729,7 @@ public class Time { long julianDay = (millis + offsetMillis) / DateUtils.DAY_IN_MILLIS; return (int) julianDay + EPOCH_JULIAN_DAY; } - + /** *

    Sets the time from the given Julian day number, which must be based on * the same timezone that is set in this Time object. The "gmtoff" field @@ -738,7 +738,7 @@ public class Time { * After this method returns all the fields will be normalized and the time * will be set to 12am at the beginning of the given Julian day. *

    - * + * *

    * The only exception to this is if 12am does not exist for that day because * of daylight saving time. For example, Cairo, Eqypt moves time ahead one @@ -746,7 +746,7 @@ public class Time { * also change daylight saving time at 12am. In those cases, the time * will be set to 1am. *

    - * + * * @param julianDay the Julian day in the timezone for this Time object * @return the UTC milliseconds for the beginning of the Julian day */ @@ -756,13 +756,13 @@ public class Time { // the day. long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS; set(millis); - + // Figure out how close we are to the requested Julian day. // We can't be off by more than a day. int approximateDay = getJulianDay(millis, gmtoff); int diff = julianDay - approximateDay; monthDay += diff; - + // Set the time to 12am and re-normalize. hour = 0; minute = 0; diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java index a19a78e4698816b60366777c9a233f2a6915f655..3b98fc3d640a65f35e0c4fe4b9d118e383d6cce0 100644 --- a/core/java/android/text/method/Touch.java +++ b/core/java/android/text/method/Touch.java @@ -44,6 +44,7 @@ public class Touch { int left = Integer.MAX_VALUE; int right = 0; Alignment a = null; + boolean ltr = true; for (int i = top; i <= bottom; i++) { left = (int) Math.min(left, layout.getLineLeft(i)); @@ -51,6 +52,7 @@ public class Touch { if (a == null) { a = layout.getParagraphAlignment(i); + ltr = layout.getParagraphDirection(i) > 0; } } @@ -58,10 +60,12 @@ public class Touch { int width = widget.getWidth(); int diff = 0; + // align_opposite does NOT mean align_right, we need the paragraph + // direction to resolve it to left or right if (right - left < width - padding) { if (a == Alignment.ALIGN_CENTER) { diff = (width - padding - (right - left)) / 2; - } else if (a == Alignment.ALIGN_OPPOSITE) { + } else if (ltr == (a == Alignment.ALIGN_OPPOSITE)) { diff = width - padding - (right - left); } } diff --git a/core/java/android/util/AndroidException.java b/core/java/android/util/AndroidException.java index a767ea1e975c2d8afbd36ebdc1752bb461023f5b..dfe00c9bd49fb2d0dca0cef891440d621f77291f 100644 --- a/core/java/android/util/AndroidException.java +++ b/core/java/android/util/AndroidException.java @@ -27,6 +27,10 @@ public class AndroidException extends Exception { super(name); } + public AndroidException(String name, Throwable cause) { + super(name, cause); + } + public AndroidException(Exception cause) { super(cause); } diff --git a/core/java/android/util/AndroidRuntimeException.java b/core/java/android/util/AndroidRuntimeException.java index 4ed17bcd31c6c8cce2b0578917efcf08ced044cf..2b824bf9cb2ae04e2d352d2ef202d60882ebd760 100644 --- a/core/java/android/util/AndroidRuntimeException.java +++ b/core/java/android/util/AndroidRuntimeException.java @@ -27,6 +27,10 @@ public class AndroidRuntimeException extends RuntimeException { super(name); } + public AndroidRuntimeException(String name, Throwable cause) { + super(name, cause); + } + public AndroidRuntimeException(Exception cause) { super(cause); } diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java index 9d91acaa5f87d8da2cd800b239d989d94f01ad17..a763a6925cb26666612d18724f995b6984732cbb 100644 --- a/core/java/android/util/CharsetUtils.java +++ b/core/java/android/util/CharsetUtils.java @@ -17,36 +17,58 @@ package android.util; import android.os.Build; +import android.text.TextUtils; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; +import java.util.Map; /** + *

    * A class containing utility methods related to character sets. This * class is primarily useful for code that wishes to be vendor-aware - * in its interpretation of Japanese encoding names. - * - *

    As of this writing, the only vendor that is recognized by this - * class is Docomo (identified case-insensitively as {@code "docomo"}).

    - * - * Note: This class is hidden in Cupcake, with a plan to - * un-hide in Donut. This was done because the first deployment to use - * this code is based on Cupcake, but the API had to be introduced - * after the public API freeze for that release. The upshot is that - * only system applications can safely use this class until Donut is - * available. - * + * in its interpretation of Japanese charset names (used in DoCoMo, + * KDDI, and SoftBank). + *

    + * + *

    + * Note: Developers will need to add an appropriate mapping for + * each vendor-specific charset. You may need to modify the C libraries + * like icu4c in order to let Android support an additional charset. + *

    + * * @hide */ public final class CharsetUtils { /** - * name of the vendor "Docomo". Note: This isn't a public + * name of the vendor "DoCoMo". Note: This isn't a public * constant, in order to keep this class from becoming a de facto * reference list of vendor names. */ private static final String VENDOR_DOCOMO = "docomo"; - + /** + * Name of the vendor "KDDI". + */ + private static final String VENDOR_KDDI = "kddi"; + /** + * Name of the vendor "SoftBank". + */ + private static final String VENDOR_SOFTBANK = "softbank"; + + /** + * Represents one-to-one mapping from a vendor name to a charset specific to the vendor. + */ + private static final Map sVendorShiftJisMap = new HashMap(); + + static { + // These variants of Shift_JIS come from icu's mapping data (convrtrs.txt) + sVendorShiftJisMap.put(VENDOR_DOCOMO, "docomo-shift_jis-2007"); + sVendorShiftJisMap.put(VENDOR_KDDI, "kddi-shift_jis-2007"); + sVendorShiftJisMap.put(VENDOR_SOFTBANK, "softbank-shift_jis-2007"); + } + /** * This class is uninstantiable. */ @@ -58,20 +80,22 @@ public final class CharsetUtils { * Returns the name of the vendor-specific character set * corresponding to the given original character set name and * vendor. If there is no vendor-specific character set for the - * given name/vendor pair, this returns the original character set - * name. The vendor name is matched case-insensitively. - * + * given name/vendor pair, this returns the original character set name. + * * @param charsetName the base character set name - * @param vendor the vendor to specialize for + * @param vendor the vendor to specialize for. All characters should be lower-cased. * @return the specialized character set name, or {@code charsetName} if * there is no specialized name */ public static String nameForVendor(String charsetName, String vendor) { - // TODO: Eventually, this may want to be table-driven. - - if (vendor.equalsIgnoreCase(VENDOR_DOCOMO) - && isShiftJis(charsetName)) { - return "docomo-shift_jis-2007"; + if (!TextUtils.isEmpty(charsetName) && !TextUtils.isEmpty(vendor)) { + // You can add your own mapping here. + if (isShiftJis(charsetName)) { + final String vendorShiftJis = sVendorShiftJisMap.get(vendor); + if (vendorShiftJis != null) { + return vendorShiftJis; + } + } } return charsetName; diff --git a/core/java/android/util/Finalizers.java b/core/java/android/util/Finalizers.java new file mode 100644 index 0000000000000000000000000000000000000000..671f2d496e638b2cac8585f0d83ef8628655ca2d --- /dev/null +++ b/core/java/android/util/Finalizers.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; + +/** + * This class can be used to implement reliable finalizers. + * + * @hide + */ +public final class Finalizers { + private static final String LOG_TAG = "Finalizers"; + + private static final Object[] sLock = new Object[0]; + private static boolean sInit; + private static Reclaimer sReclaimer; + + /** + * Subclass of PhantomReference used to reclaim resources. + */ + public static abstract class ReclaimableReference extends PhantomReference { + public ReclaimableReference(T r, ReferenceQueue q) { + super(r, q); + } + + public abstract void reclaim(); + } + + /** + * Returns the queue used to reclaim ReclaimableReferences. + * + * @return A reference queue or null before initialization + */ + public static ReferenceQueue getQueue() { + synchronized (sLock) { + if (!sInit) { + return null; + } + if (!sReclaimer.isRunning()) { + sReclaimer = new Reclaimer(sReclaimer.mQueue); + sReclaimer.start(); + } + return sReclaimer.mQueue; + } + } + + /** + * Invoked by Zygote. Don't touch! + */ + public static void init() { + synchronized (sLock) { + if (!sInit && sReclaimer == null) { + sReclaimer = new Reclaimer(); + sReclaimer.start(); + sInit = true; + } + } + } + + private static class Reclaimer extends Thread { + ReferenceQueue mQueue; + + private volatile boolean mRunning = false; + + Reclaimer() { + this(new ReferenceQueue()); + } + + Reclaimer(ReferenceQueue queue) { + super("Reclaimer"); + setDaemon(true); + mQueue = queue; + } + + @Override + public void start() { + mRunning = true; + super.start(); + } + + boolean isRunning() { + return mRunning; + } + + @SuppressWarnings({"InfiniteLoopStatement"}) + @Override + public void run() { + try { + while (true) { + try { + cleanUp(mQueue.remove()); + } catch (InterruptedException e) { + // Ignore + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "Reclaimer thread exiting: ", e); + } finally { + mRunning = false; + } + } + + private void cleanUp(Reference reference) { + do { + reference.clear(); + ((ReclaimableReference) reference).reclaim(); + } while ((reference = mQueue.poll()) != null); + } + } +} diff --git a/core/java/android/util/JsonReader.java b/core/java/android/util/JsonReader.java new file mode 100644 index 0000000000000000000000000000000000000000..3345bfab59fc565f8464fd213192427254736e6a --- /dev/null +++ b/core/java/android/util/JsonReader.java @@ -0,0 +1,1058 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import java.io.IOException; +import java.io.Reader; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; + +/** + * Reads a JSON (RFC 4627) + * encoded value as a stream of tokens. This stream includes both literal + * values (strings, numbers, booleans, and nulls) as well as the begin and + * end delimiters of objects and arrays. The tokens are traversed in + * depth-first order, the same order that they appear in the JSON document. + * Within JSON objects, name/value pairs are represented by a single token. + * + *

    Parsing JSON

    + * To create a recursive descent parser your own JSON streams, first create an + * entry point method that creates a {@code JsonReader}. + * + *

    Next, create handler methods for each structure in your JSON text. You'll + * need a method for each object type and for each array type. + *

      + *
    • Within array handling methods, first call {@link + * #beginArray} to consume the array's opening bracket. Then create a + * while loop that accumulates values, terminating when {@link #hasNext} + * is false. Finally, read the array's closing bracket by calling {@link + * #endArray}. + *
    • Within object handling methods, first call {@link + * #beginObject} to consume the object's opening brace. Then create a + * while loop that assigns values to local variables based on their name. + * This loop should terminate when {@link #hasNext} is false. Finally, + * read the object's closing brace by calling {@link #endObject}. + *
    + *

    When a nested object or array is encountered, delegate to the + * corresponding handler method. + * + *

    When an unknown name is encountered, strict parsers should fail with an + * exception. Lenient parsers should call {@link #skipValue()} to recursively + * skip the value's nested tokens, which may otherwise conflict. + * + *

    If a value may be null, you should first check using {@link #peek()}. + * Null literals can be consumed using either {@link #nextNull()} or {@link + * #skipValue()}. + * + *

    Example

    + * Suppose we'd like to parse a stream of messages such as the following:
     {@code
    + * [
    + *   {
    + *     "id": 912345678901,
    + *     "text": "How do I read JSON on Android?",
    + *     "geo": null,
    + *     "user": {
    + *       "name": "android_newb",
    + *       "followers_count": 41
    + *      }
    + *   },
    + *   {
    + *     "id": 912345678902,
    + *     "text": "@android_newb just use android.util.JsonReader!",
    + *     "geo": [50.454722, -104.606667],
    + *     "user": {
    + *       "name": "jesse",
    + *       "followers_count": 2
    + *     }
    + *   }
    + * ]}
    + * This code implements the parser for the above structure:
       {@code
    + *
    + *   public List readJsonStream(InputStream in) throws IOException {
    + *     JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
    + *     return readMessagesArray(reader);
    + *   }
    + *
    + *   public List readMessagesArray(JsonReader reader) throws IOException {
    + *     List messages = new ArrayList();
    + *
    + *     reader.beginArray();
    + *     while (reader.hasNext()) {
    + *       messages.add(readMessage(reader));
    + *     }
    + *     reader.endArray();
    + *     return messages;
    + *   }
    + *
    + *   public Message readMessage(JsonReader reader) throws IOException {
    + *     long id = -1;
    + *     String text = null;
    + *     User user = null;
    + *     List geo = null;
    + *
    + *     reader.beginObject();
    + *     while (reader.hasNext()) {
    + *       String name = reader.nextName();
    + *       if (name.equals("id")) {
    + *         id = reader.nextLong();
    + *       } else if (name.equals("text")) {
    + *         text = reader.nextString();
    + *       } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
    + *         geo = readDoublesArray(reader);
    + *       } else if (name.equals("user")) {
    + *         user = readUser(reader);
    + *       } else {
    + *         reader.skipValue();
    + *       }
    + *     }
    + *     reader.endObject();
    + *     return new Message(id, text, user, geo);
    + *   }
    + *
    + *   public List readDoublesArray(JsonReader reader) throws IOException {
    + *     List doubles = new ArrayList();
    + *
    + *     reader.beginArray();
    + *     while (reader.hasNext()) {
    + *       doubles.add(reader.nextDouble());
    + *     }
    + *     reader.endArray();
    + *     return doubles;
    + *   }
    + *
    + *   public User readUser(JsonReader reader) throws IOException {
    + *     String username = null;
    + *     int followersCount = -1;
    + *
    + *     reader.beginObject();
    + *     while (reader.hasNext()) {
    + *       String name = reader.nextName();
    + *       if (name.equals("name")) {
    + *         username = reader.nextString();
    + *       } else if (name.equals("followers_count")) {
    + *         followersCount = reader.nextInt();
    + *       } else {
    + *         reader.skipValue();
    + *       }
    + *     }
    + *     reader.endObject();
    + *     return new User(username, followersCount);
    + *   }}
    + * + *

    Number Handling

    + * This reader permits numeric values to be read as strings and string values to + * be read as numbers. For example, both elements of the JSON array {@code + * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}. + * This behavior is intended to prevent lossy numeric conversions: double is + * JavaScript's only numeric type and very large values like {@code + * 9007199254740993} cannot be represented exactly on that platform. To minimize + * precision loss, extremely large values should be written and read as strings + * in JSON. + * + *

    Each {@code JsonReader} may be used to read a single JSON stream. Instances + * of this class are not thread safe. + */ +public final class JsonReader implements Closeable { + + /** The input JSON. */ + private final Reader in; + + /** True to accept non-spec compliant JSON */ + private boolean lenient = false; + + /** + * Use a manual buffer to easily read and unread upcoming characters, and + * also so we can create strings without an intermediate StringBuilder. + */ + private final char[] buffer = new char[1024]; + private int pos = 0; + private int limit = 0; + + private final List stack = new ArrayList(); + { + push(JsonScope.EMPTY_DOCUMENT); + } + + /** + * True if we've already read the next token. If we have, the string value + * for that token will be assigned to {@code value} if such a string value + * exists. And the token type will be assigned to {@code token} if the token + * type is known. The token type may be null for literals, since we derive + * that lazily. + */ + private boolean hasToken; + + /** + * The type of the next token to be returned by {@link #peek} and {@link + * #advance}, or {@code null} if it is unknown and must first be derived + * from {@code value}. This value is undefined if {@code hasToken} is false. + */ + private JsonToken token; + + /** The text of the next name. */ + private String name; + + /** The text of the next literal value. */ + private String value; + + /** True if we're currently handling a skipValue() call. */ + private boolean skipping = false; + + /** + * Creates a new instance that reads a JSON-encoded stream from {@code in}. + */ + public JsonReader(Reader in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + this.in = in; + } + + /** + * Configure this parser to be be liberal in what it accepts. By default, + * this parser is strict and only accepts JSON as specified by RFC 4627. Setting the + * parser to lenient causes it to ignore the following syntax errors: + * + *

      + *
    • End of line comments starting with {@code //} or {@code #} and + * ending with a newline character. + *
    • C-style comments starting with {@code /*} and ending with + * {@code *}{@code /}. Such comments may not be nested. + *
    • Names that are unquoted or {@code 'single quoted'}. + *
    • Strings that are unquoted or {@code 'single quoted'}. + *
    • Array elements separated by {@code ;} instead of {@code ,}. + *
    • Unnecessary array separators. These are interpreted as if null + * was the omitted value. + *
    • Names and values separated by {@code =} or {@code =>} instead of + * {@code :}. + *
    • Name/value pairs separated by {@code ;} instead of {@code ,}. + *
    + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new array. + */ + public void beginArray() throws IOException { + expect(JsonToken.BEGIN_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endArray() throws IOException { + expect(JsonToken.END_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new object. + */ + public void beginObject() throws IOException { + expect(JsonToken.BEGIN_OBJECT); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endObject() throws IOException { + expect(JsonToken.END_OBJECT); + } + + /** + * Consumes {@code expected}. + */ + private void expect(JsonToken expected) throws IOException { + quickPeek(); + if (token != expected) { + throw new IllegalStateException("Expected " + expected + " but was " + peek()); + } + advance(); + } + + /** + * Returns true if the current array or object has another element. + */ + public boolean hasNext() throws IOException { + quickPeek(); + return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; + } + + /** + * Returns the type of the next token without consuming it. + */ + public JsonToken peek() throws IOException { + quickPeek(); + + if (token == null) { + decodeLiteral(); + } + + return token; + } + + /** + * Ensures that a token is ready. After this call either {@code token} or + * {@code value} will be non-null. To ensure {@code token} has a definitive + * value, use {@link #peek()} + */ + private JsonToken quickPeek() throws IOException { + if (hasToken) { + return token; + } + + switch (peekStack()) { + case EMPTY_DOCUMENT: + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + JsonToken firstToken = nextValue(); + if (token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) { + throw new IOException( + "Expected JSON document to start with '[' or '{' but was " + token); + } + return firstToken; + case EMPTY_ARRAY: + return nextInArray(true); + case NONEMPTY_ARRAY: + return nextInArray(false); + case EMPTY_OBJECT: + return nextInObject(true); + case DANGLING_NAME: + return objectValue(); + case NONEMPTY_OBJECT: + return nextInObject(false); + case NONEMPTY_DOCUMENT: + hasToken = true; + return token = JsonToken.END_DOCUMENT; + case CLOSED: + throw new IllegalStateException("JsonReader is closed"); + default: + throw new AssertionError(); + } + } + + /** + * Advances the cursor in the JSON stream to the next token. + */ + private JsonToken advance() throws IOException { + quickPeek(); + + JsonToken result = token; + hasToken = false; + token = null; + value = null; + name = null; + return result; + } + + /** + * Returns the next token, a {@link JsonToken#NAME property name}, and + * consumes it. + * + * @throws IOException if the next token in the stream is not a property + * name. + */ + public String nextName() throws IOException { + quickPeek(); + if (token != JsonToken.NAME) { + throw new IllegalStateException("Expected a name but was " + peek()); + } + String result = name; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#STRING string} value of the next token, + * consuming it. If the next token is a number, this method will return its + * string form. + * + * @throws IllegalStateException if the next token is not a string or if + * this reader is closed. + */ + public String nextString() throws IOException { + peek(); + if (value == null || (token != JsonToken.STRING && token != JsonToken.NUMBER)) { + throw new IllegalStateException("Expected a string but was " + peek()); + } + + String result = value; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, + * consuming it. + * + * @throws IllegalStateException if the next token is not a boolean or if + * this reader is closed. + */ + public boolean nextBoolean() throws IOException { + quickPeek(); + if (value == null || token == JsonToken.STRING) { + throw new IllegalStateException("Expected a boolean but was " + peek()); + } + + boolean result; + if (value.equalsIgnoreCase("true")) { + result = true; + } else if (value.equalsIgnoreCase("false")) { + result = false; + } else { + throw new IllegalStateException("Not a boolean: " + value); + } + + advance(); + return result; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is a + * literal null. + * + * @throws IllegalStateException if the next token is not null or if this + * reader is closed. + */ + public void nextNull() throws IOException { + quickPeek(); + if (value == null || token == JsonToken.STRING) { + throw new IllegalStateException("Expected null but was " + peek()); + } + + if (!value.equalsIgnoreCase("null")) { + throw new IllegalStateException("Not a null: " + value); + } + + advance(); + } + + /** + * Returns the {@link JsonToken#NUMBER double} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a double. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a double, or is non-finite. + */ + public double nextDouble() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected a double but was " + peek()); + } + + double result = Double.parseDouble(value); + + if ((result >= 1.0d && value.startsWith("0")) + || Double.isNaN(result) + || Double.isInfinite(result)) { + throw new NumberFormatException( + "JSON forbids octal prefixes, NaN and infinities: " + value); + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER long} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a long. If the next token's numeric value cannot be exactly + * represented by a Java {@code long}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as a long. + */ + public long nextLong() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected a long but was " + peek()); + } + + long result; + try { + result = Long.parseLong(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (long) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + if (result >= 1L && value.startsWith("0")) { + throw new NumberFormatException("JSON forbids octal prefixes: " + value); + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER int} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as an int. If the next token's numeric value cannot be exactly + * represented by a Java {@code int}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as an int. + */ + public int nextInt() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected an int but was " + peek()); + } + + int result; + try { + result = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (int) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + if (result >= 1L && value.startsWith("0")) { + throw new NumberFormatException("JSON forbids octal prefixes: " + value); + } + + advance(); + return result; + } + + /** + * Closes this JSON reader and the underlying {@link Reader}. + */ + public void close() throws IOException { + hasToken = false; + value = null; + token = null; + stack.clear(); + stack.add(JsonScope.CLOSED); + in.close(); + } + + /** + * Skips the next value recursively. If it is an object or array, all nested + * elements are skipped. This method is intended for use when the JSON token + * stream contains unrecognized or unhandled values. + */ + public void skipValue() throws IOException { + skipping = true; + try { + int count = 0; + do { + JsonToken token = advance(); + if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { + count++; + } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { + count--; + } + } while (count != 0); + } finally { + skipping = false; + } + } + + private JsonScope peekStack() { + return stack.get(stack.size() - 1); + } + + private JsonScope pop() { + return stack.remove(stack.size() - 1); + } + + private void push(JsonScope newTop) { + stack.add(newTop); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope newTop) { + stack.set(stack.size() - 1, newTop); + } + + private JsonToken nextInArray(boolean firstElement) throws IOException { + if (firstElement) { + replaceTop(JsonScope.NONEMPTY_ARRAY); + } else { + /* Look for a comma before each element after the first element. */ + switch (nextNonWhitespace()) { + case ']': + pop(); + hasToken = true; + return token = JsonToken.END_ARRAY; + case ';': + checkLenient(); // fall-through + case ',': + break; + default: + throw syntaxError("Unterminated array"); + } + } + + switch (nextNonWhitespace()) { + case ']': + if (firstElement) { + pop(); + hasToken = true; + return token = JsonToken.END_ARRAY; + } + // fall-through to handle ",]" + case ';': + case ',': + /* In lenient mode, a 0-length literal means 'null' */ + checkLenient(); + pos--; + hasToken = true; + value = "null"; + return token = JsonToken.NULL; + default: + pos--; + return nextValue(); + } + } + + private JsonToken nextInObject(boolean firstElement) throws IOException { + /* + * Read delimiters. Either a comma/semicolon separating this and the + * previous name-value pair, or a close brace to denote the end of the + * object. + */ + if (firstElement) { + /* Peek to see if this is the empty object. */ + switch (nextNonWhitespace()) { + case '}': + pop(); + hasToken = true; + return token = JsonToken.END_OBJECT; + default: + pos--; + } + } else { + switch (nextNonWhitespace()) { + case '}': + pop(); + hasToken = true; + return token = JsonToken.END_OBJECT; + case ';': + case ',': + break; + default: + throw syntaxError("Unterminated object"); + } + } + + /* Read the name. */ + int quote = nextNonWhitespace(); + switch (quote) { + case '\'': + checkLenient(); // fall-through + case '"': + name = nextString((char) quote); + break; + default: + checkLenient(); + pos--; + name = nextLiteral(); + if (name.isEmpty()) { + throw syntaxError("Expected name"); + } + } + + replaceTop(JsonScope.DANGLING_NAME); + hasToken = true; + return token = JsonToken.NAME; + } + + private JsonToken objectValue() throws IOException { + /* + * Read the name/value separator. Usually a colon ':'. In lenient mode + * we also accept an equals sign '=', or an arrow "=>". + */ + switch (nextNonWhitespace()) { + case ':': + break; + case '=': + checkLenient(); + if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') { + pos++; + } + break; + default: + throw syntaxError("Expected ':'"); + } + + replaceTop(JsonScope.NONEMPTY_OBJECT); + return nextValue(); + } + + private JsonToken nextValue() throws IOException { + int c = nextNonWhitespace(); + switch (c) { + case '{': + push(JsonScope.EMPTY_OBJECT); + hasToken = true; + return token = JsonToken.BEGIN_OBJECT; + + case '[': + push(JsonScope.EMPTY_ARRAY); + hasToken = true; + return token = JsonToken.BEGIN_ARRAY; + + case '\'': + checkLenient(); // fall-through + case '"': + value = nextString((char) c); + hasToken = true; + return token = JsonToken.STRING; + + default: + pos--; + return readLiteral(); + } + } + + /** + * Returns true once {@code limit - pos >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException { + if (limit != pos) { + limit -= pos; + System.arraycopy(buffer, pos, buffer, 0, limit); + } else { + limit = 0; + } + + pos = 0; + int total; + while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { + limit += total; + if (limit >= minimum) { + return true; + } + } + return false; + } + + private int nextNonWhitespace() throws IOException { + while (pos < limit || fillBuffer(1)) { + int c = buffer[pos++]; + switch (c) { + case '\t': + case ' ': + case '\n': + case '\r': + continue; + + case '/': + if (pos == limit && !fillBuffer(1)) { + return c; + } + + checkLenient(); + char peek = buffer[pos]; + switch (peek) { + case '*': + // skip a /* c-style comment */ + pos++; + if (!skipTo("*/")) { + throw syntaxError("Unterminated comment"); + } + pos += 2; + continue; + + case '/': + // skip a // end-of-line comment + pos++; + skipToEndOfLine(); + continue; + + default: + return c; + } + + case '#': + /* + * Skip a # hash end-of-line comment. The JSON RFC doesn't + * specify this behaviour, but it's required to parse + * existing documents. See http://b/2571423. + */ + checkLenient(); + skipToEndOfLine(); + continue; + + default: + return c; + } + } + + throw syntaxError("End of input"); + } + + private void checkLenient() throws IOException { + if (!lenient) { + throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON"); + } + } + + /** + * Advances the position until after the next newline character. If the line + * is terminated by "\r\n", the '\n' must be consumed as whitespace by the + * caller. + */ + private void skipToEndOfLine() throws IOException { + while (pos < limit || fillBuffer(1)) { + char c = buffer[pos++]; + if (c == '\r' || c == '\n') { + break; + } + } + } + + private boolean skipTo(String toFind) throws IOException { + outer: + for (; pos + toFind.length() < limit || fillBuffer(toFind.length()); pos++) { + for (int c = 0; c < toFind.length(); c++) { + if (buffer[pos + c] != toFind.charAt(c)) { + continue outer; + } + } + return true; + } + return false; + } + + /** + * Returns the string up to but not including {@code quote}, unescaping any + * character escape sequences encountered along the way. The opening quote + * should have already been read. This consumes the closing quote, but does + * not include it in the returned string. + * + * @param quote either ' or ". + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private String nextString(char quote) throws IOException { + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + + if (c == quote) { + if (skipping) { + return "skipped!"; + } else if (builder == null) { + return new String(buffer, start, pos - start - 1); + } else { + builder.append(buffer, start, pos - start - 1); + return builder.toString(); + } + + } else if (c == '\\') { + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start - 1); + builder.append(readEscapeCharacter()); + start = pos; + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + throw syntaxError("Unterminated string"); + } + + /** + * Returns the string up to but not including any delimiter characters. This + * does not consume the delimiter character. + */ + private String nextLiteral() throws IOException { + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + switch (c) { + case '/': + case '\\': + case ';': + case '#': + case '=': + checkLenient(); // fall-through + + case '{': + case '}': + case '[': + case ']': + case ':': + case ',': + case ' ': + case '\t': + case '\f': + case '\r': + case '\n': + pos--; + if (skipping) { + return "skipped!"; + } else if (builder == null) { + return new String(buffer, start, pos - start); + } else { + builder.append(buffer, start, pos - start); + return builder.toString(); + } + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + return builder.toString(); + } + + @Override public String toString() { + return getClass().getSimpleName() + " near " + getSnippet(); + } + + /** + * Unescapes the character identified by the character or characters that + * immediately follow a backslash. The backslash '\' should have already + * been read. This supports both unicode escapes "u000A" and two-character + * escapes "\n". + * + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private char readEscapeCharacter() throws IOException { + if (pos == limit && !fillBuffer(1)) { + throw syntaxError("Unterminated escape sequence"); + } + + char escaped = buffer[pos++]; + switch (escaped) { + case 'u': + if (pos + 4 > limit && !fillBuffer(4)) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = new String(buffer, pos, 4); + pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'': + case '"': + case '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. + */ + private JsonToken readLiteral() throws IOException { + String literal = nextLiteral(); + if (literal.isEmpty()) { + throw syntaxError("Expected literal value"); + } + value = literal; + hasToken = true; + return token = null; // use decodeLiteral() to get the token type + } + + /** + * Assigns {@code nextToken} based on the value of {@code nextValue}. + */ + private void decodeLiteral() throws IOException { + if (value.equalsIgnoreCase("null")) { + token = JsonToken.NULL; + } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + token = JsonToken.BOOLEAN; + } else { + try { + Double.parseDouble(value); // this work could potentially be cached + token = JsonToken.NUMBER; + } catch (NumberFormatException ignored) { + // this must be an unquoted string + checkLenient(); + token = JsonToken.STRING; + } + } + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + public IOException syntaxError(String message) throws IOException { + throw new JsonSyntaxException(message + " near " + getSnippet()); + } + + private CharSequence getSnippet() { + StringBuilder snippet = new StringBuilder(); + int beforePos = Math.min(pos, 20); + snippet.append(buffer, pos - beforePos, beforePos); + int afterPos = Math.min(limit - pos, 20); + snippet.append(buffer, pos, afterPos); + return snippet; + } + + private static class JsonSyntaxException extends IOException { + private JsonSyntaxException(String s) { + super(s); + } + } +} diff --git a/core/java/android/util/JsonScope.java b/core/java/android/util/JsonScope.java new file mode 100644 index 0000000000000000000000000000000000000000..ca534e9c83ef5caf896740daf96be1f324f47063 --- /dev/null +++ b/core/java/android/util/JsonScope.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * Lexical scoping elements within a JSON reader or writer. + */ +enum JsonScope { + + /** + * An array with no elements requires no separators or newlines before + * it is closed. + */ + EMPTY_ARRAY, + + /** + * A array with at least one value requires a comma and newline before + * the next element. + */ + NONEMPTY_ARRAY, + + /** + * An object with no name/value pairs requires no separators or newlines + * before it is closed. + */ + EMPTY_OBJECT, + + /** + * An object whose most recent element is a key. The next element must + * be a value. + */ + DANGLING_NAME, + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + NONEMPTY_OBJECT, + + /** + * No object or array has been started. + */ + EMPTY_DOCUMENT, + + /** + * A document with at an array or object. + */ + NONEMPTY_DOCUMENT, + + /** + * A document that's been closed and cannot be accessed. + */ + CLOSED, +} diff --git a/core/java/android/util/JsonToken.java b/core/java/android/util/JsonToken.java new file mode 100644 index 0000000000000000000000000000000000000000..45bc6cad6c921eb5db9075ef484306c279ee20ed --- /dev/null +++ b/core/java/android/util/JsonToken.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * A structure, name or value type in a JSON-encoded string. + */ +public enum JsonToken { + + /** + * The opening of a JSON array. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_ARRAY, + + /** + * The closing of a JSON array. Written using {@link JsonWriter#endArray} + * and read using {@link JsonReader#endArray}. + */ + END_ARRAY, + + /** + * The opening of a JSON object. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_OBJECT, + + /** + * The closing of a JSON object. Written using {@link JsonWriter#endObject} + * and read using {@link JsonReader#endObject}. + */ + END_OBJECT, + + /** + * A JSON property name. Within objects, tokens alternate between names and + * their values. Written using {@link JsonWriter#name} and read using {@link + * JsonReader#nextName} + */ + NAME, + + /** + * A JSON string. + */ + STRING, + + /** + * A JSON number represented in this API by a Java {@code double}, {@code + * long}, or {@code int}. + */ + NUMBER, + + /** + * A JSON {@code true} or {@code false}. + */ + BOOLEAN, + + /** + * A JSON {@code null}. + */ + NULL, + + /** + * The end of the JSON stream. This sentinel value is returned by {@link + * JsonReader#peek()} to signal that the JSON-encoded value has no more + * tokens. + */ + END_DOCUMENT +} diff --git a/core/java/android/util/JsonWriter.java b/core/java/android/util/JsonWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..fecc1c87377122eeb412d6e00654ec6961f367be --- /dev/null +++ b/core/java/android/util/JsonWriter.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +/** + * Writes a JSON (RFC 4627) + * encoded value to a stream, one token at a time. The stream includes both + * literal values (strings, numbers, booleans and nulls) as well as the begin + * and end delimiters of objects and arrays. + * + *

    Encoding JSON

    + * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON + * document must contain one top-level array or object. Call methods on the + * writer as you walk the structure's contents, nesting arrays and objects as + * necessary: + *
      + *
    • To write arrays, first call {@link #beginArray()}. + * Write each of the array's elements with the appropriate {@link #value} + * methods or by nesting other arrays and objects. Finally close the array + * using {@link #endArray()}. + *
    • To write objects, first call {@link #beginObject()}. + * Write each of the object's properties by alternating calls to + * {@link #name} with the property's value. Write property values with the + * appropriate {@link #value} method or by nesting other objects or arrays. + * Finally close the object using {@link #endObject()}. + *
    + * + *

    Example

    + * Suppose we'd like to encode a stream of messages such as the following:
     {@code
    + * [
    + *   {
    + *     "id": 912345678901,
    + *     "text": "How do I write JSON on Android?",
    + *     "geo": null,
    + *     "user": {
    + *       "name": "android_newb",
    + *       "followers_count": 41
    + *      }
    + *   },
    + *   {
    + *     "id": 912345678902,
    + *     "text": "@android_newb just use android.util.JsonWriter!",
    + *     "geo": [50.454722, -104.606667],
    + *     "user": {
    + *       "name": "jesse",
    + *       "followers_count": 2
    + *     }
    + *   }
    + * ]}
    + * This code encodes the above structure:
       {@code
    + *   public void writeJsonStream(OutputStream out, List messages) throws IOException {
    + *     JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
    + *     writer.setIndentSpaces(4);
    + *     writeMessagesArray(writer, messages);
    + *     writer.close();
    + *   }
    + *
    + *   public void writeMessagesArray(JsonWriter writer, List messages) throws IOException {
    + *     writer.beginArray();
    + *     for (Message message : messages) {
    + *       writeMessage(writer, message);
    + *     }
    + *     writer.endArray();
    + *   }
    + *
    + *   public void writeMessage(JsonWriter writer, Message message) throws IOException {
    + *     writer.beginObject();
    + *     writer.name("id").value(message.getId());
    + *     writer.name("text").value(message.getText());
    + *     if (message.getGeo() != null) {
    + *       writer.name("geo");
    + *       writeDoublesArray(writer, message.getGeo());
    + *     } else {
    + *       writer.name("geo").nullValue();
    + *     }
    + *     writer.name("user");
    + *     writeUser(writer, message.getUser());
    + *     writer.endObject();
    + *   }
    + *
    + *   public void writeUser(JsonWriter writer, User user) throws IOException {
    + *     writer.beginObject();
    + *     writer.name("name").value(user.getName());
    + *     writer.name("followers_count").value(user.getFollowersCount());
    + *     writer.endObject();
    + *   }
    + *
    + *   public void writeDoublesArray(JsonWriter writer, List doubles) throws IOException {
    + *     writer.beginArray();
    + *     for (Double value : doubles) {
    + *       writer.value(value);
    + *     }
    + *     writer.endArray();
    + *   }}
    + * + *

    Each {@code JsonWriter} may be used to write a single JSON stream. + * Instances of this class are not thread safe. Calls that would result in a + * malformed JSON string will fail with an {@link IllegalStateException}. + */ +public final class JsonWriter implements Closeable { + + /** The output data, containing at most one top-level array or object. */ + private final Writer out; + + private final List stack = new ArrayList(); + { + stack.add(JsonScope.EMPTY_DOCUMENT); + } + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private String indent; + + /** + * The name/value separator; either ":" or ": ". + */ + private String separator = ":"; + + /** + * Creates a new instance that writes a JSON-encoded stream to {@code out}. + * For best performance, ensure {@link Writer} is buffered; wrapping in + * {@link java.io.BufferedWriter BufferedWriter} if necessary. + */ + public JsonWriter(Writer out) { + if (out == null) { + throw new NullPointerException("out == null"); + } + this.out = out; + } + + /** + * Sets the indentation string to be repeated for each level of indentation + * in the encoded document. If {@code indent.isEmpty()} the encoded document + * will be compact. Otherwise the encoded document will be more + * human-readable. + * + * @param indent a string containing only whitespace. + */ + public void setIndent(String indent) { + if (indent.isEmpty()) { + this.indent = null; + this.separator = ":"; + } else { + this.indent = indent; + this.separator = ": "; + } + } + + /** + * Begins encoding a new array. Each call to this method must be paired with + * a call to {@link #endArray}. + * + * @return this writer. + */ + public JsonWriter beginArray() throws IOException { + return open(JsonScope.EMPTY_ARRAY, "["); + } + + /** + * Ends encoding the current array. + * + * @return this writer. + */ + public JsonWriter endArray() throws IOException { + return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, "]"); + } + + /** + * Begins encoding a new object. Each call to this method must be paired + * with a call to {@link #endObject}. + * + * @return this writer. + */ + public JsonWriter beginObject() throws IOException { + return open(JsonScope.EMPTY_OBJECT, "{"); + } + + /** + * Ends encoding the current object. + * + * @return this writer. + */ + public JsonWriter endObject() throws IOException { + return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(JsonScope empty, String openBracket) throws IOException { + beforeValue(true); + stack.add(empty); + out.write(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(JsonScope empty, JsonScope nonempty, String closeBracket) + throws IOException { + JsonScope context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem: " + stack); + } + + stack.remove(stack.size() - 1); + if (context == nonempty) { + newline(); + } + out.write(closeBracket); + return this; + } + + /** + * Returns the value on the top of the stack. + */ + private JsonScope peek() { + return stack.get(stack.size() - 1); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope topOfStack) { + stack.set(stack.size() - 1, topOfStack); + } + + /** + * Encodes the property name. + * + * @param name the name of the forthcoming value. May not be null. + * @return this writer. + */ + public JsonWriter name(String name) throws IOException { + if (name == null) { + throw new NullPointerException("name == null"); + } + beforeName(); + string(name); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter value(String value) throws IOException { + if (value == null) { + return nullValue(); + } + beforeValue(false); + string(value); + return this; + } + + /** + * Encodes {@code null}. + * + * @return this writer. + */ + public JsonWriter nullValue() throws IOException { + beforeValue(false); + out.write("null"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(boolean value) throws IOException { + beforeValue(false); + out.write(value ? "true" : "false"); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this writer. + */ + public JsonWriter value(double value) throws IOException { + if (Double.isNaN(value) || Double.isInfinite(value)) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(false); + out.append(Double.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(long value) throws IOException { + beforeValue(false); + out.write(Long.toString(value)); + return this; + } + + /** + * Ensures all buffered data is written to the underlying {@link Writer} + * and flushes that writer. + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Flushes and closes this writer and the underlying {@link Writer}. + * + * @throws IOException if the JSON document is incomplete. + */ + public void close() throws IOException { + out.close(); + + if (peek() != JsonScope.NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + } + + private void string(String value) throws IOException { + out.write("\""); + for (int i = 0, length = value.length(); i < length; i++) { + char c = value.charAt(i); + + /* + * From RFC 4627, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + */ + switch (c) { + case '"': + case '\\': + case '/': + out.write('\\'); + out.write(c); + break; + + case '\t': + out.write("\\t"); + break; + + case '\b': + out.write("\\b"); + break; + + case '\n': + out.write("\\n"); + break; + + case '\r': + out.write("\\r"); + break; + + case '\f': + out.write("\\f"); + break; + + default: + if (c <= 0x1F) { + out.write(String.format("\\u%04x", (int) c)); + } else { + out.write(c); + } + break; + } + + } + out.write("\""); + } + + private void newline() throws IOException { + if (indent == null) { + return; + } + + out.write("\n"); + for (int i = 1; i < stack.size(); i++) { + out.write(indent); + } + } + + /** + * Inserts any necessary separators and whitespace before a name. Also + * adjusts the stack to expect the name's value. + */ + private void beforeName() throws IOException { + JsonScope context = peek(); + if (context == JsonScope.NONEMPTY_OBJECT) { // first in object + out.write(','); + } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object! + throw new IllegalStateException("Nesting problem: " + stack); + } + newline(); + replaceTop(JsonScope.DANGLING_NAME); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + * + * @param root true if the value is a new array or object, the two values + * permitted as top-level elements. + */ + private void beforeValue(boolean root) throws IOException { + switch (peek()) { + case EMPTY_DOCUMENT: // first in document + if (!root) { + throw new IllegalStateException( + "JSON must start with an array or an object."); + } + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + break; + + case EMPTY_ARRAY: // first in array + replaceTop(JsonScope.NONEMPTY_ARRAY); + newline(); + break; + + case NONEMPTY_ARRAY: // another in array + out.append(','); + newline(); + break; + + case DANGLING_NAME: // value for name + out.append(separator); + replaceTop(JsonScope.NONEMPTY_OBJECT); + break; + + case NONEMPTY_DOCUMENT: + throw new IllegalStateException( + "JSON must have only one top-level value."); + + default: + throw new IllegalStateException("Nesting problem: " + stack); + } + } +} diff --git a/core/java/android/util/Patterns.java b/core/java/android/util/Patterns.java index 5cbfd295890d66afcd3abfea855b27184a1a867b..3bcd2660e4d8601416bd4ca3f42127a26465fa8a 100644 --- a/core/java/android/util/Patterns.java +++ b/core/java/android/util/Patterns.java @@ -25,7 +25,7 @@ import java.util.regex.Pattern; public class Patterns { /** * Regular expression to match all IANA top-level domains. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -53,8 +53,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw])"; /** @@ -65,7 +65,7 @@ public class Patterns { /** * Regular expression to match all IANA top-level domains for WEB_URL. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -94,8 +94,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw]))"; /** diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java index 1c8b3305afdd42b2b6f6edd144aa186e91c62c30..7fc43b92c5490100796a5747d203a1e0cea0c0c2 100644 --- a/core/java/android/util/SparseArray.java +++ b/core/java/android/util/SparseArray.java @@ -90,6 +90,16 @@ public class SparseArray { delete(key); } + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + private void gc() { // Log.e("SparseArray", "gc start with " + mSize); diff --git a/core/java/android/util/TimingLogger.java b/core/java/android/util/TimingLogger.java index 0f39c974a4d985e494c372f511109e17bd4f1709..be442dac630af0c28da7084cae85cc0ccdd2b530 100644 --- a/core/java/android/util/TimingLogger.java +++ b/core/java/android/util/TimingLogger.java @@ -24,22 +24,26 @@ import android.os.SystemClock; * A utility class to help log timings splits throughout a method call. * Typical usage is: * - * TimingLogger timings = new TimingLogger(TAG, "methodA"); - * ... do some work A ... - * timings.addSplit("work A"); - * ... do some work B ... - * timings.addSplit("work B"); - * ... do some work C ... - * timings.addSplit("work C"); - * timings.dumpToLog(); + *

    + *     TimingLogger timings = new TimingLogger(TAG, "methodA");
    + *     // ... do some work A ...
    + *     timings.addSplit("work A");
    + *     // ... do some work B ...
    + *     timings.addSplit("work B");
    + *     // ... do some work C ...
    + *     timings.addSplit("work C");
    + *     timings.dumpToLog();
    + * 
    * - * The dumpToLog call would add the following to the log: + *

    The dumpToLog call would add the following to the log:

    * - * D/TAG ( 3459): methodA: begin - * D/TAG ( 3459): methodA: 9 ms, work A - * D/TAG ( 3459): methodA: 1 ms, work B - * D/TAG ( 3459): methodA: 6 ms, work C - * D/TAG ( 3459): methodA: end, 16 ms + *
    + *     D/TAG     ( 3459): methodA: begin
    + *     D/TAG     ( 3459): methodA:      9 ms, work A
    + *     D/TAG     ( 3459): methodA:      1 ms, work B
    + *     D/TAG     ( 3459): methodA:      6 ms, work C
    + *     D/TAG     ( 3459): methodA: end, 16 ms
    + * 
    */ public class TimingLogger { diff --git a/core/java/android/view/ActionMode.java b/core/java/android/view/ActionMode.java new file mode 100644 index 0000000000000000000000000000000000000000..bfafa987ceda16d2dd22bdfb8a55b053ae07c7c8 --- /dev/null +++ b/core/java/android/view/ActionMode.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + + +/** + * Represents a contextual mode of the user interface. Action modes can be used for + * modal interactions with content and replace parts of the normal UI until finished. + * Examples of good action modes include selection modes, search, content editing, etc. + */ +public abstract class ActionMode { + /** + * Set the title of the action mode. This method will have no visible effect if + * a custom view has been set. + * + * @param title Title string to set + * + * @see #setTitle(int) + * @see #setCustomView(View) + */ + public abstract void setTitle(CharSequence title); + + /** + * Set the title of the action mode. This method will have no visible effect if + * a custom view has been set. + * + * @param resId Resource ID of a string to set as the title + * + * @see #setTitle(CharSequence) + * @see #setCustomView(View) + */ + public abstract void setTitle(int resId); + + /** + * Set the subtitle of the action mode. This method will have no visible effect if + * a custom view has been set. + * + * @param subtitle Subtitle string to set + * + * @see #setSubtitle(int) + * @see #setCustomView(View) + */ + public abstract void setSubtitle(CharSequence subtitle); + + /** + * Set the subtitle of the action mode. This method will have no visible effect if + * a custom view has been set. + * + * @param resId Resource ID of a string to set as the subtitle + * + * @see #setSubtitle(CharSequence) + * @see #setCustomView(View) + */ + public abstract void setSubtitle(int resId); + + /** + * Set a custom view for this action mode. The custom view will take the place of + * the title and subtitle. Useful for things like search boxes. + * + * @param view Custom view to use in place of the title/subtitle. + * + * @see #setTitle(CharSequence) + * @see #setSubtitle(CharSequence) + */ + public abstract void setCustomView(View view); + + /** + * Invalidate the action mode and refresh menu content. The mode's + * {@link ActionMode.Callback} will have its + * {@link Callback#onPrepareActionMode(ActionMode, Menu)} method called. + * If it returns true the menu will be scanned for updated content and any relevant changes + * will be reflected to the user. + */ + public abstract void invalidate(); + + /** + * Finish and close this action mode. The action mode's {@link ActionMode.Callback} will + * have its {@link Callback#onDestroyActionMode(ActionMode)} method called. + */ + public abstract void finish(); + + /** + * Returns the menu of actions that this action mode presents. + * @return The action mode's menu. + */ + public abstract Menu getMenu(); + + /** + * Returns the current title of this action mode. + * @return Title text + */ + public abstract CharSequence getTitle(); + + /** + * Returns the current subtitle of this action mode. + * @return Subtitle text + */ + public abstract CharSequence getSubtitle(); + + /** + * Returns the current custom view for this action mode. + * @return The current custom view + */ + public abstract View getCustomView(); + + /** + * Returns a {@link MenuInflater} with the ActionMode's context. + */ + public abstract MenuInflater getMenuInflater(); + + /** + * Callback interface for action modes. Supplied to + * {@link View#startActionMode(Callback)}, a Callback + * configures and handles events raised by a user's interaction with an action mode. + * + *

    An action mode's lifecycle is as follows: + *

      + *
    • {@link Callback#onCreateActionMode(ActionMode, Menu)} once on initial + * creation
    • + *
    • {@link Callback#onPrepareActionMode(ActionMode, Menu)} after creation + * and any time the {@link ActionMode} is invalidated
    • + *
    • {@link Callback#onActionItemClicked(ActionMode, MenuItem)} any time a + * contextual action button is clicked
    • + *
    • {@link Callback#onDestroyActionMode(ActionMode)} when the action mode + * is closed
    • + *
    + */ + public interface Callback { + /** + * Called when action mode is first created. The menu supplied will be used to + * generate action buttons for the action mode. + * + * @param mode ActionMode being created + * @param menu Menu used to populate action buttons + * @return true if the action mode should be created, false if entering this + * mode should be aborted. + */ + public boolean onCreateActionMode(ActionMode mode, Menu menu); + + /** + * Called to refresh an action mode's action menu whenever it is invalidated. + * + * @param mode ActionMode being prepared + * @param menu Menu used to populate action buttons + * @return true if the menu or action mode was updated, false otherwise. + */ + public boolean onPrepareActionMode(ActionMode mode, Menu menu); + + /** + * Called to report a user click on an action button. + * + * @param mode The current ActionMode + * @param item The item that was clicked + * @return true if this callback handled the event, false if the standard MenuItem + * invocation should continue. + */ + public boolean onActionItemClicked(ActionMode mode, MenuItem item); + + /** + * Called when an action mode is about to be exited and destroyed. + * + * @param mode The current ActionMode being destroyed + */ + public void onDestroyActionMode(ActionMode mode); + } +} \ No newline at end of file diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 15fb83903e502ae5b2f0e67605fb8318eec7a1f2..8ad9a620e26abeec0d48c8bf8c799397b0b707c1 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -266,7 +266,7 @@ public class FocusFinder { /** - * Do the "beams" w.r.t the given direcition's axos of rect1 and rect2 overlap? + * Do the "beams" w.r.t the given direcition's axis of rect1 and rect2 overlap? * @param direction the direction (up, down, left, right) * @param rect1 The first rectangle * @param rect2 The second rectangle diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java new file mode 100644 index 0000000000000000000000000000000000000000..f9170011f5a98b9f27f8b700076e5598a36db1b0 --- /dev/null +++ b/core/java/android/view/GLES20Canvas.java @@ -0,0 +1,839 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.DrawFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.Shader; +import android.graphics.TemporaryBuffer; +import android.text.GraphicsOperations; +import android.text.SpannableString; +import android.text.SpannedString; +import android.text.TextUtils; + +import javax.microedition.khronos.opengles.GL; + +/** + * An implementation of Canvas on top of OpenGL ES 2.0. + */ +class GLES20Canvas extends Canvas { + @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) + private final GL mGl; + private final boolean mOpaque; + private int mRenderer; + + private int mWidth; + private int mHeight; + + private final float[] mPoint = new float[2]; + private final float[] mLine = new float[4]; + + private final Rect mClipBounds = new Rect(); + + private DrawFilter mFilter; + + private boolean mContextLocked; + + /////////////////////////////////////////////////////////////////////////// + // JNI + /////////////////////////////////////////////////////////////////////////// + + private static native boolean nIsAvailable(); + private static boolean sIsAvailable = nIsAvailable(); + + static boolean isAvailable() { + return sIsAvailable; + } + + /////////////////////////////////////////////////////////////////////////// + // Constructors + /////////////////////////////////////////////////////////////////////////// + + GLES20Canvas(GL gl, boolean translucent) { + mGl = gl; + mOpaque = !translucent; + + mRenderer = nCreateRenderer(); + if (mRenderer == 0) { + throw new IllegalStateException("Could not create GLES20Canvas renderer"); + } + } + + private native int nCreateRenderer(); + + /** + * This method must be called before releasing a + * reference to a GLES20Canvas. This method is responsible for freeing + * native resources associated with the hardware. Not invoking this + * method properly can result in memory leaks. + * + * @hide + */ + public synchronized void destroy() { + if (mRenderer != 0) { + nDestroyRenderer(mRenderer); + mRenderer = 0; + } + } + + private native void nDestroyRenderer(int renderer); + + /////////////////////////////////////////////////////////////////////////// + // Canvas management + /////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isHardwareAccelerated() { + return true; + } + + @Override + public void setBitmap(Bitmap bitmap) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + /////////////////////////////////////////////////////////////////////////// + // Setup + /////////////////////////////////////////////////////////////////////////// + + @Override + public void setViewport(int width, int height) { + mWidth = width; + mHeight = height; + + nSetViewport(mRenderer, width, height); + } + + private native void nSetViewport(int renderer, int width, int height); + + void onPreDraw() { + nPrepare(mRenderer); + } + + private native void nPrepare(int renderer); + + void onPostDraw() { + nFinish(mRenderer); + } + + private native void nFinish(int renderer); + + @Override + public boolean acquireContext() { + if (!mContextLocked) { + nAcquireContext(mRenderer); + mContextLocked = true; + } + return mContextLocked; + } + + private native void nAcquireContext(int renderer); + + @Override + public void releaseContext() { + if (mContextLocked) { + nReleaseContext(mRenderer); + mContextLocked = false; + } + } + + private native void nReleaseContext(int renderer); + + /////////////////////////////////////////////////////////////////////////// + // Clipping + /////////////////////////////////////////////////////////////////////////// + + @Override + public boolean clipPath(Path path) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipPath(Path path, Region.Op op) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipRect(float left, float top, float right, float bottom) { + return nClipRect(mRenderer, left, top, right, bottom, Region.Op.INTERSECT.nativeInt); + } + + private native boolean nClipRect(int renderer, float left, float top, + float right, float bottom, int op); + + @Override + public boolean clipRect(float left, float top, float right, float bottom, Region.Op op) { + return nClipRect(mRenderer, left, top, right, bottom, op.nativeInt); + } + + @Override + public boolean clipRect(int left, int top, int right, int bottom) { + return nClipRect(mRenderer, left, top, right, bottom, Region.Op.INTERSECT.nativeInt); + } + + private native boolean nClipRect(int renderer, int left, int top, int right, int bottom, int op); + + @Override + public boolean clipRect(Rect rect) { + return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, + Region.Op.INTERSECT.nativeInt); + } + + @Override + public boolean clipRect(Rect rect, Region.Op op) { + return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, op.nativeInt); + } + + @Override + public boolean clipRect(RectF rect) { + return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, + Region.Op.INTERSECT.nativeInt); + } + + @Override + public boolean clipRect(RectF rect, Region.Op op) { + return nClipRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, op.nativeInt); + } + + @Override + public boolean clipRegion(Region region) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipRegion(Region region, Region.Op op) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getClipBounds(Rect bounds) { + return nGetClipBounds(mRenderer, bounds); + } + + private native boolean nGetClipBounds(int renderer, Rect bounds); + + @Override + public boolean quickReject(float left, float top, float right, float bottom, EdgeType type) { + return nQuickReject(mRenderer, left, top, right, bottom, type.nativeInt); + } + + private native boolean nQuickReject(int renderer, float left, float top, + float right, float bottom, int edge); + + @Override + public boolean quickReject(Path path, EdgeType type) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean quickReject(RectF rect, EdgeType type) { + return quickReject(rect.left, rect.top, rect.right, rect.bottom, type); + } + + /////////////////////////////////////////////////////////////////////////// + // Transformations + /////////////////////////////////////////////////////////////////////////// + + @Override + public void translate(float dx, float dy) { + nTranslate(mRenderer, dx, dy); + } + + private native void nTranslate(int renderer, float dx, float dy); + + @Override + public void skew(float sx, float sy) { + throw new UnsupportedOperationException(); + } + + @Override + public void rotate(float degrees) { + nRotate(mRenderer, degrees); + } + + private native void nRotate(int renderer, float degrees); + + @Override + public void scale(float sx, float sy) { + nScale(mRenderer, sx, sy); + } + + private native void nScale(int renderer, float sx, float sy); + + @Override + public void setMatrix(Matrix matrix) { + nSetMatrix(mRenderer, matrix.native_instance); + } + + private native void nSetMatrix(int renderer, int matrix); + + @Override + public void getMatrix(Matrix matrix) { + nGetMatrix(mRenderer, matrix.native_instance); + } + + private native void nGetMatrix(int renderer, int matrix); + + @Override + public void concat(Matrix matrix) { + nConcatMatrix(mRenderer, matrix.native_instance); + } + + private native void nConcatMatrix(int renderer, int matrix); + + /////////////////////////////////////////////////////////////////////////// + // State management + /////////////////////////////////////////////////////////////////////////// + + @Override + public int save() { + return nSave(mRenderer, Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG); + } + + @Override + public int save(int saveFlags) { + return nSave(mRenderer, saveFlags); + } + + private native int nSave(int renderer, int flags); + + @Override + public int saveLayer(RectF bounds, Paint paint, int saveFlags) { + return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags); + } + + @Override + public int saveLayer(float left, float top, float right, float bottom, Paint paint, + int saveFlags) { + int nativePaint = paint == null ? 0 : paint.mNativePaint; + return nSaveLayer(mRenderer, left, top, right, bottom, nativePaint, saveFlags); + } + + private native int nSaveLayer(int renderer, float left, float top, float right, float bottom, + int paint, int saveFlags); + + @Override + public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags) { + return saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, + alpha, saveFlags); + } + + @Override + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, + int saveFlags) { + return nSaveLayerAlpha(mRenderer, left, top, right, bottom, alpha, saveFlags); + } + + private native int nSaveLayerAlpha(int renderer, float left, float top, float right, + float bottom, int alpha, int saveFlags); + + @Override + public void restore() { + nRestore(mRenderer); + } + + private native void nRestore(int renderer); + + @Override + public void restoreToCount(int saveCount) { + nRestoreToCount(mRenderer, saveCount); + } + + private native void nRestoreToCount(int renderer, int saveCount); + + @Override + public int getSaveCount() { + return nGetSaveCount(mRenderer); + } + + private native int nGetSaveCount(int renderer); + + /////////////////////////////////////////////////////////////////////////// + // Filtering + /////////////////////////////////////////////////////////////////////////// + + @Override + public void setDrawFilter(DrawFilter filter) { + mFilter = filter; + } + + @Override + public DrawFilter getDrawFilter() { + return mFilter; + } + + /////////////////////////////////////////////////////////////////////////// + // Drawing + /////////////////////////////////////////////////////////////////////////// + + @Override + public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, + Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawARGB(int a, int r, int g, int b) { + drawColor((a & 0xFF) << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF)); + } + + @Override + public void drawPatch(Bitmap bitmap, byte[] chunks, RectF dst, Paint paint) { + // Shaders are ignored when drawing patches + boolean hasColorFilter = paint != null && setupColorFilter(paint); + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawPatch(mRenderer, bitmap.mNativeBitmap, chunks, dst.left, dst.top, + dst.right, dst.bottom, nativePaint); + if (hasColorFilter) nResetModifiers(mRenderer); + } + + private native void nDrawPatch(int renderer, int bitmap, byte[] chunks, float left, float top, + float right, float bottom, int paint); + + @Override + public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { + // Shaders are ignored when drawing bitmaps + boolean hasColorFilter = paint != null && setupColorFilter(paint); + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, left, top, nativePaint); + if (hasColorFilter) nResetModifiers(mRenderer); + } + + private native void nDrawBitmap(int renderer, int bitmap, float left, float top, int paint); + + @Override + public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) { + // Shaders are ignored when drawing bitmaps + boolean hasColorFilter = paint != null && setupColorFilter(paint); + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, matrix.native_instance, nativePaint); + if (hasColorFilter) nResetModifiers(mRenderer); + } + + private native void nDrawBitmap(int renderer, int bitmap, int matrix, int paint); + + @Override + public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { + // Shaders are ignored when drawing bitmaps + boolean hasColorFilter = paint != null && setupColorFilter(paint); + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + + int left, top, right, bottom; + if (src == null) { + left = top = 0; + right = bitmap.getWidth(); + bottom = bitmap.getHeight(); + } else { + left = src.left; + right = src.right; + top = src.top; + bottom = src.bottom; + } + + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, left, top, right, bottom, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); + if (hasColorFilter) nResetModifiers(mRenderer); + } + + @Override + public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) { + // Shaders are ignored when drawing bitmaps + boolean hasColorFilter = paint != null && setupColorFilter(paint); + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, src.left, src.top, src.right, src.bottom, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); + if (hasColorFilter) nResetModifiers(mRenderer); + } + + private native void nDrawBitmap(int renderer, int bitmap, + float srcLeft, float srcTop, float srcRight, float srcBottom, + float left, float top, float right, float bottom, int paint); + + @Override + public void drawBitmap(int[] colors, int offset, int stride, float x, float y, + int width, int height, boolean hasAlpha, Paint paint) { + // Shaders are ignored when drawing bitmaps + boolean hasColorFilter = paint != null && setupColorFilter(paint); + final Bitmap.Config config = hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + final Bitmap b = Bitmap.createBitmap(colors, offset, stride, width, height, config); + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, b.mNativeBitmap, x, y, nativePaint); + b.recycle(); + if (hasColorFilter) nResetModifiers(mRenderer); + } + + @Override + public void drawBitmap(int[] colors, int offset, int stride, int x, int y, + int width, int height, boolean hasAlpha, Paint paint) { + // Shaders are ignored when drawing bitmaps + drawBitmap(colors, offset, stride, (float) x, (float) y, width, height, hasAlpha, paint); + } + + @Override + public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, + int vertOffset, int[] colors, int colorOffset, Paint paint) { + // TODO: Implement + } + + @Override + public void drawCircle(float cx, float cy, float radius, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawColor(int color) { + drawColor(color, PorterDuff.Mode.SRC_OVER); + } + + @Override + public void drawColor(int color, PorterDuff.Mode mode) { + nDrawColor(mRenderer, color, mode.nativeInt); + } + + private native void nDrawColor(int renderer, int color, int mode); + + @Override + public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) { + mLine[0] = startX; + mLine[1] = startY; + mLine[2] = stopX; + mLine[3] = stopY; + drawLines(mLine, 0, 4, paint); + } + + @Override + public void drawLines(float[] pts, int offset, int count, Paint paint) { + if ((offset | count) < 0 || offset + count > pts.length) { + throw new IllegalArgumentException("The lines array must contain 4 elements per line."); + } + boolean hasModifier = setupModifiers(paint); + nDrawLines(mRenderer, pts, offset, count, paint.mNativePaint); + if (hasModifier) nResetModifiers(mRenderer); + } + + private native void nDrawLines(int renderer, float[] points, int offset, int count, int paint); + + @Override + public void drawLines(float[] pts, Paint paint) { + drawLines(pts, 0, pts.length, paint); + } + + @Override + public void drawOval(RectF oval, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPaint(Paint paint) { + final Rect r = mClipBounds; + nGetClipBounds(mRenderer, r); + drawRect(r.left, r.top, r.right, r.bottom, paint); + } + + @Override + public void drawPath(Path path, Paint paint) { + boolean hasModifier = setupModifiers(paint); + if (path.isSimplePath) { + if (path.rects != null) { + nDrawRects(mRenderer, path.rects.mNativeRegion, paint.mNativePaint); + } + } else { + nDrawPath(mRenderer, path.mNativePath, paint.mNativePaint); + } + if (hasModifier) nResetModifiers(mRenderer); + } + + private native void nDrawPath(int renderer, int path, int paint); + private native void nDrawRects(int renderer, int region, int paint); + + @Override + public void drawPicture(Picture picture) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPicture(Picture picture, Rect dst) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPicture(Picture picture, RectF dst) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPoint(float x, float y, Paint paint) { + mPoint[0] = x; + mPoint[1] = y; + drawPoints(mPoint, 0, 1, paint); + } + + @Override + public void drawPoints(float[] pts, int offset, int count, Paint paint) { + // TODO: Implement + } + + @Override + public void drawPoints(float[] pts, Paint paint) { + drawPoints(pts, 0, pts.length / 2, paint); + } + + @Override + public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPosText(String text, float[] pos, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawRect(float left, float top, float right, float bottom, Paint paint) { + boolean hasModifier = setupModifiers(paint); + nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint); + if (hasModifier) nResetModifiers(mRenderer); + } + + private native void nDrawRect(int renderer, float left, float top, float right, float bottom, + int paint); + + @Override + public void drawRect(Rect r, Paint paint) { + drawRect(r.left, r.top, r.right, r.bottom, paint); + } + + @Override + public void drawRect(RectF r, Paint paint) { + drawRect(r.left, r.top, r.right, r.bottom, paint); + } + + @Override + public void drawRGB(int r, int g, int b) { + drawColor(0xFF000000 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF)); + } + + @Override + public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) { + // TODO: Implement + } + + @Override + public void drawText(char[] text, int index, int count, float x, float y, Paint paint) { + if ((index | count | (index + count) | (text.length - index - count)) < 0) { + throw new IndexOutOfBoundsException(); + } + + boolean hasModifier = setupModifiers(paint); + try { + nDrawText(mRenderer, text, index, count, x, y, paint.mBidiFlags, paint.mNativePaint); + } finally { + if (hasModifier) nResetModifiers(mRenderer); + } + } + + private native void nDrawText(int renderer, char[] text, int index, int count, float x, float y, + int bidiFlags, int paint); + + @Override + public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { + boolean hasModifier = setupModifiers(paint); + try { + if (text instanceof String || text instanceof SpannedString || + text instanceof SpannableString) { + nDrawText(mRenderer, text.toString(), start, end, x, y, paint.mBidiFlags, + paint.mNativePaint); + } else if (text instanceof GraphicsOperations) { + ((GraphicsOperations) text).drawText(this, start, end, x, y, + paint); + } else { + char[] buf = TemporaryBuffer.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + nDrawText(mRenderer, buf, 0, end - start, x, y, paint.mBidiFlags, paint.mNativePaint); + TemporaryBuffer.recycle(buf); + } + } finally { + if (hasModifier) nResetModifiers(mRenderer); + } + } + + @Override + public void drawText(String text, int start, int end, float x, float y, Paint paint) { + if ((start | end | (end - start) | (text.length() - end)) < 0) { + throw new IndexOutOfBoundsException(); + } + + boolean hasModifier = setupModifiers(paint); + try { + nDrawText(mRenderer, text, start, end, x, y, paint.mBidiFlags, paint.mNativePaint); + } finally { + if (hasModifier) nResetModifiers(mRenderer); + } + } + + private native void nDrawText(int renderer, String text, int start, int end, float x, float y, + int bidiFlags, int paint); + + @Override + public void drawText(String text, float x, float y, Paint paint) { + boolean hasModifier = setupModifiers(paint); + try { + nDrawText(mRenderer, text, 0, text.length(), x, y, paint.mBidiFlags, + paint.mNativePaint); + } finally { + if (hasModifier) nResetModifiers(mRenderer); + } + } + + @Override + public void drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, + float vOffset, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, + float x, float y, int dir, Paint paint) { + if ((index | count | text.length - index - count) < 0) { + throw new IndexOutOfBoundsException(); + } + if (dir != DIRECTION_LTR && dir != DIRECTION_RTL) { + throw new IllegalArgumentException("Unknown direction: " + dir); + } + + boolean hasModifier = setupModifiers(paint); + try { + nDrawTextRun(mRenderer, text, index, count, contextIndex, contextCount, x, y, dir, + paint.mNativePaint); + } finally { + if (hasModifier) nResetModifiers(mRenderer); + } + } + + private native void nDrawTextRun(int renderer, char[] text, int index, int count, + int contextIndex, int contextCount, float x, float y, int dir, int nativePaint); + + @Override + public void drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, + float x, float y, int dir, Paint paint) { + if ((start | end | end - start | text.length() - end) < 0) { + throw new IndexOutOfBoundsException(); + } + + boolean hasModifier = setupModifiers(paint); + try { + int flags = dir == 0 ? 0 : 1; + if (text instanceof String || text instanceof SpannedString || + text instanceof SpannableString) { + nDrawTextRun(mRenderer, text.toString(), start, end, contextStart, + contextEnd, x, y, flags, paint.mNativePaint); + } else if (text instanceof GraphicsOperations) { + ((GraphicsOperations) text).drawTextRun(this, start, end, + contextStart, contextEnd, x, y, flags, paint); + } else { + int contextLen = contextEnd - contextStart; + int len = end - start; + char[] buf = TemporaryBuffer.obtain(contextLen); + TextUtils.getChars(text, contextStart, contextEnd, buf, 0); + nDrawTextRun(mRenderer, buf, start - contextStart, len, 0, contextLen, + x, y, flags, paint.mNativePaint); + TemporaryBuffer.recycle(buf); + } + } finally { + if (hasModifier) nResetModifiers(mRenderer); + } + } + + private native void nDrawTextRun(int renderer, String text, int start, int end, + int contextStart, int contextEnd, float x, float y, int flags, int nativePaint); + + @Override + public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset, + float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, + int indexOffset, int indexCount, Paint paint) { + // TODO: Implement + } + + private boolean setupModifiers(Paint paint) { + boolean hasModifier = false; + + if (paint.hasShadow) { + nSetupShadow(mRenderer, paint.shadowRadius, paint.shadowDx, paint.shadowDy, + paint.shadowColor); + hasModifier = true; + } + + final Shader shader = paint.getShader(); + if (shader != null) { + nSetupShader(mRenderer, shader.native_shader); + hasModifier = true; + } + + final ColorFilter filter = paint.getColorFilter(); + if (filter != null) { + nSetupColorFilter(mRenderer, filter.nativeColorFilter); + hasModifier = true; + } + + return hasModifier; + } + + private boolean setupColorFilter(Paint paint) { + final ColorFilter filter = paint.getColorFilter(); + if (filter != null) { + nSetupColorFilter(mRenderer, filter.nativeColorFilter); + return true; + } + return false; + } + + private native void nSetupShader(int renderer, int shader); + private native void nSetupColorFilter(int renderer, int colorFilter); + private native void nSetupShadow(int renderer, float radius, float dx, float dy, int color); + + private native void nResetModifiers(int renderer); +} diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..3796994bc8eff49cf0dd0ce07d95f2f0bc407abf --- /dev/null +++ b/core/java/android/view/HardwareRenderer.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.view; + +import android.graphics.Canvas; +import android.os.SystemClock; +import android.util.Log; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; + +/** + * Interface for rendering a ViewRoot using hardware acceleration. + * + * @hide + */ +public abstract class HardwareRenderer { + private static final String LOG_TAG = "HardwareRenderer"; + + /** + * A process can set this flag to false to prevent the use of hardware + * rendering. + * + * @hide + */ + public static boolean sRendererDisabled = false; + + private boolean mEnabled; + private boolean mRequested = true; + + /** + * Indicates that the current process cannot use hardware rendering. + * + * @hide + */ + public static void disable() { + sRendererDisabled = true; + } + + /** + * Indicates whether hardware acceleration is available under any form for + * the view hierarchy. + * + * @return True if the view hierarchy can potentially be hardware accelerated, + * false otherwise + */ + public static boolean isAvailable() { + return GLES20Canvas.isAvailable(); + } + + /** + * Destroys the hardware rendering context. + * + * @param full If true, destroys all associated resources. + */ + abstract void destroy(boolean full); + + /** + * Initializes the hardware renderer for the specified surface. + * + * @param holder The holder for the surface to hardware accelerate. + * + * @return True if the initialization was successful, false otherwise. + */ + abstract boolean initialize(SurfaceHolder holder); + + /** + * Setup the hardware renderer for drawing. This is called for every + * frame to draw. + * + * @param width Width of the drawing surface. + * @param height Height of the drawing surface. + */ + abstract void setup(int width, int height); + + /** + * Draws the specified view. + * + * @param view The view to draw. + * @param attachInfo AttachInfo tied to the specified view. + */ + abstract void draw(View view, View.AttachInfo attachInfo, int yOffset); + + /** + * Initializes the hardware renderer for the specified surface and setup the + * renderer for drawing, if needed. This is invoked when the ViewRoot has + * potentially lost the hardware renderer. The hardware renderer should be + * reinitialized and setup when the render {@link #isRequested()} and + * {@link #isEnabled()}. + * + * @param width The width of the drawing surface. + * @param height The height of the drawing surface. + * @param attachInfo The + * @param holder + */ + void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo, + SurfaceHolder holder) { + if (isRequested()) { + // We lost the gl context, so recreate it. + if (!isEnabled()) { + if (initialize(holder)) { + setup(width, height); + } + } + } + } + + /** + * Creates a hardware renderer using OpenGL. + * + * @param glVersion The version of OpenGL to use (1 for OpenGL 1, 11 for OpenGL 1.1, etc.) + * @param translucent True if the surface is translucent, false otherwise + * + * @return A hardware renderer backed by OpenGL. + */ + static HardwareRenderer createGlRenderer(int glVersion, boolean translucent) { + switch (glVersion) { + case 2: + return Gl20Renderer.create(translucent); + } + throw new IllegalArgumentException("Unknown GL version: " + glVersion); + } + + /** + * Indicates whether hardware acceleration is currently enabled. + * + * @return True if hardware acceleration is in use, false otherwise. + */ + boolean isEnabled() { + return mEnabled; + } + + /** + * Indicates whether hardware acceleration is currently enabled. + * + * @param enabled True if the hardware renderer is in use, false otherwise. + */ + void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + /** + * Indicates whether hardware acceleration is currently request but not + * necessarily enabled yet. + * + * @return True if requested, false otherwise. + */ + boolean isRequested() { + return mRequested; + } + + /** + * Indicates whether hardware acceleration is currently request but not + * necessarily enabled yet. + * + * @return True to request hardware acceleration, false otherwise. + */ + void setRequested(boolean requested) { + mRequested = requested; + } + + @SuppressWarnings({"deprecation"}) + static abstract class GlRenderer extends HardwareRenderer { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + static EGLContext sEglContext; + static EGL10 sEgl; + static EGLDisplay sEglDisplay; + static EGLConfig sEglConfig; + + private static Thread sEglThread; + + EGLSurface mEglSurface; + + GL mGl; + GLES20Canvas mCanvas; + + final int mGlVersion; + final boolean mTranslucent; + + private boolean mDestroyed; + + GlRenderer(int glVersion, boolean translucent) { + mGlVersion = glVersion; + mTranslucent = translucent; + } + + /** + * Checks for OpenGL errors. If an error has occured, {@link #destroy(boolean)} + * is invoked and the requested flag is turned off. The error code is + * also logged as a warning. + */ + void checkEglErrors() { + if (isEnabled()) { + int error = sEgl.eglGetError(); + if (error != EGL10.EGL_SUCCESS) { + // something bad has happened revert to + // normal rendering. + destroy(true); + if (error != EGL11.EGL_CONTEXT_LOST) { + // we'll try again if it was context lost + setRequested(false); + } + Log.w(LOG_TAG, "EGL error: " + Integer.toHexString(error)); + } + } + } + + @Override + boolean initialize(SurfaceHolder holder) { + if (isRequested() && !isEnabled()) { + initializeEgl(); + mGl = createEglSurface(holder); + mDestroyed = false; + + if (mGl != null) { + int err = sEgl.eglGetError(); + if (err != EGL10.EGL_SUCCESS) { + destroy(true); + setRequested(false); + } else { + if (mCanvas == null) { + mCanvas = createCanvas(); + } + if (mCanvas != null) { + setEnabled(true); + } else { + Log.w(LOG_TAG, "Hardware accelerated Canvas could not be created"); + } + } + + return mCanvas != null; + } + } + return false; + } + + abstract GLES20Canvas createCanvas(); + + void initializeEgl() { + if (sEglContext != null) return; + + sEglThread = Thread.currentThread(); + sEgl = (EGL10) EGLContext.getEGL(); + + // Get to the default display. + sEglDisplay = sEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (sEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + // We can now initialize EGL for that display + int[] version = new int[2]; + if (!sEgl.eglInitialize(sEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + sEglConfig = getConfigChooser(mGlVersion).chooseConfig(sEgl, sEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + sEglContext = createContext(sEgl, sEglDisplay, sEglConfig); + } + + GL createEglSurface(SurfaceHolder holder) { + // Check preconditions. + if (sEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (sEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (sEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + if (Thread.currentThread() != sEglThread) { + throw new IllegalStateException("HardwareRenderer cannot be used " + + "from multiple threads"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + /* + * Unbind and destroy the old EGL surface, if + * there is one. + */ + sEgl.eglMakeCurrent(sEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + sEgl.eglDestroySurface(sEglDisplay, mEglSurface); + } + + // Create an EGL surface we can render into. + mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, holder, null); + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = sEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + return null; + } + throw new RuntimeException("createWindowSurface failed"); + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, sEglContext)) { + throw new RuntimeException("eglMakeCurrent failed"); + } + + return sEglContext.getGL(); + } + + EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { + int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, mGlVersion, EGL10.EGL_NONE }; + + return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, + mGlVersion != 0 ? attrib_list : null); + } + + @Override + void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo, + SurfaceHolder holder) { + if (isRequested()) { + checkEglErrors(); + super.initializeIfNeeded(width, height, attachInfo, holder); + } + } + + @Override + void destroy(boolean full) { + if (full && mCanvas != null) { + mCanvas.destroy(); + mCanvas = null; + } + + if (!isEnabled() || mDestroyed) return; + + mDestroyed = true; + + sEgl.eglMakeCurrent(sEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + sEgl.eglDestroySurface(sEglDisplay, mEglSurface); + + mEglSurface = null; + mGl = null; + + setEnabled(false); + } + + @Override + void setup(int width, int height) { + mCanvas.setViewport(width, height); + } + + boolean canDraw() { + return mGl != null && mCanvas != null; + } + + void onPreDraw() { + } + + void onPostDraw() { + } + + /** + * Defines the EGL configuration for this renderer. The default configuration + * is RGBX, no depth, no stencil. + * + * @return An {@link android.view.HardwareRenderer.GlRenderer.EglConfigChooser}. + * @param glVersion + */ + EglConfigChooser getConfigChooser(int glVersion) { + return new ComponentSizeChooser(glVersion, 8, 8, 8, 8, 0, 0); + } + + @Override + void draw(View view, View.AttachInfo attachInfo, int yOffset) { + if (canDraw()) { + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + attachInfo.mIgnoreDirtyState = true; + view.mPrivateFlags |= View.DRAWN; + + checkCurrent(); + + onPreDraw(); + + Canvas canvas = mCanvas; + int saveCount = canvas.save(); + canvas.translate(0, -yOffset); + + try { + view.draw(canvas); + } finally { + canvas.restoreToCount(saveCount); + } + + onPostDraw(); + + attachInfo.mIgnoreDirtyState = false; + + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + checkEglErrors(); + } + } + + private void checkCurrent() { + // TODO: Don't check the current context when we have one per UI thread + // TODO: Use a threadlocal flag to know whether the surface has changed + if (sEgl.eglGetCurrentContext() != sEglContext || + sEgl.eglGetCurrentSurface(EGL10.EGL_DRAW) != mEglSurface) { + if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, sEglContext)) { + throw new RuntimeException("eglMakeCurrent failed"); + } + } + } + + static abstract class EglConfigChooser { + final int[] mConfigSpec; + private final int mGlVersion; + + EglConfigChooser(int glVersion, int[] configSpec) { + mGlVersion = glVersion; + mConfigSpec = filterConfigSpec(configSpec); + } + + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] index = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, index)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = index[0]; + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, index)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); + + private int[] filterConfigSpec(int[] configSpec) { + if (mGlVersion != 2) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1); + newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE; + newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ + newConfigSpec[len + 1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + static class ComponentSizeChooser extends EglConfigChooser { + private int[] mValue; + + private int mRedSize; + private int mGreenSize; + private int mBlueSize; + private int mAlphaSize; + private int mDepthSize; + private int mStencilSize; + + ComponentSizeChooser(int glVersion, int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(glVersion, new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE }); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); + if (d >= mDepthSize && s >= mStencilSize) { + int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); + if (r >= mRedSize && g >= mGreenSize && b >= mBlueSize && a >= mAlphaSize) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, + int attribute, int defaultValue) { + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + + return defaultValue; + } + } + } + + /** + * Hardware renderer using OpenGL ES 2.0. + */ + static class Gl20Renderer extends GlRenderer { + private GLES20Canvas mGlCanvas; + + Gl20Renderer(boolean translucent) { + super(2, translucent); + } + + @Override + GLES20Canvas createCanvas() { + return mGlCanvas = new GLES20Canvas(mGl, true); + } + + @Override + void onPreDraw() { + mGlCanvas.onPreDraw(); + } + + @Override + void onPostDraw() { + mGlCanvas.onPostDraw(); + } + + static HardwareRenderer create(boolean translucent) { + if (GLES20Canvas.isAvailable()) { + return new Gl20Renderer(translucent); + } + return null; + } + } +} diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index 194c01357613851154efbc8009734832de4df72a..d24af52303d8b21477dbd209ee455c2597d07b9f 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -16,15 +16,15 @@ package android.view; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import android.content.Context; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import java.io.IOException; import java.lang.reflect.Constructor; import java.util.HashMap; @@ -67,15 +67,16 @@ public abstract class LayoutInflater { // these are optional, set by the caller private boolean mFactorySet; private Factory mFactory; + private Factory2 mFactory2; private Filter mFilter; private final Object[] mConstructorArgs = new Object[2]; - private static final Class[] mConstructorSignature = new Class[] { + private static final Class[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class}; - private static final HashMap sConstructorMap = - new HashMap(); + private static final HashMap> sConstructorMap = + new HashMap>(); private HashMap mFilterMap; @@ -97,6 +98,7 @@ public abstract class LayoutInflater { * * @return True if this class is allowed to be inflated, or false otherwise */ + @SuppressWarnings("unchecked") boolean onLoadClass(Class clazz); } @@ -121,12 +123,33 @@ public abstract class LayoutInflater { public View onCreateView(String name, Context context, AttributeSet attrs); } - private static class FactoryMerger implements Factory { + public interface Factory2 extends Factory { + /** + * Version of {@link #onCreateView(String, Context, AttributeSet)} + * that also supplies the parent that the view created view will be + * placed in. + * + * @param parent The parent that the created view will be placed + * in; note that this may be null. + * @param name Tag name to be inflated. + * @param context The context the view is being created in. + * @param attrs Inflation attributes as specified in XML file. + * + * @return View Newly created view. Return null for the default + * behavior. + */ + public View onCreateView(View parent, String name, Context context, AttributeSet attrs); + } + + private static class FactoryMerger implements Factory2 { private final Factory mF1, mF2; + private final Factory2 mF12, mF22; - FactoryMerger(Factory f1, Factory f2) { + FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) { mF1 = f1; mF2 = f2; + mF12 = f12; + mF22 = f22; } public View onCreateView(String name, Context context, AttributeSet attrs) { @@ -134,6 +157,14 @@ public abstract class LayoutInflater { if (v != null) return v; return mF2.onCreateView(name, context, attrs); } + + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs) + : mF1.onCreateView(name, context, attrs); + if (v != null) return v; + return mF22 != null ? mF22.onCreateView(parent, name, context, attrs) + : mF2.onCreateView(name, context, attrs); + } } /** @@ -161,6 +192,7 @@ public abstract class LayoutInflater { protected LayoutInflater(LayoutInflater original, Context newContext) { mContext = newContext; mFactory = original.mFactory; + mFactory2 = original.mFactory2; mFilter = original.mFilter; } @@ -199,7 +231,7 @@ public abstract class LayoutInflater { } /** - * Return the current factory (or null). This is called on each element + * Return the current {@link Factory} (or null). This is called on each element * name. If the factory returns a View, add that to the hierarchy. If it * returns null, proceed to call onCreateView(name). */ @@ -207,6 +239,17 @@ public abstract class LayoutInflater { return mFactory; } + /** + * Return the current {@link Factory2}. Returns null if no factory is set + * or the set factory does not implement the {@link Factory2} interface. + * This is called on each element + * name. If the factory returns a View, add that to the hierarchy. If it + * returns null, proceed to call onCreateView(name). + */ + public final Factory2 getFactory2() { + return mFactory2; + } + /** * Attach a custom Factory interface for creating views while using * this LayoutInflater. This must not be null, and can only be set once; @@ -233,7 +276,26 @@ public abstract class LayoutInflater { if (mFactory == null) { mFactory = factory; } else { - mFactory = new FactoryMerger(factory, mFactory); + mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); + } + } + + /** + * Like {@link #setFactory}, but allows you to set a {@link Factory2} + * interface. + */ + public void setFactory2(Factory2 factory) { + if (mFactorySet) { + throw new IllegalStateException("A factory has already been set on this LayoutInflater"); + } + if (factory == null) { + throw new NullPointerException("Given factory can not be null"); + } + mFactorySet = true; + if (mFactory == null) { + mFactory = mFactory2 = factory; + } else { + mFactory = new FactoryMerger(factory, factory, mFactory, mFactory2); } } @@ -380,10 +442,10 @@ public abstract class LayoutInflater { + "ViewGroup root and attachToRoot=true"); } - rInflate(parser, root, attrs); + rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml - View temp = createViewFromTag(name, attrs); + View temp = createViewFromTag(root, name, attrs); ViewGroup.LayoutParams params = null; @@ -405,7 +467,7 @@ public abstract class LayoutInflater { System.out.println("-----> start inflating children"); } // Inflate all children under temp - rInflate(parser, temp, attrs); + rInflate(parser, temp, attrs, true); if (DEBUG) { System.out.println("-----> done inflating children"); } @@ -458,18 +520,18 @@ public abstract class LayoutInflater { * @param name The full name of the class to be instantiated. * @param attrs The XML attributes supplied for this instance. * - * @return View The newly instantied view, or null. + * @return View The newly instantiated view, or null. */ public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { - Constructor constructor = sConstructorMap.get(name); - Class clazz = null; + 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 clazz = mContext.getClassLoader().loadClass( - prefix != null ? (prefix + name) : name); + prefix != null ? (prefix + name) : name).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); @@ -487,7 +549,7 @@ public abstract class LayoutInflater { if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( - prefix != null ? (prefix + name) : name); + prefix != null ? (prefix + name) : name).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); @@ -502,7 +564,7 @@ public abstract class LayoutInflater { Object[] args = mConstructorArgs; args[1] = attrs; - return (View) constructor.newInstance(args); + return constructor.newInstance(args); } catch (NoSuchMethodException e) { InflateException ie = new InflateException(attrs.getPositionDescription() @@ -511,6 +573,13 @@ public abstract class LayoutInflater { ie.initCause(e); throw ie; + } catch (ClassCastException e) { + // If loaded class is not a View subclass + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Class is not a View " + + (prefix != null ? (prefix + name) : name)); + ie.initCause(e); + throw ie; } catch (ClassNotFoundException e) { // If loadClass fails, we should propagate the exception. throw e; @@ -524,7 +593,7 @@ public abstract class LayoutInflater { } /** - * Throw an excpetion because the specified class is not allowed to be inflated. + * Throw an exception because the specified class is not allowed to be inflated. */ private void failNotAllowed(String name, String prefix, AttributeSet attrs) { InflateException ie = new InflateException(attrs.getPositionDescription() @@ -549,10 +618,27 @@ public abstract class LayoutInflater { return createView(name, "android.view.", attrs); } + /** + * Version of {@link #onCreateView(String, AttributeSet)} that also + * takes the future parent of the view being constructure. The default + * implementation simply calls {@link #onCreateView(String, AttributeSet)}. + * + * @param parent The future parent of the returned view. Note that + * this may be null. + * @param name The fully qualified class name of the View to be create. + * @param attrs An AttributeSet of attributes to apply to the View. + * + * @return View The View created. + */ + protected View onCreateView(View parent, String name, AttributeSet attrs) + throws ClassNotFoundException { + return onCreateView(name, attrs); + } + /* * default visibility so the BridgeInflater can override it. */ - View createViewFromTag(String name, AttributeSet attrs) { + View createViewFromTag(View parent, String name, AttributeSet attrs) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } @@ -560,12 +646,14 @@ public abstract class LayoutInflater { if (DEBUG) System.out.println("******** Creating view: " + name); try { - View view = (mFactory == null) ? null : mFactory.onCreateView(name, - mContext, attrs); + View view; + if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs); + else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs); + else view = null; if (view == null) { if (-1 == name.indexOf('.')) { - view = onCreateView(name, attrs); + view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } @@ -595,8 +683,8 @@ public abstract class LayoutInflater { * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). */ - private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs) - throws XmlPullParserException, IOException { + private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, + boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; @@ -620,15 +708,15 @@ public abstract class LayoutInflater { } else if (TAG_MERGE.equals(name)) { throw new InflateException(" must be the root element"); } else { - final View view = createViewFromTag(name, attrs); + final View view = createViewFromTag(parent, name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); - rInflate(parser, view, attrs); + rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } } - parent.onFinishInflate(); + if (finishInflate) parent.onFinishInflate(); } private void parseRequestFocus(XmlPullParser parser, View parent) @@ -679,9 +767,9 @@ public abstract class LayoutInflater { if (TAG_MERGE.equals(childName)) { // Inflate all children. - rInflate(childParser, parent, childAttrs); + rInflate(childParser, parent, childAttrs, false); } else { - final View view = createViewFromTag(childName, childAttrs); + final View view = createViewFromTag(parent, childName, childAttrs); final ViewGroup group = (ViewGroup) parent; // We try to load the layout params set in the tag. If @@ -704,7 +792,7 @@ public abstract class LayoutInflater { } // Inflate all children. - rInflate(childParser, view, childAttrs); + rInflate(childParser, view, childAttrs, true); // Attempt to override the included layout's android:id with the // one set on the tag itself. diff --git a/core/java/android/view/MenuInflater.java b/core/java/android/view/MenuInflater.java index 46c805c681ba2037d2fe0ebd94199af508cc9626..7d5dcd86d1b0fba8ec1aed3f6bed87d023768058 100644 --- a/core/java/android/view/MenuInflater.java +++ b/core/java/android/view/MenuInflater.java @@ -18,8 +18,6 @@ package android.view; import com.android.internal.view.menu.MenuItemImpl; -import java.io.IOException; - import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -30,6 +28,10 @@ import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + /** * This class is used to instantiate menu XML files into Menu objects. *

    @@ -51,6 +53,8 @@ public class MenuInflater { private static final int NO_ID = 0; + private static final Class[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[]{Context.class}; + private Context mContext; /** @@ -166,6 +170,41 @@ public class MenuInflater { } } + private static class InflatedOnMenuItemClickListener + implements MenuItem.OnMenuItemClickListener { + private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class }; + + private Context mContext; + private Method mMethod; + + public InflatedOnMenuItemClickListener(Context context, String methodName) { + mContext = context; + Class c = context.getClass(); + try { + mMethod = c.getMethod(methodName, PARAM_TYPES); + } catch (Exception e) { + InflateException ex = new InflateException( + "Couldn't resolve menu item onClick handler " + methodName + + " in class " + c.getName()); + ex.initCause(e); + throw ex; + } + } + + public boolean onMenuItemClick(MenuItem item) { + try { + if (mMethod.getReturnType() == Boolean.TYPE) { + return (Boolean) mMethod.invoke(mContext, item); + } else { + mMethod.invoke(mContext, item); + return true; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + /** * State for the current menu. *

    @@ -205,6 +244,20 @@ public class MenuInflater { private boolean itemVisible; private boolean itemEnabled; + /** + * Sync to attrs.xml enum, values in MenuItem: + * - 0: never + * - 1: ifRoom + * - 2: always + * - -1: Safe sentinel for "no value". + */ + private int itemShowAsAction; + + private int itemActionViewLayout; + private String itemActionViewClassName; + + private String itemListenerMethodName; + private static final int defaultGroupId = NO_ID; private static final int defaultItemId = NO_ID; private static final int defaultItemCategory = 0; @@ -276,6 +329,10 @@ public class MenuInflater { itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); + itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1); + itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); + itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0); + itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass); a.recycle(); @@ -299,9 +356,38 @@ public class MenuInflater { .setIcon(itemIconResId) .setAlphabeticShortcut(itemAlphabeticShortcut) .setNumericShortcut(itemNumericShortcut); + + if (itemShowAsAction >= 0) { + item.setShowAsAction(itemShowAsAction); + } + + if (itemListenerMethodName != null) { + if (mContext.isRestricted()) { + throw new IllegalStateException("The android:onClick attribute cannot " + + "be used within a restricted context"); + } + item.setOnMenuItemClickListener( + new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName)); + } - if (itemCheckable >= 2) { - ((MenuItemImpl) item).setExclusiveCheckable(true); + if (item instanceof MenuItemImpl) { + MenuItemImpl impl = (MenuItemImpl) item; + if (itemCheckable >= 2) { + impl.setExclusiveCheckable(true); + } + } + + if (itemActionViewClassName != null) { + try { + final Class clazz = Class.forName(itemActionViewClassName); + Constructor c = clazz.getConstructor(ACTION_VIEW_CONSTRUCTOR_SIGNATURE); + item.setActionView((View) c.newInstance(mContext)); + } catch (Exception e) { + throw new InflateException(e); + } + } else if (itemActionViewLayout > 0) { + final LayoutInflater inflater = LayoutInflater.from(mContext); + item.setActionView(inflater.inflate(itemActionViewLayout, null)); } } diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java index fcebec5d8766f1bd2eb5822e093a74c6531f7a30..8b9d659da1973adc073218d41f6f2377f9c486c2 100644 --- a/core/java/android/view/MenuItem.java +++ b/core/java/android/view/MenuItem.java @@ -31,6 +31,21 @@ import android.view.View.OnCreateContextMenuListener; * For a feature set of specific menu types, see {@link Menu}. */ public interface MenuItem { + /* + * These should be kept in sync with attrs.xml enum constants for showAsAction + */ + /** Never show this item as a button in an Action Bar. */ + public static final int SHOW_AS_ACTION_NEVER = 0; + /** Show this item as a button in an Action Bar if the system decides there is room for it. */ + public static final int SHOW_AS_ACTION_IF_ROOM = 1; + /** + * Always show this item as a button in an Action Bar. + * Use sparingly! If too many items are set to always show in the Action Bar it can + * crowd the Action Bar and degrade the user experience on devices with smaller screens. + * A good rule of thumb is to have no more than 2 items set to always show at a time. + */ + public static final int SHOW_AS_ACTION_ALWAYS = 2; + /** * Interface definition for a callback to be invoked when a menu item is * clicked. @@ -381,4 +396,38 @@ public interface MenuItem { * menu item to the menu. This can be null. */ public ContextMenuInfo getMenuInfo(); + + /** + * Sets how this item should display in the presence of an Action Bar. + * + * @param actionEnum How the item should display. One of + * {@link #SHOW_AS_ACTION_ALWAYS}, {@link #SHOW_AS_ACTION_IF_ROOM}, or + * {@link #SHOW_AS_ACTION_NEVER}. SHOW_AS_ACTION_NEVER is the default. + * + * @see android.app.ActionBar + * @see #setActionView(View) + */ + public void setShowAsAction(int actionEnum); + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + * + * @param view View to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * + * @see #setShowAsAction(int) + */ + public MenuItem setActionView(View view); + + /** + * Returns the currently set action view for this menu item. + * + * @return This item's action view + * + * @see #setActionView(View) + * @see #setShowAsAction(int) + */ + public View getActionView(); } \ No newline at end of file diff --git a/core/java/android/view/ScaleGestureDetector.java b/core/java/android/view/ScaleGestureDetector.java index 09995987e65d87dc137e1a4a3430861f2e3d594a..17b5dd7f87fe6181fbb9b13733192f9512d0c027 100644 --- a/core/java/android/view/ScaleGestureDetector.java +++ b/core/java/android/view/ScaleGestureDetector.java @@ -25,7 +25,7 @@ import android.util.FloatMath; * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener} * callback will notify users when a particular gesture event has occurred. * This class should only be used with {@link MotionEvent}s reported via touch. - * + * * To use this class: *

    - The test results appear in the JUnit view. This is divided into an upper summary pane, - and a lower stack trace pane. + The following lines are an example of this message sequence:

    + +[2010-07-01 12:44:40 - MyTest] HOME is up on device 'emulator-5554'
    +[2010-07-01 12:44:40 - MyTest] Uploading MyTest.apk onto device 'emulator-5554'
    +[2010-07-01 12:44:40 - MyTest] Installing MyTest.apk...
    +[2010-07-01 12:44:49 - MyTest] Success!
    +
    +
    +
      +
    • + Next, if you have not yet installed the application under test to the device or + emulator, you see the message +

      + Project dependency found, installing: appfile +

      +

      + then the message Uploading appfile onto device + 'devicename-port' +

      +

      + then the message Installing appfile +

      +

      + and finally the message Success! +

      +
    • +

    - The upper pane contains test information. In the pane's header, you see the following - information: + The following lines are an example of this message sequence:

    -
      -
    • - Total time elapsed for the test application (labeled Finished after x seconds). -
    • -
    • - Number of runs (Runs:) - the number of tests in the entire test class. -
    • -
    • - Number of errors (Errors:) - the number of program errors and exceptions encountered - during the test run. -
    • -
    • - Number of failures (Failures:) - the number of test failures encountered during the test - run. This is the number of assertion failures. A test can fail even if the program does - not encounter an error. -
    • -
    • - A progress bar. The progress bar extends from left to right as the tests run. If all the - tests succeed, the bar remains green. If a test fails, the bar turns from green to red. -
    • -
    + +[2010-07-01 12:44:49 - MyTest] Project dependency found, installing: MyApp
    +[2010-07-01 12:44:49 - MyApp] Uploading MyApp.apk onto device 'emulator-5554'
    +[2010-07-01 12:44:49 - MyApp] Installing MyApp.apk...
    +[2010-07-01 12:44:54 - MyApp] Success!
    +
    +
    +
      +
    • + Next, you see the message + Launching instrumentation instrumentation_class on device + devicename-port +

      + instrumentation_class is the fully-qualified class name of the + instrumentation test runner you have specified (usually + {@link android.test.InstrumentationTestRunner}. +

      +
    • +
    • + Next, as {@link android.test.InstrumentationTestRunner} builds a list of tests to run, + you see the message +

      + Collecting test information +

      +

      + followed by +

      +

      + Sending test information to Eclipse +

      +
    • +
    • + Finally, you see the message Running tests, which indicates that your tests + are running. At this point, you should start seeing the test results in the JUnit view. + When the tests are finished, you see the console message Test run complete. + This indicates that your tests are finished. +
    • +
    +

    + The following lines are an example of this message sequence: +

    + +[2010-01-01 12:45:02 - MyTest] Launching instrumentation android.test.InstrumentationTestRunner on device emulator-5554
    +[2010-01-01 12:45:02 - MyTest] Collecting test information
    +[2010-01-01 12:45:02 - MyTest] Sending test information to Eclipse
    +[2010-01-01 12:45:02 - MyTest] Running tests...
    +[2010-01-01 12:45:22 - MyTest] Test run complete
    +
    +
    +

    + The test results appear in the JUnit view. This is divided into an upper summary pane, + and a lower stack trace pane. +

    +

    + The upper pane contains test information. In the pane's header, you see the following + information: +

    +
      +
    • + Total time elapsed for the test package (labeled Finished after x seconds). +
    • +
    • + Number of runs (Runs:) - the number of tests in the entire test class. +
    • +
    • + Number of errors (Errors:) - the number of program errors and exceptions encountered + during the test run. +
    • +
    • + Number of failures (Failures:) - the number of test failures encountered during the test + run. This is the number of assertion failures. A test can fail even if the program does + not encounter an error. +
    • +
    • + A progress bar. The progress bar extends from left to right as the tests run. If all the + tests succeed, the bar remains green. If a test fails, the bar turns from green to red. +
    • +

    The body of the upper pane contains the details of the test run. For each test case class that was run, you see a line with the class name. To look at the results for the individual @@ -362,9 +504,31 @@ page.title=Testing In Eclipse, with ADT If you double-click the method name, Eclipse opens the test class source in an editor view pane and moves the focus to the first line of the test method.

    +

    + The results of a successful test are shown in + Figure 1. Messages for a successful test: +

    + + Messages for a successful test + +

    + Figure 1. Messages for a successful test +

    The lower pane is for stack traces. If you highlight a failed test in the upper pane, the lower pane contains a stack trace for the test. If a line corresponds to a point in your test code, you can double-click it to display the code in an editor view pane, with the line highlighted. For a successful test, the lower pane is empty.

    +

    + The results of a failed test are shown in + Figure 2. Messages for a test failure +

    + + Messages for a test failure + +

    + Figure 2. Messages for a test failure +

    diff --git a/docs/html/guide/developing/testing/testing_otheride.jd b/docs/html/guide/developing/testing/testing_otheride.jd index 2bdf4d0b0b70de530b7b896e0a8503202e4bc55e..523a8e57f36e3330e94044810c74350631c4f057 100644 --- a/docs/html/guide/developing/testing/testing_otheride.jd +++ b/docs/html/guide/developing/testing/testing_otheride.jd @@ -2,122 +2,128 @@ page.title=Testing In Other IDEs @jd:body

    - This document describes how to create and run tests directly from the command line. - You can use the techniques described here if you are developing in an IDE other than Eclipse - or if you prefer to work from the command line. This document assumes that you already know how - to create a Android application in your programming environment. Before you start this - document, you should read the document Testing and Instrumentation, - which provides an overview of Android testing. + This document describes how to create and run tests directly from the command line. + You can use the techniques described here if you are developing in an IDE other than Eclipse + or if you prefer to work from the command line. This document assumes that you already know how + to create a Android application in your programming environment. Before you start this + document, you should read the topic + Testing Fundamentals, + which provides an overview of Android testing.

    - If you are developing in Eclipse with ADT, you can set up and run your tests -directly in Eclipse. For more information, please read Testing in Eclipse, with ADT. + If you are developing in Eclipse with ADT, you can set up and run your tests + directly in Eclipse. For more information, please read + + Testing in Eclipse, with ADT.

    Working with Test Projects

    - You use the android tool to create test projects. - You also use android to convert existing test code into an Android test project, - or to add the run-tests Ant target to an existing Android test project. - These operations are described in more detail in the section Updating a test project. - The run-tests target is described in Quick build and run with Ant. + You use the android tool to create test projects. + You also use android to convert existing test code into an Android test project, + or to add the run-tests Ant target to an existing Android test project. + These operations are described in more detail in the section + Updating a test project. The run-tests target is described in + Quick build and run with Ant.

    Creating a test project

    - To create a test project with the android tool, enter: -

    android create test-project -m <main_path> -n <project_name> -p <test_path>
    + To create a test project with the android tool, enter: +

    +
    +android create test-project -m <main_path> -n <project_name> -p <test_path>
    +

    - You must supply all the flags. The following table explains them in detail: + You must supply all the flags. The following table explains them in detail:

    - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
    FlagValueDescription
    -m, --main - Path to the project of the application under test, relative to the test application - directory. - - For example, if the application under test is in source/HelloAndroid, and you - want to create the test project in source/HelloAndroidTest, then the value of - --main should be ../HelloAndroid. -
    -n, --nameName that you want to give the test project. 
    -p, --pathDirectory in which you want to create the new test project. - The android tool creates the test project files and directory structure in this - directory. If the directory does not exist, android creates it. -
    FlagValueDescription
    -m, --main + Path to the project of the application under test, relative to the test package + directory. + + For example, if the application under test is in source/HelloAndroid, and + you want to create the test project in source/HelloAndroidTest, then the + value of --main should be ../HelloAndroid. +

    + To learn more about choosing the location of test projects, please read + + Testing Fundamentals. +

    +
    -n, --nameName that you want to give the test project. 
    -p, --pathDirectory in which you want to create the new test project. + The android tool creates the test project files and directory structure + in this directory. If the directory does not exist, android creates it. +

    If the operation is successful, android lists to STDOUT the names of the files @@ -135,11 +141,10 @@ directly in Eclipse. For more information, please read

    - For example, suppose you create the Hello, World tutorial application - in the directory ~/source/HelloAndroid. In the tutorial, this application uses the - package name com.example.helloandroid and the activity name - HelloAndroid. You can to create the test for this in + For example, suppose you create the + Hello, World tutorial application in the directory ~/source/HelloAndroid. + In the tutorial, this application uses the package name com.example.helloandroid + and the activity name HelloAndroid. You can to create the test for this in ~/source/HelloAndroidTest. To do so, you enter:

    @@ -196,7 +201,7 @@ $ android create test-project -m ../HelloAndroid -n HelloAndroidTest -p HelloAnd
     

    Note: If you change the Android package name of the application under test, you must manually change the value of the <android:targetPackage> - attribute within the AndroidManifest.xml file of the test application. + attribute within the AndroidManifest.xml file of the test package. Running android update test-project does not do this.

    @@ -205,38 +210,38 @@ $ android create test-project -m ../HelloAndroid -n HelloAndroidTest -p HelloAnd

    android update-test-project -m <main_path> -p <test_path>
    - - - - - - - - - - - - - - - + + + + + + + + + + + + + + +
    FlagValueDescription
    -m, --mainThe path to the project of the application under test, relative to the test project - For example, if the application under test is in source/HelloAndroid, and - the test project is in source/HelloAndroidTest, then the value for - --main is ../HelloAndroid. -
    -p, --pathThe of the test project. - For example, if the test project is in source/HelloAndroidTest, then the - value for --path is HelloAndroidTest. -
    FlagValueDescription
    -m, --mainThe path to the project of the application under test, relative to the test project + For example, if the application under test is in source/HelloAndroid, and + the test project is in source/HelloAndroidTest, then the value for + --main is ../HelloAndroid. +
    -p, --pathThe of the test project. + For example, if the test project is in source/HelloAndroidTest, then the + value for --path is HelloAndroidTest. +

    If the operation is successful, android lists to STDOUT the names of the files and directories it has created.

    -

    Creating a Test Application

    +

    Creating a Test Package

    - Once you have created a test project, you populate it with a test application. + Once you have created a test project, you populate it with a test package. The application does not require an {@link android.app.Activity Activity}, - although you can define one if you wish. Although your test application can + although you can define one if you wish. Although your test package can combine Activities, Android test class extensions, JUnit extensions, or ordinary classes, you should extend one of the Android test classes or JUnit classes, because these provide the best testing features. @@ -248,7 +253,7 @@ $ android create test-project -m ../HelloAndroid -n HelloAndroidTest -p HelloAnd

    - To create a test application, start with one of Android's test classes in the Java package + To create a test package, start with one of Android's test classes in the Java package {@link android.test android.test}. These extend the JUnit {@link junit.framework.TestCase TestCase} class. With a few exceptions, the Android test classes also provide instrumentation for testing. @@ -282,24 +287,17 @@ $ android create test-project -m ../HelloAndroid -n HelloAndroidTest -p HelloAnd test results are suspect, regardless of whether or not the tests succeeded.

    - To learn more about creating test applications, see the topic Testing and Instrumentation, + To learn more about creating test packages, see the topic Testing Fundamentals, which provides an overview of Android testing. If you prefer to follow a tutorial, try the Activity Testing tutorial, which leads you through the creation of tests for an actual Android application.

    Running Tests

    - If you are not developing in Eclipse with ADT, you need to run tests from the command line. - You can do this either with Ant or with the {@link android.app.ActivityManager ActivityManager} - command line interface. -

    -

    - You can also run tests from the command line even if you are using Eclipse with ADT to develop - them. To do this, you need to create the proper files and directory structure in the test - project, using the android tool with the option create test-project. - This is described in the section Working with Test Projects. + You run tests from the command line, either with Ant or with an + + Android Debug Bridge (adb) shell.

    Quick build and run with Ant

    @@ -316,57 +314,63 @@ $ android create test-project -m ../HelloAndroid -n HelloAndroidTest -p HelloAnd You can update an existing test project to use this feature. To do this, use the android tool with the update test-project option. This is described in the section Updating a test project. +

    Running tests on a device or emulator

    - When you run tests from the command line with the ActivityManager (am) - command-line tool, you get more options for choosing the tests to run than with any other - method. You can select individual test methods, filter tests according to their annotation, or - specify testing options. Since the test run is controlled entirely from a command line, you can - customize your testing with shell scripts in various ways. + When you run tests from the command line with + + Android Debug Bridge (adb), you get more options for choosing the tests + to run than with any other method. You can select individual test methods, filter tests + according to their annotation, or specify testing options. Since the test run is controlled + entirely from a command line, you can customize your testing with shell scripts in various ways. +

    +

    + To run a test from the command line, you run adb shell to start a command-line + shell on your device or emulator, and then in the shell run the am instrument + command. You control am and your tests with command-line flags.

    - You run the am tool on an Android device or emulator using the - Android Debug Bridge - (adb) shell. When you do this, you use the ActivityManager - instrument option to run your test application using an Android test runner - (usually {@link android.test.InstrumentationTestRunner}). You set am - options with command-line flags. + As a shortcut, you can start an adb shell, call am instrument, and + specify command-line flags all on one input line. The shell opens on the device or emulator, + runs your tests, produces output, and then returns to the command line on your computer.

    - To run a test with am: + To run a test with am instrument:

      -
    1. - If necessary, re-build your main application and test application. -
    2. -
    3. - Install your test application and main application Android package files - (.apk files) to your current Android device or emulator
    4. -
    5. - At the command line, enter: +
    6. + If necessary, rebuild your main application and test package. +
    7. +
    8. + Install your test package and main application Android package files + (.apk files) to your current Android device or emulator
    9. +
    10. + At the command line, enter:
       $ adb shell am instrument -w <test_package_name>/<runner_class>
       
      -

      - where <test_package_name> is the Android package name of your test - application, and <runner_class> is the name of the Android test runner - class you are using. The Android package name is the value of the package - attribute of the manifest element in the manifest file - (AndroidManifest.xml) of your test application. The Android test runner - class is usually InstrumentationTestRunner. -

      -

      Your test results appear in STDOUT.

      -
    11. +

      + where <test_package_name> is the Android package name of your test + application, and <runner_class> is the name of the Android test + runner class you are using. The Android package name is the value of the + package attribute of the manifest element in the manifest file + (AndroidManifest.xml) of your test package. The Android test runner + class is usually {@link android.test.InstrumentationTestRunner}. +

      +

      + Your test results appear in STDOUT. +

      +

    - This operation starts an adb shell, then runs am instrument in it + This operation starts an adb shell, then runs am instrument with the specified parameters. This particular form of the command will run all of the tests - in your test application. You can control this behavior with flags that you pass to + in your test package. You can control this behavior with flags that you pass to am instrument. These flags are described in the next section.

    -

    Using the Instrument Command

    +

    Using the am instrument Command

    - The general syntax of the am instrument command is: + The general syntax of the am instrument command is:

         am instrument [flags] <test_package>/<runner_class>
    @@ -391,11 +395,11 @@ $ adb shell am instrument -w <test_package_name>/<runner_class>
                 <test_package>
             
             
    -            The Android package name of the test application.
    +            The Android package name of the test package.
             
             
                 The value of the package attribute of the manifest
    -            element in the test application's manifest file.
    +            element in the test package's manifest file.
             
         
         
    @@ -411,7 +415,7 @@ $ adb shell am instrument -w <test_package_name>/<runner_class>
         
     
     

    -The flags for am instrument are described in the following table: + The flags for am instrument are described in the following table:

    @@ -461,20 +465,21 @@ The flags for am instrument are described in the following table: <test_options>
    - Provides testing options , in the form of key-value pairs. The + Provides testing options as key-value pairs. The am instrument tool passes these to the specified instrumentation class via its onCreate() method. You can specify multiple occurrences of - -e <test_options. The keys and values are described in the next table. + -e <test_options>. The keys and values are described in the + section am instrument options.

    - The only instrumentation class that understands these key-value pairs is - InstrumentationTestRunner (or a subclass). Using them with + The only instrumentation class that uses these key-value pairs is + {@link android.test.InstrumentationTestRunner} (or a subclass). Using them with any other class has no effect.

    -

    Instrument options

    +

    am instrument options

    The am instrument tool passes testing options to InstrumentationTestRunner or a subclass in the form of key-value pairs, @@ -484,123 +489,127 @@ The flags for am instrument are described in the following table: -e <key> <value>

    - Where applicable, a <key> may have multiple values separated by a comma (,). + Some keys accept multiple values. You specify multiple values in a comma-separated list. For example, this invocation of InstrumentationTestRunner provides multiple values for the package key: +

    -$ adb shell am instrument -w -e package com.android.test.package1,com.android.test.package2 com.android.test/android.test.InstrumentationTestRunner
    +$ adb shell am instrument -w -e package com.android.test.package1,com.android.test.package2 \
    +> com.android.test/android.test.InstrumentationTestRunner
     

    The following table describes the key-value pairs and their result. Please review the Usage Notes following the table.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyValueDescription
    - package - - <Java_package_name> - - The fully-qualified Java package name for one of the packages in the test - application. Any test case class that uses this package name is executed. Notice that this - is not an Android package name; a test application has a single Android package - name but may have several Java packages within it. -
    class<class_name> - The fully-qualified Java class name for one of the test case classes. Only this test case - class is executed. -
    <class_name>#method name - A fully-qualified test case class name, and one of its methods. Only this method is - executed. Note the hash mark (#) between the class name and the method name. -
    functrue - Runs all test classes that extend {@link android.test.InstrumentationTestCase}. -
    unittrue - Runs all test classes that do not extend either - {@link android.test.InstrumentationTestCase} or {@link android.test.PerformanceTestCase}. -
    size[small | medium | large] - - Runs a test method annotated by size. The annotations are @SmallTest, - @MediumTest, and @LargeTest. -
    perftrue - Runs all test classes that implement {@link android.test.PerformanceTestCase}. - When you use this option, also specify the -r flag for - am instrument, so that the output is kept in raw format and not - re-formatted as test results. -
    debugtrue - Runs tests in debug mode. -
    logtrue - Loads and logs all specified tests, but does not run them. The test - information appears in STDOUT. Use this to verify combinations of other filters - and test specifications. -
    emmatrue - Runs an EMMA code coverage analysis and writes the output to /data//coverage.ec - on the device. To override the file location, use the coverageFile key that - is described in the following entry. -

    - Note: This option requires an EMMA-instrumented build of the test - application, which you can generate with the coverage target. -

    -
    coverageFile<filename> - Overrides the default location of the EMMA coverage file on the device. Specify this - value as a path and filename in UNIX format. The default filename is described in the - entry for the emma key. -
    KeyValueDescription
    + package + + <Java_package_name> + + The fully-qualified Java package name for one of the packages in the test + application. Any test case class that uses this package name is executed. Notice that + this is not an Android package name; a test package has a single + Android package name but may have several Java packages within it. +
    class<class_name> + The fully-qualified Java class name for one of the test case classes. Only this test + case class is executed. +
    <class_name>#method name + A fully-qualified test case class name, and one of its methods. Only this method is + executed. Note the hash mark (#) between the class name and the method name. +
    functrue + Runs all test classes that extend {@link android.test.InstrumentationTestCase}. +
    unittrue + Runs all test classes that do not extend either + {@link android.test.InstrumentationTestCase} or + {@link android.test.PerformanceTestCase}. +
    size + [small | medium | large] + + Runs a test method annotated by size. The annotations are @SmallTest, + @MediumTest, and @LargeTest. +
    perftrue + Runs all test classes that implement {@link android.test.PerformanceTestCase}. + When you use this option, also specify the -r flag for + am instrument, so that the output is kept in raw format and not + re-formatted as test results. +
    debugtrue + Runs tests in debug mode. +
    logtrue + Loads and logs all specified tests, but does not run them. The test + information appears in STDOUT. Use this to verify combinations of other + filters and test specifications. +
    emmatrue + Runs an EMMA code coverage analysis and writes the output to + /data//coverage.ec on the device. To override the file location, use the + coverageFile key that is described in the following entry. +

    + Note: This option requires an EMMA-instrumented build of the test + application, which you can generate with the coverage target. +

    +
    coverageFile<filename> + Overrides the default location of the EMMA coverage file on the device. Specify this + value as a path and filename in UNIX format. The default filename is described in the + entry for the emma key. +
    -e Flag Usage Notes
      @@ -618,13 +627,13 @@ $ adb shell am instrument -w -e package com.android.test.package1,com.android.te The func key and unit key are mutually exclusive.
    -

    Instrument examples

    +

    Usage examples

    -Here are some examples of using am instrument to run tests. They are based on -the following structure:

    +The following sections provide examples of using am instrument to run tests. +They are based on the following structure:

    • - The test application has the Android package name com.android.demo.app.tests + The test package has the Android package name com.android.demo.app.tests
    • There are three test classes: @@ -647,35 +656,35 @@ the following structure:

      The test runner is {@link android.test.InstrumentationTestRunner}.
    -

    Running the Entire Test Application

    +

    Running the entire test package

    - To run all of the test classes in the test application, enter: + To run all of the test classes in the test package, enter:

     $ adb shell am instrument -w com.android.demo.app.tests/android.test.InstrumentationTestRunner
     
    -

    Running All Tests in a Test Case Class

    +

    Running all tests in a test case class

    To run all of the tests in the class UnitTests, enter:

     $ adb shell am instrument -w  \
    --e class com.android.demo.app.tests.UnitTests \
    -com.android.demo.app.tests/android.test.InstrumentationTestRunner
    +> -e class com.android.demo.app.tests.UnitTests \
    +> com.android.demo.app.tests/android.test.InstrumentationTestRunner
     

    am instrument gets the value of the -e flag, detects the class keyword, and runs all the methods in the UnitTests class.

    -

    Selecting a Subset of Tests

    +

    Selecting a subset of tests

    - To run all of the tests in UnitTests, and the testCamera method in - FunctionTests, enter: + To run all of the tests in UnitTests, and the testCamera method in + FunctionTests, enter:

     $ adb shell am instrument -w \
    --e class com.android.demo.app.tests.UnitTests,com.android.demo.app.tests.FunctionTests#testCamera \
    -com.android.demo.app.tests/android.test.InstrumentationTestRunner
    +> -e class com.android.demo.app.tests.UnitTests,com.android.demo.app.tests.FunctionTests#testCamera \
    +> com.android.demo.app.tests/android.test.InstrumentationTestRunner
     

    You can find more examples of the command in the documentation for diff --git a/docs/html/guide/guide_toc.cs b/docs/html/guide/guide_toc.cs index 35ce17e26eaeaa5eaa0dd0dd38e897b5ef31be84..9793748e8ce6ea9124836998a404e56bc5e75186 100644 --- a/docs/html/guide/guide_toc.cs +++ b/docs/html/guide/guide_toc.cs @@ -254,9 +254,30 @@

  • Searchable Configuration
  • -
  • - Testing and Instrumentation - new!
  • +
  • +
    + + Testing + new! +
    + +
  • diff --git a/docs/html/guide/topics/fundamentals.jd b/docs/html/guide/topics/fundamentals.jd index 6d6abd8b15c999672e32a74cc3fa60122e9b84c3..db06efcb6c40bc59a60af6d1abce6f8e185078e8 100644 --- a/docs/html/guide/topics/fundamentals.jd +++ b/docs/html/guide/topics/fundamentals.jd @@ -292,8 +292,8 @@ onActivityResult()} method.
  • A service is started (or new instructions are given to an ongoing service) by passing an Intent object to {@link android.content.Context#startService Context.startService()}. -Android calls the service's {@link android.app.Service#onStart -onStart()} method and passes it the Intent object.

    +Android calls the service's {@link android.app.Service#onStartCommand +onStartCommand()} method and passes it the Intent object.

    Similarly, an intent can be passed to {@link @@ -1508,9 +1508,9 @@ a music playback service could create the thread where the music will be played in {@code onCreate()}, and then stop the thread in {@code onDestroy()}.

  • The active lifetime of a service begins with a call to -{@link android.app.Service#onStart onStart()}. This method +{@link android.app.Service#onStartCommand onStartCommand()}. This method is handed the Intent object that was passed to {@code startService()}. -The music service would open the Intent to discover which music to +The music service would open the Intent to discover which music to play, and begin the playback.

    @@ -1525,7 +1525,7 @@ services, whether they're started by {@link android.content.Context#startService Context.startService()} or {@link android.content.Context#bindService Context.bindService()}. -However, {@code onStart()} is called only for services started by {@code +However, {@code onStartCommand()} is called only for services started by {@code startService()}.

    @@ -1629,7 +1629,7 @@ to the activity that the user is interacting with.

  • It has a {@link android.app.Service} object that's executing one of its lifecycle callbacks ({@link android.app.Service#onCreate -onCreate()}, {@link android.app.Service#onStart onStart()}, +onCreate()}, {@link android.app.Service#onStartCommand onStartCommand()}, or {@link android.app.Service#onDestroy onDestroy()}).

  • It has a {@link android.content.BroadcastReceiver} object that's diff --git a/docs/html/guide/topics/resources/animation-resource.jd b/docs/html/guide/topics/resources/animation-resource.jd index e0ce0515d6149464755cc88fd221e5172dd8f7d2..972dd729cb1df89005c86427414abebf9d128c4d 100644 --- a/docs/html/guide/topics/resources/animation-resource.jd +++ b/docs/html/guide/topics/resources/animation-resource.jd @@ -65,10 +65,10 @@ In XML: @[package:]anim/filename android:pivotX="float" android:pivotY="float" /> <translate - android:fromX="float" - android:toX="float" - android:fromY="float" - android:toY="float" /> + android:fromXDelta="float" + android:toXDelta="float" + android:fromYDelta="float" + android:toYDelta="float" /> <rotate android:fromDegrees="float" android:toDegrees="float" @@ -212,10 +212,10 @@ inherrited by this element).

    android:shareInterpolator="false"> <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator" - android:fromXScale="1.0" - android:toXScale="1.4" - android:fromYScale="1.0" - android:toYScale="0.6" + android:fromXScale="1.0" + android:toXScale="1.4" + android:fromYScale="1.0" + android:toYScale="0.6" android:pivotX="50%" android:pivotY="50%" android:fillAfter="false" @@ -224,18 +224,18 @@ inherrited by this element).

    android:interpolator="@android:anim/accelerate_interpolator" android:startOffset="700"> <scale - android:fromXScale="1.4" + android:fromXScale="1.4" android:toXScale="0.0" android:fromYScale="0.6" - android:toYScale="0.0" - android:pivotX="50%" - android:pivotY="50%" + android:toYScale="0.0" + android:pivotX="50%" + android:pivotY="50%" android:duration="400" /> <rotate - android:fromDegrees="0" + android:fromDegrees="0" android:toDegrees="-45" - android:toYScale="0.0" - android:pivotX="50%" + android:toYScale="0.0" + android:pivotX="50%" android:pivotY="50%" android:duration="400" /> </set> diff --git a/docs/html/guide/topics/resources/string-resource.jd b/docs/html/guide/topics/resources/string-resource.jd index 81c5d55d6dba6a36528a0df9868a97d082ad95a6..2db38f17eafaab121469f3f70bb674d8fd2f99ec 100644 --- a/docs/html/guide/topics/resources/string-resource.jd +++ b/docs/html/guide/topics/resources/string-resource.jd @@ -12,8 +12,8 @@ your application with strings:

    XML resource that provides a single string.
    String Array
    XML resource that provides an array of strings.
    -
    Plurals
    -
    XML resource that carries different strings for different pluralizations +
    Quantity Strings (Plurals)
    +
    XML resource that carries different strings for different quantities of the same word or phrase.
    @@ -218,13 +218,30 @@ getStringArray}(R.array.planets_array); -

    Plurals

    +

    Quantity Strings (Plurals)

    -

    A pair of strings that each provide a different plural form of the same word or phrase, -which you can collectively reference from the application. When you request the plurals -resource using a method such as {@link android.content.res.Resources#getQuantityString(int,int) -getQuantityString()}, you must pass a "count", which will determine the plural form you -require and return that string to you.

    +

    Different languages have different rules for grammatical agreement with quantity. In English, +for example, the quantity 1 is a special case. We write "1 book", but for any other quantity we'd +write "n books". This distinction between singular and plural is very common, but other +languages make finer distinctions. The full set supported by Android is zero, +one, two, few, many, and other. + +

    The rules for deciding which case to use for a given language and quantity can be very complex, +so Android provides you with methods such as +{@link android.content.res.Resources#getQuantityString(int,int) getQuantityString()} to select +the appropriate resource for you. + +

    Note that the selection is made based on grammatical necessity. A string for zero +in English will be ignored even if the quantity is 0, because 0 isn't grammatically different +from 2, or any other number except 1 ("zero books", "one book", "two books", et cetera). +Don't be misled either by the fact that, say, two sounds like it could only apply to +the quantity 2: a language may require that 2, 12, 102 (et cetera) are all treated like one +another but differently to other quantities. Rely on your translator to know what distinctions +their language actually insists upon. + +

    It's often possible to avoid quantity strings by using quantity-neutral formulations such as +"Books: 1". This will make your life and your translators' lives easier, if it's a style that's +in keeping with your application.

    Note: A plurals collection is a simple resource that is referenced using the value provided in the {@code name} attribute (not the name of the XML @@ -251,7 +268,7 @@ In Java: R.plurals.plural_name <plurals name="plural_name"> <item - quantity=["one" | "other"] + quantity=["zero" | "one" | "two" | "few" | "many" | "other"] >text_string</item> </plurals> </resources> @@ -285,16 +302,27 @@ Styling, below, for information about to properly style and format your stri

    attributes:

    quantity
    -
    Keyword. A value indicating the case in which this string should be used. Valid -values: +
    Keyword. A value indicating when this string should be used. Valid +values, with non-exhaustive examples in parentheses: - + + + + + + + + + + - + + + +
    ValueDescription
    {@code one}When there is one (a singular string).{@code zero}When the language requires special treatment of the number 0 (as in Arabic).
    {@code one}When the language requires special treatment of numbers like one (as with the number 1 in English and most other languages; in Russian, any number ending in 1 but not ending in 11 is in this class).
    {@code two}When the language requires special treatment of numbers like two (as in Welsh).
    {@code few}When the language requires special treatment of "small" numbers (as with 2, 3, and 4 in Czech; or numbers ending 2, 3, or 4 but not 12, 13, or 14 in Polish).
    {@code other}When the quantity is anything other than one (a plural -string, but also used when the count is zero).{@code many}When the language requires special treatment of "large" numbers (as with numbers ending 11-99 in Maltese).
    {@code other}When the language does not require special treatment of the given quantity.
    @@ -314,6 +342,17 @@ string, but also used when the count is zero). <item quantity="other">%d songs found.</item> </plurals> </resources> +
  • +

    XML file saved at {@code res/values-pl/strings.xml}:

    +
    +<?xml version="1.0" encoding="utf-8"?>
    +<resources>
    +    <plurals name="numberOfSongsAvailable">
    +        <item quantity="one">Znaleziono jedną piosenkę.</item>
    +        <item quantity="few">Znaleziono %d piosenki.</item>
    +        <item quantity="other">Znaleziono %d piosenek.</item>
    +    </plurals>
    +</resources>
     

    Java code:

    diff --git a/docs/html/guide/topics/testing/activity_testing.jd b/docs/html/guide/topics/testing/activity_testing.jd
    new file mode 100644
    index 0000000000000000000000000000000000000000..6392ad7e64907cc6cd5d99ce0a224c937532355d
    --- /dev/null
    +++ b/docs/html/guide/topics/testing/activity_testing.jd
    @@ -0,0 +1,392 @@
    +page.title=Activity Testing
    +@jd:body
    +
    +
    +
    +

    In this document

    +
      +
    1. + The Activity Testing API +
        +
      1. + ActivityInstrumentationTestCase2 +
      2. +
      3. + ActivityUnitTestCase +
      4. +
      5. + SingleLaunchActivityTestCase +
      6. +
      7. + Mock objects and activity testing +
      8. +
      9. + Assertions for activity testing +
      10. +
      +
    2. +
    3. + What to Test +
    4. +
    5. + Next Steps +
    6. +
    7. + Appendix: UI Testing Notes +
        +
      1. + Testing on the UI thread +
      2. +
      3. + Turning off touch mode +
      4. +
      5. + Unlocking the Emulator or Device +
      6. +
      7. + Troubleshooting UI tests +
      8. +
      +
    8. +
    +

    Key Classes

    +
      +
    1. {@link android.test.InstrumentationTestRunner}
    2. +
    3. {@link android.test.ActivityInstrumentationTestCase2}
    4. +
    5. {@link android.test.ActivityUnitTestCase}
    6. +
    +

    Related Tutorials

    +
      +
    1. + + Hello, Testing +
    2. +
    3. + Activity Testing +
    4. +
    +

    See Also

    +
      +
    1. + + Testing in Eclipse, with ADT +
    2. +
    3. + + Testing in Other IDEs +
    4. +
    +
    +
    +

    + Activity testing is particularly dependent on the the Android instrumentation framework. + Unlike other components, activities have a complex lifecycle based on callback methods; these + can't be invoked directly except by instrumentation. Also, the only way to send events to the + user interface from a program is through instrumentation. +

    +

    + This document describes how to test activities using instrumentation and other test + facilities. The document assumes you have already read + Testing Fundamentals, + the introduction to the Android testing and instrumentation framework. +

    +

    The Activity Testing API

    +

    + The activity testing API base class is {@link android.test.InstrumentationTestCase}, + which provides instrumentation to the test case subclasses you use for Activities. +

    +

    + For activity testing, this base class provides these functions: +

    +
      +
    • + Lifecycle control: With instrumentation, you can start the activity under test, pause it, + and destroy it, using methods provided by the test case classes. +
    • +
    • + Dependency injection: Instrumentation allows you to create mock system objects such as + Contexts or Applications and use them to run the activity under test. This + helps you control the test environment and isolate it from production systems. You can + also set up customized Intents and start an activity with them. +
    • +
    • + User interface interaction: You use instrumentation to send keystrokes or touch events + directly to the UI of the activity under test. +
    • +
    +

    + The activity testing classes also provide the JUnit framework by extending + {@link junit.framework.TestCase} and {@link junit.framework.Assert}. +

    +

    + The two main testing subclasses are {@link android.test.ActivityInstrumentationTestCase2} and + {@link android.test.ActivityUnitTestCase}. To test an Activity that is launched in a mode + other than standard, you use {@link android.test.SingleLaunchActivityTestCase}. +

    +

    ActivityInstrumentationTestCase2

    +

    + The {@link android.test.ActivityInstrumentationTestCase2} test case class is designed to do + functional testing of one or more Activities in an application, using a normal system + infrastructure. It runs the Activities in a normal instance of the application under test, + using a standard system Context. It allows you to send mock Intents to the activity under + test, so you can use it to test an activity that responds to multiple types of intents, or + an activity that expects a certain type of data in the intent, or both. Notice, though, that it + does not allow mock Contexts or Applications, so you can not isolate the test from the rest of + a production system. +

    +

    ActivityUnitTestCase

    +

    + The {@link android.test.ActivityUnitTestCase} test case class tests a single activity in + isolation. Before you start the activity, you can inject a mock Context or Application, or both. + You use it to run activity tests in isolation, and to do unit testing of methods + that do not interact with Android. You can not send mock Intents to the activity under test, + although you can call + {@link android.app.Activity#startActivity(Intent) Activity.startActivity(Intent)} and then + look at arguments that were received. +

    +

    SingleLaunchActivityTestCase

    +

    + The {@link android.test.SingleLaunchActivityTestCase} class is a convenience class for + testing a single activity in an environment that doesn't change from test to test. + It invokes {@link junit.framework.TestCase#setUp() setUp()} and + {@link junit.framework.TestCase#tearDown() tearDown()} only once, instead of once per + method call. It does not allow you to inject any mock objects. +

    +

    + This test case is useful for testing an activity that runs in a mode other than + standard. It ensures that the test fixture is not reset between tests. You + can then test that the activity handles multiple calls correctly. +

    +

    Mock objects and activity testing

    +

    + This section contains notes about the use of the mock objects defined in + {@link android.test.mock} with activity tests. +

    +

    + The mock object {@link android.test.mock.MockApplication} is only available for activity + testing if you use the {@link android.test.ActivityUnitTestCase} test case class. + By default, ActivityUnitTestCase, creates a hidden MockApplication + object that is used as the application under test. You can inject your own object using + {@link android.test.ActivityUnitTestCase#setApplication(Application) setApplication()}. +

    +

    Assertions for activity testing

    +

    + {@link android.test.ViewAsserts} defines assertions for Views. You use it to verify the + alignment and position of View objects, and to look at the state of ViewGroup objects. +

    +

    What To Test

    +
      +
    • + Input validation: Test that an activity responds correctly to input values in an + EditText View. Set up a keystroke sequence, send it to the activity, and then + use {@link android.view.View#findViewById(int)} to examine the state of the View. You can + verify that a valid keystroke sequence enables an OK button, while an invalid one leaves the + button disabled. You can also verify that the Activity responds to invalid input by + setting error messages in the View. +
    • +
    • + Lifecycle events: Test that each of your application's activities handles lifecycle events + correctly. In general, lifecycle events are actions, either from the system or from the + user, that trigger a callback method such as onCreate() or + onClick(). For example, an activity should respond to pause or destroy events + by saving its state. Remember that even a change in screen orientation causes the current + activity to be destroyed, so you should test that accidental device movements don't + accidentally lose the application state. +
    • +
    • + Intents: Test that each activity correctly handles the intents listed in the intent + filter specified in its manifest. You can use + {@link android.test.ActivityInstrumentationTestCase2} to send mock Intents to the + activity under test. +
    • +
    • + Runtime configuration changes: Test that each activity responds correctly to the + possible changes in the device's configuration while your application is running. These + include a change to the device's orientation, a change to the current language, and so + forth. Handling these changes is described in detail in the topic + Handling Runtime + Changes. +
    • +
    • + Screen sizes and resolutions: Before you publish your application, make sure to test it on + all of the screen sizes and densities on which you want it to run. You can test the + application on multiple sizes and densities using AVDs, or you can test your application + directly on the devices that you are targeting. For more information, see the topic + Supporting Multiple Screens. +
    • +
    +

    Next Steps

    +

    + To learn how to set up and run tests in Eclipse, please refer to Testing in + Eclipse, with ADT. If you're not working in Eclipse, refer to Testing in Other + IDEs. +

    +

    + If you want a step-by-step introduction to testing activities, try one of the + testing tutorials: +

    +
      +
    • + The Hello, + Testing tutorial introduces basic testing concepts and procedures in the + context of the Hello, World application. +
    • +
    • + The Activity + Testing tutorial is an excellent follow-up to the Hello, Testing tutorial. + It guides you through a more complex testing scenario that you develop against a + more realistic activity-oriented application. +
    • +
    +

    Appendix: UI Testing Notes

    +

    + The following sections have tips for testing the UI of your Android application, specifically + to help you handle actions that run in the UI thread, touch screen and keyboard events, and home + screen unlock during testing. +

    +

    Testing on the UI thread

    +

    + An application's activities run on the application's UI thread. Once the + UI is instantiated, for example in the activity's onCreate() method, then all + interactions with the UI must run in the UI thread. When you run the application normally, it + has access to the thread and does not have to do anything special. +

    +

    + This changes when you run tests against the application. With instrumentation-based classes, + you can invoke methods against the UI of the application under test. The other test classes + don't allow this. To run an entire test method on the UI thread, you can annotate the thread + with @UIThreadTest. Notice that this will run all of the method statements + on the UI thread. Methods that do not interact with the UI are not allowed; for example, you + can't invoke Instrumentation.waitForIdleSync(). +

    +

    + To run a subset of a test method on the UI thread, create an anonymous class of type + Runnable, put the statements you want in the run() method, and + instantiate a new instance of the class as a parameter to the method + appActivity.runOnUiThread(), where appActivity is + the instance of the application you are testing. +

    +

    + For example, this code instantiates an activity to test, requests focus (a UI action) for the + Spinner displayed by the activity, and then sends a key to it. Notice that the calls to + waitForIdleSync and sendKeys aren't allowed to run on the UI thread: +

    +
    +  private MyActivity mActivity; // MyActivity is the class name of the app under test
    +  private Spinner mSpinner;
    +
    +  ...
    +
    +  protected void setUp() throws Exception {
    +      super.setUp();
    +      mInstrumentation = getInstrumentation();
    +
    +      mActivity = getActivity(); // get a references to the app under test
    +
    +      /*
    +       * Get a reference to the main widget of the app under test, a Spinner
    +       */
    +      mSpinner = (Spinner) mActivity.findViewById(com.android.demo.myactivity.R.id.Spinner01);
    +
    +  ...
    +
    +  public void aTest() {
    +      /*
    +       * request focus for the Spinner, so that the test can send key events to it
    +       * This request must be run on the UI thread. To do this, use the runOnUiThread method
    +       * and pass it a Runnable that contains a call to requestFocus on the Spinner.
    +       */
    +      mActivity.runOnUiThread(new Runnable() {
    +          public void run() {
    +              mSpinner.requestFocus();
    +          }
    +      });
    +
    +      mInstrumentation.waitForIdleSync();
    +
    +      this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
    +
    + +

    Turning off touch mode

    +

    + To control the emulator or a device with key events you send from your tests, you must turn off + touch mode. If you do not do this, the key events are ignored. +

    +

    + To turn off touch mode, you invoke + ActivityInstrumentationTestCase2.setActivityTouchMode(false) + before you call getActivity() to start the activity. You must invoke the + method in a test method that is not running on the UI thread. For this reason, you + can't invoke the touch mode method from a test method that is annotated with + @UIThread. Instead, invoke the touch mode method from setUp(). +

    +

    Unlocking the emulator or device

    +

    + You may find that UI tests don't work if the emulator's or device's home screen is disabled with + the keyguard pattern. This is because the application under test can't receive key events sent + by sendKeys(). The best way to avoid this is to start your emulator or device + first and then disable the keyguard for the home screen. +

    +

    + You can also explicitly disable the keyguard. To do this, + you need to add a permission in the manifest file (AndroidManifest.xml) and + then disable the keyguard in your application under test. Note, though, that you either have to + remove this before you publish your application, or you have to disable it with code in + the published application. +

    +

    + To add the the permission, add the element + <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/> + as a child of the <manifest> element. To disable the KeyGuard, add the + following code to the onCreate() method of activities you intend to test: +

    +
    +  mKeyGuardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
    +  mLock = mKeyGuardManager.newKeyguardLock("activity_classname");
    +  mLock.disableKeyguard();
    +
    +

    where activity_classname is the class name of the activity.

    +

    Troubleshooting UI tests

    +

    + This section lists some of the common test failures you may encounter in UI testing, and their + causes: +

    +
    +
    WrongThreadException
    +
    +

    Problem:

    + For a failed test, the Failure Trace contains the following error message: + + android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created + a view hierarchy can touch its views. + +

    Probable Cause:

    + This error is common if you tried to send UI events to the UI thread from outside the UI + thread. This commonly happens if you send UI events from the test application, but you don't + use the @UIThread annotation or the runOnUiThread() method. The + test method tried to interact with the UI outside the UI thread. +

    Suggested Resolution:

    + Run the interaction on the UI thread. Use a test class that provides instrumentation. See + the previous section Testing on the UI Thread + for more details. +
    +
    java.lang.RuntimeException
    +
    +

    Problem:

    + For a failed test, the Failure Trace contains the following error message: + + java.lang.RuntimeException: This method can not be called from the main application thread + +

    Probable Cause:

    + This error is common if your test method is annotated with @UiThreadTest but + then tries to do something outside the UI thread or tries to invoke + runOnUiThread(). +

    Suggested Resolution:

    + Remove the @UiThreadTest annotation, remove the runOnUiThread() + call, or re-factor your tests. +
    +
    diff --git a/docs/html/guide/topics/testing/contentprovider_testing.jd b/docs/html/guide/topics/testing/contentprovider_testing.jd new file mode 100644 index 0000000000000000000000000000000000000000..893b5c9b43299160945de59ffa30efe3edcfce7d --- /dev/null +++ b/docs/html/guide/topics/testing/contentprovider_testing.jd @@ -0,0 +1,224 @@ +page.title=Content Provider Testing +@jd:body + +
    +
    +

    In this document

    +
      +
    1. + Content Provider Design and Testing +
    2. +
    3. + The Content Provider Testing API +
        +
      1. + ProviderTestCase2 +
      2. +
      3. + Mock object classes +
      4. +
      +
    4. +
    5. + What To Test +
    6. +
    7. + Next Steps +
    8. +
    +

    Key Classes

    +
      +
    1. {@link android.test.InstrumentationTestRunner}
    2. +
    3. {@link android.test.ProviderTestCase2}
    4. +
    5. {@link android.test.IsolatedContext}
    6. +
    7. {@link android.test.mock.MockContentResolver}
    8. +
    +

    See Also

    +
      +
    1. + + Testing Fundamentals +
    2. +
    3. + + Testing in Eclipse, with ADT +
    4. +
    5. + + Testing in Other IDEs +
    6. +
    +
    +
    +

    + Content providers, which store and retrieve data and make it accessible across applications, + are a key part of the Android API. As an application developer you're allowed to provide your + own public providers for use by other applications. If you do, then you should test them + using the API you publish. +

    +

    + This document describes how to test public content providers, although the information is + also applicable to providers that you keep private to your own application. If you aren't + familiar with content providers or the Android testing framework, please read + Content Providers, + the guide to developing content providers, and + Testing Fundamentals, + the introduction to the Android testing and instrumentation framework. +

    +

    Content Provider Design and Testing

    +

    + In Android, content providers are viewed externally as data APIs that provide + tables of data, with their internals hidden from view. A content provider may have many + public constants, but it usually has few if any public methods and no public variables. + This suggests that you should write your tests based only on the provider's public members. + A content provider that is designed like this is offering a contract between itself and its + users. +

    +

    + The base test case class for content providers, + {@link android.test.ProviderTestCase2}, allows you to test your content provider in an + isolated environment. Android mock objects such as {@link android.test.IsolatedContext} and + {@link android.test.mock.MockContentResolver} also help provide an isolated test environment. +

    +

    + As with other Android tests, provider test packages are run under the control of the test + runner {@link android.test.InstrumentationTestRunner}. The section + + Running Tests With InstrumentationTestRunner describes the test runner in + more detail. The topic + Testing in Eclipse, with ADT shows you how to run a test package in Eclipse, and the + topic + Testing in Other IDEs + shows you how to run a test package from the command line. +

    +

    Content Provider Testing API

    +

    + The main focus of the provider testing API is to provide an isolated testing environment. This + ensures that tests always run against data dependencies set explicitly in the test case. It + also prevents tests from modifying actual user data. For example, you want to avoid writing + a test that fails because there was data left over from a previous test, and you want to + avoid adding or deleting contact information in a actual provider. +

    +

    + The test case class and mock object classes for provider testing set up this isolated testing + environment for you. +

    +

    ProviderTestCase2

    +

    + You test a provider with a subclass of {@link android.test.ProviderTestCase2}. This base class + extends {@link android.test.AndroidTestCase}, so it provides the JUnit testing framework as well + as Android-specific methods for testing application permissions. The most important + feature of this class is its initialization, which creates the isolated test environment. +

    +

    + The initialization is done in the constructor for {@link android.test.ProviderTestCase2}, which + subclasses call in their own constructors. The {@link android.test.ProviderTestCase2} + constructor creates an {@link android.test.IsolatedContext} object that allows file and + database operations but stubs out other interactions with the Android system. + The file and database operations themselves take place in a directory that is local to the + device or emulator and has a special prefix. +

    +

    + The constructor then creates a {@link android.test.mock.MockContentResolver} to use as the + resolver for the test. The {@link android.test.mock.MockContentResolver} class is described in + detail in the section + Mock object classes. +

    +

    + Lastly, the constructor creates an instance of the provider under test. This is a normal + {@link android.content.ContentProvider} object, but it takes all of its environment information + from the {@link android.test.IsolatedContext}, so it is restricted to + working in the isolated test environment. All of the tests done in the test case class run + against this isolated object. +

    +

    Mock object classes

    +

    + {@link android.test.ProviderTestCase2} uses {@link android.test.IsolatedContext} and + {@link android.test.mock.MockContentResolver}, which are standard mock object classes. To + learn more about them, please read + + Testing Fundamentals. +

    +

    What To Test

    +

    + The topic What To Test + lists general considerations for testing Android components. + Here are some specific guidelines for testing content providers. +

    +
      +
    • + Test with resolver methods: Even though you can instantiate a provider object in + {@link android.test.ProviderTestCase2}, you should always test with a resolver object + using the appropriate URI. This ensures that you are testing the provider using the same + interaction that a regular application would use. +
    • +
    • + Test a public provider as a contract: If you intent your provider to be public and + available to other applications, you should test it as a contract. This includes + the following ideas: +
        +
      • + Test with constants that your provider publicly exposes. For + example, look for constants that refer to column names in one of the provider's + data tables. These should always be constants publicly defined by the provider. +
      • +
      • + Test all the URIs offered by your provider. Your provider may offer several URIs, + each one referring to a different aspect of the data. The + Note Pad sample, + for example, features a provider that offers one URI for retrieving a list of notes, + another for retrieving an individual note by it's database ID, and a third for + displaying notes in a live folder. The sample test package for Note Pad, + Note Pad Test, has + unit tests for two of these URIs. +
      • +
      • + Test invalid URIs: Your unit tests should deliberately call the provider with an + invalid URI, and look for errors. Good provider design is to throw an + IllegalArgumentException for invalid URIs. + +
      • +
      +
    • +
    • + Test the standard provider interactions: Most providers offer six access methods: + query, insert, delete, update, getType, and onCreate(). Your tests should verify that all + of these methods work. These are described in more detail in the topic + Content Providers. +
    • +
    • + Test business logic: Don't forget to test the business logic that your provider should + enforce. Business logic includes handling of invalid values, financial or arithmetic + calculations, elimination or combining of duplicates, and so forth. A content provider + does not have to have business logic, because it may be implemented by activities that + modify the data. If the provider does implement business logic, you should test it. +
    • +
    +

    Next Steps

    +

    + To learn how to set up and run tests in Eclipse, please refer to Testing in + Eclipse, with ADT. If you're not working in Eclipse, refer to Testing in Other + IDEs. +

    +

    + If you want a step-by-step introduction to testing activities, try one of the + testing tutorials: +

    +
      +
    • + The Hello, + Testing tutorial introduces basic testing concepts and procedures in the + context of the Hello, World application. +
    • +
    • + The Activity + Testing tutorial is an excellent follow-up to the Hello, Testing tutorial. + It guides you through a more complex testing scenario that you develop against a + more realistic activity-oriented application. +
    • +
    diff --git a/docs/html/guide/topics/testing/index.jd b/docs/html/guide/topics/testing/index.jd new file mode 100644 index 0000000000000000000000000000000000000000..92ed5a703b9a6b75e4f017ae9c8c8816cbecf338 --- /dev/null +++ b/docs/html/guide/topics/testing/index.jd @@ -0,0 +1,80 @@ +page.title=Testing +@jd:body +

    + The Android development environment includes an integrated testing framework that helps you + test all aspects of your application. +

    +

    Fundamentals

    +

    + To start learning how to use the framework to create tests for your applications, please + read the topic + Testing Fundamentals. +

    +

    Concepts

    +
      +
    • + Testing Tools describes the Eclipse with ADT and command-line tools you use to test + Android applications. +
    • +
    • + What to Test is an overview of the types of testing you should do. It focuses on testing + system-wide aspects of Android that can affect every component in your application. +
    • +
    • + + Activity Testing focuses on testing activities. It describes how instrumentation allows + you to control activities outside the normal application lifecycle. It also lists + activity-specific features you should test, and it provides tips for testing Android + user interfaces. +
    • +
    • + + Content Provider Testing focuses on testing content providers. It describes the + mock system objects you can use, provides tips for designing providers so that they + can be tested, and lists provider-specific features you should test. +
    • +
    • + + Service Testing focuses on testing services. It also lists service-specific features + you should test. +
    • +
    +

    Procedures

    + +

    Tutorials

    +
      +
    • + The + Hello, Testing tutorial introduces basic testing concepts and procedures. +
    • +
    • + For a more advanced tutorial, try + Activity Testing, + which guides you through a more complex testing scenario. +
    • +
    +

    Samples

    +
      +
    • + Note Pad Provider + Test is a test package for the + Note Pad sample + application. It provides a simple example of unit testing + a {@link android.content.ContentProvider}. +
    • +
    • + The Alarm Service Test + is a test package for the Alarm + sample application. It provides a simple example of unit + testing a {@link android.app.Service}. +
    • +
    diff --git a/docs/html/guide/topics/testing/service_testing.jd b/docs/html/guide/topics/testing/service_testing.jd new file mode 100644 index 0000000000000000000000000000000000000000..3979f3c8d6bfc95cb119d6c8197d085f83e66d47 --- /dev/null +++ b/docs/html/guide/topics/testing/service_testing.jd @@ -0,0 +1,178 @@ +page.title=Service Testing +@jd:body + +
    +
    +

    In this document

    +
      +
    1. + Service Design and Testing +
    2. +
    3. + ServiceTestCase +
    4. +
    5. + Mock object classes +
    6. +
    7. + What to Test +
    8. +
    +

    Key Classes

    +
      +
    1. {@link android.test.InstrumentationTestRunner}
    2. +
    3. {@link android.test.ServiceTestCase}
    4. +
    5. {@link android.test.mock.MockApplication}
    6. +
    7. {@link android.test.RenamingDelegatingContext}
    8. +
    +

    Related Tutorials

    +
      +
    1. + + Hello, Testing +
    2. +
    3. + Activity Testing +
    4. +
    +

    See Also

    +
      +
    1. + + Testing in Eclipse, with ADT +
    2. +
    3. + + Testing in Other IDEs +
    4. +
    +
    +
    +

    + Android provides a testing framework for Service objects that can run them in + isolation and provides mock objects. The test case class for Service objects is + {@link android.test.ServiceTestCase}. Since the Service class assumes that it is separate + from its clients, you can test a Service object without using instrumentation. +

    +

    + This document describes techniques for testing Service objects. If you aren't familiar with the + Service class, please read + Application Fundamentals. If you aren't familiar with Android testing, please read + Testing Fundamentals, + the introduction to the Android testing and instrumentation framework. +

    +

    Service Design and Testing

    +

    + When you design a Service, you should consider how your tests can examine the various states + of the Service lifecycle. If the lifecycle methods that start up your Service, such as + {@link android.app.Service#onCreate() onCreate()} or + {@link android.app.Service#onStartCommand(Intent, int, int) onStartCommand()} do not normally + set a global variable to indicate that they were successful, you may want to provide such a + variable for testing purposes. +

    +

    + Most other testing is facilitated by the methods in the {@link android.test.ServiceTestCase} + test case class. For example, the {@link android.test.ServiceTestCase#getService()} method + returns a handle to the Service under test, which you can test to confirm that the Service is + running even at the end of your tests. +

    +

    ServiceTestCase

    +

    + {@link android.test.ServiceTestCase} extends the JUnit {@link junit.framework.TestCase} class + with with methods for testing application permissions and for controlling the application and + Service under test. It also provides mock application and Context objects that isolate your + test from the rest of the system. +

    +

    + {@link android.test.ServiceTestCase} defers initialization of the test environment until you + call {@link android.test.ServiceTestCase#startService(Intent) ServiceTestCase.startService()} or + {@link android.test.ServiceTestCase#bindService(Intent) ServiceTestCase.bindService()}. This + allows you to set up your test environment, particularly your mock objects, before the Service + is started. +

    +

    + Notice that the parameters to ServiceTestCase.bindService()are different from + those for Service.bindService(). For the ServiceTestCase version, + you only provide an Intent. Instead of returning a boolean, + ServiceTestCase.bindService() returns an object that subclasses + {@link android.os.IBinder}. +

    +

    + The {@link android.test.ServiceTestCase#setUp()} method for {@link android.test.ServiceTestCase} + is called before each test. It sets up the test fixture by making a copy of the current system + Context before any test methods touch it. You can retrieve this Context by calling + {@link android.test.ServiceTestCase#getSystemContext()}. If you override this method, you must + call super.setUp() as the first statement in the override. +

    +

    + The methods {@link android.test.ServiceTestCase#setApplication(Application) setApplication()} + and {@link android.test.AndroidTestCase#setContext(Context)} setContext()} allow you to set + a mock Context or mock Application (or both) for the Service, before you start it. These mock + objects are described in Mock object classes. +

    +

    + By default, {@link android.test.ServiceTestCase} runs the test method + {@link android.test.AndroidTestCase#testAndroidTestCaseSetupProperly()}, which asserts that + the base test case class successfully set up a Context before running. +

    +

    Mock object classes

    +

    + ServiceTestCase assumes that you will use a mock Context or mock Application + (or both) for the test environment. These objects isolate the test environment from the + rest of the system. If you don't provide your own instances of these objects before you + start the Service, then {@link android.test.ServiceTestCase} will create its own internal + instances and inject them into the Service. You can override this behavior by creating and + injecting your own instances before starting the Service +

    +

    + To inject a mock Application object into the Service under test, first create a subclass of + {@link android.test.mock.MockApplication}. MockApplication is a subclass of + {@link android.app.Application} in which all the methods throw an Exception, so to use it + effectively you subclass it and override the methods you need. You then inject it into the + Service with the + {@link android.test.ServiceTestCase#setApplication(Application) setApplication()} method. + This mock object allows you to control the application values that the Service sees, and + isolates it from the real system. In addition, any hidden dependencies your Service has on + its application reveal themselves as exceptions when you run the test. +

    +

    + You inject a mock Context into the Service under test with the + {@link android.test.AndroidTestCase#setContext(Context) setContext()} method. The mock + Context classes you can use are described in more detail in + + Testing Fundamentals. +

    +

    What to Test

    +

    + The topic What To Test + lists general considerations for testing Android components. + Here are some specific guidelines for testing a Service: +

    +
      +
    • + Ensure that the {@link android.app.Service#onCreate()} is called in response to + {@link android.content.Context#startService(Intent) Context.startService()} or + {@link android.content.Context#bindService(Intent,ServiceConnection,int) Context.bindService()}. + Similarly, you should ensure that {@link android.app.Service#onDestroy()} is called in + response to {@link android.content.Context#stopService(Intent) Context.stopService()}, + {@link android.content.Context#unbindService(ServiceConnection) Context.unbindService()}, + {@link android.app.Service#stopSelf()}, or + {@link android.app.Service#stopSelfResult(int) stopSelfResult()}. +
    • +
    • + Test that your Service correctly handles multiple calls from + Context.startService(). Only the first call triggers + Service.onCreate(), but all calls trigger a call to + Service.onStartCommand(). +

      + In addition, remember that startService() calls don't + nest, so a single call to Context.stopService() or + Service.stopSelf() (but not stopSelf(int)) + will stop the Service. You should test that your Service stops at the correct point. +

      +
    • +
    • + Test any business logic that your Service implements. Business logic includes checking for + invalid values, financial and arithmetic calculations, and so forth. +
    • +
    diff --git a/docs/html/guide/topics/testing/testing_android.jd b/docs/html/guide/topics/testing/testing_android.jd index 46ba7693e20097b55eabd827337430f88d6235c1..513e4728d12395307180e7b29983921d8f9181d5 100755 --- a/docs/html/guide/topics/testing/testing_android.jd +++ b/docs/html/guide/topics/testing/testing_android.jd @@ -1,4 +1,4 @@ -page.title=Testing and Instrumentation +page.title=Testing Fundamentals @jd:body
    @@ -6,65 +6,62 @@ page.title=Testing and Instrumentation

    In this document

    1. - Overview + Test Structure +
    2. +
    3. + Test Projects
    4. The Testing API
      1. - JUnit test case classes + JUnit +
      2. +
      3. + Instrumentation
      4. - Instrumentation test case classes + Test case classes
      5. - Assert classes + Assertion classes
      6. - Mock object classes + Mock object classes
      7. -
      8. - Instrumentation Test Runner -
    5. - Working in the Test Environment + Running Tests
    6. - What to Test + Seeing Test Results
    7. - Appendix: UI Testing Notes -
        -
      1. - Testing on the UI thread -
      2. -
      3. - Turning off touch mode -
      4. -
      5. - Unlocking the Emulator or Device -
      6. -
      7. - Troubleshooting UI tests -
      8. -
      + Monkey and MonkeyRunner +
    8. +
    9. + Working With Package Names +
    10. +
    11. + What To Test +
    12. +
    13. + Next Steps
    -

    Key Classes

    +

    Key Classes and Packages

    1. {@link android.test.InstrumentationTestRunner}
    2. -
    3. {@link android.test.ActivityInstrumentationTestCase2}
    4. -
    5. {@link android.test.ActivityUnitTestCase}
    6. -
    7. {@link android.test.ApplicationTestCase}
    8. -
    9. {@link android.test.ProviderTestCase2}
    10. -
    11. {@link android.test.ServiceTestCase}
    12. +
    13. {@link android.test}
    14. +
    15. {@link android.test.mock}
    16. +
    17. {@link junit.framework}

    Related Tutorials

    1. - Hello, Testing + + Hello, Testing
    2. Activity Testing @@ -73,445 +70,590 @@ page.title=Testing and Instrumentation

      See Also

      1. - Testing in Eclipse, with ADT + + Testing in Eclipse, with ADT
      2. - Testing in Other IDEs + + Testing in Other IDEs
    - -

    Android includes a powerful set of testing tools that extend the -industry-standard JUnit test framework with features specific to the Android -environment. Although you can test an Android application with JUnit, the -Android tools allow you to write much more sophisticated tests for every aspect -of your application, both at the unit and framework levels.

    - -

    Key features of the Android testing environment include:

    - +

    + The Android testing framework, an integral part of the development environment, + provides an architecture and powerful tools that help you test every aspect of your application + at every level from unit to framework. +

    +

    + The testing framework has these key features: +

      -
    • Android extensions to the JUnit framework that provide access to Android -system objects.
    • -
    • An instrumentation framework that lets tests control and examine the -application.
    • -
    • Mock versions of commonly-used Android system objects.
    • -
    • Tools for running single tests or test suites, with or without -instrumentation.
    • -
    • Support for managing tests and test projects in the ADT Plugin for Eclipse -and at the command line.
    • +
    • + Android test suites are based on JUnit. You can use plain JUnit to test a class that doesn't + call the Android API, or Android's JUnit extensions to test Android components. If you're + new to Android testing, you can start with general-purpose test case classes such as {@link + android.test.AndroidTestCase} and then go on to use more sophisticated classes. +
    • +
    • + The Android JUnit extensions provide component-specific test case classes. These classes + provide helper methods for creating mock objects and methods that help you control the + lifecycle of a component. +
    • +
    • + Test suites are contained in test packages that are similar to main application packages, so + you don't need to learn a new set of tools or techniques for designing and building tests. +
    • +
    • + The SDK tools for building and tests are available in Eclipse with ADT, and also in + command-line form for use with other IDES. These tools get information from the project of + the application under test and use this information to automatically create the build files, + manifest file, and directory structure for the test package. +
    • +
    • + The SDK also provides + MonkeyRunner, an API for + testing devices with Jython scripts, and Monkey, a command-line tool for + stress-testing UIs by sending pseudo-random events to a device. +
    +

    + This document describes the fundamentals of the Android testing framework, including the + structure of tests, the APIs that you use to develop tests, and the tools that you use to run + tests and view results. The document assumes you have a basic knowledge of Android application + programming and JUnit testing methodology. +

    +

    + The following diagram summarizes the testing framework: +

    +
    + + The Android testing framework + +
    +

    Test Structure

    +

    + Android's build and test tools assume that test projects are organized into a standard + structure of tests, test case classes, test packages, and test projects. +

    +

    + Android testing is based on JUnit. In general, a JUnit test is a method whose + statements test a part of the application under test. You organize test methods into classes + called test cases (or test suites). Each test is an isolated test of an individual module in + the application under test. Each class is a container for related test methods, although it + often provides helper methods as well. +

    +

    + In JUnit, you build one or more test source files into a class file. Similarly, in Android you + use the SDK's build tools to build one or more test source files into class files in an + Android test package. In JUnit, you use a test runner to execute test classes. In Android, you + use test tools to load the test package and the application under test, and the tools then + execute an Android-specific test runner. +

    +

    Test Projects

    +

    + Tests, like Android applications, are organized into projects. +

    +

    + A test project is a directory or Eclipse project in which you create the source code, manifest + file, and other files for a test package. The Android SDK contains tools for Eclipse with ADT + and for the command line that create and update test projects for you. The tools create the + directories you use for source code and resources and the manifest file for the test package. + The command-line tools also create the Ant build files you need. +

    +

    + You should always use Android tools to create a test project. Among other benefits, + the tools: +

    +
      +
    • + Automatically set up your test package to use + {@link android.test.InstrumentationTestRunner} as the test case runner. You must use + InstrumentationTestRunner (or a subclass) to run JUnit tests. +
    • +
    • + Create an appropriate name for the test package. If the application + under test has a package name of com.mydomain.myapp, then the + Android tools set the test package name to com.mydomain.myapp.test. This + helps you identify their relationship, while preventing conflicts within the system. +
    • +
    • + Automatically create the proper build files, manifest file, and directory + structure for the test project. This helps you to build the test package without + having to modify build files and sets up the linkage between your test package and + the application under test. + The +
    • +
    +

    + You can create a test project anywhere in your file system, but the best approach is to + add the test project so that its root directory tests/ is at the same level + as the src/ directory of the main application's project. This helps you find the + tests associated with an application. For example, if your application project's root directory + is MyProject, then you should use the following directory structure: +

    +
    +  MyProject/
    +      AndroidManifest.xml
    +      res/
    +          ... (resources for main application)
    +      src/
    +          ... (source code for main application) ...
    +      tests/
    +          AndroidManifest.xml
    +          res/
    +              ... (resources for tests)
    +          src/
    +              ... (source code for tests)
    +
    +

    The Testing API

    +

    + The Android testing API is based on the JUnit API and extended with a instrumentation + framework and Android-specific testing classes. +

    +

    JUnit

    +

    + You can use the JUnit {@link junit.framework.TestCase TestCase} class to do unit testing on + a plain Java object. TestCase is also the base class for + {@link android.test.AndroidTestCase}, which you can use to test Android-dependent objects. + Besides providing the JUnit framework, AndroidTestCase offers Android-specific setup, + teardown, and helper methods. +

    +

    + You use the JUnit {@link junit.framework.Assert} class to display test results. + The assert methods compare values you expect from a test to the actual results and + throw an exception if the comparison fails. Android also provides a class of assertions that + extend the possible types of comparisons, and another class of assertions for testing the UI. + These are described in more detail in the section + Assertion classes +

    +

    + To learn more about JUnit, you can read the documentation on the + junit.org home page. + Note that the Android testing API supports JUnit 3 code style, but not JUnit 4. Also, you must + use Android's instrumented test runner {@link android.test.InstrumentationTestRunner} to run + your test case classes. This test runner is described in the + section Running Tests. +

    +

    Instrumentation

    +

    + Android instrumentation is a set of control methods or "hooks" in the Android system. These hooks + control an Android component independently of its normal lifecycle. They also control how + Android loads applications. +

    +

    + Normally, an Android component runs in a lifecycle determined by the system. For example, an + Activity object's lifecycle starts when the Activity is activated by an Intent. The object's + onCreate() method is called, followed by onResume(). When the user + starts another application, the onPause() method is called. If the Activity + code calls the finish() method, the onDestroy() method is called. + The Android framework API does not provide a way for your code to invoke these callback + methods directly, but you can do so using instrumentation. +

    +

    + Also, the system runs all the components of an application into the same + process. You can allow some components, such as content providers, to run in a separate process, + but you can't force an application to run in the same process as another application that is + already running. +

    +

    + With Android instrumentation, though, you can invoke callback methods in your test code. + This allows you to run through the lifecycle of a component step by step, as if you were + debugging the component. The following test code snippet demonstrates how to use this to + test that an Activity saves and restores its state: +

    + +
    +    // Start the main activity of the application under test
    +    mActivity = getActivity();
     
    -

    This document is an overview of the Android testing environment and the way -you use it. The document assumes you have a basic knowledge of Android -application programming and JUnit testing methodology.

    - -

    Overview

    - -

    At the heart of the Android testing environment is an instrumentation -framework that your test application uses to precisely control the application -under test. With instrumentation, you can set up mock system objects such as -Contexts before the main application starts, control your application at various -points of its lifecycle, send UI events to the application, and examine the -application's state during its execution. The instrumentation framework -accomplishes this by running both the main application and the test application -in the same process.

    - -

    Your test application is linked to the application under test by means of an -<instrumentation> -element in the test application's manifest file. The attributes of the element -specify the package name of the application under test and also tell Android how -to run the test application. Instrumentation is described in more detail in the -section Instrumentation Test -Runner.

    - -

    The following diagram summarizes the Android testing environment:

    - - + // Get a handle to the Activity object's main UI widget, a Spinner + mSpinner = (Spinner)mActivity.findViewById(com.android.example.spinner.R.id.Spinner01); -

    In Android, test applications are themselves Android applications, so you -write them in much the same way as the application you are testing. The SDK -tools help you create a main application project and its test project at the same -time. You can run Android tests within Eclipse with ADT or from the command -line. Eclipse with ADT provides an extensive set of tools for creating tests, -running them, and viewing their results. You can also use the adb -tool to run tests, or use a built-in Ant target.

    + // Set the Spinner to a known position + mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION); -

    To learn how to set up and run tests in Eclipse, please refer to Testing in -Eclipse, with ADT. If you're not working in Eclipse, refer to Testing in Other -IDEs.

    + // Stop the activity - The onDestroy() method should save the state of the Spinner + mActivity.finish(); -

    If you want a step-by-step introduction to Android testing, try one of the -testing tutorials:

    + // Re-start the Activity - the onResume() method should restore the state of the Spinner + mActivity = getActivity(); -
      -
    • The Hello, -Testing tutorial introduces basic testing concepts and procedures in the -context of the Hello, World application.
    • -
    • The Activity -Testing tutorial is an excellent follow-up to the Hello, Testing tutorial. -It guides you through a more complex testing scenario that you develop against a -more realistic application.
    • -
    + // Get the Spinner's current position + int currentPosition = mActivity.getSpinnerPosition(); -

    The Testing API

    + // Assert that the current position is the same as the starting position + assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition); +

    - For writing tests and test applications in the Java programming language, Android provides a - testing API that is based in part on the JUnit test framework. Adding to that, Android includes - a powerful instrumentation framework that lets your tests access the state and runtime objects - of the application under tests. + The key method used here is + {@link android.test.ActivityInstrumentationTestCase2#getActivity()}, which is a + part of the instrumentation API. The Activity under test is not started until you call this + method. You can set up the test fixture in advance, and then call this method to start the + Activity.

    -

    The sections below describe the major components of the testing API available in Android.

    -

    JUnit test case classes

    - Some of the classes in the testing API extend the JUnit {@link junit.framework.TestCase TestCase} but do not use the instrumentation framework. These classes - contain methods for accessing system objects such as the Context of the application under test. With this Context, you can look at its resources, files, databases, - and so forth. The base class is {@link android.test.AndroidTestCase}, but you usually use a subclass associated with a particular component. + Also, instrumentation can load both a test package and the application under test into the + same process. Since the application components and their tests are in the same process, the + tests can invoke methods in the components, and modify and examine fields in the components. +

    +

    Test case classes

    - The subclasses are: + Android provides several test case classes that extend {@link junit.framework.TestCase} and + {@link junit.framework.Assert} with Android-specific setup, teardown, and helper methods.

    -
      -
    • - {@link android.test.ApplicationTestCase} - A class for testing an entire application. It allows you to inject a mock Context into the application, - set up initial test parameters before the application starts, and examine the application after it finishes but before it is destroyed. -
    • -
    • - {@link android.test.ProviderTestCase2} - A class for isolated testing of a single {@link android.content.ContentProvider}. Since it is restricted to using a - {@link android.test.mock.MockContentResolver} for the provider, and it injects an {@link android.test.IsolatedContext}, your provider testing is isolated - from the rest of the OS. -
    • -
    • - {@link android.test.ServiceTestCase} - a class for isolated testing of a single {@link android.app.Service}. You can inject a mock Context or - mock Application (or both), or let Android provide you a full Context and a {@link android.test.mock.MockApplication}. -
    • -
    -

    Instrumentation test case classes

    +

    AndroidTestCase

    - The API for testing activities extends the JUnit {@link junit.framework.TestCase TestCase} class and also uses the instrumentation framework. With instrumentation, - Android can automate UI testing by sending events to the application under test, precisely control the start of an activity, and monitor the state of the - activity during its life cycle. + A useful general test case class, especially if you are + just starting out with Android testing, is {@link android.test.AndroidTestCase}. It extends + both {@link junit.framework.TestCase} and {@link junit.framework.Assert}. It provides the + JUnit-standard setUp() and tearDown() methods, as well as well as + all of JUnit's Assert methods. In addition, it provides methods for testing permissions, and a + method that guards against memory leaks by clearing out certain class references.

    +

    Component-specific test cases

    - The base class is {@link android.test.InstrumentationTestCase}. All of its subclasses have the ability to send a keystroke or touch event to the UI of the application - under test. The subclasses can also inject a mock Intent. - The subclasses are: + A key feature of the Android testing framework is its component-specific test case classes. + These address specific component testing needs with methods for fixture setup and + teardown and component lifecycle control. They also provide methods for setting up mock objects. + These classes are described in the component-specific testing topics:

    -
      +
      • - {@link android.test.ActivityTestCase} - A base class for activity test classes. + Activity Testing
      • - {@link android.test.SingleLaunchActivityTestCase} - A convenience class for testing a single activity. - It invokes {@link junit.framework.TestCase#setUp() setUp()} and {@link junit.framework.TestCase#tearDown() tearDown()} only - once, instead of once per method call. Use it when all of your test methods run against the same activity. + + Content Provider Testing
      • - {@link android.test.SyncBaseInstrumentation} - A class that tests synchronization of a content provider. It uses instrumentation to cancel and disable - existing synchronizations before starting the test synchronization. + Service Testing
      • +
      +

      + Android does not provide a separate test case class for BroadcastReceiver. Instead, test a + BroadcastReceiver by testing the component that sends it Intent objects, to verify that the + BroadcastReceiver responds correctly. +

      +

      ApplicationTestCase

      +

      + You use the {@link android.test.ApplicationTestCase} test case class to test the setup and + teardown of {@link android.app.Application} objects. These objects maintain the global state of + information that applies to all the components in an application package. The test case can + be useful in verifying that the <application> element in the manifest file is correctly + set up. Note, however, that this test case does not allow you to control testing of the + components within your application package. +

      +

      InstrumentationTestCase

      +

      + If you want to use instrumentation methods in a test case class, you must use + {@link android.test.InstrumentationTestCase} or one of its subclasses. The + {@link android.app.Activity} test cases extend this base class with other functionality that + assists in Activity testing. +

      + +

      Assertion classes

      +

      + Because Android test case classes extend JUnit, you can use assertion methods to display the + results of tests. An assertion method compares an actual value returned by a test to an + expected value, and throws an AssertionException if the comparison test fails. Using assertions + is more convenient than doing logging, and provides better test performance. +

      +

      + Besides the JUnit {@link junit.framework.Assert} class methods, the testing API also provides + the {@link android.test.MoreAsserts} and {@link android.test.ViewAsserts} classes: +

      +
      • - {@link android.test.ActivityUnitTestCase} - This class does an isolated test of a single activity. With it, you can inject a mock context or application, or both. - It is intended for doing unit tests of an activity, and is the activity equivalent of the test classes described in JUnit test case classes. -

        Unlike the other instrumentation classes, this test class cannot inject a mock Intent.

        + {@link android.test.MoreAsserts} contains more powerful assertions such as + {@link android.test.MoreAsserts#assertContainsRegex}, which does regular expression + matching.
      • - {@link android.test.ActivityInstrumentationTestCase2} - This class tests a single activity within the normal system environment. - You cannot inject a mock Context, but you can inject mock Intents. Also, you can run a test method on the UI thread (the main thread of the application under test), - which allows you to send key and touch events to the application UI. + {@link android.test.ViewAsserts} contains useful assertions about Views. For example + it contains {@link android.test.ViewAsserts#assertHasScreenCoordinates} that tests if a View + has a particular X and Y position on the visible screen. These asserts simplify testing of + geometry and alignment in the UI.
      • -
      -

      Assert classes

      -

      - Android also extends the JUnit {@link junit.framework.Assert} class that is the basis of assert() calls in tests. - There are two extensions to this class, {@link android.test.MoreAsserts} and {@link android.test.ViewAsserts}: -

      -
        -
      • - The MoreAsserts class contains more powerful assertions such as {@link android.test.MoreAsserts#assertContainsRegex} that does regular expression matching. -
      • -
      • - The {@link android.test.ViewAsserts} class contains useful assertions about Android Views, such as {@link android.test.ViewAsserts#assertHasScreenCoordinates} that tests if a View has a particular X and Y - position on the visible screen. These asserts simplify testing of geometry and alignment in the UI. -
      -

      Mock object classes

      -

      - Android has convenience classes for creating mock system objects such as applications, contexts, content resolvers, and resources. Android also provides - methods in some test classes for creating mock Intents. Use these mocks to facilitate dependency injection, since they are easier to use than creating their - real counterparts. These convenience classes are found in {@link android.test} and {@link android.test.mock}. They are: -

      -
        -
      • - {@link android.test.IsolatedContext} - Mocks a Context so that the application using it runs in isolation. - At the same time, it has enough stub code to satisfy OS code that tries to communicate with contexts. This class is useful in unit testing. -
      • -
      • - {@link android.test.RenamingDelegatingContext} - Delegates most context functions to an existing, normal context while changing the default file and database - names in the context. Use this to test file and database operations with a normal system context, using test names. -
      • -
      • - {@link android.test.mock.MockApplication}, {@link android.test.mock.MockContentResolver}, {@link android.test.mock.MockContext}, - {@link android.test.mock.MockDialogInterface}, {@link android.test.mock.MockPackageManager}, - {@link android.test.mock.MockResources} - Classes that create mock Android system objects for use in testing. They expose only those methods that are - useful in managing the object. The default implementations of these methods simply throw an Exception. You are expected to extend the classes and - override any methods that are called by the application under test. -
      • -
      -

      Instrumentation Test Runner

      +

      Mock object classes

      - Android provides a custom class for running tests with instrumentation called called - {@link android.test.InstrumentationTestRunner}. This class - controls of the application under test, runs the test application and the main application in the same process, and routes - test output to the appropriate place. Using instrumentation is key to the ability of InstrumentationTestRunner to control the entire test - environment at runtime. Notice that you use this test runner even if your test class does not itself use instrumentation. + To facilitate dependency injection in testing, Android provides classes that create mock system + objects such as {@link android.content.Context} objects, + {@link android.content.ContentProvider} objects, {@link android.content.ContentResolver} + objects, and {@link android.app.Service} objects. Some test cases also provide mock + {@link android.content.Intent} objects. You use these mocks both to isolate tests + from the rest of the system and to facilitate dependency injection for testing. These classes + are found in the Java packages {@link android.test} and {@link android.test.mock}.

      - When you run a test application, you first run a system utility called Activity Manager. Activity Manager uses the instrumentation framework to start and control the test runner, which in turn uses instrumentation to shut down any running instances - of the main application, starts the test application, and then starts the main application in the same process. This allows various aspects of the test application to work directly with the main application. + Mock objects isolate tests from a running system by stubbing out or overriding + normal operations. For example, a {@link android.test.mock.MockContentResolver} + replaces the normal resolver framework with its own local framework, which is isolated + from the rest of the system. MockContentResolver also also stubs out the + {@link android.content.ContentResolver#notifyChange(Uri, ContentObserver, boolean)} method + so that observer objects outside the test environment are not accidentally triggered.

      - If you are developing in Eclipse, the ADT plugin assists you in the setup of InstrumentationTestRunner or other test runners. - The plugin UI prompts you to specify the test runner class to use, as well as the package name of the application under test. - The plugin then adds an <instrumentation> element with appropriate attributes to the manifest file of the test application. - Eclipse with ADT automatically starts a test application under the control of Activity Manager using instrumentation, - and redirects the test output to the Eclipse window's JUnit view. + Mock object classes also facilitate dependency injection by providing a subclass of the + normal object that is non-functional except for overrides you define. For example, the + {@link android.test.mock.MockResources} object provides a subclass of + {@link android.content.res.Resources} in which all the methods throw Exceptions when called. + To use it, you override only those methods that must provide information.

      - If you prefer working from the command line, you can use Ant and the android - tool to help you set up your test projects. To run tests with instrumentation, you can access the - Activity Manager through the Android Debug - Bridge (adb) tool and the output is directed to STDOUT. + These are the mock object classes available in Android:

      -

      Working in the Test Environment

      +

      Simple mock object classes

      - The tests for an Android application are contained in a test application, which itself is an Android application. A test application resides in a separate Android project that has the - same files and directories as a regular Android application. The test project is linked to the project of the application it tests - (known as the application under test) by its manifest file. + {@link android.test.mock.MockApplication}, {@link android.test.mock.MockContext}, + {@link android.test.mock.MockContentProvider}, {@link android.test.mock.MockCursor}, + {@link android.test.mock.MockDialogInterface}, {@link android.test.mock.MockPackageManager}, and + {@link android.test.mock.MockResources} provide a simple and useful mock strategy. They are + stubbed-out versions of the corresponding system object class, and all of their methods throw an + {@link java.lang.UnsupportedOperationException} exception if called. To use them, you override + the methods you need in order to provide mock dependencies.

      +

      Note: + {@link android.test.mock.MockContentProvider} + and {@link android.test.mock.MockCursor} are new as of API level 8. +

      +

      Resolver mock objects

      - Each test application contains one or more test case classes based on an Android class for a - particular type of component. The test case class contains methods that define tests on some part of the application under test. When you run the test application, Android - starts it, loads the application under test into the same process, and then invokes each method in the test case class. + {@link android.test.mock.MockContentResolver} provides isolated testing of content providers by + masking out the normal system resolver framework. Instead of looking in the system to find a + content provider given an authority string, MockContentResolver uses its own internal table. You + must explicitly add providers to this table using + {@link android.test.mock.MockContentResolver#addProvider(String,ContentProvider)}.

      - The tools and procedures you use with testing depend on the development environment you are using. If you use Eclipse, then the ADT plug in for Eclipse provides tools that - allow you to develop and run tests entirely within Eclipse. This is documented in the topic Testing in Eclipse, with ADT. - If you use another development environment, then you use Android's command-line tools, as documented in the topic Testing in Other IDEs. + With this feature, you can associate a mock content provider with an authority. You can create + an instance of a real provider but use test data in it. You can even set the provider for an + authority to null. In effect, a MockContentResolver object isolates your test + from providers that contain real data. You can control the + function of the provider, and you can prevent your test from affecting real data.

      -

      Working with test projects

      +

      Contexts for testing

      - To start testing an Android application, you create a test project for it using Android tools. The tools create the project directory and the files and subdirectories needed. - The tools also create a manifest file that links the application in the test project to the application under test. The procedure for creating a test project in Eclipse with - ADT is documented in Testing in Eclipse, with ADT. The procedure for creating a test project for use with development - tools other than Eclipse is documented in Testing in Other IDEs. + Android provides two Context classes that are useful for testing:

      -

      Working with test case classes

      +
        +
      • + {@link android.test.IsolatedContext} provides an isolated {@link android.content.Context}, + File, directory, and database operations that use this Context take place in a test area. + Though its functionality is limited, this Context has enough stub code to respond to + system calls. +

        + This class allows you to test an application's data operations without affecting real + data that may be present on the device. +

        +
      • +
      • + {@link android.test.RenamingDelegatingContext} provides a Context in which + most functions are handled by an existing {@link android.content.Context}, but + file and database operations are handled by a {@link android.test.IsolatedContext}. + The isolated part uses a test directory and creates special file and directory names. + You can control the naming yourself, or let the constructor determine it automatically. +

        + This object provides a quick way to set up an isolated area for data operations, + while keeping normal functionality for all other Context operations. +

        +
      • +
      +

      Running Tests

      - A test application contains one or more test case classes that extend an Android test case class. You choose a test case class based on the type of Android component you are testing and the - tests you are doing. A test application can test different components, but each test case class is designed to test a single type of component. - The Android test case classes are described in the section The Testing API. + Test cases are run by a test runner class that loads the test case class, set ups, + runs, and tears down each test. An Android test runner must also be instrumented, so that + the system utility for starting applications can control how the test package + loads test cases and the application under test. You tell the Android platform + which instrumented test runner to use by setting a value in the test package's manifest file.

      - Some Android components have more than one associated test case class. In this case, you choose among the available classes based on the type of tests you want to do. For activities, - for example, you have the choice of either {@link android.test.ActivityInstrumentationTestCase2} or {@link android.test.ActivityUnitTestCase}. + {@link android.test.InstrumentationTestRunner} is the primary Android test runner class. It + extends the JUnit test runner framework and is also instrumented. It can run any of the test + case classes provided by Android and supports all possible types of testing. +

      - ActivityInstrumentationTestCase2 is designed to do functional testing, so it tests activities in a normal system infrastructure. You can inject mocked Intents, but not - mocked Contexts. In general, you can't mock dependencies for the activity under test. + You specify InstrumentationTestRunner or a subclass in your test package's + manifest file, in the + instrumentation element. Also, InstrumentationTestRunner code resides + in the shared library android.test.runner, which is not normally linked to + Android code. To include it, you must specify it in a + uses-library element. + You do not have to set up these elements yourself. Both Eclipse with ADT and the + android command-line tool construct them automatically and add them to your + test package's manifest file. +

      +

      + Note: If you use a test runner other than + InstrumentationTestRunner, you must change the <instrumentation> + element to point to the class you want to use.

      - In comparison, ActivityUnitTestCase is designed for unit testing, so it tests activities in an isolated system infrastructure. You can inject mocked or wrappered dependencies for - the activity under test, particularly mocked Contexts. On the other hand, when you use this test case class the activity under test runs in isolation and can't interact with other activities. + To run {@link android.test.InstrumentationTestRunner}, you use internal system classes called by + Android tools. When you run a test in Eclipse with ADT, the classes are called automatically. + When you run a test from the command line, you run these classes with + Android Debug Bridge (adb).

      - As a rule of thumb, if you wanted to test an activity's interaction with the rest of Android, you would use ActivityInstrumentationTestCase2. If you wanted to do regression testing - on an activity, you would use ActivityUnitTestCase. + The system classes load and start the test package, kill any processes that + are running an instance of the application under test, and then load a new instance of the + application under test. They then pass control to + {@link android.test.InstrumentationTestRunner}, which runs + each test case class in the test package. You can also control which test cases and + methods are run using settings in Eclipse with ADT, or using flags with the command-line tools.

      -

      Working with test methods

      - Each test case class provides methods that you use to set up the test environment and control the application under test. For example, all test case classes provide the JUnit {@link junit.framework.TestCase#setUp() setUp()} - method that you can override to set up fixtures. In addition, you add methods to the class to define individual tests. Each method you add is run once each time you run the test application. If you override the setUp() - method, it runs before each of your methods. Similarly, the JUnit {@link junit.framework.TestCase#tearDown() tearDown()} method is run once after each of your methods. + Neither the system classes nor {@link android.test.InstrumentationTestRunner} run + the application under test. Instead, the test case does this directly. It either calls methods + in the application under test, or it calls its own methods that trigger lifecycle events in + the application under test. The application is under the complete control of the test case, + which allows it to set up the test environment (the test fixture) before running a test. This + is demonstrated in the previous code snippet that tests an + Activity that displays a Spinner widget.

      - The test case classes give you substantial control over starting and stopping components. For this reason, you have to specifically tell Android to start a component before you run tests against it. For example, you use the - {@link android.test.ActivityInstrumentationTestCase2#getActivity()} method to start the activity under test. You can call this method once during the entire test case, or once for each test method. You can even destroy the - activity under test by calling its {@link android.app.Activity#finish()} method and then restart it with getActivity() within a single test method. + To learn more about running tests, please read the topics + + Testing in Eclipse, with ADT or + + Testing in Other IDes.

      -

      Running tests and seeing the results

      +

      Seeing Test Results

      - To run your tests, you build your test project and then run the test application using the system utility Activity Manager with instrumentation. You provide to Activity Manager the name of the test runner (usually - {@link android.test.InstrumentationTestRunner}) you specified for your application; the name includes both your test application's package name and the test runner class name. Activity Manager loads and starts your - test application, kills any instances of the application under test, loads an instance of the application under test into the same process as the test application, and then passes control to the first test case - class in your test application. The test runner then takes control of the tests, running each of your test methods against the application under test until all the methods in all the classes have been run. + The Android testing framework returns test results back to the tool that started the test. + If you run a test in Eclipse with ADT, the results are displayed in a new JUnit view pane. If + you run a test from the command line, the results are displayed in STDOUT. In + both cases, you see a test summary that displays the name of each test case and method that + was run. You also see all the assertion failures that occurred. These include pointers to the + line in the test code where the failure occurred. Assertion failures also list the expected + value and actual value.

      - If you run a test within Eclipse with ADT, the output appears in a new JUnit view pane. If you run a test from the command line, the output goes to STDOUT. + The test results have a format that is specific to the IDE that you are using. The test + results format for Eclipse with ADT is described in + + Testing in Eclipse, with ADT. The test results format for tests run from the + command line is described in + + Testing in Other IDEs.

      -

      What to Test

      +

      Monkey and MonkeyRunner

      - In addition to the functional areas you would normally test, here are some areas - of Android application testing that you should consider: + The SDK provides two tools for functional-level application testing:

      -
        -
      • - Activity lifecycle events: You should test that your activities handle lifecycle events correctly. For example - an activity should respond to pause or destroy events by saving its state. Remember that even a change in screen orientation - causes the current activity to be destroyed, so you should test that accidental device movements don't accidentally lose the - application state. -
      • -
      • - Database operations: You should ensure that database operations correctly handle changes to the application's state. - To do this, use mock objects from the package {@link android.test.mock android.test.mock}. -
      • -
      • - Screen sizes and resolutions: Before you publish your application, make sure to test it on all of the - screen sizes and densities on which you want it to run. You can test the application on multiple sizes and densities using - AVDs, or you can test your application directly on the devices that you are targeting. For more information, see - the topic Supporting Multiple Screens. -
      • -
      +
        +
      • + Monkey is a command-line + tool that sends pseudo-random streams of keystrokes, touches, and gestures to a + device. You run it with the + Android Debug Bridge (adb) tool. You use it to stress-test your application and + report back errors that are encountered. You can repeat a stream of events by + running the tool each time with the same random number seed. +
      • +
      • + MonkeyRunner is a + Jython API that you use in test programs written in Python. The API includes functions + for connecting to a device, installing and uninstalling packages, taking screenshots, + comparing two images, and running a test package against an application. Using the API + with Python, you can write a wide range of large, powerful, and complex tests. +
      • +
      +

      Working With Package names

      - When possible, you should run these tests on an actual device. If this is not possible, you can - use the Android Emulator with - Android Virtual Devices configured for - the hardware, screens, and versions you want to test. + In the test environment, you work with both Android application package names and + Java package identifiers. Both use the same naming format, but they represent substantially + different entities. You need to know the difference to set up your tests correctly.

      -

      Appendix: UI Testing Notes

      - The following sections have tips for testing the UI of your Android application, specifically - to help you handle actions that run in the UI thread, touch screen and keyboard events, and home - screen unlock during testing. + An Android package name is a unique system name for a .apk file, set by the + "android:package" attribute of the <manifest> element in the package's + manifest. The Android package name of your test package must be different from the + Android package name of the application under test. By default, Android tools create the + test package name by appending ".test" to the package name of the application under test.

      -

      Testing on the UI thread

      - An application's activities run on the application's UI thread. Once the - UI is instantiated, for example in the activity's onCreate() method, then all - interactions with the UI must run in the UI thread. When you run the application normally, it - has access to the thread and does not have to do anything special. + The test package also uses an Android package name to target the application package it + tests. This is set in the "android:targetPackage" attribute of the + <instrumentation> element in the test package's manifest.

      - This changes when you run tests against the application. With instrumentation-based classes, - you can invoke methods against the UI of the application under test. The other test classes don't allow this. - To run an entire test method on the UI thread, you can annotate the thread with @UIThreadTest. - Notice that this will run all of the method statements on the UI thread. Methods that do not interact with the UI - are not allowed; for example, you can't invoke Instrumentation.waitForIdleSync(). + A Java package identifier applies to a source file. This package name reflects the directory + path of the source file. It also affects the visibility of classes and members to each other.

      - To run a subset of a test method on the UI thread, create an anonymous class of type - Runnable, put the statements you want in the run() method, and instantiate a new - instance of the class as a parameter to the method appActivity.runOnUiThread(), where - appActivity is the instance of the app you are testing. + Android tools that create test projects set up an Android test package name for you. + From your input, the tools set up the test package name and the target package name for the + application under test. For these tools to work, the application project must already exist.

      - For example, this code instantiates an activity to test, requests focus (a UI action) for the Spinner displayed - by the activity, and then sends a key to it. Notice that the calls to waitForIdleSync and sendKeys - aren't allowed to run on the UI thread:

      -
      -  private MyActivity mActivity; // MyActivity is the class name of the app under test
      -  private Spinner mSpinner;
      -
      -  ...
      -
      -  protected void setUp() throws Exception {
      -      super.setUp();
      -      mInstrumentation = getInstrumentation();
      -
      -      mActivity = getActivity(); // get a references to the app under test
      -
      -      /*
      -       * Get a reference to the main widget of the app under test, a Spinner
      -       */
      -      mSpinner = (Spinner) mActivity.findViewById(com.android.demo.myactivity.R.id.Spinner01);
      -
      -  ...
      -
      -  public void aTest() {
      -      /*
      -       * request focus for the Spinner, so that the test can send key events to it
      -       * This request must be run on the UI thread. To do this, use the runOnUiThread method
      -       * and pass it a Runnable that contains a call to requestFocus on the Spinner.
      -       */
      -      mActivity.runOnUiThread(new Runnable() {
      -          public void run() {
      -              mSpinner.requestFocus();
      -          }
      -      });
      -
      -      mInstrumentation.waitForIdleSync();
      -
      -      this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
      -
      - -

      Turning off touch mode

      + By default, these tools set the Java package identifier for the test class to be the same + as the Android package identifier. You may want to change this if you want to expose + members in the application under test by giving them package visibility. If you do this, + change only the Java package identifier, not the Android package names, and change only the + test case source files. Do not change the Java package name of the generated + R.java class in your test package, because it will then conflict with the + R.java class in the application under test. Do not change the Android package name + of your test package to be the same as the application it tests, because then their names + will no longer be unique in the system. +

      +

      What to Test

      - To control the emulator or a device with key events you send from your tests, you must turn off - touch mode. If you do not do this, the key events are ignored. + The topic What To Test + describes the key functionality you should test in an Android application, and the key + situations that might affect that functionality.

      - To turn off touch mode, you invoke ActivityInstrumentationTestCase2.setActivityTouchMode(false) - before you call getActivity() to start the activity. You must invoke the method in a test method - that is not running on the UI thread. For this reason, you can't invoke the touch mode method - from a test method that is annotated with @UIThread. Instead, invoke the touch mode method from setUp(). + Most unit testing is specific to the Android component you are testing. + The topics Activity Testing, + + Content Provider Testing, and + Service Testing each have a section entitled "What To Test" that lists possible testing + areas.

      -

      Unlocking the emulator or device

      - You may find that UI tests don't work if the emulator's or device's home screen is disabled with the keyguard pattern. - This is because the application under test can't receive key events sent by sendKeys(). The best - way to avoid this is to start your emulator or device first and then disable the keyguard for the home screen. + When possible, you should run these tests on an actual device. If this is not possible, you can + use the Android Emulator with + Android Virtual Devices configured for + the hardware, screens, and versions you want to test.

      +

      Next Steps

      - You can also explicitly disable the keyguard. To do this, - you need to add a permission in the manifest file (AndroidManifest.xml) and - then disable the keyguard in your application under test. Note, though, that you either have to remove this before - you publish your application, or you have to disable it programmatically in the published app. + To learn how to set up and run tests in Eclipse, please refer to Testing in + Eclipse, with ADT. If you're not working in Eclipse, refer to Testing in Other + IDEs.

      - To add the the permission, add the element <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/> - as a child of the <manifest> element. To disable the KeyGuard, add the following code - to the onCreate() method of activities you intend to test: + If you want a step-by-step introduction to Android testing, try one of the + testing tutorials or sample test packages:

      -
      -  mKeyGuardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
      -  mLock = mKeyGuardManager.newKeyguardLock("activity_classname");
      -  mLock.disableKeyguard();
      -
      -

      where activity_classname is the class name of the activity.

      -

      Troubleshooting UI tests

      -

      - This section lists some of the common test failures you may encounter in UI testing, and their causes: -

      -
      -
      WrongThreadException
      -
      -

      Problem:

      - For a failed test, the Failure Trace contains the following error message: - - android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. - -

      Probable Cause:

      - This error is common if you tried to send UI events to the UI thread from outside the UI thread. This commonly happens if you send UI events - from the test application, but you don't use the @UIThread annotation or the runOnUiThread() method. The test method tried to interact with the UI outside the UI thread. -

      Suggested Resolution:

      - Run the interaction on the UI thread. Use a test class that provides instrumentation. See the previous section Testing on the UI Thread - for more details. -
      -
      java.lang.RuntimeException
      -
      -

      Problem:

      - For a failed test, the Failure Trace contains the following error message: - - java.lang.RuntimeException: This method can not be called from the main application thread - -

      Probable Cause:

      - This error is common if your test method is annotated with @UiThreadTest but then tries to - do something outside the UI thread or tries to invoke runOnUiThread(). -

      Suggested Resolution:

      - Remove the @UiThreadTest annotation, remove the runOnUiThread() call, or re-factor your tests. -
      -
      +
        +
      • + The Hello, + Testing tutorial introduces basic testing concepts and procedures in the + context of the Hello, World application. +
      • +
      • + The Activity + Testing tutorial is an excellent follow-up to the Hello, Testing tutorial. + It guides you through a more complex testing scenario that you develop against a + more realistic application. +
      • +
      • + The sample test package + Note Pad Test is an example of + testing a {@link android.content.ContentProvider}. It contains a set of unit tests for the + Note Pad sample application's {@link android.content.ContentProvider}. +
      • +
      • + The sample test package + Alarm Service Test is an example of testing a {@link android.app.Service}. It contains + a set of unit tests for the Alarm Service sample application's {@link android.app.Service}. +
      • +
      diff --git a/docs/html/guide/topics/testing/what_to_test.jd b/docs/html/guide/topics/testing/what_to_test.jd new file mode 100644 index 0000000000000000000000000000000000000000..e13538ad139d8b373aa586e47c0c6e892fb79e87 --- /dev/null +++ b/docs/html/guide/topics/testing/what_to_test.jd @@ -0,0 +1,84 @@ +page.title=What To Test +@jd:body +

      + As you develop Android applications, knowing what to test is as important as knowing how to + test. This document lists some most common Android-related situations that you should consider + when you test, even at the unit test level. This is not an exhaustive list, and you consult the + documentation for the features that you use for more ideas. The + android-developers Google Groups + site is another resource for information about testing. +

      +

      Ideas for Testing

      +

      + The following sections are organized by behaviors or situations that you should test. Each + section contains a scenario that further illustrates the situation and the test or tests you + should do. +

      +

      Change in orientation

      +

      + For devices that support multiple orientations, Android detects a change in orientation when + the user turns the device so that the display is "landscape" (long edge is horizontal) instead + of "portrait" (long edge is vertical). +

      +

      + When Android detects a change in orientation, its default behavior is to destroy and then + re-start the foreground Activity. You should consider testing the following: +

      +
        +
      • + Is the screen re-drawn correctly? Any custom UI code you have should handle changes in the + orientation. +
      • +
      • + Does the application maintain its state? The Activity should not lose anything that the + user has already entered into the UI. The application should not "forget" its place in the + current transaction. +
      • +
      +

      Change in configuration

      +

      + A situation that is more general than a change in orientation is a change in the device's + configuration, such as a change in the availability of a keyboard or a change in system + language. +

      +

      + A change in configuration also triggers the default behavior of destroying and then restarting + the foreground Activity. Besides testing that the application maintains the UI and its + transaction state, you should also test that the application updates itself to respond + correctly to the new configuration. +

      +

      Battery life

      +

      + Mobile devices primarily run on battery power. A device has finite "battery budget", and when it + is gone, the device is useless until it is recharged. You need to write your application to + minimize battery usage, you need to test its battery performance, and you need to test the + methods that manage battery usage. +

      +

      + Techniques for minimizing battery usage were presented at the 2010 Google I/O conference in the + presentation + + Coding for Life -- Battery Life, That Is. This presentation describes the impact on battery + life of various operations, and the ways you can design your application to minimize these + impacts. When you code your application to reduce battery usage, you also write the + appropriate unit tests. +

      +

      Dependence on external resources

      +

      + If your application depends on network access, SMS, Bluetooth, or GPS, then you should + test what happens when the resource or resources are not available. +

      +

      + For example, if your application uses the network,it can notify the user if access is + unavailable, or disable network-related features, or do both. For GPS, it can switch to + IP-based location awareness. It can also wait for WiFi access before doing large data transfers, + since WiFi transfers maximize battery usage compared to transfers over 3G or EDGE. +

      +

      + You can use the emulator to test network access and bandwidth. To learn more, please see + Network Speed Emulation. + To test GPS, you can use the emulator console and {@link android.location.LocationManager}. To + learn more about the emulator console, please see + + Using the Emulator Console. +

      diff --git a/docs/html/guide/topics/ui/dialogs.jd b/docs/html/guide/topics/ui/dialogs.jd index 74b544bb985a5f80168d335e3a2ddfc67583a66f..f47a709bc681effdd7344a83cef8730d682b4e23 100644 --- a/docs/html/guide/topics/ui/dialogs.jd +++ b/docs/html/guide/topics/ui/dialogs.jd @@ -472,18 +472,25 @@ public class NotificationTest extends Activity { progressDialog = new ProgressDialog(NotificationTest.this); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setMessage("Loading..."); - progressThread = new ProgressThread(handler); - progressThread.start(); return progressDialog; default: return null; } } + @Override + protected void onPrepareDialog(int id, Dialog dialog) { + switch(id) { + case PROGRESS_DIALOG: + progressDialog.setProgress(0); + progressThread = new ProgressThread(handler); + progressThread.start(); + } + // Define the Handler that receives messages from the thread and update the progress final Handler handler = new Handler() { public void handleMessage(Message msg) { - int total = msg.getData().getInt("total"); + int total = msg.arg1; progressDialog.setProgress(total); if (total >= 100){ dismissDialog(PROGRESS_DIALOG); @@ -514,9 +521,7 @@ public class NotificationTest extends Activity { Log.e("ERROR", "Thread Interrupted"); } Message msg = mHandler.obtainMessage(); - Bundle b = new Bundle(); - b.putInt("total", total); - msg.setData(b); + msg.arg1 = total; mHandler.sendMessage(msg); total++; } diff --git a/docs/html/images/testing/android_test_framework.png b/docs/html/images/testing/android_test_framework.png index 6f80530021d749d43369c02201663142e218da43..459975c06c124072a6c76d7f13561621670d021f 100755 Binary files a/docs/html/images/testing/android_test_framework.png and b/docs/html/images/testing/android_test_framework.png differ diff --git a/docs/html/images/testing/eclipse_test_results.png b/docs/html/images/testing/eclipse_test_results.png new file mode 100644 index 0000000000000000000000000000000000000000..105e149e8af9e47fcd8b1cb830738fe978daf946 Binary files /dev/null and b/docs/html/images/testing/eclipse_test_results.png differ diff --git a/docs/html/images/testing/eclipse_test_run_failure.png b/docs/html/images/testing/eclipse_test_run_failure.png new file mode 100644 index 0000000000000000000000000000000000000000..81111273184e439e6e1accd25c55fa112101ed72 Binary files /dev/null and b/docs/html/images/testing/eclipse_test_run_failure.png differ diff --git a/docs/html/images/testing/test_framework.png b/docs/html/images/testing/test_framework.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc5fc2ad42b09229254f33fda18f99c97f0b32d Binary files /dev/null and b/docs/html/images/testing/test_framework.png differ diff --git a/docs/html/intl/ja/community/index.jd b/docs/html/intl/ja/community/index.jd index 659aee7d68f12939caf16d4b20ceb9bbb6605fa6..490b23ff5d78e509bf600112091a75961d6b2e22 100644 --- a/docs/html/intl/ja/community/index.jd +++ b/docs/html/intl/ja/community/index.jd @@ -4,9 +4,9 @@ page.title=コミュニティ

      コミュニティ

      -

      Android デベロッパー コミュニティã¸ã‚ˆã†ã“ã。コミュニティã§ã®ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã«ãœã²å‚加ã—ã¦ãã ã•ã„。投稿ã™ã‚‹å‰ã«ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„るグループã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。

      +

      Android デベロッパー コミュニティã¸ã‚ˆã†ã“ã。コミュニティã§ã®ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã«ãœã²å‚加ã—ã¦ãã ã•ã„。投稿ã™ã‚‹å‰ã«ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„るグループã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。

      -

      注: Android ソース コード(アプリケーション開発ã§ã¯ãªã)ã«é–¢ã™ã‚‹ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã¯ã€ã‚ªãƒ¼ãƒ—ンソース プロジェクトã®ãƒ¡ãƒ¼ãƒªãƒ³ã‚° リスト(英語)をå‚ç…§ã—ã¦ãã ã•ã„。

      +

      注: Android ソース コード(アプリケーション開発ã§ã¯ãªã)ã«é–¢ã™ã‚‹ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã¯ã€ã‚ªãƒ¼ãƒ—ンソース プロジェクトã®ãƒ¡ãƒ¼ãƒªãƒ³ã‚° リスト(英語)をå‚ç…§ã—ã¦ãã ã•ã„。

      目次

        @@ -28,7 +28,7 @@ page.title=コミュニティ

        質å•ã¸ã®ç­”ãˆãŒè¦‹ã¤ã‹ã‚‰ãªã„å ´åˆã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã§è³ªå•ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚投稿ã™ã‚‹éš›ã¯ã€æ¬¡ã®æ‰‹é †ã«å¾“ã£ã¦ãã ã•ã„。

          -
        1. コミュニティ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„ã‚‹Android メーリングリストã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。 +
        2. コミュニティ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„ã‚‹Android メーリングリストã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。
        3. 質å•ã«æœ€é©ãªãƒ¡ãƒ¼ãƒªãƒ³ã‚° ãƒªã‚¹ãƒˆã‚’é¸æŠžã—ã¦ãã ã•ã„。後述ã™ã‚‹ã‚ˆã†ã«ã€ãƒ‡ãƒ™ãƒ­ãƒƒãƒ‘ーå‘ã‘ã®ãƒ¡ãƒ¼ãƒªãƒ³ã‚° リストã¯ä½•種類ã‹ã«åˆ†ã‹ã‚Œã¦ã„ã¾ã™ã€‚
        4. diff --git a/docs/html/intl/ja/resources/community-groups.jd b/docs/html/intl/ja/resources/community-groups.jd index c99b1f8cd623ad9f5a93ea5fb09594b52bad3e9f..ecedde1c52429993eee19808c1c9601769597377 100644 --- a/docs/html/intl/ja/resources/community-groups.jd +++ b/docs/html/intl/ja/resources/community-groups.jd @@ -4,9 +4,9 @@ page.title=コミュニティ

          コミュニティ

          -

          Android デベロッパー コミュニティã¸ã‚ˆã†ã“ã。コミュニティã§ã®ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã«ãœã²å‚加ã—ã¦ãã ã•ã„。投稿ã™ã‚‹å‰ã«ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„るグループã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。

          +

          Android デベロッパー コミュニティã¸ã‚ˆã†ã“ã。コミュニティã§ã®ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã«ãœã²å‚加ã—ã¦ãã ã•ã„。投稿ã™ã‚‹å‰ã«ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„るグループã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。

          -

          注: Android ソース コード(アプリケーション開発ã§ã¯ãªã)ã«é–¢ã™ã‚‹ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã¯ã€ã‚ªãƒ¼ãƒ—ンソース プロジェクトã®ãƒ¡ãƒ¼ãƒªãƒ³ã‚° リスト(英語)をå‚ç…§ã—ã¦ãã ã•ã„。

          +

          注: Android ソース コード(アプリケーション開発ã§ã¯ãªã)ã«é–¢ã™ã‚‹ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã¯ã€ã‚ªãƒ¼ãƒ—ンソース プロジェクトã®ãƒ¡ãƒ¼ãƒªãƒ³ã‚° リスト(英語)をå‚ç…§ã—ã¦ãã ã•ã„。

          目次

            @@ -28,7 +28,7 @@ page.title=コミュニティ

            質å•ã¸ã®ç­”ãˆãŒè¦‹ã¤ã‹ã‚‰ãªã„å ´åˆã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã§è³ªå•ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚投稿ã™ã‚‹éš›ã¯ã€æ¬¡ã®æ‰‹é †ã«å¾“ã£ã¦ãã ã•ã„。

              -
            1. コミュニティ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„ã‚‹Android メーリングリストã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。 +
            2. コミュニティ ガイドラインãŒè¨˜è¼‰ã•れã¦ã„ã‚‹Android メーリングリストã®è¶£æ„ã‚’ãŠèª­ã¿ãã ã•ã„。
            3. 質å•ã«æœ€é©ãªãƒ¡ãƒ¼ãƒªãƒ³ã‚° ãƒªã‚¹ãƒˆã‚’é¸æŠžã—ã¦ãã ã•ã„。後述ã™ã‚‹ã‚ˆã†ã«ã€ãƒ‡ãƒ™ãƒ­ãƒƒãƒ‘ーå‘ã‘ã®ãƒ¡ãƒ¼ãƒªãƒ³ã‚° リストã¯ä½•種類ã‹ã«åˆ†ã‹ã‚Œã¦ã„ã¾ã™ã€‚
            4. diff --git a/docs/html/resources/articles/painless-threading.jd b/docs/html/resources/articles/painless-threading.jd index 921f4df8c793cbb8e157a356ada6d071c7fe7485..17cec352900098b50f0fdb812611b81615baa3dd 100644 --- a/docs/html/resources/articles/painless-threading.jd +++ b/docs/html/resources/articles/painless-threading.jd @@ -108,7 +108,7 @@ you. Our previous example can easily be rewritten with new DownloadImageTask().execute("http://example.com/image.png"); } -private class DownloadImageTask extends AsyncTask<string, void,="" bitmap=""> { +private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); } diff --git a/docs/html/resources/browser.jd b/docs/html/resources/browser.jd new file mode 100644 index 0000000000000000000000000000000000000000..8a0876996ccf1aaa1b3174db132dadbe04b51cec --- /dev/null +++ b/docs/html/resources/browser.jd @@ -0,0 +1,49 @@ +page.title=Technical Resources +@jd:body + + + + + + +
              +

              Filter:

              +

              Showing all technical resources:

              +
              + + + +
              +
              No results.
              +
              + + diff --git a/docs/html/resources/community-groups.jd b/docs/html/resources/community-groups.jd index 651edbc8ed49ee7eab1ef4f1cda0841aaf8218ed..599c4aef46b96e0c148f432d987770c470bca88e 100644 --- a/docs/html/resources/community-groups.jd +++ b/docs/html/resources/community-groups.jd @@ -22,7 +22,7 @@ page.title=Developer Forums

              Welcome to the Android developers community! We're glad you're here and invite you to participate in discussions with other Android application developers on topics that interest you.

              -

              The lists on this page are primarily for discussion about Android application development. If you are seeking discussion about Android source code (not application development), then please refer to the Open Source Project Mailing lists.

              +

              The lists on this page are primarily for discussion about Android application development. If you are seeking discussion about Android source code (not application development), then please refer to the Open Source Project Mailing lists.

              Stack Overflow

              @@ -56,7 +56,7 @@ page.title=Developer Forums As you write your post, please do the following:
              1. Read -the mailing list charter that covers the community guidelines. +the mailing list charter that covers the community guidelines.
              2. Select the most appropriate mailing list for your question. There are several different lists for developers, described below.
              3. diff --git a/docs/html/resources/index.jd b/docs/html/resources/index.jd index 166872131a970d951f9fe63d5964cb35571a94bd..9055868840181c80356b55e97f7334cc6fb2e87f 100644 --- a/docs/html/resources/index.jd +++ b/docs/html/resources/index.jd @@ -1,38 +1,90 @@ page.title=Developer Resources @jd:body + +

                -This section provides technical articles, tutorials, sample code, and other +This section provides articles, tutorials, sample code, and other information to help you quickly implement the features you want in your -application. +application. To return to this page later, just click the "Resources" +tab while any Resources page is loaded.

                -
                -
                Technical Articles
                -
                Focused discussions about Android development subjects, including -optimizations, tips, interesting implementations, -and so on. Most of the articles provide "how-to" instructions for adding -features or functionality to your app. The articles are drawn from posts to the -Android Developers Blog. -
                - -
                Tutorials
                -
                Step-by-step instructions demonstrating how to build an Android application -that has the specific features you want.
                - -
                Sample Code
                -
                Fully-functioning sample applications that you can look at or build and run, -to learn about how Android works. Feel free to reuse any of the code or -techniques that you find in the samples!
                +

                Technical Resources

                + + + + + + + + +
                + + + +

                + Sample Code +

                +

                Fully-functioning sample applications that you can build and run + to learn about how Android works. Feel free to reuse any of the code or + techniques in the samples.

                +
                + + + +

                + Articles +

                +

                Focused discussions about Android development subjects, including + optimizations, tips, interesting implementations, "how-tos", + and so on.

                +
                + + + +

                + Tutorials +

                +

                Step-by-step instructions demonstrating how to build an Android application + that has the specific features you want.

                +
                +

                Other Resources

                + +
                Community
                Links to the Android discussion groups and information about other ways to collaborate with other developers.
                +
                Device Dashboard
                +
                Device distribution data, grouped by various dimensions such as screen size +and Android platform version.
                +
                More
                Quick development tips, troubleshooting information, and frequently asked questions (FAQs).
                - -

                To return to this page later, just click the "Resources" tab while any -Resources page is loaded.

                \ No newline at end of file diff --git a/docs/html/resources/resources-data.js b/docs/html/resources/resources-data.js new file mode 100644 index 0000000000000000000000000000000000000000..d06b695351fea68203c96bda4693a0dda56eea0e --- /dev/null +++ b/docs/html/resources/resources-data.js @@ -0,0 +1,632 @@ +var ANDROID_TAGS = { + type: { + 'article': 'Article', + 'tutorial': 'Tutorial', + 'sample': 'Sample', + 'video': 'Video', + 'library': 'Code Library' + }, + topic: { + 'accessibility': 'Accessibility', + 'accountsync': 'Accounts & Sync', + 'bestpractice': 'Best Practices', + 'communication': 'Communication', + 'compatibility': 'Compatibility', + 'data': 'Data Access', + 'drawing': 'Canvas Drawing', + 'gamedev': 'Game Development', + 'gl': 'OpenGL ES', + 'input': 'Input Methods', + 'intent': 'Intents', + 'layout': 'Layouts/Views', + 'media': 'Multimedia', + 'newfeature': 'New Features', + 'performance': 'Performance', + 'search': 'Search', + 'testing': 'Testing', + 'ui': 'User Interface', + 'web': 'Web Content' + }, + misc: { + 'external': 'External', + 'new': 'New' + } +}; + +var ANDROID_RESOURCES = [ + +////////////////////////// +/// TECHNICAL ARTICLES /// +////////////////////////// + + { + tags: ['article', 'performance', 'bestpractice'], + path: 'articles/avoiding-memory-leaks.html', + title: { + en: 'Avoiding Memory Leaks' + }, + description: { + en: 'Mobile devices often have limited memory, and memory leaks can cause your application to waste this valuable resource without your knowledge. This article provides tips to help you avoid common causes of memory leaks on the Android platform.' + } + }, + { + tags: ['article', 'compatibility'], + path: 'articles/backward-compatibility.html', + title: { + en: 'Backward Compatibility' + }, + description: { + en: 'The Android platform strives to ensure backwards compatibility. However, sometimes you want to use new features which aren\'t supported on older platforms. This article discusses strategies for selectively using these features based on availability, allowing you to keep your applications portable across a wide range of devices.' + } + }, + { + tags: ['article', 'intent'], + path: 'articles/can-i-use-this-intent.html', + title: { + en: 'Can I Use this Intent?' + }, + description: { + en: 'Android offers a very powerful and yet easy-to-use message type called an intent. You can use intents to turn applications into high-level libraries and make code modular and reusable. While it is nice to be able to make use of a loosely coupled API, there is no guarantee that the intent you send will be received by another application. This article describes a technique you can use to find out whether the system contains any application capable of responding to the intent you want to use.' + } + }, + { + tags: ['article', 'input'], + path: 'articles/creating-input-method.html', + title: { + en: 'Creating an Input Method' + }, + description: { + en: 'Input Method Editors (IMEs) provide the mechanism for entering text into text fields and other Views. Android devices come bundled with at least one IME, but users can install additional IMEs. This article covers the basics of developing an IME for the Android platform.' + } + }, + { + tags: ['article', 'drawing', 'ui'], + path: 'articles/drawable-mutations.html', + title: { + en: 'Drawable Mutations' + }, + description: { + en: 'Drawables are pluggable drawing containers that allow applications to display graphics. This article explains some common pitfalls when trying to modify the properties of multiple Drawables.' + } + }, + { + tags: ['article', 'bestpractice', 'ui'], + path: 'articles/faster-screen-orientation-change.html', + title: { + en: 'Faster Screen Orientation Change' + }, + description: { + en: 'When an Android device changes its orientation, the default behavior is to automatically restart the current activity with a new configuration. However, this can become a bottleneck in applications that access a large amount of external data. This article discusses how to gracefully handle this situation without resorting to manually processing configuration changes.' + } + }, + { + tags: ['article', 'compatibility'], + path: 'articles/future-proofing.html', + title: { + en: 'Future-Proofing Your Apps' + }, + description: { + en: 'A collection of common sense advice to help you ensure that your applications don\'t break when new versions of the Android platform are released.' + } + }, + { + tags: ['article', 'input'], + path: 'articles/gestures.html', + title: { + en: 'Gestures' + }, + description: { + en: 'Touch screens allow users to perform gestures, such as tapping, dragging, flinging, or sliding, to perform various actions. The gestures API enables your application to recognize even complicated gestures with ease. This article explains how to integrate this API into an application.' + } + }, + { + tags: ['article', 'gamedev', 'gl'], + path: 'articles/glsurfaceview.html', + title: { + en: 'Introducing GLSurfaceView' + }, + description: { + en: 'This article provides an overview of GLSurfaceView, a class that makes it easy to implement 2D or 3D OpenGL rendering inside of an Android application.' + } + }, + { + tags: ['article', 'ui', 'layout'], + path: 'articles/layout-tricks-reuse.html', + title: { + en: 'Layout Tricks: Creating Reusable UI Components' + }, + description: { + en: 'Learn how to combine multiple standard UI widgets into a single high-level component, which can be reused throughout your application.' + } + }, + { + tags: ['article', 'layout', 'ui', 'performance', 'bestpractice'], + path: 'articles/layout-tricks-efficiency.html', + title: { + en: 'Layout Tricks: Creating Efficient Layouts' + }, + description: { + en: 'Learn how to optimize application layouts as this article walks you through converting a LinearLayout into a RelativeLayout, and analyzes the resulting implications on performance.' + } + }, + { + tags: ['article', 'layout', 'ui', 'performance', 'bestpractice'], + path: 'articles/layout-tricks-stubs.html', + title: { + en: 'Layout Tricks: Using ViewStubs' + }, + description: { + en: 'Learn about using ViewStubs inside an application\'s layout in order to inflate rarely used UI elements, without the performance implications which would otherwise be caused by using the <include> tag.' + } + }, + { + tags: ['article', 'layout', 'ui', 'performance', 'bestpractice'], + path: 'articles/layout-tricks-merge.html', + title: { + en: 'Layout Tricks: Merging Layouts' + }, + description: { + en: 'Learn how to use the <merge> tag in your XML layouts in order to avoid unnecessary levels of hierarchy within an application\'s view tree.' + } + }, + { + tags: ['article', 'ui', 'performance'], + path: 'articles/listview-backgrounds.html', + title: { + en: 'ListView Backgrounds: An Optimization' + }, + description: { + en: 'ListViews are very popular widgets within the Android framework. This article describes some of the optimizations used by the ListView widget, and how to avoid some common issues that this causes when trying to use a custom background.' + } + }, + { + tags: ['article', 'ui', 'newfeature'], + path: 'articles/live-folders.html', + title: { + en: 'Live Folders' + }, + description: { + en: 'Live Folders allow users to display any source of data on their home screen without launching an application. This article discusses how to export an application\'s data in a format suitable for display inside of a live folder.' + } + }, + { + tags: ['article', 'ui', 'newfeature'], + path: 'articles/live-wallpapers.html', + title: { + en: 'Live Wallpapers' + }, + description: { + en: 'Live wallpapers are richer, animated, interactive backgrounds that users can display in their home screens. Learn how to create a live wallpaper and bundle it in an application that users can install on their devices.' + } + }, + { + tags: ['article', 'input'], + path: 'articles/on-screen-inputs.html', + title: { + en: 'Onscreen Input Methods' + }, + description: { + en: 'The Input Method Framework (IMF) allows users to take advantage of on-screen input methods, such as software keyboards. This article provides an overview of Input Method Editors (IMEs) and how applications interact with them.' + } + }, + { + tags: ['article', 'performance', 'bestpractice'], + path: 'articles/painless-threading.html', + title: { + en: 'Painless Threading' + }, + description: { + en: 'This article discusses the threading model used by Android applications and how applications can ensure best UI performance by spawning worker threads to handle long-running operations, rather than handling them in the main thread. The article also explains the API that your application can use to interact with Android UI toolkit components running on the main thread and spawn managed worker threads.' + } + }, + { + tags: ['article', 'ui', 'search'], + path: 'articles/qsb.html', + title: { + en: 'Quick Search Box' + }, + description: { + en: 'Quick Search Box (QSB) is a powerful, system-wide search framework. QSB makes it possible for users to quickly and easily find what they\'re looking for, both on their devices and on the web. This article discusses how to work with the QSB framework to add new search results for an installed application.' + } + }, + { + tags: ['article', 'ui'], + path: 'articles/touch-mode.html', + title: { + en: 'Touch Mode' + }, + description: { + en: 'This article explains the touch mode, one of the most important principles of Android\'s UI toolkit. Whenever a user interacts with a device\'s touch screen, the system enters touch mode. While simple in concept, there are important implications touch mode that are often overlooked.' + } + }, + { + tags: ['article', 'performance', 'bestpractice'], + path: 'articles/track-mem.html', + title: { + en: 'Tracking Memory Allocations' + }, + description: { + en: 'This article discusses how to use the Allocation Tracker tool to observe memory allocations and avoid performance problems that would otherwise be caused by ignoring the effect of Dalvik\'s garbage collector.' + } + }, + { + tags: ['article', 'newfeature'], + path: 'articles/ui-1.5.html', + title: { + en: 'UI Framework Changes in Android 1.5' + }, + description: { + en: 'Explore the UI changes that were introduced in Android 1.5, compared with the UI provided in Android 1.0 and 1.1.' + } + }, + { + tags: ['article', 'newfeature'], + path: 'articles/ui-1.6.html', + title: { + en: 'UI Framework Changes in Android 1.6' + }, + description: { + en: 'Explore the UI changes that were introduced in Android 1.6, compared with the UI provided in Android 1.5. In particular, this article discusses changes to RelativeLayouts and click listeners.' + } + }, + { + tags: ['article', 'ui', 'bestpractice'], + path: 'articles/timed-ui-updates.html', + title: { + en: 'Updating the UI from a Timer' + }, + description: { + en: 'Learn about how to use Handlers as a more efficient replacement for java.util.Timer on the Android platform.' + } + }, + { + tags: ['article', 'ui', 'accessibility'], + path: 'articles/tts.html', + title: { + en: 'Using Text-to-Speech' + }, + description: { + en: 'The text-to-speech API lets your application "speak" to users, in any of several languages. This article provides an overview of the TTS API and how you use to add speech capabilities to your application.' + } + }, + { + tags: ['article', 'ui', 'web'], + path: 'articles/using-webviews.html', + title: { + en: 'Using WebViews' + }, + description: { + en: 'WebViews allow an application to dynamically display HTML and execute JavaScript, without relinquishing control to a separate browser application. This article introduces the WebView classes and provides a sample application that demonstrates its use.' + } + }, + { + tags: ['article', 'ui'], + path: 'articles/wikinotes-linkify.html', + title: { + en: 'WikiNotes: Linkify your Text!' + }, + description: { + en: 'This article introduces WikiNotes for Android, part of the Apps for Android project. It covers the use of Linkify to turn ordinary text views into richer, link-oriented content that causes Android intents to fire when a link is selected.' + } + }, + { + tags: ['article', 'intent'], + path: 'articles/wikinotes-intents.html', + title: { + en: 'WikiNotes: Routing Intents' + }, + description: { + en: 'This article illustrates how an application, in this case the WikiNotes sample app, can use intents to route various types of linked text to the application that handles that type of data. For example, an app can use intents to route a linked telephone number to a dialer app and a web URL to a browser.' + } + }, + { + tags: ['article', 'ui', 'performance'], + path: 'articles/window-bg-speed.html', + title: { + en: 'Window Backgrounds & UI Speed' + }, + description: { + en: 'Some Android applications need to squeeze every bit of performance out of the UI toolkit and there are many ways to do so. In this article, you will discover how to speed up the drawing and the perceived startup time of your activities. Both of these techniques rely on a single feature, the window\'s background drawable.' + } + }, + { + tags: ['article', 'performance', 'bestpractice'], + path: 'articles/zipalign.html', + title: { + en: 'Zipalign: an Easy Optimization' + }, + description: { + en: 'The Android SDK includes a tool called zipalign that optimizes the way an application is packaged. Running zipalign against your application enables Android to interact with it more efficiently at run time and thus has the potential to make it and the overall system run faster. This article provides a high-level overview of the zipalign tool and its use.' + } + }, + +/////////////////// +/// SAMPLE CODE /// +/////////////////// + + { + tags: ['sample', 'layout', 'ui'], + path: 'samples/ApiDemos/index.html', + title: { + en: 'API Demos' + }, + description: { + en: 'A variety of small applications that demonstrate an extensive collection of framework topics.' + } + }, + { + tags: ['sample', 'data', 'newfeature', 'accountsync', 'new'], + path: 'samples/BackupRestore/index.html', + title: { + en: 'Backup and Restore' + }, + description: { + en: 'Illustrates a few different approaches that an application developer can take when integrating with the Android Backup Manager using the BackupAgent API introduced in Android 2.2.' + } + }, + { + tags: ['sample', 'communication'], + path: 'samples/BluetoothChat/index.html', + title: { + en: 'Bluetooth Chat' + }, + description: { + en: 'An application for two-way text messaging over Bluetooth.' + } + }, + { + tags: ['sample', 'accountsync'], + path: 'samples/BusinessCard/index.html', + title: { + en: 'BusinessCard' + }, + description: { + en: 'An application that demonstrates how to launch the built-in contact picker from within an activity. This sample also uses reflection to ensure that the correct version of the contacts API is used, depending on which API level the application is running under.' + } + }, + { + tags: ['sample', 'accountsync'], + path: 'samples/ContactManager/index.html', + title: { + en: 'Contact Manager' + }, + description: { + en: 'An application that demonstrates how to query the system contacts provider using the ContactsContract API, as well as insert contacts into a specific account.' + } + }, + { + tags: ['sample'], + path: 'samples/Home/index.html', + title: { + en: 'Home' + }, + description: { + en: 'A home screen replacement application.' + } + }, + { + tags: ['sample', 'gamedev', 'media'], + path: 'samples/JetBoy/index.html', + title: { + en: 'JetBoy' + }, + description: { + en: 'A game that demonstrates the SONiVOX JET interactive music technology, with JetPlayer.' + } + }, + { + tags: ['sample', 'ui', 'newfeature'], + path: 'samples/CubeLiveWallpaper/index.html', + title: { + en: 'Live Wallpaper' + }, + description: { + en: 'An application that demonstrates how to create a live wallpaper and bundle it in an application that users can install on their devices.' + } + }, + { + tags: ['sample', 'gamedev', 'media'], + path: 'samples/LunarLander/index.html', + title: { + en: 'Lunar Lander' + }, + description: { + en: 'A classic Lunar Lander game.' + } + }, + { + tags: ['sample', 'ui', 'bestpractice', 'layout'], + path: 'samples/MultiResolution/index.html', + title: { + en: 'Multiple Resolutions' + }, + description: { + en: 'A sample application that shows how to use resource directory qualifiers to provide different resources for different screen configurations.' + } + }, + { + tags: ['sample', 'data'], + path: 'samples/NotePad/index.html', + title: { + en: 'Note Pad' + }, + description: { + en: 'An application for saving notes. Similar (but not identical) to the Notepad tutorial.' + } + }, + { + tags: ['sample', 'accountsync'], + path: 'samples/SampleSyncAdapter/index.html', + title: { + en: 'SampleSyncAdapter' + }, + description: { + en: 'Demonstrates how an application can communicate with a cloud-based service and synchronize its data with data stored locally in a content provider. The sample uses two related parts of the Android framework — the account manager and the synchronization manager (through a sync adapter).' + } + }, + { + tags: ['sample', 'ui', 'search', 'new'], + path: 'samples/SearchableDictionary/index.html', + title: { + en: 'Searchable Dictionary v2' + }, + description: { + en: 'A sample application that demonstrates Android\'s search framework, including how to provide search suggestions for Quick Search Box.' + } + }, + { + tags: ['sample', 'layout', 'ui'], + path: 'samples/Snake/index.html', + title: { + en: 'Snake' + }, + description: { + en: 'An implementation of the classic game "Snake."' + } + }, + { + tags: ['sample', 'testing', 'new'], + path: 'samples/Spinner/index.html', + title: { + en: 'Spinner' + }, + description: { + en: 'A simple application that serves as an application under test for the SpinnerTest example.' + } + }, + { + tags: ['sample', 'testing', 'new'], + path: 'samples/SpinnerTest/index.html', + title: { + en: 'SpinnerTest' + }, + description: { + en: 'The test application for the Activity Testing tutorial. It tests the Spinner example application.' + } + }, + { + tags: ['sample', 'newfeature', 'new'], + path: 'samples/TicTacToeLib/index.html', + title: { + en: 'TicTacToeLib' + }, + description: { + en: 'An example of an Android library project, a type of project that lets you store and manage shared code and resources in one place, then make them available to your other Android applications.' + } + }, + { + tags: ['sample', 'newfeature', 'new'], + path: 'samples/TicTacToeMain/index.html', + title: { + en: 'TicTacToeMain' + }, + description: { + en: 'Demonstrates how an application can make use of shared code and resources stored in an Android library project.' + } + }, + { + tags: ['sample', 'input'], + path: 'samples/SoftKeyboard/index.html', + title: { + en: 'Soft Keyboard' + }, + description: { + en: 'An example of writing an input method for a software keyboard.' + } + }, + { + tags: ['sample', 'ui'], + path: 'samples/Wiktionary/index.html', + title: { + en: 'Wiktionary' + }, + description: { + en: 'An example of creating interactive widgets for display on the Android home screen.' + } + }, + { + tags: ['sample', 'ui'], + path: 'samples/WiktionarySimple/index.html', + title: { + en: 'Wiktionary (Simplified)' + }, + description: { + en: 'A simple Android home screen widgets example.' + } + }, + { + tags: ['sample', 'layout', 'new'], + path: 'samples/XmlAdapters/index.html', + title: { + en: 'XML Adapters' + }, + description: { + en: 'Binding data to views using XML Adapters examples.' + } + }, + +///////////////// +/// TUTORIALS /// +///////////////// + + { + tags: ['tutorial'], + path: 'tutorials/hello-world.html', + title: { + en: 'Hello World' + }, + description: { + en: 'Beginning basic application development with the Android SDK.' + } + }, + { + tags: ['tutorial', 'ui', 'layout'], + path: 'tutorials/views/index.html', + title: { + en: 'Hello Views' + }, + description: { + en: 'A walk-through of the various types of layouts and views available in the Android SDK.' + } + }, + { + tags: ['tutorial', 'ui', 'bestpractice'], + path: 'tutorials/localization/index.html', + title: { + en: 'Hello Localization' + }, + description: { + en: 'The basics of localizing your applications for multiple languages and locales.' + } + }, + { + tags: ['tutorial', 'data'], + path: 'tutorials/notepad/index.html', + title: { + en: 'Notepad Tutorial' + }, + description: { + en: 'A multi-part tutorial discussing intermediate-level concepts such as data access.' + } + }, + { + tags: ['tutorial', 'testing', 'new'], + path: 'tutorials/testing/helloandroid_test.html', + title: { + en: 'Hello Testing' + }, + description: { + en: 'A basic introduction to the Android testing framework.' + } + }, + { + tags: ['tutorial', 'testing', 'new'], + path: 'tutorials/testing/activity_test.html', + title: { + en: 'Activity Testing' + }, + description: { + en: 'A more advanced demonstration of the Android testing framework and tools.' + } + } +]; diff --git a/docs/html/resources/resources_toc.cs b/docs/html/resources/resources_toc.cs index 52689b63bce9709f2dbdf11ad82107e61c26cd4c..a1711b51148b1fa50c98742aa4e1c97439c5cb81 100644 --- a/docs/html/resources/resources_toc.cs +++ b/docs/html/resources/resources_toc.cs @@ -1,4 +1,55 @@ - -
    \ No newline at end of file diff --git a/drm/common/Android.mk b/drm/common/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..808b2c283eff718838de5cf191958e5b03a7002d --- /dev/null +++ b/drm/common/Android.mk @@ -0,0 +1,43 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + DrmConstraints.cpp \ + DrmConvertedStatus.cpp \ + DrmEngineBase.cpp \ + DrmInfo.cpp \ + DrmInfoRequest.cpp \ + DrmInfoStatus.cpp \ + DrmRights.cpp \ + DrmSupportInfo.cpp \ + IDrmIOService.cpp \ + IDrmManagerService.cpp \ + IDrmServiceListener.cpp \ + DrmInfoEvent.cpp \ + ReadWriteUtils.cpp + +LOCAL_C_INCLUDES := \ + $(TOP)/frameworks/base/include \ + $(TOP)/frameworks/base/drm/libdrmframework/include \ + $(TOP)/frameworks/base/drm/libdrmframework/plugins/common/include + +LOCAL_MODULE:= libdrmframeworkcommon + +LOCAL_MODULE_TAGS := optional + +include $(BUILD_STATIC_LIBRARY) diff --git a/drm/common/DrmConstraints.cpp b/drm/common/DrmConstraints.cpp new file mode 100644 index 0000000000000000000000000000000000000000..11ce410bf8301f3b34ef8ed36deaa10d3e77e8d0 --- /dev/null +++ b/drm/common/DrmConstraints.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +using namespace android; + +const String8 DrmConstraints::MAX_REPEAT_COUNT("max_repeat_count"); +const String8 DrmConstraints::REMAINING_REPEAT_COUNT("remaining_repeat_count"); +const String8 DrmConstraints::LICENSE_START_TIME("license_start_time"); +const String8 DrmConstraints::LICENSE_EXPIRY_TIME("license_expiry_time"); +const String8 DrmConstraints::LICENSE_AVAILABLE_TIME("license_available_time"); +const String8 DrmConstraints::EXTENDED_METADATA("extended_metadata"); + +int DrmConstraints::getCount(void) const { + return mConstraintMap.size(); +} + +status_t DrmConstraints::put(const String8* key, const char* value) { + int length = strlen(value); + char* charValue = new char[length + 1]; + if (NULL != charValue) { + strncpy(charValue, value, length); + charValue[length] = '\0'; + mConstraintMap.add(*key, charValue); + } + return DRM_NO_ERROR; +} + +String8 DrmConstraints::get(const String8& key) const { + if (NULL != getValue(&key)) { + return String8(getValue(&key)); + } + return String8(""); +} + +const char* DrmConstraints::getValue(const String8* key) const { + if (NAME_NOT_FOUND != mConstraintMap.indexOfKey(*key)) { + return mConstraintMap.valueFor(*key); + } + return NULL; +} + +const char* DrmConstraints::getAsByteArray(const String8* key) const { + return getValue(key); +} + +bool DrmConstraints::KeyIterator::hasNext() { + return mIndex < mDrmConstraints->mConstraintMap.size(); +} + +const String8& DrmConstraints::KeyIterator::next() { + const String8& key = mDrmConstraints->mConstraintMap.keyAt(mIndex); + mIndex++; + return key; +} + +DrmConstraints::KeyIterator DrmConstraints::keyIterator() { + return KeyIterator(this); +} + +DrmConstraints::KeyIterator::KeyIterator(const DrmConstraints::KeyIterator& keyIterator) + : mDrmConstraints(keyIterator.mDrmConstraints), + mIndex(keyIterator.mIndex) { + LOGV("DrmConstraints::KeyIterator::KeyIterator"); +} + +DrmConstraints::KeyIterator& DrmConstraints::KeyIterator::operator=( + const DrmConstraints::KeyIterator& keyIterator) { + LOGV("DrmConstraints::KeyIterator::operator="); + mDrmConstraints = keyIterator.mDrmConstraints; + mIndex = keyIterator.mIndex; + return *this; +} + + +DrmConstraints::Iterator DrmConstraints::iterator() { + return Iterator(this); +} + +DrmConstraints::Iterator::Iterator(const DrmConstraints::Iterator& iterator) : + mDrmConstraints(iterator.mDrmConstraints), + mIndex(iterator.mIndex) { + LOGV("DrmConstraints::Iterator::Iterator"); +} + +DrmConstraints::Iterator& DrmConstraints::Iterator::operator=( + const DrmConstraints::Iterator& iterator) { + LOGV("DrmConstraints::Iterator::operator="); + mDrmConstraints = iterator.mDrmConstraints; + mIndex = iterator.mIndex; + return *this; +} + +bool DrmConstraints::Iterator::hasNext() { + return mIndex < mDrmConstraints->mConstraintMap.size(); +} + +String8 DrmConstraints::Iterator::next() { + String8 value = String8(mDrmConstraints->mConstraintMap.editValueAt(mIndex)); + mIndex++; + return value; +} + diff --git a/drm/common/DrmConvertedStatus.cpp b/drm/common/DrmConvertedStatus.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5d035f5b08aa4240e57f14e23a63adf89dea071a --- /dev/null +++ b/drm/common/DrmConvertedStatus.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +using namespace android; + +DrmConvertedStatus::DrmConvertedStatus( + int _statusCode, const DrmBuffer* _convertedData, int _offset) : + statusCode(_statusCode), + convertedData(_convertedData), + offset(_offset) { + +} + diff --git a/drm/common/DrmEngineBase.cpp b/drm/common/DrmEngineBase.cpp new file mode 100644 index 0000000000000000000000000000000000000000..70398e8ba4e0eb860e5dc8895449ca9f8adc768c --- /dev/null +++ b/drm/common/DrmEngineBase.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "DrmEngineBase.h" + +using namespace android; + +DrmEngineBase::DrmEngineBase() { + +} + +DrmEngineBase::~DrmEngineBase() { + +} + +DrmConstraints* DrmEngineBase::getConstraints( + int uniqueId, const String8* path, int action) { + return onGetConstraints(uniqueId, path, action); +} + +status_t DrmEngineBase::initialize(int uniqueId) { + return onInitialize(uniqueId); +} + +status_t DrmEngineBase::setOnInfoListener( + int uniqueId, const IDrmEngine::OnInfoListener* infoListener) { + return onSetOnInfoListener(uniqueId, infoListener); +} + +status_t DrmEngineBase::terminate(int uniqueId) { + return onTerminate(uniqueId); +} + +bool DrmEngineBase::canHandle(int uniqueId, const String8& path) { + return onCanHandle(uniqueId, path); +} + +DrmInfoStatus* DrmEngineBase::processDrmInfo(int uniqueId, const DrmInfo* drmInfo) { + return onProcessDrmInfo(uniqueId, drmInfo); +} + +void DrmEngineBase::saveRights( + int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) { + return onSaveRights(uniqueId, drmRights, rightsPath, contentPath); +} + +DrmInfo* DrmEngineBase::acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest) { + return onAcquireDrmInfo(uniqueId, drmInfoRequest); +} + +String8 DrmEngineBase::getOriginalMimeType(int uniqueId, const String8& path) { + return onGetOriginalMimeType(uniqueId, path); +} + +int DrmEngineBase::getDrmObjectType(int uniqueId, const String8& path, const String8& mimeType) { + return onGetDrmObjectType(uniqueId, path, mimeType); +} + +int DrmEngineBase::checkRightsStatus(int uniqueId, const String8& path, int action) { + return onCheckRightsStatus(uniqueId, path, action); +} + +void DrmEngineBase::consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve) { + onConsumeRights(uniqueId, decryptHandle, action, reserve); +} + +void DrmEngineBase::setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position) { + onSetPlaybackStatus(uniqueId, decryptHandle, playbackStatus, position); +} + +bool DrmEngineBase::validateAction( + int uniqueId, const String8& path, + int action, const ActionDescription& description) { + return onValidateAction(uniqueId, path, action, description); +} + +void DrmEngineBase::removeRights(int uniqueId, const String8& path) { + onRemoveRights(uniqueId, path); +} + +void DrmEngineBase::removeAllRights(int uniqueId) { + onRemoveAllRights(uniqueId); +} + +void DrmEngineBase::openConvertSession(int uniqueId, int convertId) { + onOpenConvertSession(uniqueId, convertId); +} + +DrmConvertedStatus* DrmEngineBase::convertData( + int uniqueId, int convertId, const DrmBuffer* inputData) { + return onConvertData(uniqueId, convertId, inputData); +} + +DrmConvertedStatus* DrmEngineBase::closeConvertSession(int uniqueId, int convertId) { + return onCloseConvertSession(uniqueId, convertId); +} + +DrmSupportInfo* DrmEngineBase::getSupportInfo(int uniqueId) { + return onGetSupportInfo(uniqueId); +} + +status_t DrmEngineBase::openDecryptSession( + int uniqueId, DecryptHandle* decryptHandle, int fd, int offset, int length) { + return onOpenDecryptSession(uniqueId, decryptHandle, fd, offset, length); +} + +void DrmEngineBase::closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) { + onCloseDecryptSession(uniqueId, decryptHandle); +} + +void DrmEngineBase::initializeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId, const DrmBuffer* headerInfo) { + onInitializeDecryptUnit(uniqueId, decryptHandle, decryptUnitId, headerInfo); +} + +status_t DrmEngineBase::decrypt( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId, + const DrmBuffer* encBuffer, DrmBuffer** decBuffer) { + return onDecrypt(uniqueId, decryptHandle, decryptUnitId, encBuffer, decBuffer); +} + +void DrmEngineBase::finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) { + onFinalizeDecryptUnit(uniqueId, decryptHandle, decryptUnitId); +} + +ssize_t DrmEngineBase::pread( + int uniqueId, DecryptHandle* decryptHandle, void* buffer, ssize_t numBytes, off_t offset) { + return onPread(uniqueId, decryptHandle, buffer, numBytes, offset); +} + diff --git a/drm/common/DrmInfo.cpp b/drm/common/DrmInfo.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ddcab334e82e82c69c52014f2d30873454e962b1 --- /dev/null +++ b/drm/common/DrmInfo.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +using namespace android; + +DrmInfo::DrmInfo(int infoType, const DrmBuffer& drmBuffer, const String8& mimeType) : + mInfoType(infoType), + mData(drmBuffer), + mMimeType(mimeType) { + +} + +int DrmInfo::getInfoType(void) const { + return mInfoType; +} + +String8 DrmInfo::getMimeType(void) const { + return mMimeType; +} + +const DrmBuffer& DrmInfo::getData(void) const { + return mData; +} + +int DrmInfo::getCount(void) const { + return mAttributes.size(); +} + +status_t DrmInfo::put(const String8& key, const String8& value) { + mAttributes.add(key, value); + return DRM_NO_ERROR; +} + +String8 DrmInfo::get(const String8& key) const { + if (NAME_NOT_FOUND != mAttributes.indexOfKey(key)) { + return mAttributes.valueFor(key); + } + return String8(""); +} + +int DrmInfo::indexOfKey(const String8& key) const { + return mAttributes.indexOfKey(key); +} + +DrmInfo::KeyIterator DrmInfo::keyIterator() const { + return KeyIterator(this); +} + +DrmInfo::Iterator DrmInfo::iterator() const { + return Iterator(this); +} + +// KeyIterator implementation +DrmInfo::KeyIterator::KeyIterator(const DrmInfo::KeyIterator& keyIterator) : + mDrmInfo(keyIterator.mDrmInfo), mIndex(keyIterator.mIndex) { + +} + +bool DrmInfo::KeyIterator::hasNext() { + return (mIndex < mDrmInfo->mAttributes.size()); +} + +const String8& DrmInfo::KeyIterator::next() { + const String8& key = mDrmInfo->mAttributes.keyAt(mIndex); + mIndex++; + return key; +} + +DrmInfo::KeyIterator& DrmInfo::KeyIterator::operator=(const DrmInfo::KeyIterator& keyIterator) { + mDrmInfo = keyIterator.mDrmInfo; + mIndex = keyIterator.mIndex; + return *this; +} + +// Iterator implementation +DrmInfo::Iterator::Iterator(const DrmInfo::Iterator& iterator) + : mDrmInfo(iterator.mDrmInfo), mIndex(iterator.mIndex) { + +} + +DrmInfo::Iterator& DrmInfo::Iterator::operator=(const DrmInfo::Iterator& iterator) { + mDrmInfo = iterator.mDrmInfo; + mIndex = iterator.mIndex; + return *this; +} + +bool DrmInfo::Iterator::hasNext() { + return mIndex < mDrmInfo->mAttributes.size(); +} + +String8& DrmInfo::Iterator::next() { + String8& value = mDrmInfo->mAttributes.editValueAt(mIndex); + mIndex++; + return value; +} + diff --git a/drm/common/DrmInfoEvent.cpp b/drm/common/DrmInfoEvent.cpp new file mode 100644 index 0000000000000000000000000000000000000000..eb581291e000bb95eb6aed55eda92356ffc8cea6 --- /dev/null +++ b/drm/common/DrmInfoEvent.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmInfoEvent" +#include "utils/Log.h" + +#include +#include + +using namespace android; + +DrmInfoEvent::DrmInfoEvent(int uniqueId, int infoType, const String8& message) + : mUniqueId(uniqueId), + mInfoType(infoType), + mMessage(message) { + +} + +int DrmInfoEvent::getUniqueId() const { + return mUniqueId; +} + +int DrmInfoEvent::getType() const { + return mInfoType; +} + +const String8& DrmInfoEvent::getMessage() const { + return mMessage; +} + diff --git a/drm/common/DrmInfoRequest.cpp b/drm/common/DrmInfoRequest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a64685904b66c253f7f1f2be4adbb86e6b2490f3 --- /dev/null +++ b/drm/common/DrmInfoRequest.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +using namespace android; + +const String8 DrmInfoRequest::ACCOUNT_ID("account_id"); +const String8 DrmInfoRequest::SUBSCRIPTION_ID("subscription_id"); + +DrmInfoRequest::DrmInfoRequest(int infoType, const String8& mimeType) : + mInfoType(infoType), mMimeType(mimeType) { + +} + +String8 DrmInfoRequest::getMimeType(void) const { + return mMimeType; +} + +int DrmInfoRequest::getInfoType(void) const { + return mInfoType; +} + +int DrmInfoRequest::getCount(void) const { + return mRequestInformationMap.size(); +} + +status_t DrmInfoRequest::put(const String8& key, const String8& value) { + mRequestInformationMap.add(key, value); + return DRM_NO_ERROR; +} + +String8 DrmInfoRequest::get(const String8& key) const { + if (NAME_NOT_FOUND != mRequestInformationMap.indexOfKey(key)) { + return mRequestInformationMap.valueFor(key); + } + return String8(""); +} + +DrmInfoRequest::KeyIterator DrmInfoRequest::keyIterator() const { + return KeyIterator(this); +} + +DrmInfoRequest::Iterator DrmInfoRequest::iterator() const { + return Iterator(this); +} + +// KeyIterator implementation +DrmInfoRequest::KeyIterator::KeyIterator(const DrmInfoRequest::KeyIterator& keyIterator) + : mDrmInfoRequest(keyIterator.mDrmInfoRequest), + mIndex(keyIterator.mIndex) { + +} + +bool DrmInfoRequest::KeyIterator::hasNext() { + return (mIndex < mDrmInfoRequest->mRequestInformationMap.size()); +} + +const String8& DrmInfoRequest::KeyIterator::next() { + const String8& key = mDrmInfoRequest->mRequestInformationMap.keyAt(mIndex); + mIndex++; + return key; +} + +DrmInfoRequest::KeyIterator& DrmInfoRequest::KeyIterator::operator=( + const DrmInfoRequest::KeyIterator& keyIterator) { + mDrmInfoRequest = keyIterator.mDrmInfoRequest; + mIndex = keyIterator.mIndex; + return *this; +} + +// Iterator implementation +DrmInfoRequest::Iterator::Iterator(const DrmInfoRequest::Iterator& iterator) : + mDrmInfoRequest(iterator.mDrmInfoRequest), mIndex(iterator.mIndex) { +} + +DrmInfoRequest::Iterator& DrmInfoRequest::Iterator::operator=( + const DrmInfoRequest::Iterator& iterator) { + mDrmInfoRequest = iterator.mDrmInfoRequest; + mIndex = iterator.mIndex; + return *this; +} + +bool DrmInfoRequest::Iterator::hasNext() { + return mIndex < mDrmInfoRequest->mRequestInformationMap.size(); +} + +String8& DrmInfoRequest::Iterator::next() { + String8& value = mDrmInfoRequest->mRequestInformationMap.editValueAt(mIndex); + mIndex++; + return value; +} + diff --git a/drm/common/DrmInfoStatus.cpp b/drm/common/DrmInfoStatus.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f3a1516168a2fa00e7f30117d89e720e44c27c79 --- /dev/null +++ b/drm/common/DrmInfoStatus.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +using namespace android; + +DrmInfoStatus::DrmInfoStatus( + int _statusCode, const DrmBuffer* _drmBuffer, const String8& _mimeType) : + statusCode(_statusCode), + drmBuffer(_drmBuffer), + mimeType(_mimeType) { + +} + diff --git a/drm/common/DrmRights.cpp b/drm/common/DrmRights.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dc1e6c519c29a03e85349a37da6200fb8cd39c33 --- /dev/null +++ b/drm/common/DrmRights.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +using namespace android; + +DrmRights::DrmRights(const String8& rightsFilePath, const String8& mimeType, + const String8& accountId, const String8& subscriptionId) { + /** + * TODO Read DrmRights from rights file + */ +} + +DrmRights::DrmRights(const DrmBuffer& rightsData, const String8& mimeType, + const String8& accountId, const String8& subscriptionId) : + mData(rightsData), + mMimeType(mimeType), + mAccountId(accountId), + mSubscriptionId(subscriptionId) { +} + +const DrmBuffer& DrmRights::getData(void) const { + return mData; +} + +String8 DrmRights::getMimeType(void) const { + return mMimeType; +} + +String8 DrmRights::getAccountId(void) const { + return mAccountId; +} + +String8 DrmRights::getSubscriptionId(void) const { + return mSubscriptionId; +} + diff --git a/drm/common/DrmSupportInfo.cpp b/drm/common/DrmSupportInfo.cpp new file mode 100644 index 0000000000000000000000000000000000000000..35e83fcdeab385300f0b37ffd02aa7e408384710 --- /dev/null +++ b/drm/common/DrmSupportInfo.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +using namespace android; + +DrmSupportInfo::DrmSupportInfo() { + +} + +DrmSupportInfo::DrmSupportInfo(const DrmSupportInfo& drmSupportInfo): + mMimeTypeVector(drmSupportInfo.mMimeTypeVector), + mFileSuffixVector(drmSupportInfo.mFileSuffixVector), + mDescription(drmSupportInfo.mDescription) { + +} + +bool DrmSupportInfo::operator<(const DrmSupportInfo& drmSupportInfo) const { + // Do we need to check mMimeTypeVector & mFileSuffixVector ? + // Note Vector doesn't overrides "<" operator + return mDescription < drmSupportInfo.mDescription; +} + +bool DrmSupportInfo::operator==(const DrmSupportInfo& drmSupportInfo) const { + // Do we need to check mMimeTypeVector & mFileSuffixVector ? + // Note Vector doesn't overrides "==" operator + return (mDescription == drmSupportInfo.mDescription); +} + +bool DrmSupportInfo::isSupportedMimeType(const String8& mimeType) const { + for (int i = 0; i < mMimeTypeVector.size(); i++) { + const String8 item = mMimeTypeVector.itemAt(i); + + if (String8("") != mimeType && item.find(mimeType) != -1) { + return true; + } + } + return false; +} + +bool DrmSupportInfo::isSupportedFileSuffix(const String8& fileType) const { + for (int i = 0; i < mFileSuffixVector.size(); i++) { + const String8 item = mFileSuffixVector.itemAt(i); + + if (String8("") != fileType && item.find(fileType) != -1) { + return true; + } + } + return false; +} + +DrmSupportInfo& DrmSupportInfo::operator=(const DrmSupportInfo& drmSupportInfo) { + mMimeTypeVector = drmSupportInfo.mMimeTypeVector; + mFileSuffixVector = drmSupportInfo.mFileSuffixVector; + mDescription = drmSupportInfo.mDescription; + return *this; +} + +int DrmSupportInfo::getMimeTypeCount(void) const { + return mMimeTypeVector.size(); +} + +int DrmSupportInfo::getFileSuffixCount(void) const { + return mFileSuffixVector.size(); +} + +status_t DrmSupportInfo::addMimeType(const String8& mimeType) { + mMimeTypeVector.push(mimeType); + return DRM_NO_ERROR; +} + +status_t DrmSupportInfo::addFileSuffix(const String8& fileSuffix) { + mFileSuffixVector.push(fileSuffix); + return DRM_NO_ERROR; +} + +status_t DrmSupportInfo::setDescription(const String8& description) { + mDescription = description; + return DRM_NO_ERROR; +} + +String8 DrmSupportInfo::getDescription() const { + return mDescription; +} + +DrmSupportInfo::FileSuffixIterator DrmSupportInfo::getFileSuffixIterator() { + return FileSuffixIterator(this); +} + +DrmSupportInfo::MimeTypeIterator DrmSupportInfo::getMimeTypeIterator() { + return MimeTypeIterator(this); +} + +DrmSupportInfo::FileSuffixIterator::FileSuffixIterator( + const DrmSupportInfo::FileSuffixIterator& iterator) : + mDrmSupportInfo(iterator.mDrmSupportInfo), + mIndex(iterator.mIndex) { + +} + +DrmSupportInfo::FileSuffixIterator& DrmSupportInfo::FileSuffixIterator::operator=( + const DrmSupportInfo::FileSuffixIterator& iterator) { + mDrmSupportInfo = iterator.mDrmSupportInfo; + mIndex = iterator.mIndex; + return *this; +} + +bool DrmSupportInfo::FileSuffixIterator::hasNext() { + return mIndex < mDrmSupportInfo->mFileSuffixVector.size(); +} + +String8& DrmSupportInfo::FileSuffixIterator::next() { + String8& value = mDrmSupportInfo->mFileSuffixVector.editItemAt(mIndex); + mIndex++; + return value; +} + +DrmSupportInfo::MimeTypeIterator::MimeTypeIterator( + const DrmSupportInfo::MimeTypeIterator& iterator) : + mDrmSupportInfo(iterator.mDrmSupportInfo), + mIndex(iterator.mIndex) { + +} + +DrmSupportInfo::MimeTypeIterator& DrmSupportInfo::MimeTypeIterator::operator=( + const DrmSupportInfo::MimeTypeIterator& iterator) { + mDrmSupportInfo = iterator.mDrmSupportInfo; + mIndex = iterator.mIndex; + return *this; +} + +bool DrmSupportInfo::MimeTypeIterator::hasNext() { + return mIndex < mDrmSupportInfo->mMimeTypeVector.size(); +} + +String8& DrmSupportInfo::MimeTypeIterator::next() { + String8& value = mDrmSupportInfo->mMimeTypeVector.editItemAt(mIndex); + mIndex++; + return value; +} + diff --git a/drm/common/IDrmIOService.cpp b/drm/common/IDrmIOService.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7ce45e7fdcb8019f37f59e23d4ae81a89557bcf6 --- /dev/null +++ b/drm/common/IDrmIOService.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "IDrmIOService" +#include + +#include +#include +#include +#include +#include +#include "IDrmIOService.h" + +using namespace android; + +void BpDrmIOService::writeToFile(const String8& filePath, const String8& dataBuffer) { + Parcel data, reply; + + data.writeInterfaceToken(IDrmIOService::getInterfaceDescriptor()); + data.writeString8(filePath); + data.writeString8(dataBuffer); + + remote()->transact(WRITE_TO_FILE, data, &reply); +} + +String8 BpDrmIOService::readFromFile(const String8& filePath) { + + Parcel data, reply; + + data.writeInterfaceToken(IDrmIOService::getInterfaceDescriptor()); + data.writeString8(filePath); + + remote()->transact(READ_FROM_FILE, data, &reply); + return reply.readString8(); +} + +IMPLEMENT_META_INTERFACE(DrmIOService, "drm.IDrmIOService"); + +status_t BnDrmIOService::onTransact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { + + switch (code) { + case WRITE_TO_FILE: + { + CHECK_INTERFACE(IDrmIOService, data, reply); + + writeToFile(data.readString8(), data.readString8()); + return DRM_NO_ERROR; + } + + case READ_FROM_FILE: + { + CHECK_INTERFACE(IDrmIOService, data, reply); + + String8 dataBuffer = readFromFile(data.readString8()); + reply->writeString8(dataBuffer); + return DRM_NO_ERROR; + } + + default: + return BBinder::onTransact(code, data, reply, flags); + } +} + diff --git a/drm/common/IDrmManagerService.cpp b/drm/common/IDrmManagerService.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4fc828a93000fc615c6879b46ca2c1edd28a514f --- /dev/null +++ b/drm/common/IDrmManagerService.cpp @@ -0,0 +1,1358 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "IDrmManagerService(Native)" +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "IDrmManagerService.h" + +#define INVALID_BUFFER_LENGTH -1 + +using namespace android; + +status_t BpDrmManagerService::loadPlugIns(int uniqueId) { + LOGV("load plugins"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + remote()->transact(LOAD_PLUGINS, data, &reply); + return reply.readInt32(); +} + +status_t BpDrmManagerService::loadPlugIns(int uniqueId, const String8& plugInDirPath) { + LOGV("load plugins from path"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(plugInDirPath); + + remote()->transact(LOAD_PLUGINS_FROM_PATH, data, &reply); + return reply.readInt32(); +} + +status_t BpDrmManagerService::setDrmServiceListener( + int uniqueId, const sp& drmServiceListener) { + LOGV("setDrmServiceListener"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeStrongBinder(drmServiceListener->asBinder()); + remote()->transact(SET_DRM_SERVICE_LISTENER, data, &reply); + return reply.readInt32(); +} + +status_t BpDrmManagerService::unloadPlugIns(int uniqueId) { + LOGV("unload plugins"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + remote()->transact(UNLOAD_PLUGINS, data, &reply); + return reply.readInt32(); +} + +status_t BpDrmManagerService::installDrmEngine(int uniqueId, const String8& drmEngineFile) { + LOGV("Install DRM Engine"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(drmEngineFile); + + remote()->transact(INSTALL_DRM_ENGINE, data, &reply); + return reply.readInt32(); +} + +DrmConstraints* BpDrmManagerService::getConstraints( + int uniqueId, const String8* path, const int action) { + LOGV("Get Constraints"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(*path); + data.writeInt32(action); + + remote()->transact(GET_CONSTRAINTS_FROM_CONTENT, data, &reply); + + DrmConstraints* drmConstraints = NULL; + if (0 != reply.dataAvail()) { + //Filling Drm Constraints + drmConstraints = new DrmConstraints(); + + const int size = reply.readInt32(); + for (int index = 0; index < size; ++index) { + const String8 key(reply.readString8()); + const int bufferSize = reply.readInt32(); + char* data = NULL; + if (0 < bufferSize) { + data = new char[bufferSize]; + reply.read(data, bufferSize); + } + drmConstraints->put(&key, data); + } + } + return drmConstraints; +} + +bool BpDrmManagerService::canHandle(int uniqueId, const String8& path, const String8& mimeType) { + LOGV("Can Handle"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeString8(path); + data.writeString8(mimeType); + + remote()->transact(CAN_HANDLE, data, &reply); + + return static_cast(reply.readInt32()); +} + +DrmInfoStatus* BpDrmManagerService::processDrmInfo(int uniqueId, const DrmInfo* drmInfo) { + LOGV("Process DRM Info"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + //Filling DRM info + data.writeInt32(drmInfo->getInfoType()); + const DrmBuffer dataBuffer = drmInfo->getData(); + const int dataBufferSize = dataBuffer.length; + data.writeInt32(dataBufferSize); + if (0 < dataBufferSize) { + data.write(dataBuffer.data, dataBufferSize); + } + data.writeString8(drmInfo->getMimeType()); + + data.writeInt32(drmInfo->getCount()); + DrmInfo::KeyIterator keyIt = drmInfo->keyIterator(); + + while (keyIt.hasNext()) { + const String8 key = keyIt.next(); + data.writeString8(key); + const String8 value = drmInfo->get(key); + data.writeString8((value == String8("")) ? String8("NULL") : value); + } + + remote()->transact(PROCESS_DRM_INFO, data, &reply); + + DrmInfoStatus* drmInfoStatus = NULL; + if (0 != reply.dataAvail()) { + //Filling DRM Info Status + const int statusCode = reply.readInt32(); + const String8 mimeType = reply.readString8(); + + DrmBuffer* drmBuffer = NULL; + if (0 != reply.dataAvail()) { + const int bufferSize = reply.readInt32(); + char* data = NULL; + if (0 < bufferSize) { + data = new char[bufferSize]; + reply.read(data, bufferSize); + } + drmBuffer = new DrmBuffer(data, bufferSize); + } + drmInfoStatus = new DrmInfoStatus(statusCode, drmBuffer, mimeType); + } + return drmInfoStatus; +} + +DrmInfo* BpDrmManagerService::acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInforequest) { + LOGV("Acquire DRM Info"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + //Filling DRM Info Request + data.writeInt32(drmInforequest->getInfoType()); + data.writeString8(drmInforequest->getMimeType()); + + data.writeInt32(drmInforequest->getCount()); + DrmInfoRequest::KeyIterator keyIt = drmInforequest->keyIterator(); + + while (keyIt.hasNext()) { + const String8 key = keyIt.next(); + data.writeString8(key); + const String8 value = drmInforequest->get(key); + data.writeString8((value == String8("")) ? String8("NULL") : value); + } + + remote()->transact(ACQUIRE_DRM_INFO, data, &reply); + + DrmInfo* drmInfo = NULL; + if (0 != reply.dataAvail()) { + //Filling DRM Info + const int infoType = reply.readInt32(); + const int bufferSize = reply.readInt32(); + char* data = NULL; + + if (0 < bufferSize) { + data = new char[bufferSize]; + reply.read(data, bufferSize); + } + drmInfo = new DrmInfo(infoType, DrmBuffer(data, bufferSize), reply.readString8()); + + const int size = reply.readInt32(); + for (int index = 0; index < size; ++index) { + const String8 key(reply.readString8()); + const String8 value(reply.readString8()); + drmInfo->put(key, (value == String8("NULL")) ? String8("") : value); + } + } + return drmInfo; +} + +void BpDrmManagerService::saveRights( + int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) { + LOGV("Save Rights"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + //Filling Drm Rights + const DrmBuffer dataBuffer = drmRights.getData(); + data.writeInt32(dataBuffer.length); + data.write(dataBuffer.data, dataBuffer.length); + + const String8 mimeType = drmRights.getMimeType(); + data.writeString8((mimeType == String8("")) ? String8("NULL") : mimeType); + + const String8 accountId = drmRights.getAccountId(); + data.writeString8((accountId == String8("")) ? String8("NULL") : accountId); + + const String8 subscriptionId = drmRights.getSubscriptionId(); + data.writeString8((subscriptionId == String8("")) ? String8("NULL") : subscriptionId); + + data.writeString8((rightsPath == String8("")) ? String8("NULL") : rightsPath); + data.writeString8((contentPath == String8("")) ? String8("NULL") : contentPath); + + remote()->transact(SAVE_RIGHTS, data, &reply); +} + +String8 BpDrmManagerService::getOriginalMimeType(int uniqueId, const String8& path) { + LOGV("Get Original MimeType"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(path); + + remote()->transact(GET_ORIGINAL_MIMETYPE, data, &reply); + return reply.readString8(); +} + +int BpDrmManagerService::getDrmObjectType( + int uniqueId, const String8& path, const String8& mimeType) { + LOGV("Get Drm object type"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(path); + data.writeString8(mimeType); + + remote()->transact(GET_DRM_OBJECT_TYPE, data, &reply); + + return reply.readInt32(); +} + +int BpDrmManagerService::checkRightsStatus(int uniqueId, const String8& path, int action) { + LOGV("checkRightsStatus"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(path); + data.writeInt32(action); + + remote()->transact(CHECK_RIGHTS_STATUS, data, &reply); + + return reply.readInt32(); +} + +void BpDrmManagerService::consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve) { + LOGV("consumeRights"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeInt32(decryptHandle->decryptId); + data.writeString8(decryptHandle->mimeType); + data.writeInt32(decryptHandle->decryptApiType); + data.writeInt32(decryptHandle->status); + + if (NULL != decryptHandle->decryptInfo) { + data.writeInt32(decryptHandle->decryptInfo->decryptBufferLength); + } else { + data.writeInt32(INVALID_BUFFER_LENGTH); + } + + data.writeInt32(action); + data.writeInt32(static_cast< int>(reserve)); + + remote()->transact(CONSUME_RIGHTS, data, &reply); +} + +void BpDrmManagerService::setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position) { + LOGV("setPlaybackStatus"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeInt32(decryptHandle->decryptId); + data.writeString8(decryptHandle->mimeType); + data.writeInt32(decryptHandle->decryptApiType); + data.writeInt32(decryptHandle->status); + + if (NULL != decryptHandle->decryptInfo) { + data.writeInt32(decryptHandle->decryptInfo->decryptBufferLength); + } else { + data.writeInt32(INVALID_BUFFER_LENGTH); + } + + data.writeInt32(playbackStatus); + data.writeInt32(position); + + remote()->transact(SET_PLAYBACK_STATUS, data, &reply); +} + +bool BpDrmManagerService::validateAction( + int uniqueId, const String8& path, + int action, const ActionDescription& description) { + LOGV("validateAction"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(path); + data.writeInt32(action); + data.writeInt32(description.outputType); + data.writeInt32(description.configuration); + + remote()->transact(VALIDATE_ACTION, data, &reply); + + return static_cast(reply.readInt32()); +} + +void BpDrmManagerService::removeRights(int uniqueId, const String8& path) { + LOGV("removeRights"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(path); + + remote()->transact(REMOVE_RIGHTS, data, &reply); +} + +void BpDrmManagerService::removeAllRights(int uniqueId) { + LOGV("removeAllRights"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + remote()->transact(REMOVE_ALL_RIGHTS, data, &reply); +} + +int BpDrmManagerService::openConvertSession(int uniqueId, const String8& mimeType) { + LOGV("openConvertSession"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeString8(mimeType); + + remote()->transact(OPEN_CONVERT_SESSION, data, &reply); + return reply.readInt32(); +} + +DrmConvertedStatus* BpDrmManagerService::convertData( + int uniqueId, int convertId, const DrmBuffer* inputData) { + LOGV("convertData"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeInt32(convertId); + data.writeInt32(inputData->length); + data.write(inputData->data, inputData->length); + + remote()->transact(CONVERT_DATA, data, &reply); + + DrmConvertedStatus* drmConvertedStatus = NULL; + + if (0 != reply.dataAvail()) { + //Filling DRM Converted Status + const int statusCode = reply.readInt32(); + const int offset = reply.readInt32(); + + DrmBuffer* convertedData = NULL; + if (0 != reply.dataAvail()) { + const int bufferSize = reply.readInt32(); + char* data = NULL; + if (0 < bufferSize) { + data = new char[bufferSize]; + reply.read(data, bufferSize); + } + convertedData = new DrmBuffer(data, bufferSize); + } + drmConvertedStatus = new DrmConvertedStatus(statusCode, convertedData, offset); + } + return drmConvertedStatus; +} + +DrmConvertedStatus* BpDrmManagerService::closeConvertSession(int uniqueId, int convertId) { + LOGV("closeConvertSession"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + data.writeInt32(convertId); + + remote()->transact(CLOSE_CONVERT_SESSION, data, &reply); + + DrmConvertedStatus* drmConvertedStatus = NULL; + + if (0 != reply.dataAvail()) { + //Filling DRM Converted Status + const int statusCode = reply.readInt32(); + const int offset = reply.readInt32(); + + DrmBuffer* convertedData = NULL; + if (0 != reply.dataAvail()) { + const int bufferSize = reply.readInt32(); + char* data = NULL; + if (0 < bufferSize) { + data = new char[bufferSize]; + reply.read(data, bufferSize); + } + convertedData = new DrmBuffer(data, bufferSize); + } + drmConvertedStatus = new DrmConvertedStatus(statusCode, convertedData, offset); + } + return drmConvertedStatus; +} + +status_t BpDrmManagerService::getAllSupportInfo( + int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray) { + LOGV("Get All Support Info"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + remote()->transact(GET_ALL_SUPPORT_INFO, data, &reply); + + //Filling DRM Support Info + const int arraySize = reply.readInt32(); + if (0 < arraySize) { + *drmSupportInfoArray = new DrmSupportInfo[arraySize]; + + for (int index = 0; index < arraySize; ++index) { + DrmSupportInfo drmSupportInfo; + + const int fileSuffixVectorSize = reply.readInt32(); + for (int i = 0; i < fileSuffixVectorSize; ++i) { + drmSupportInfo.addFileSuffix(reply.readString8()); + } + + const int mimeTypeVectorSize = reply.readInt32(); + for (int i = 0; i < mimeTypeVectorSize; ++i) { + drmSupportInfo.addMimeType(reply.readString8()); + } + + drmSupportInfo.setDescription(reply.readString8()); + (*drmSupportInfoArray)[index] = drmSupportInfo; + } + } + *length = arraySize; + return reply.readInt32(); +} + +DecryptHandle* BpDrmManagerService::openDecryptSession( + int uniqueId, int fd, int offset, int length) { + LOGV("Entering BpDrmManagerService::openDecryptSession"); + Parcel data, reply; + + const String16 interfaceDescriptor = IDrmManagerService::getInterfaceDescriptor(); + LOGV("BpDrmManagerService::openDecryptSession: InterfaceDescriptor name is %s", + interfaceDescriptor.string()); + data.writeInterfaceToken(interfaceDescriptor); + data.writeInt32(uniqueId); + data.writeFileDescriptor(fd); + data.writeInt32(offset); + data.writeInt32(length); + + LOGV("try to invoke remote onTransact() with code OPEN_DECRYPT_SESSION"); + remote()->transact(OPEN_DECRYPT_SESSION, data, &reply); + + DecryptHandle* handle = NULL; + if (0 != reply.dataAvail()) { + handle = new DecryptHandle(); + handle->decryptId = reply.readInt32(); + handle->mimeType = reply.readString8(); + handle->decryptApiType = reply.readInt32(); + handle->status = reply.readInt32(); + handle->decryptInfo = NULL; + if (0 != reply.dataAvail()) { + handle->decryptInfo = new DecryptInfo(); + handle->decryptInfo->decryptBufferLength = reply.readInt32(); + } + } else { + LOGE("no decryptHandle is generated in service side"); + } + return handle; +} + +void BpDrmManagerService::closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) { + LOGV("closeDecryptSession"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeInt32(decryptHandle->decryptId); + data.writeString8(decryptHandle->mimeType); + data.writeInt32(decryptHandle->decryptApiType); + data.writeInt32(decryptHandle->status); + + if (NULL != decryptHandle->decryptInfo) { + data.writeInt32(decryptHandle->decryptInfo->decryptBufferLength); + } else { + data.writeInt32(INVALID_BUFFER_LENGTH); + } + + remote()->transact(CLOSE_DECRYPT_SESSION, data, &reply); + + if (NULL != decryptHandle->decryptInfo) { + LOGV("deleting decryptInfo"); + delete decryptHandle->decryptInfo; decryptHandle->decryptInfo = NULL; + } + delete decryptHandle; decryptHandle = NULL; +} + +void BpDrmManagerService::initializeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo) { + LOGV("initializeDecryptUnit"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeInt32(decryptHandle->decryptId); + data.writeString8(decryptHandle->mimeType); + data.writeInt32(decryptHandle->decryptApiType); + data.writeInt32(decryptHandle->status); + + if (NULL != decryptHandle->decryptInfo) { + data.writeInt32(decryptHandle->decryptInfo->decryptBufferLength); + } else { + data.writeInt32(INVALID_BUFFER_LENGTH); + } + data.writeInt32(decryptUnitId); + + data.writeInt32(headerInfo->length); + data.write(headerInfo->data, headerInfo->length); + + remote()->transact(INITIALIZE_DECRYPT_UNIT, data, &reply); +} + +status_t BpDrmManagerService::decrypt( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId, + const DrmBuffer* encBuffer, DrmBuffer** decBuffer) { + LOGV("decrypt"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeInt32(decryptHandle->decryptId); + data.writeString8(decryptHandle->mimeType); + data.writeInt32(decryptHandle->decryptApiType); + data.writeInt32(decryptHandle->status); + + if (NULL != decryptHandle->decryptInfo) { + data.writeInt32(decryptHandle->decryptInfo->decryptBufferLength); + } else { + data.writeInt32(INVALID_BUFFER_LENGTH); + } + + data.writeInt32(decryptUnitId); + data.writeInt32((*decBuffer)->length); + + data.writeInt32(encBuffer->length); + data.write(encBuffer->data, encBuffer->length); + + remote()->transact(DECRYPT, data, &reply); + + const status_t status = reply.readInt32(); + LOGV("Return value of decrypt() is %d", status); + + const int size = reply.readInt32(); + (*decBuffer)->length = size; + reply.read((void *)(*decBuffer)->data, size); + + return status; +} + +void BpDrmManagerService::finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) { + LOGV("finalizeDecryptUnit"); + Parcel data, reply; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeInt32(decryptHandle->decryptId); + data.writeString8(decryptHandle->mimeType); + data.writeInt32(decryptHandle->decryptApiType); + data.writeInt32(decryptHandle->status); + + if (NULL != decryptHandle->decryptInfo) { + data.writeInt32(decryptHandle->decryptInfo->decryptBufferLength); + } else { + data.writeInt32(INVALID_BUFFER_LENGTH); + } + + data.writeInt32(decryptUnitId); + + remote()->transact(FINALIZE_DECRYPT_UNIT, data, &reply); +} + +ssize_t BpDrmManagerService::pread( + int uniqueId, DecryptHandle* decryptHandle, void* buffer, + ssize_t numBytes, off_t offset) { + LOGV("read"); + Parcel data, reply; + int result; + + data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor()); + data.writeInt32(uniqueId); + + data.writeInt32(decryptHandle->decryptId); + data.writeString8(decryptHandle->mimeType); + data.writeInt32(decryptHandle->decryptApiType); + data.writeInt32(decryptHandle->status); + + if (NULL != decryptHandle->decryptInfo) { + data.writeInt32(decryptHandle->decryptInfo->decryptBufferLength); + } else { + data.writeInt32(INVALID_BUFFER_LENGTH); + } + + data.writeInt32(numBytes); + data.writeInt32(offset); + + remote()->transact(PREAD, data, &reply); + result = reply.readInt32(); + if (0 < result) { + reply.read(buffer, result); + } + return result; +} + +IMPLEMENT_META_INTERFACE(DrmManagerService, "drm.IDrmManagerService"); + +status_t BnDrmManagerService::onTransact( + uint32_t code, const Parcel& data, + Parcel* reply, uint32_t flags) { + LOGV("Entering BnDrmManagerService::onTransact with code %d", code); + + switch (code) { + case LOAD_PLUGINS: + { + LOGV("BnDrmManagerService::onTransact :LOAD_PLUGINS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + status_t status = loadPlugIns(data.readInt32()); + + reply->writeInt32(status); + return DRM_NO_ERROR; + + } + + case LOAD_PLUGINS_FROM_PATH: + { + LOGV("BnDrmManagerService::onTransact :LOAD_PLUGINS_FROM_PATH"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + status_t status = loadPlugIns(data.readInt32(), data.readString8()); + + reply->writeInt32(status); + return DRM_NO_ERROR; + } + + case SET_DRM_SERVICE_LISTENER: + { + LOGV("BnDrmManagerService::onTransact :SET_DRM_SERVICE_LISTENER"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + const sp drmServiceListener + = interface_cast (data.readStrongBinder()); + + status_t status = setDrmServiceListener(uniqueId, drmServiceListener); + + reply->writeInt32(status); + return DRM_NO_ERROR; + } + + case UNLOAD_PLUGINS: + { + LOGV("BnDrmManagerService::onTransact :UNLOAD_PLUGINS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + status_t status = unloadPlugIns(data.readInt32()); + + reply->writeInt32(status); + return DRM_NO_ERROR; + } + + case INSTALL_DRM_ENGINE: + { + LOGV("BnDrmManagerService::onTransact :INSTALL_DRM_ENGINE"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + status_t status = installDrmEngine(data.readInt32(), data.readString8()); + + reply->writeInt32(status); + return DRM_NO_ERROR; + } + + case GET_CONSTRAINTS_FROM_CONTENT: + { + LOGV("BnDrmManagerService::onTransact :GET_CONSTRAINTS_FROM_CONTENT"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + const String8 path = data.readString8(); + + DrmConstraints* drmConstraints = getConstraints(uniqueId, &path, data.readInt32()); + + if (NULL != drmConstraints) { + //Filling DRM Constraints contents + reply->writeInt32(drmConstraints->getCount()); + + DrmConstraints::KeyIterator keyIt = drmConstraints->keyIterator(); + while (keyIt.hasNext()) { + const String8 key = keyIt.next(); + reply->writeString8(key); + const char* value = drmConstraints->getAsByteArray(&key); + int bufferSize = 0; + if (NULL != value) { + bufferSize = strlen(value); + } + reply->writeInt32(bufferSize + 1); + reply->write(value, bufferSize + 1); + } + } + delete drmConstraints; drmConstraints = NULL; + return DRM_NO_ERROR; + } + + case CAN_HANDLE: + { + LOGV("BnDrmManagerService::onTransact :CAN_HANDLE"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + const String8 path = data.readString8(); + const String8 mimeType = data.readString8(); + + bool result = canHandle(uniqueId, path, mimeType); + + reply->writeInt32(result); + return DRM_NO_ERROR; + } + + case PROCESS_DRM_INFO: + { + LOGV("BnDrmManagerService::onTransact :PROCESS_DRM_INFO"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + //Filling DRM info + const int infoType = data.readInt32(); + const int bufferSize = data.readInt32(); + char* buffer = NULL; + if (0 < bufferSize) { + buffer = (char *)data.readInplace(bufferSize); + } + const DrmBuffer drmBuffer(buffer, bufferSize); + DrmInfo* drmInfo = new DrmInfo(infoType, drmBuffer, data.readString8()); + + const int size = data.readInt32(); + for (int index = 0; index < size; ++index) { + const String8 key(data.readString8()); + const String8 value(data.readString8()); + drmInfo->put(key, (value == String8("NULL")) ? String8("") : value); + } + + DrmInfoStatus* drmInfoStatus = processDrmInfo(uniqueId, drmInfo); + + if (NULL != drmInfoStatus) { + //Filling DRM Info Status contents + reply->writeInt32(drmInfoStatus->statusCode); + reply->writeString8(drmInfoStatus->mimeType); + + if (NULL != drmInfoStatus->drmBuffer) { + const DrmBuffer* drmBuffer = drmInfoStatus->drmBuffer; + const int bufferSize = drmBuffer->length; + reply->writeInt32(bufferSize); + if (0 < bufferSize) { + reply->write(drmBuffer->data, bufferSize); + } + delete [] drmBuffer->data; + delete drmBuffer; drmBuffer = NULL; + } + } + delete drmInfo; drmInfo = NULL; + delete drmInfoStatus; drmInfoStatus = NULL; + return DRM_NO_ERROR; + } + + case ACQUIRE_DRM_INFO: + { + LOGV("BnDrmManagerService::onTransact :ACQUIRE_DRM_INFO"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + //Filling DRM info Request + DrmInfoRequest* drmInfoRequest = new DrmInfoRequest(data.readInt32(), data.readString8()); + + const int size = data.readInt32(); + for (int index = 0; index < size; ++index) { + const String8 key(data.readString8()); + const String8 value(data.readString8()); + drmInfoRequest->put(key, (value == String8("NULL")) ? String8("") : value); + } + + DrmInfo* drmInfo = acquireDrmInfo(uniqueId, drmInfoRequest); + + if (NULL != drmInfo) { + //Filling DRM Info + const DrmBuffer drmBuffer = drmInfo->getData(); + reply->writeInt32(drmInfo->getInfoType()); + + const int bufferSize = drmBuffer.length; + reply->writeInt32(bufferSize); + if (0 < bufferSize) { + reply->write(drmBuffer.data, bufferSize); + } + reply->writeString8(drmInfo->getMimeType()); + reply->writeInt32(drmInfo->getCount()); + + DrmInfo::KeyIterator keyIt = drmInfo->keyIterator(); + while (keyIt.hasNext()) { + const String8 key = keyIt.next(); + reply->writeString8(key); + const String8 value = drmInfo->get(key); + reply->writeString8((value == String8("")) ? String8("NULL") : value); + } + delete [] drmBuffer.data; + } + delete drmInfoRequest; drmInfoRequest = NULL; + delete drmInfo; drmInfo = NULL; + return DRM_NO_ERROR; + } + + case SAVE_RIGHTS: + { + LOGV("BnDrmManagerService::onTransact :SAVE_RIGHTS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + //Filling DRM Rights + const int bufferSize = data.readInt32(); + const DrmBuffer drmBuffer((char *)data.readInplace(bufferSize), bufferSize); + + const String8 mimeType(data.readString8()); + const String8 accountId(data.readString8()); + const String8 subscriptionId(data.readString8()); + const String8 rightsPath(data.readString8()); + const String8 contentPath(data.readString8()); + + DrmRights drmRights(drmBuffer, + ((mimeType == String8("NULL")) ? String8("") : mimeType), + ((accountId == String8("NULL")) ? String8("") : accountId), + ((subscriptionId == String8("NULL")) ? String8("") : subscriptionId)); + + saveRights(uniqueId, drmRights, + ((rightsPath == String8("NULL")) ? String8("") : rightsPath), + ((contentPath == String8("NULL")) ? String8("") : contentPath)); + + return DRM_NO_ERROR; + } + + case GET_ORIGINAL_MIMETYPE: + { + LOGV("BnDrmManagerService::onTransact :GET_ORIGINAL_MIMETYPE"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const String8 originalMimeType = getOriginalMimeType(data.readInt32(), data.readString8()); + + reply->writeString8(originalMimeType); + return DRM_NO_ERROR; + } + + case GET_DRM_OBJECT_TYPE: + { + LOGV("BnDrmManagerService::onTransact :GET_DRM_OBJECT_TYPE"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int drmObjectType + = getDrmObjectType(data.readInt32(), data.readString8(), data.readString8()); + + reply->writeInt32(drmObjectType); + return DRM_NO_ERROR; + } + + case CHECK_RIGHTS_STATUS: + { + LOGV("BnDrmManagerService::onTransact :CHECK_RIGHTS_STATUS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int result + = checkRightsStatus(data.readInt32(), data.readString8(), data.readInt32()); + + reply->writeInt32(result); + return DRM_NO_ERROR; + } + + case CONSUME_RIGHTS: + { + LOGV("BnDrmManagerService::onTransact :CONSUME_RIGHTS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + DecryptHandle handle; + handle.decryptId = data.readInt32(); + handle.mimeType = data.readString8(); + handle.decryptApiType = data.readInt32(); + handle.status = data.readInt32(); + handle.decryptInfo = NULL; + + const int bufferLength = data.readInt32(); + if (INVALID_BUFFER_LENGTH != bufferLength) { + handle.decryptInfo = new DecryptInfo(); + handle.decryptInfo->decryptBufferLength = bufferLength; + } + + consumeRights(uniqueId, &handle, data.readInt32(), static_cast(data.readInt32())); + + delete handle.decryptInfo; handle.decryptInfo = NULL; + return DRM_NO_ERROR; + } + + case SET_PLAYBACK_STATUS: + { + LOGV("BnDrmManagerService::onTransact :SET_PLAYBACK_STATUS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + DecryptHandle handle; + handle.decryptId = data.readInt32(); + handle.mimeType = data.readString8(); + handle.decryptApiType = data.readInt32(); + handle.status = data.readInt32(); + handle.decryptInfo = NULL; + + const int bufferLength = data.readInt32(); + if (INVALID_BUFFER_LENGTH != bufferLength) { + handle.decryptInfo = new DecryptInfo(); + handle.decryptInfo->decryptBufferLength = bufferLength; + } + + setPlaybackStatus(uniqueId, &handle, data.readInt32(), data.readInt32()); + + delete handle.decryptInfo; handle.decryptInfo = NULL; + return DRM_NO_ERROR; + } + + case VALIDATE_ACTION: + { + LOGV("BnDrmManagerService::onTransact :VALIDATE_ACTION"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + bool result = validateAction( + data.readInt32(), + data.readString8(), + data.readInt32(), + ActionDescription(data.readInt32(), data.readInt32())); + + reply->writeInt32(result); + return DRM_NO_ERROR; + } + + case REMOVE_RIGHTS: + { + LOGV("BnDrmManagerService::onTransact :REMOVE_RIGHTS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + removeRights(data.readInt32(), data.readString8()); + + return DRM_NO_ERROR; + } + + case REMOVE_ALL_RIGHTS: + { + LOGV("BnDrmManagerService::onTransact :REMOVE_ALL_RIGHTS"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + removeAllRights(data.readInt32()); + + return DRM_NO_ERROR; + } + + case OPEN_CONVERT_SESSION: + { + LOGV("BnDrmManagerService::onTransact :OPEN_CONVERT_SESSION"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int convertId = openConvertSession(data.readInt32(), data.readString8()); + + reply->writeInt32(convertId); + return DRM_NO_ERROR; + } + + case CONVERT_DATA: + { + LOGV("BnDrmManagerService::onTransact :CONVERT_DATA"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + const int convertId = data.readInt32(); + + //Filling input data + const int bufferSize = data.readInt32(); + DrmBuffer* inputData = new DrmBuffer((char *)data.readInplace(bufferSize), bufferSize); + + DrmConvertedStatus* drmConvertedStatus = convertData(uniqueId, convertId, inputData); + + if (NULL != drmConvertedStatus) { + //Filling Drm Converted Ststus + reply->writeInt32(drmConvertedStatus->statusCode); + reply->writeInt32(drmConvertedStatus->offset); + + if (NULL != drmConvertedStatus->convertedData) { + const DrmBuffer* convertedData = drmConvertedStatus->convertedData; + const int bufferSize = convertedData->length; + reply->writeInt32(bufferSize); + if (0 < bufferSize) { + reply->write(convertedData->data, bufferSize); + } + delete [] convertedData->data; + delete convertedData; convertedData = NULL; + } + } + delete inputData; inputData = NULL; + delete drmConvertedStatus; drmConvertedStatus = NULL; + return DRM_NO_ERROR; + } + + case CLOSE_CONVERT_SESSION: + { + LOGV("BnDrmManagerService::onTransact :CLOSE_CONVERT_SESSION"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + DrmConvertedStatus* drmConvertedStatus + = closeConvertSession(data.readInt32(), data.readInt32()); + + if (NULL != drmConvertedStatus) { + //Filling Drm Converted Ststus + reply->writeInt32(drmConvertedStatus->statusCode); + reply->writeInt32(drmConvertedStatus->offset); + + if (NULL != drmConvertedStatus->convertedData) { + const DrmBuffer* convertedData = drmConvertedStatus->convertedData; + const int bufferSize = convertedData->length; + reply->writeInt32(bufferSize); + if (0 < bufferSize) { + reply->write(convertedData->data, bufferSize); + } + delete [] convertedData->data; + delete convertedData; convertedData = NULL; + } + } + delete drmConvertedStatus; drmConvertedStatus = NULL; + return DRM_NO_ERROR; + } + + case GET_ALL_SUPPORT_INFO: + { + LOGV("BnDrmManagerService::onTransact :GET_ALL_SUPPORT_INFO"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + int length = 0; + DrmSupportInfo* drmSupportInfoArray = NULL; + + status_t status = getAllSupportInfo(uniqueId, &length, &drmSupportInfoArray); + + reply->writeInt32(length); + for (int i = 0; i < length; ++i) { + DrmSupportInfo drmSupportInfo = drmSupportInfoArray[i]; + + reply->writeInt32(drmSupportInfo.getFileSuffixCount()); + DrmSupportInfo::FileSuffixIterator fileSuffixIt + = drmSupportInfo.getFileSuffixIterator(); + while (fileSuffixIt.hasNext()) { + reply->writeString8(fileSuffixIt.next()); + } + + reply->writeInt32(drmSupportInfo.getMimeTypeCount()); + DrmSupportInfo::MimeTypeIterator mimeTypeIt = drmSupportInfo.getMimeTypeIterator(); + while (mimeTypeIt.hasNext()) { + reply->writeString8(mimeTypeIt.next()); + } + reply->writeString8(drmSupportInfo.getDescription()); + } + delete [] drmSupportInfoArray; drmSupportInfoArray = NULL; + reply->writeInt32(status); + return DRM_NO_ERROR; + } + + case OPEN_DECRYPT_SESSION: + { + LOGV("BnDrmManagerService::onTransact :OPEN_DECRYPT_SESSION"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + const int fd = data.readFileDescriptor(); + + DecryptHandle* handle + = openDecryptSession(uniqueId, fd, data.readInt32(), data.readInt32()); + + if (NULL != handle) { + reply->writeInt32(handle->decryptId); + reply->writeString8(handle->mimeType); + reply->writeInt32(handle->decryptApiType); + reply->writeInt32(handle->status); + if (NULL != handle->decryptInfo) { + reply->writeInt32(handle->decryptInfo->decryptBufferLength); + delete handle->decryptInfo; handle->decryptInfo = NULL; + } + } else { + LOGE("NULL decryptHandle is returned"); + } + delete handle; handle = NULL; + return DRM_NO_ERROR; + } + + case CLOSE_DECRYPT_SESSION: + { + LOGV("BnDrmManagerService::onTransact :CLOSE_DECRYPT_SESSION"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + DecryptHandle* handle = new DecryptHandle(); + handle->decryptId = data.readInt32(); + handle->mimeType = data.readString8(); + handle->decryptApiType = data.readInt32(); + handle->status = data.readInt32(); + handle->decryptInfo = NULL; + + const int bufferLength = data.readInt32(); + if (INVALID_BUFFER_LENGTH != bufferLength) { + handle->decryptInfo = new DecryptInfo(); + handle->decryptInfo->decryptBufferLength = bufferLength; + } + + closeDecryptSession(uniqueId, handle); + return DRM_NO_ERROR; + } + + case INITIALIZE_DECRYPT_UNIT: + { + LOGV("BnDrmManagerService::onTransact :INITIALIZE_DECRYPT_UNIT"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + DecryptHandle handle; + handle.decryptId = data.readInt32(); + handle.mimeType = data.readString8(); + handle.decryptApiType = data.readInt32(); + handle.status = data.readInt32(); + handle.decryptInfo = NULL; + + const int bufferLength = data.readInt32(); + if (INVALID_BUFFER_LENGTH != bufferLength) { + handle.decryptInfo = new DecryptInfo(); + handle.decryptInfo->decryptBufferLength = bufferLength; + } + const int decryptUnitId = data.readInt32(); + + //Filling Header info + const int bufferSize = data.readInt32(); + DrmBuffer* headerInfo = NULL; + headerInfo = new DrmBuffer((char *)data.readInplace(bufferSize), bufferSize); + + initializeDecryptUnit(uniqueId, &handle, decryptUnitId, headerInfo); + + delete handle.decryptInfo; handle.decryptInfo = NULL; + delete headerInfo; headerInfo = NULL; + return DRM_NO_ERROR; + } + + case DECRYPT: + { + LOGV("BnDrmManagerService::onTransact :DECRYPT"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + DecryptHandle handle; + handle.decryptId = data.readInt32(); + handle.mimeType = data.readString8(); + handle.decryptApiType = data.readInt32(); + handle.status = data.readInt32(); + handle.decryptInfo = NULL; + + const int bufferLength = data.readInt32(); + if (INVALID_BUFFER_LENGTH != bufferLength) { + handle.decryptInfo = new DecryptInfo(); + handle.decryptInfo->decryptBufferLength = bufferLength; + } + const int decryptUnitId = data.readInt32(); + const int decBufferSize = data.readInt32(); + + const int encBufferSize = data.readInt32(); + DrmBuffer* encBuffer + = new DrmBuffer((char *)data.readInplace(encBufferSize), encBufferSize); + + char* buffer = NULL; + buffer = new char[decBufferSize]; + DrmBuffer* decBuffer = new DrmBuffer(buffer, decBufferSize); + + const status_t status = decrypt(uniqueId, &handle, decryptUnitId, encBuffer, &decBuffer); + + reply->writeInt32(status); + + const int size = decBuffer->length; + reply->writeInt32(size); + reply->write(decBuffer->data, size); + + delete handle.decryptInfo; handle.decryptInfo = NULL; + delete encBuffer; encBuffer = NULL; + delete decBuffer; decBuffer = NULL; + delete [] buffer; buffer = NULL; + return DRM_NO_ERROR; + } + + case FINALIZE_DECRYPT_UNIT: + { + LOGV("BnDrmManagerService::onTransact :FINALIZE_DECRYPT_UNIT"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + DecryptHandle handle; + handle.decryptId = data.readInt32(); + handle.mimeType = data.readString8(); + handle.decryptApiType = data.readInt32(); + handle.status = data.readInt32(); + handle.decryptInfo = NULL; + + const int bufferLength = data.readInt32(); + if (INVALID_BUFFER_LENGTH != bufferLength) { + handle.decryptInfo = new DecryptInfo(); + handle.decryptInfo->decryptBufferLength = bufferLength; + } + + finalizeDecryptUnit(uniqueId, &handle, data.readInt32()); + + delete handle.decryptInfo; handle.decryptInfo = NULL; + return DRM_NO_ERROR; + } + + case PREAD: + { + LOGV("BnDrmManagerService::onTransact :READ"); + CHECK_INTERFACE(IDrmManagerService, data, reply); + + const int uniqueId = data.readInt32(); + + DecryptHandle handle; + handle.decryptId = data.readInt32(); + handle.mimeType = data.readString8(); + handle.decryptApiType = data.readInt32(); + handle.status = data.readInt32(); + handle.decryptInfo = NULL; + + const int bufferLength = data.readInt32(); + if (INVALID_BUFFER_LENGTH != bufferLength) { + handle.decryptInfo = new DecryptInfo(); + handle.decryptInfo->decryptBufferLength = bufferLength; + } + + const int numBytes = data.readInt32(); + char* buffer = new char[numBytes]; + + const off_t offset = data.readInt32(); + + ssize_t result = pread(uniqueId, &handle, buffer, numBytes, offset); + reply->writeInt32(result); + if (0 < result) { + reply->write(buffer, result); + } + + delete handle.decryptInfo; handle.decryptInfo = NULL; + delete [] buffer, buffer = NULL; + return DRM_NO_ERROR; + } + + default: + return BBinder::onTransact(code, data, reply, flags); + } +} + diff --git a/drm/common/IDrmServiceListener.cpp b/drm/common/IDrmServiceListener.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0a69115a4845d75564db1cd8c9e566043e068020 --- /dev/null +++ b/drm/common/IDrmServiceListener.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "IDrmServiceListener" +#include + +#include +#include +#include +#include +#include +#include +#include "IDrmServiceListener.h" + +using namespace android; + +status_t BpDrmServiceListener::notify(const DrmInfoEvent& event) { + Parcel data, reply; + + data.writeInterfaceToken(IDrmServiceListener::getInterfaceDescriptor()); + data.writeInt32(event.getUniqueId()); + data.writeInt32(event.getType()); + data.writeString8(event.getMessage()); + + remote()->transact(NOTIFY, data, &reply); + return reply.readInt32(); +} + +IMPLEMENT_META_INTERFACE(DrmServiceListener, "drm.IDrmServiceListener"); + +status_t BnDrmServiceListener::onTransact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { + + switch (code) { + case NOTIFY: + { + CHECK_INTERFACE(IDrmServiceListener, data, reply); + int uniqueId = data.readInt32(); + int type = data.readInt32(); + const String8& message = data.readString8(); + + status_t status = notify(DrmInfoEvent(uniqueId, type, message)); + reply->writeInt32(status); + + return DRM_NO_ERROR; + } + default: + return BBinder::onTransact(code, data, reply, flags); + } +} + diff --git a/drm/common/ReadWriteUtils.cpp b/drm/common/ReadWriteUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4319c1ce902f012df97c8b17e56a638504e2bae6 --- /dev/null +++ b/drm/common/ReadWriteUtils.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace android; + +#define FAILURE -1 + +String8 ReadWriteUtils::readBytes(const String8& filePath) { + FILE* file = NULL; + file = fopen(filePath.string(), "r"); + + String8 string(""); + if (NULL != file) { + int fd = fileno(file); + struct stat sb; + + if (fstat(fd, &sb) == 0 && sb.st_size > 0) { + FileMap* fileMap = new FileMap(); + if (fileMap->create(filePath.string(), fd, 0, sb.st_size, true)) { + char* addr = (char*)fileMap->getDataPtr(); + string.append(addr, sb.st_size); + fileMap->release(); + } + } + fclose(file); + } + return string; +} + +void ReadWriteUtils::writeToFile(const String8& filePath, const String8& data) { + FILE* file = NULL; + file = fopen(filePath.string(), "w+"); + + if (NULL != file) { + int fd = fileno(file); + + int size = data.size(); + if (FAILURE != ftruncate(fd, size)) { + FileMap* fileMap = NULL; + fileMap = new FileMap(); + if (fileMap->create(filePath.string(), fd, 0, size, false)) { + char* addr = (char*)fileMap->getDataPtr(); + memcpy(addr, data.string(), size); + fileMap->release(); + } + } + fclose(file); + } +} + +void ReadWriteUtils::appendToFile(const String8& filePath, const String8& data) { + FILE* file = NULL; + file = fopen(filePath.string(), "a+"); + + if (NULL != file) { + int fd = fileno(file); + + int offset = lseek(fd, 0, SEEK_END); + if (FAILURE != offset) { + int newEntrySize = data.size(); + int fileSize = offset + newEntrySize; + + if (FAILURE != ftruncate(fd, fileSize)) { + FileMap* fileMap = NULL; + fileMap = new FileMap(); + if (fileMap->create(filePath.string(), fd, offset, fileSize, false)) { + char* addr = (char*)fileMap->getDataPtr(); + memcpy(addr, data.string(), data.size()); + fileMap->release(); + } + } + } + fclose(file); + } +} + diff --git a/drm/drmioserver/Android.mk b/drm/drmioserver/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..11571c7904bcdb10012f714c3f2630d4657a87a1 --- /dev/null +++ b/drm/drmioserver/Android.mk @@ -0,0 +1,43 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + main_drmioserver.cpp \ + DrmIOService.cpp + +LOCAL_SHARED_LIBRARIES := \ + libutils \ + libbinder + +ifeq ($(TARGET_SIMULATOR),true) + LOCAL_LDLIBS += -ldl +else + LOCAL_SHARED_LIBRARIES += libdl +endif + +LOCAL_STATIC_LIBRARIES := libdrmframeworkcommon + +LOCAL_C_INCLUDES := \ + $(TOP)/frameworks/base/drm/libdrmframework/include \ + $(TOP)/frameworks/base/include + +LOCAL_MODULE:= drmioserver + +LOCAL_MODULE_TAGS := optional + +include $(BUILD_EXECUTABLE) diff --git a/drm/drmioserver/DrmIOService.cpp b/drm/drmioserver/DrmIOService.cpp new file mode 100644 index 0000000000000000000000000000000000000000..67cfd39a6377bfe53546f3b848285de8054b8dd6 --- /dev/null +++ b/drm/drmioserver/DrmIOService.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmIOService" +#include + +#include +#include "DrmIOService.h" +#include "ReadWriteUtils.h" + +using namespace android; + +void DrmIOService::instantiate() { + LOGV("instantiate"); + defaultServiceManager()->addService(String16("drm.drmIOService"), new DrmIOService()); +} + +DrmIOService::DrmIOService() { + LOGV("created"); +} + +DrmIOService::~DrmIOService() { + LOGV("Destroyed"); +} + +void DrmIOService::writeToFile(const String8& filePath, const String8& dataBuffer) { + LOGV("Entering writeToFile"); + ReadWriteUtils::writeToFile(filePath, dataBuffer); +} + +String8 DrmIOService::readFromFile(const String8& filePath) { + LOGV("Entering readFromFile"); + return ReadWriteUtils::readBytes(filePath); +} + diff --git a/drm/drmioserver/main_drmioserver.cpp b/drm/drmioserver/main_drmioserver.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7ed048d6738abbb2b1e4f9f98eb15a3d387aad16 --- /dev/null +++ b/drm/drmioserver/main_drmioserver.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace android; + +int main(int argc, char** argv) +{ + sp proc(ProcessState::self()); + sp sm = defaultServiceManager(); + LOGI("ServiceManager: %p", sm.get()); + DrmIOService::instantiate(); + ProcessState::self()->startThreadPool(); + IPCThreadState::self()->joinThreadPool(); +} + diff --git a/drm/drmserver/Android.mk b/drm/drmserver/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..5df2ff87c17eb9a157b694518f116dc0ad9cba2d --- /dev/null +++ b/drm/drmserver/Android.mk @@ -0,0 +1,46 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + main_drmserver.cpp \ + DrmManager.cpp \ + DrmManagerService.cpp \ + StringTokenizer.cpp + +LOCAL_SHARED_LIBRARIES := \ + libutils \ + libbinder + +ifeq ($(TARGET_SIMULATOR),true) + LOCAL_LDLIBS += -ldl +else + LOCAL_SHARED_LIBRARIES += libdl +endif + +LOCAL_STATIC_LIBRARIES := libdrmframeworkcommon + +LOCAL_C_INCLUDES := \ + $(TOP)/frameworks/base/include \ + $(TOP)/frameworks/base/drm/libdrmframework/include \ + $(TOP)/frameworks/base/drm/libdrmframework/plugins/common/include + +LOCAL_MODULE:= drmserver + +LOCAL_MODULE_TAGS := optional + +include $(BUILD_EXECUTABLE) diff --git a/drm/drmserver/DrmManager.cpp b/drm/drmserver/DrmManager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..44886f9a5cc9093884576a7620680f8e8b9d0755 --- /dev/null +++ b/drm/drmserver/DrmManager.cpp @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmManager(Native)" +#include "utils/Log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DrmManager.h" +#include "ReadWriteUtils.h" + +#define DECRYPT_FILE_ERROR -1 + +using namespace android; + +const String8 DrmManager::EMPTY_STRING(""); + +DrmManager::DrmManager() : + mDecryptSessionId(0), + mConvertId(0) { + +} + +DrmManager::~DrmManager() { + +} + +status_t DrmManager::loadPlugIns(int uniqueId) { + String8 pluginDirPath("/system/lib/drm/plugins/native"); + return loadPlugIns(uniqueId, pluginDirPath); +} + +status_t DrmManager::loadPlugIns(int uniqueId, const String8& plugInDirPath) { + if (mSupportInfoToPlugInIdMap.isEmpty()) { + mPlugInManager.loadPlugIns(plugInDirPath); + + initializePlugIns(uniqueId); + + populate(uniqueId); + } else { + initializePlugIns(uniqueId); + } + + return DRM_NO_ERROR; +} + +status_t DrmManager::setDrmServiceListener( + int uniqueId, const sp& drmServiceListener) { + Mutex::Autolock _l(mLock); + mServiceListeners.add(uniqueId, drmServiceListener); + return DRM_NO_ERROR; +} + +status_t DrmManager::unloadPlugIns(int uniqueId) { + Vector plugInIdList = mPlugInManager.getPlugInIdList(); + + for (unsigned int index = 0; index < plugInIdList.size(); index++) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInIdList.itemAt(index)); + rDrmEngine.terminate(uniqueId); + } + + mConvertSessionMap.clear(); + mDecryptSessionMap.clear(); + mSupportInfoToPlugInIdMap.clear(); + mPlugInManager.unloadPlugIns(); + return DRM_NO_ERROR; +} + +DrmConstraints* DrmManager::getConstraints(int uniqueId, const String8* path, const int action) { + const String8 plugInId = getSupportedPlugInIdFromPath(uniqueId, *path); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + return rDrmEngine.getConstraints(uniqueId, path, action); + } + return NULL; +} + +status_t DrmManager::installDrmEngine(int uniqueId, const String8& absolutePath) { + mPlugInManager.loadPlugIn(absolutePath); + + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(absolutePath); + rDrmEngine.initialize(uniqueId); + rDrmEngine.setOnInfoListener(uniqueId, this); + + DrmSupportInfo* info = rDrmEngine.getSupportInfo(uniqueId); + mSupportInfoToPlugInIdMap.add(*info, absolutePath); + + return DRM_NO_ERROR; +} + +bool DrmManager::canHandle(int uniqueId, const String8& path, const String8& mimeType) { + const String8 plugInId = getSupportedPlugInId(mimeType); + bool result = (EMPTY_STRING != plugInId) ? true : false; + + if (NULL != path) { + if (result) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + result = rDrmEngine.canHandle(uniqueId, path); + } else { + result = canHandle(uniqueId, path); + } + } + return result; +} + +DrmInfoStatus* DrmManager::processDrmInfo(int uniqueId, const DrmInfo* drmInfo) { + const String8 plugInId = getSupportedPlugInId(drmInfo->getMimeType()); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + return rDrmEngine.processDrmInfo(uniqueId, drmInfo); + } + return NULL; +} + +bool DrmManager::canHandle(int uniqueId, const String8& path) { + bool result = false; + Vector plugInPathList = mPlugInManager.getPlugInIdList(); + + for (unsigned int i = 0; i < plugInPathList.size(); ++i) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInPathList[i]); + result = rDrmEngine.canHandle(uniqueId, path); + + if (result) { + break; + } + } + return result; +} + +DrmInfo* DrmManager::acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest) { + const String8 plugInId = getSupportedPlugInId(drmInfoRequest->getMimeType()); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + return rDrmEngine.acquireDrmInfo(uniqueId, drmInfoRequest); + } + return NULL; +} + +void DrmManager::saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) { + const String8 plugInId = getSupportedPlugInId(drmRights.getMimeType()); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + rDrmEngine.saveRights(uniqueId, drmRights, rightsPath, contentPath); + } +} + +String8 DrmManager::getOriginalMimeType(int uniqueId, const String8& path) { + const String8 plugInId = getSupportedPlugInIdFromPath(uniqueId, path); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + return rDrmEngine.getOriginalMimeType(uniqueId, path); + } + return EMPTY_STRING; +} + +int DrmManager::getDrmObjectType(int uniqueId, const String8& path, const String8& mimeType) { + const String8 plugInId = getSupportedPlugInId(uniqueId, path, mimeType); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + return rDrmEngine.getDrmObjectType(uniqueId, path, mimeType); + } + return DrmObjectType::UNKNOWN; +} + +int DrmManager::checkRightsStatus(int uniqueId, const String8& path, int action) { + const String8 plugInId = getSupportedPlugInIdFromPath(uniqueId, path); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + return rDrmEngine.checkRightsStatus(uniqueId, path, action); + } + return RightsStatus::RIGHTS_INVALID; +} + +void DrmManager::consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve) { + if (mDecryptSessionMap.indexOfKey(decryptHandle->decryptId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mDecryptSessionMap.valueFor(decryptHandle->decryptId); + drmEngine->consumeRights(uniqueId, decryptHandle, action, reserve); + } +} + +void DrmManager::setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position) { + + if (mDecryptSessionMap.indexOfKey(decryptHandle->decryptId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mDecryptSessionMap.valueFor(decryptHandle->decryptId); + drmEngine->setPlaybackStatus(uniqueId, decryptHandle, playbackStatus, position); + } +} + +bool DrmManager::validateAction( + int uniqueId, const String8& path, int action, const ActionDescription& description) { + const String8 plugInId = getSupportedPlugInIdFromPath(uniqueId, path); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + return rDrmEngine.validateAction(uniqueId, path, action, description); + } + return false; +} + +void DrmManager::removeRights(int uniqueId, const String8& path) { + const String8 plugInId = getSupportedPlugInIdFromPath(uniqueId, path); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + rDrmEngine.removeRights(uniqueId, path); + } +} + +void DrmManager::removeAllRights(int uniqueId) { + Vector plugInIdList = mPlugInManager.getPlugInIdList(); + + for (unsigned int index = 0; index < plugInIdList.size(); index++) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInIdList.itemAt(index)); + rDrmEngine.removeAllRights(uniqueId); + } +} + +int DrmManager::openConvertSession(int uniqueId, const String8& mimeType) { + int convertId = -1; + + const String8 plugInId = getSupportedPlugInId(mimeType); + if (EMPTY_STRING != plugInId) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + + Mutex::Autolock _l(mConvertLock); + ++mConvertId; + convertId = mConvertId; + mConvertSessionMap.add(mConvertId, &rDrmEngine); + + rDrmEngine.openConvertSession(uniqueId, mConvertId); + } + return convertId; +} + +DrmConvertedStatus* DrmManager::convertData( + int uniqueId, int convertId, const DrmBuffer* inputData) { + DrmConvertedStatus *drmConvertedStatus = NULL; + + if (mConvertSessionMap.indexOfKey(convertId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mConvertSessionMap.valueFor(convertId); + drmConvertedStatus = drmEngine->convertData(uniqueId, convertId, inputData); + } + return drmConvertedStatus; +} + +DrmConvertedStatus* DrmManager::closeConvertSession(int uniqueId, int convertId) { + DrmConvertedStatus *drmConvertedStatus = NULL; + + if (mConvertSessionMap.indexOfKey(convertId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mConvertSessionMap.valueFor(convertId); + drmConvertedStatus = drmEngine->closeConvertSession(uniqueId, convertId); + mConvertSessionMap.removeItem(convertId); + } + return drmConvertedStatus; +} + +status_t DrmManager::getAllSupportInfo( + int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray) { + Vector plugInPathList = mPlugInManager.getPlugInIdList(); + int size = plugInPathList.size(); + int validPlugins = 0; + + if (0 < size) { + Vector drmSupportInfoList; + + for (int i = 0; i < size; ++i) { + String8 plugInPath = plugInPathList[i]; + DrmSupportInfo* drmSupportInfo + = mPlugInManager.getPlugIn(plugInPath).getSupportInfo(uniqueId); + if (NULL != drmSupportInfo) { + drmSupportInfoList.add(*drmSupportInfo); + delete drmSupportInfo; drmSupportInfo = NULL; + } + } + + validPlugins = drmSupportInfoList.size(); + if (0 < validPlugins) { + *drmSupportInfoArray = new DrmSupportInfo[validPlugins]; + for (int i = 0; i < validPlugins; ++i) { + (*drmSupportInfoArray)[i] = drmSupportInfoList[i]; + } + } + } + *length = validPlugins; + return DRM_NO_ERROR; +} + +DecryptHandle* DrmManager::openDecryptSession(int uniqueId, int fd, int offset, int length) { + LOGV("Entering DrmManager::openDecryptSession"); + status_t result = DRM_ERROR_CANNOT_HANDLE; + Vector plugInIdList = mPlugInManager.getPlugInIdList(); + + DecryptHandle* handle = new DecryptHandle(); + if (NULL != handle) { + Mutex::Autolock _l(mDecryptLock); + handle->decryptId = mDecryptSessionId + 1; + + for (unsigned int index = 0; index < plugInIdList.size(); index++) { + String8 plugInId = plugInIdList.itemAt(index); + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId); + result = rDrmEngine.openDecryptSession(uniqueId, handle, fd, offset, length); + + LOGV("plug-in %s return value = %d", plugInId.string(), result); + + if (DRM_NO_ERROR == result) { + ++mDecryptSessionId; + mDecryptSessionMap.add(mDecryptSessionId, &rDrmEngine); + LOGV("plug-in %s is selected", plugInId.string()); + break; + } + } + } + + if (DRM_ERROR_CANNOT_HANDLE == result) { + delete handle; handle = NULL; + LOGE("DrmManager::openDecryptSession: no capable plug-in found"); + } + + return handle; +} + +void DrmManager::closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) { + if (mDecryptSessionMap.indexOfKey(decryptHandle->decryptId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mDecryptSessionMap.valueFor(decryptHandle->decryptId); + drmEngine->closeDecryptSession(uniqueId, decryptHandle); + + mDecryptSessionMap.removeItem(decryptHandle->decryptId); + } +} + +void DrmManager::initializeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId, const DrmBuffer* headerInfo) { + if (mDecryptSessionMap.indexOfKey(decryptHandle->decryptId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mDecryptSessionMap.valueFor(decryptHandle->decryptId); + drmEngine->initializeDecryptUnit(uniqueId, decryptHandle, decryptUnitId, headerInfo); + } +} + +status_t DrmManager::decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer) { + status_t status = DRM_ERROR_UNKNOWN; + if (mDecryptSessionMap.indexOfKey(decryptHandle->decryptId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mDecryptSessionMap.valueFor(decryptHandle->decryptId); + status = drmEngine->decrypt(uniqueId, decryptHandle, decryptUnitId, encBuffer, decBuffer); + } + return status; +} + +void DrmManager::finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) { + if (mDecryptSessionMap.indexOfKey(decryptHandle->decryptId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mDecryptSessionMap.valueFor(decryptHandle->decryptId); + drmEngine->finalizeDecryptUnit(uniqueId, decryptHandle, decryptUnitId); + } +} + +ssize_t DrmManager::pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset) { + ssize_t result = DECRYPT_FILE_ERROR; + + if (mDecryptSessionMap.indexOfKey(decryptHandle->decryptId) != NAME_NOT_FOUND) { + IDrmEngine* drmEngine = mDecryptSessionMap.valueFor(decryptHandle->decryptId); + result = drmEngine->pread(uniqueId, decryptHandle, buffer, numBytes, offset); + } + return result; +} + +void DrmManager::initializePlugIns(int uniqueId) { + Vector plugInIdList = mPlugInManager.getPlugInIdList(); + + for (unsigned int index = 0; index < plugInIdList.size(); index++) { + IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInIdList.itemAt(index)); + rDrmEngine.initialize(uniqueId); + rDrmEngine.setOnInfoListener(uniqueId, this); + } +} + +void DrmManager::populate(int uniqueId) { + Vector plugInPathList = mPlugInManager.getPlugInIdList(); + + for (unsigned int i = 0; i < plugInPathList.size(); ++i) { + String8 plugInPath = plugInPathList[i]; + DrmSupportInfo* info = mPlugInManager.getPlugIn(plugInPath).getSupportInfo(uniqueId); + if (NULL != info) { + mSupportInfoToPlugInIdMap.add(*info, plugInPath); + } + } +} + +String8 DrmManager::getSupportedPlugInId( + int uniqueId, const String8& path, const String8& mimeType) { + String8 plugInId(""); + + if (EMPTY_STRING != mimeType) { + plugInId = getSupportedPlugInId(mimeType); + } else { + plugInId = getSupportedPlugInIdFromPath(uniqueId, path); + } + return plugInId; +} + +String8 DrmManager::getSupportedPlugInId(const String8& mimeType) { + String8 plugInId(""); + + if (EMPTY_STRING != mimeType) { + for (unsigned int index = 0; index < mSupportInfoToPlugInIdMap.size(); index++) { + const DrmSupportInfo& drmSupportInfo = mSupportInfoToPlugInIdMap.keyAt(index); + + if (drmSupportInfo.isSupportedMimeType(mimeType)) { + plugInId = mSupportInfoToPlugInIdMap.valueFor(drmSupportInfo); + break; + } + } + } + return plugInId; +} + +String8 DrmManager::getSupportedPlugInIdFromPath(int uniqueId, const String8& path) { + String8 plugInId(""); + const String8 fileSuffix = path.getPathExtension(); + + for (unsigned int index = 0; index < mSupportInfoToPlugInIdMap.size(); index++) { + const DrmSupportInfo& drmSupportInfo = mSupportInfoToPlugInIdMap.keyAt(index); + + if (drmSupportInfo.isSupportedFileSuffix(fileSuffix)) { + String8 key = mSupportInfoToPlugInIdMap.valueFor(drmSupportInfo); + IDrmEngine& drmEngine = mPlugInManager.getPlugIn(key); + + if (drmEngine.canHandle(uniqueId, path)) { + plugInId = key; + break; + } + } + } + return plugInId; +} + +void DrmManager::onInfo(const DrmInfoEvent& event) { + Mutex::Autolock _l(mLock); + for (unsigned int index = 0; index < mServiceListeners.size(); index++) { + int uniqueId = mServiceListeners.keyAt(index); + + if (uniqueId == event.getUniqueId()) { + sp serviceListener = mServiceListeners.valueFor(uniqueId); + serviceListener->notify(event); + } + } +} + diff --git a/drm/drmserver/DrmManagerService.cpp b/drm/drmserver/DrmManagerService.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9d000e9de28aea5356cc28deac5ae1391ce44d0d --- /dev/null +++ b/drm/drmserver/DrmManagerService.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmManagerService(Native)" +#include + +#include +#include +#include +#include +#include "DrmManagerService.h" +#include "DrmManager.h" + +using namespace android; + +#define SUCCESS 0 +#define DRM_DIRECTORY_PERMISSION 0700 + +void DrmManagerService::instantiate() { + LOGV("instantiate"); + + int res = mkdir("/data/drm/plugins", DRM_DIRECTORY_PERMISSION); + if (SUCCESS == res || EEXIST == errno) { + res = mkdir("/data/drm/plugins/native", DRM_DIRECTORY_PERMISSION); + if (SUCCESS == res || EEXIST == errno) { + res = mkdir("/data/drm/plugins/native/databases", DRM_DIRECTORY_PERMISSION); + if (SUCCESS == res || EEXIST == errno) { + defaultServiceManager() + ->addService(String16("drm.drmManager"), new DrmManagerService()); + } + } + } +} + +DrmManagerService::DrmManagerService() { + LOGV("created"); + mDrmManager = NULL; + mDrmManager = new DrmManager(); +} + +DrmManagerService::~DrmManagerService() { + LOGV("Destroyed"); + delete mDrmManager; mDrmManager = NULL; +} + +status_t DrmManagerService::loadPlugIns(int uniqueId) { + LOGV("Entering load plugins"); + return mDrmManager->loadPlugIns(uniqueId); +} + +status_t DrmManagerService::loadPlugIns(int uniqueId, const String8& plugInDirPath) { + LOGV("Entering load plugins from path"); + return mDrmManager->loadPlugIns(uniqueId, plugInDirPath); +} + +status_t DrmManagerService::setDrmServiceListener( + int uniqueId, const sp& drmServiceListener) { + LOGV("Entering setDrmServiceListener"); + mDrmManager->setDrmServiceListener(uniqueId, drmServiceListener); + return DRM_NO_ERROR; +} + +status_t DrmManagerService::unloadPlugIns(int uniqueId) { + LOGV("Entering unload plugins"); + return mDrmManager->unloadPlugIns(uniqueId); +} + +status_t DrmManagerService::installDrmEngine(int uniqueId, const String8& drmEngineFile) { + LOGV("Entering installDrmEngine"); + return mDrmManager->installDrmEngine(uniqueId, drmEngineFile); +} + +DrmConstraints* DrmManagerService::getConstraints( + int uniqueId, const String8* path, const int action) { + LOGV("Entering getConstraints from content"); + return mDrmManager->getConstraints(uniqueId, path, action); +} + +bool DrmManagerService::canHandle(int uniqueId, const String8& path, const String8& mimeType) { + LOGV("Entering canHandle"); + return mDrmManager->canHandle(uniqueId, path, mimeType); +} + +DrmInfoStatus* DrmManagerService::processDrmInfo(int uniqueId, const DrmInfo* drmInfo) { + LOGV("Entering processDrmInfo"); + return mDrmManager->processDrmInfo(uniqueId, drmInfo); +} + +DrmInfo* DrmManagerService::acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest) { + LOGV("Entering acquireDrmInfo"); + return mDrmManager->acquireDrmInfo(uniqueId, drmInfoRequest); +} + +void DrmManagerService::saveRights( + int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) { + LOGV("Entering saveRights"); + return mDrmManager->saveRights(uniqueId, drmRights, rightsPath, contentPath); +} + +String8 DrmManagerService::getOriginalMimeType(int uniqueId, const String8& path) { + LOGV("Entering getOriginalMimeType"); + return mDrmManager->getOriginalMimeType(uniqueId, path); +} + +int DrmManagerService::getDrmObjectType( + int uniqueId, const String8& path, const String8& mimeType) { + LOGV("Entering getDrmObjectType"); + return mDrmManager->getDrmObjectType(uniqueId, path, mimeType); +} + +int DrmManagerService::checkRightsStatus( + int uniqueId, const String8& path, int action) { + LOGV("Entering checkRightsStatus"); + return mDrmManager->checkRightsStatus(uniqueId, path, action); +} + +void DrmManagerService::consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve) { + LOGV("Entering consumeRights"); + mDrmManager->consumeRights(uniqueId, decryptHandle, action, reserve); +} + +void DrmManagerService::setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position) { + LOGV("Entering setPlaybackStatus"); + mDrmManager->setPlaybackStatus(uniqueId, decryptHandle, playbackStatus, position); +} + +bool DrmManagerService::validateAction( + int uniqueId, const String8& path, + int action, const ActionDescription& description) { + LOGV("Entering validateAction"); + return mDrmManager->validateAction(uniqueId, path, action, description); +} + +void DrmManagerService::removeRights(int uniqueId, const String8& path) { + LOGV("Entering removeRights"); + mDrmManager->removeRights(uniqueId, path); +} + +void DrmManagerService::removeAllRights(int uniqueId) { + LOGV("Entering removeAllRights"); + mDrmManager->removeAllRights(uniqueId); +} + +int DrmManagerService::openConvertSession(int uniqueId, const String8& mimeType) { + LOGV("Entering openConvertSession"); + return mDrmManager->openConvertSession(uniqueId, mimeType); +} + +DrmConvertedStatus* DrmManagerService::convertData( + int uniqueId, int convertId, const DrmBuffer* inputData) { + LOGV("Entering convertData"); + return mDrmManager->convertData(uniqueId, convertId, inputData); +} + +DrmConvertedStatus* DrmManagerService::closeConvertSession(int uniqueId, int convertId) { + LOGV("Entering closeConvertSession"); + return mDrmManager->closeConvertSession(uniqueId, convertId); +} + +status_t DrmManagerService::getAllSupportInfo( + int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray) { + LOGV("Entering getAllSupportInfo"); + return mDrmManager->getAllSupportInfo(uniqueId, length, drmSupportInfoArray); +} + +DecryptHandle* DrmManagerService::openDecryptSession( + int uniqueId, int fd, int offset, int length) { + LOGV("Entering DrmManagerService::openDecryptSession"); + return mDrmManager->openDecryptSession(uniqueId, fd, offset, length); +} + +void DrmManagerService::closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) { + LOGV("Entering closeDecryptSession"); + mDrmManager->closeDecryptSession(uniqueId, decryptHandle); +} + +void DrmManagerService::initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo) { + LOGV("Entering initializeDecryptUnit"); + mDrmManager->initializeDecryptUnit(uniqueId,decryptHandle, decryptUnitId, headerInfo); +} + +status_t DrmManagerService::decrypt( + int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer) { + LOGV("Entering decrypt"); + return mDrmManager->decrypt(uniqueId, decryptHandle, decryptUnitId, encBuffer, decBuffer); +} + +void DrmManagerService::finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) { + LOGV("Entering finalizeDecryptUnit"); + mDrmManager->finalizeDecryptUnit(uniqueId, decryptHandle, decryptUnitId); +} + +ssize_t DrmManagerService::pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset) { + LOGV("Entering pread"); + return mDrmManager->pread(uniqueId, decryptHandle, buffer, numBytes, offset); +} + diff --git a/drm/drmserver/StringTokenizer.cpp b/drm/drmserver/StringTokenizer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..367c9bd4dbfe54187e106c8073b2a1bde312e665 --- /dev/null +++ b/drm/drmserver/StringTokenizer.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "StringTokenizer.h" + +using namespace android; + +StringTokenizer::StringTokenizer(const String8& string, const String8& delimiter) { + splitString(string, delimiter); +} + +void StringTokenizer::splitString(const String8& string, const String8& delimiter) { + for (unsigned int i = 0; i < string.length(); i++) { + unsigned int position = string.find(delimiter.string(), i); + if (string.length() != position) { + String8 token(string.string()+i, position-i); + if (token.length()) { + mStringTokenizerVector.push(token); + i = position + delimiter.length() - 1; + } + } else { + mStringTokenizerVector.push(String8(string.string()+i, string.length()-i)); + break; + } + } +} + +StringTokenizer::Iterator StringTokenizer::iterator() { + return Iterator(this); +} + +StringTokenizer::Iterator::Iterator(const StringTokenizer::Iterator& iterator) : + mStringTokenizer(iterator.mStringTokenizer), + mIndex(iterator.mIndex) { + LOGV("StringTokenizer::Iterator::Iterator"); +} + +StringTokenizer::Iterator& StringTokenizer::Iterator::operator=( + const StringTokenizer::Iterator& iterator) { + LOGV("StringTokenizer::Iterator::operator="); + mStringTokenizer = iterator.mStringTokenizer; + mIndex = iterator.mIndex; + return *this; +} + +bool StringTokenizer::Iterator::hasNext() { + return mIndex < mStringTokenizer->mStringTokenizerVector.size(); +} + +String8& StringTokenizer::Iterator::next() { + String8& value = mStringTokenizer->mStringTokenizerVector.editItemAt(mIndex); + mIndex++; + return value; +} + diff --git a/drm/drmserver/main_drmserver.cpp b/drm/drmserver/main_drmserver.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6d10646b608235215bad9903efac2d7d88b081cb --- /dev/null +++ b/drm/drmserver/main_drmserver.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace android; + +int main(int argc, char** argv) +{ + sp proc(ProcessState::self()); + sp sm = defaultServiceManager(); + LOGI("ServiceManager: %p", sm.get()); + DrmManagerService::instantiate(); + ProcessState::self()->startThreadPool(); + IPCThreadState::self()->joinThreadPool(); +} + diff --git a/drm/java/android/drm/DrmConvertedStatus.java b/drm/java/android/drm/DrmConvertedStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..f2005527da1da07bb28339533288996336bb2ec6 --- /dev/null +++ b/drm/java/android/drm/DrmConvertedStatus.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +/** + * This is an entity class which wraps the status of the conversion, the converted + * data/checksum data and the offset. Offset is going to be used in the case of close + * session where the agent will inform where the header and body signature should be added + * + * As a result of {@link DrmManagerClient#convertData(int, byte [])} and + * {@link DrmManagerClient#closeConvertSession(int)} an instance of DrmConvertedStatus + * would be returned. + * + */ +public class DrmConvertedStatus { + // Should be in sync with DrmConvertedStatus.cpp + public static final int STATUS_OK = 1; + public static final int STATUS_INPUTDATA_ERROR = 2; + public static final int STATUS_ERROR = 3; + + public final int statusCode; + public final byte[] convertedData; + public final int offset; + + /** + * constructor to create DrmConvertedStatus object with given parameters + * + * @param _statusCode Status of the conversion + * @param _convertedData Converted data/checksum data + * @param _offset Offset value + */ + public DrmConvertedStatus(int _statusCode, byte[] _convertedData, int _offset) { + statusCode = _statusCode; + convertedData = _convertedData; + offset = _offset; + } +} + diff --git a/drm/java/android/drm/DrmEvent.java b/drm/java/android/drm/DrmEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..44c4b434bfdfc48a648e6d0178fa592a1e6dc88a --- /dev/null +++ b/drm/java/android/drm/DrmEvent.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +/** + * This is the base class which would be used to notify the caller + * about any event occurred in DRM framework. + * + */ +public class DrmEvent { + private final int mUniqueId; + private final int mType; + private String mMessage = ""; + + /** + * constructor for DrmEvent class + * + * @param uniqueId Unique session identifier + * @param type Type of information + * @param message Message description + */ + protected DrmEvent(int uniqueId, int type, String message) { + mUniqueId = uniqueId; + mType = type; + + if (null != message) { + mMessage = message; + } + } + + /** + * Returns the Unique Id associated with this object + * + * @return Unique Id + */ + public int getUniqueId() { + return mUniqueId; + } + + /** + * Returns the Type of information associated with this object + * + * @return Type of information + */ + public int getType() { + return mType; + } + + /** + * Returns the message description associated with this object + * + * @return message description + */ + public String getMessage() { + return mMessage; + } +} + diff --git a/drm/java/android/drm/DrmInfo.java b/drm/java/android/drm/DrmInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..7d3fbf1fd8486c4e278ab91ab741ae2df2d91457 --- /dev/null +++ b/drm/java/android/drm/DrmInfo.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; + +/** + * This is an entity class in which necessary information required to transact + * between device and online DRM server is described. DRM Framework achieves + * server registration, license acquisition and any other server related transaction + * by passing an instance of this class to {@link DrmManagerClient#processDrmInfo(DrmInfo)}. + * + * Caller can retrieve the {@link DrmInfo} instance by using + * {@link DrmManagerClient#acquireDrmInfo(DrmInfoRequest)} + * by passing {@link DrmInfoRequest} instance. + * + */ +public class DrmInfo { + private byte[] mData; + private final String mMimeType; + private final int mInfoType; + // It would be used to add attributes specific to + // DRM scheme such as account id, path or multiple path's + private final HashMap mAttributes = new HashMap(); + + /** + * constructor to create DrmInfo object with given parameters + * + * @param infoType Type of information + * @param data Trigger data + * @param mimeType MIME type + */ + public DrmInfo(int infoType, byte[] data, String mimeType) { + mInfoType = infoType; + mMimeType = mimeType; + mData = data; + } + + /** + * constructor to create DrmInfo object with given parameters + * + * @param infoType Type of information + * @param path Trigger data + * @param mimeType MIME type + */ + public DrmInfo(int infoType, String path, String mimeType) { + mInfoType = infoType; + mMimeType = mimeType; + try { + mData = DrmUtils.readBytes(path); + } catch (IOException e) { + // As the given path is invalid, + // set mData = null, so that further processDrmInfo() + // call would fail with IllegalArgumentException because of mData = null + mData = null; + } + } + + /** + * Adds optional information as pair to this object + * + * @param key Key to add + * @param value Value to add + * To put custom object into DrmInfo, custom object has to + * override toString() implementation. + */ + public void put(String key, Object value) { + mAttributes.put(key, value); + } + + /** + * Retrieves the value of given key, if not found returns null + * + * @param key Key whose value to be retrieved + * @return The value or null + */ + public Object get(String key) { + return mAttributes.get(key); + } + + /** + * Returns Iterator object to walk through the keys associated with this instance + * + * @return Iterator object + */ + public Iterator keyIterator() { + return mAttributes.keySet().iterator(); + } + + /** + * Returns Iterator object to walk through the values associated with this instance + * + * @return Iterator object + */ + public Iterator iterator() { + return mAttributes.values().iterator(); + } + + /** + * Returns the trigger data associated with this object + * + * @return Trigger data + */ + public byte[] getData() { + return mData; + } + + /** + * Returns the mimetype associated with this object + * + * @return MIME type + */ + public String getMimeType() { + return mMimeType; + } + + /** + * Returns information type associated with this instance + * + * @return Information type + */ + public int getInfoType() { + return mInfoType; + } + + /** + * Returns whether this instance is valid or not + * + * @return + * true if valid + * false if invalid + */ + boolean isValid() { + return (null != mMimeType && !mMimeType.equals("") + && null != mData && mData.length > 0 && DrmInfoRequest.isValidType(mInfoType)); + } +} + diff --git a/drm/java/android/drm/DrmInfoEvent.java b/drm/java/android/drm/DrmInfoEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..be1b009e123470f9b7283d7365a9037af8750d5a --- /dev/null +++ b/drm/java/android/drm/DrmInfoEvent.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +/** + * This is an entity class which would be passed to caller in + * {@link DrmManagerClient.OnInfoListener#onInfo(DrmManagerClient, DrmInfoEvent)} + * + */ +public class DrmInfoEvent extends DrmEvent { + /** + * TYPE_ALREADY_REGISTERED_BY_ANOTHER_ACCOUNT, when registration has been already done + * by another account ID. + */ + public static final int TYPE_ALREADY_REGISTERED_BY_ANOTHER_ACCOUNT = 0x0000001; + /** + * TYPE_REMOVE_RIGHTS, when the rights needs to be removed completely. + */ + public static final int TYPE_REMOVE_RIGHTS = 0x0000002; + /** + * TYPE_RIGHTS_INSTALLED, when the rights are downloaded and installed ok. + */ + public static final int TYPE_RIGHTS_INSTALLED = 0x0000003; + /** + * TYPE_RIGHTS_NOT_INSTALLED, when something went wrong installing the rights. + */ + public static final int TYPE_RIGHTS_NOT_INSTALLED = 0x0000004; + /** + * TYPE_RIGHTS_RENEWAL_NOT_ALLOWED, when the server rejects renewal of rights. + */ + public static final int TYPE_RIGHTS_RENEWAL_NOT_ALLOWED = 0x0000005; + /** + * TYPE_NOT_SUPPORTED, when answer from server can not be handled by the native agent. + */ + public static final int TYPE_NOT_SUPPORTED = 0x0000006; + /** + * TYPE_WAIT_FOR_RIGHTS, rights object is on it's way to phone, + * wait before calling checkRights again. + */ + public static final int TYPE_WAIT_FOR_RIGHTS = 0x0000007; + /** + * TYPE_OUT_OF_MEMORY, when memory allocation fail during renewal. + * Can in the future perhaps be used to trigger garbage collector. + */ + public static final int TYPE_OUT_OF_MEMORY = 0x0000008; + /** + * TYPE_NO_INTERNET_CONNECTION, when the Internet connection is missing and no attempt + * can be made to renew rights. + */ + public static final int TYPE_NO_INTERNET_CONNECTION = 0x0000009; + + /** + * constructor to create DrmInfoEvent object with given parameters + * + * @param uniqueId Unique session identifier + * @param type Type of information + * @param message Message description + */ + public DrmInfoEvent(int uniqueId, int type, String message) { + super(uniqueId, type, message); + } +} + diff --git a/drm/java/android/drm/DrmInfoRequest.java b/drm/java/android/drm/DrmInfoRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..a5a799c438c27032594c5c6838f141b51fd20c5f --- /dev/null +++ b/drm/java/android/drm/DrmInfoRequest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * This is an entity class used to pass required parameters to get + * the necessary information to communicate with online DRM server + * + * An instance of this class is passed to {@link DrmManagerClient#acquireDrmInfo(DrmInfoRequest)} + * to get the instance of {@link DrmInfo} + * + */ +public class DrmInfoRequest { + // Changes in following constants should be in sync with DrmInfoRequest.cpp + /** + * Constants defines the type of {@link DrmInfoRequest} + */ + public static final int TYPE_REGISTRATION_INFO = 1; + public static final int TYPE_UNREGISTRATION_INFO = 2; + public static final int TYPE_RIGHTS_ACQUISITION_INFO = 3; + public static final int TYPE_RIGHTS_ACQUISITION_PROGRESS_INFO = 4; + + /** + * Key to pass the unique id for the account or the user + */ + public static final String ACCOUNT_ID = "account_id"; + + /** + * Key to pass the unique id used for subscription + */ + public static final String SUBSCRIPTION_ID = "subscription_id"; + + private final int mInfoType; + private final String mMimeType; + private final HashMap mRequestInformation = new HashMap(); + + /** + * constructor to create DrmInfoRequest object with type and mimetype + * + * @param infoType Type of information + * @param mimeType MIME type + */ + public DrmInfoRequest(int infoType, String mimeType) { + mInfoType = infoType; + mMimeType = mimeType; + } + + /** + * Returns the mimetype associated with this object + * + * @return MIME type + */ + public String getMimeType() { + return mMimeType; + } + + /** + * Returns Information type associated with this instance + * + * @return Information type + */ + public int getInfoType() { + return mInfoType; + } + + /** + * Adds optional information as pair to this object. + * + * @param key Key to add + * @param value Value to add + */ + public void put(String key, Object value) { + mRequestInformation.put(key, value); + } + + /** + * Retrieves the value of given key, if not found returns null + * + * @param key Key whose value to be retrieved + * @return The value or null + */ + public Object get(String key) { + return mRequestInformation.get(key); + } + + /** + * Returns Iterator object to walk through the keys associated with this instance + * + * @return Iterator object + */ + public Iterator keyIterator() { + return mRequestInformation.keySet().iterator(); + } + + /** + * Returns Iterator object to walk through the values associated with this instance + * + * @return Iterator object + */ + public Iterator iterator() { + return mRequestInformation.values().iterator(); + } + + /** + * Returns whether this instance is valid or not + * + * @return + * true if valid + * false if invalid + */ + boolean isValid() { + return (null != mMimeType && !mMimeType.equals("") + && null != mRequestInformation && isValidType(mInfoType)); + } + + /* package */ static boolean isValidType(int infoType) { + boolean isValid = false; + + switch (infoType) { + case TYPE_REGISTRATION_INFO: + case TYPE_UNREGISTRATION_INFO: + case TYPE_RIGHTS_ACQUISITION_INFO: + case TYPE_RIGHTS_ACQUISITION_PROGRESS_INFO: + isValid = true; + break; + } + return isValid; + } +} + diff --git a/drm/java/android/drm/DrmInfoStatus.java b/drm/java/android/drm/DrmInfoStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..7e9ca3e2e1b3ec10e96dc8addb756ddc84d3c631 --- /dev/null +++ b/drm/java/android/drm/DrmInfoStatus.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +/** + * This is an entity class which wraps the result of communication between device + * and online DRM server. + * + * As a result of {@link DrmManagerClient#processDrmInfo(DrmInfo)} an instance of DrmInfoStatus + * would be returned. This class holds {@link ProcessedData}, which could be used to instantiate + * {@link DrmRights#DrmRights(ProcessedData, String)} in license acquisition. + * + */ +public class DrmInfoStatus { + // Should be in sync with DrmInfoStatus.cpp + public static final int STATUS_OK = 1; + public static final int STATUS_ERROR = 2; + + public final int statusCode; + public final String mimeType; + public final ProcessedData data; + + /** + * constructor to create DrmInfoStatus object with given parameters + * + * @param _statusCode Status of the communication + * @param _data The processed data + * @param _mimeType MIME type + */ + public DrmInfoStatus(int _statusCode, ProcessedData _data, String _mimeType) { + statusCode = _statusCode; + data = _data; + mimeType = _mimeType; + } +} + diff --git a/drm/java/android/drm/DrmManagerClient.java b/drm/java/android/drm/DrmManagerClient.java new file mode 100644 index 0000000000000000000000000000000000000000..7ec70daa08c2062ffe35a91c9fdc15af68cf91bc --- /dev/null +++ b/drm/java/android/drm/DrmManagerClient.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +import android.content.ContentValues; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * Interface of DRM Framework. + * Java application will instantiate this class + * to access DRM agent through DRM Framework. + * + */ +public class DrmManagerClient { + private static final String TAG = "DrmManager"; + + static { + // Load the respective library + System.loadLibrary("drmframework_jni"); + } + + /** + * Interface definition of a callback to be invoked to communicate + * some info and/or warning about DrmManagerClient. + */ + public interface OnInfoListener { + /** + * Called to indicate an info or a warning. + * + * @param client DrmManagerClient instance + * @param event instance which wraps reason and necessary information + */ + public void onInfo(DrmManagerClient client, DrmInfoEvent event); + } + + private static final int STATE_UNINITIALIZED = 0x00000000; + private static final int STATE_INITIALIZED = 0x00000001; + + private int mUniqueId; + private int mNativeContext; + private EventHandler mEventHandler; + private OnInfoListener mOnInfoListener; + private int mCurrentState = STATE_UNINITIALIZED; + + /** + * {@hide} + */ + public static void notify(Object thisReference, int uniqueId, int infoType, String message) { + DrmManagerClient instance = (DrmManagerClient)((WeakReference)thisReference).get(); + + if (null != instance && null != instance.mEventHandler) { + Message m = instance.mEventHandler.obtainMessage( + EventHandler.INFO_EVENT_TYPE, uniqueId, infoType, message); + instance.mEventHandler.sendMessage(m); + } + } + + private class EventHandler extends Handler { + public static final int INFO_EVENT_TYPE = 1; + + public EventHandler(Looper looper) { + super(looper); + } + + public void handleMessage(Message msg) { + + switch (msg.what) { + case EventHandler.INFO_EVENT_TYPE: + int uniqueId = msg.arg1; + int infoType = msg.arg2; + String message = msg.obj.toString(); + + if (infoType == DrmInfoEvent.TYPE_REMOVE_RIGHTS) { + try { + DrmUtils.removeFile(message); + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (null != mOnInfoListener) { + DrmInfoEvent event = new DrmInfoEvent(uniqueId, infoType, message); + mOnInfoListener.onInfo(DrmManagerClient.this, event); + } + return; + default: + Log.e(TAG, "Unknown message type " + msg.what); + return; + } + } + } + + /** + * To instantiate DrmManagerClient + * + * @param context context of the caller + */ + public DrmManagerClient(Context context) { + Looper looper; + + if (null != (looper = Looper.myLooper())) { + mEventHandler = new EventHandler(looper); + } else if (null != (looper = Looper.getMainLooper())) { + mEventHandler = new EventHandler(looper); + } else { + mEventHandler = null; + } + + // save the unique id + mUniqueId = hashCode(); + } + + /** + * Register a callback to be invoked when the caller required to receive + * necessary information + * + * @param infoListener + */ + public void setOnInfoListener(OnInfoListener infoListener) { + synchronized(this) { + if (null != infoListener) { + mOnInfoListener = infoListener; + } + } + } + + /** + * Initializes DrmFramework, which loads all available plug-ins + * in the default plug-in directory path + * + */ + public void loadPlugIns() { + if (getState() == STATE_UNINITIALIZED) { + _loadPlugIns(mUniqueId, new WeakReference(this)); + + mCurrentState = STATE_INITIALIZED; + } + } + + /** + * Finalize DrmFramework, which release resources associated with each plug-in + * and unload all plug-ins. + */ + public void unloadPlugIns() { + if (getState() == STATE_INITIALIZED) { + _unloadPlugIns(mUniqueId); + + mCurrentState = STATE_UNINITIALIZED; + } + } + + /** + * Retrieves informations about all the plug-ins registered with DrmFramework. + * + * @return Array of DrmEngine plug-in strings + */ + public String[] getAvailableDrmEngines() { + if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + + DrmSupportInfo[] supportInfos = _getAllSupportInfo(mUniqueId); + ArrayList descriptions = new ArrayList(); + + for (int i = 0; i < supportInfos.length; i++) { + descriptions.add(supportInfos[i].getDescriprition()); + } + + String[] drmEngines = new String[descriptions.size()]; + return descriptions.toArray(drmEngines); + } + + /** + * Get constraints information evaluated from DRM content + * + * @param path Content path from where DRM constraints would be retrieved. + * @param action Actions defined in {@link DrmStore.Action} + * @return ContentValues instance in which constraints key-value pairs are embedded + * or null in case of failure + */ + public ContentValues getConstraints(String path, int action) { + if (null == path || path.equals("") || !DrmStore.Action.isValid(action)) { + throw new IllegalArgumentException("Given usage or path is invalid/null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _getConstraints(mUniqueId, path, action); + } + + /** + * Save DRM rights to specified rights path + * and make association with content path. + * + *

    In case of OMA or WM-DRM, rightsPath and contentPath could be null.

    + * + * @param drmRights DrmRights to be saved + * @param rightsPath File path where rights to be saved + * @param contentPath File path where content was saved + * @throws IOException if failed to save rights information in the given path + */ + public void saveRights( + DrmRights drmRights, String rightsPath, String contentPath) throws IOException { + if (null == drmRights || !drmRights.isValid() + || null == contentPath || contentPath.equals("")) { + throw new IllegalArgumentException("Given drmRights or contentPath is not valid"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + if (null != rightsPath && !rightsPath.equals("")) { + DrmUtils.writeToFile(rightsPath, drmRights.getData()); + } + _saveRights(mUniqueId, drmRights, rightsPath, contentPath); + } + + /** + * Install new DRM Engine Plug-in at the runtime + * + * @param engineFilePath Path of the plug-in file to be installed + * {@hide} + */ + public void installDrmEngine(String engineFilePath) { + if (null == engineFilePath || engineFilePath.equals("")) { + throw new IllegalArgumentException( + "Given engineFilePath: "+ engineFilePath + "is not valid"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + _installDrmEngine(mUniqueId, engineFilePath); + } + + /** + * Check whether the given mimetype or path can be handled. + * + * @param path Path of the content to be handled + * @param mimeType Mimetype of the object to be handled + * @return + * true - if the given mimeType or path can be handled. + * false - cannot be handled. false will be returned in case + * the state is uninitialized + */ + public boolean canHandle(String path, String mimeType) { + if ((null == path || path.equals("")) && (null == mimeType || mimeType.equals(""))) { + throw new IllegalArgumentException("Path or the mimetype should be non null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _canHandle(mUniqueId, path, mimeType); + } + + /** + * Executes given drm information based on its type + * + * @param drmInfo Information needs to be processed + * @return DrmInfoStatus Instance as a result of processing given input + */ + public DrmInfoStatus processDrmInfo(DrmInfo drmInfo) { + if (null == drmInfo || !drmInfo.isValid()) { + throw new IllegalArgumentException("Given drmInfo is invalid/null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _processDrmInfo(mUniqueId, drmInfo); + } + + /** + * Retrieves necessary information for register, unregister or rights acquisition. + * + * @param drmInfoRequest Request information to retrieve drmInfo + * @return DrmInfo Instance as a result of processing given input + */ + public DrmInfo acquireDrmInfo(DrmInfoRequest drmInfoRequest) { + if (null == drmInfoRequest || !drmInfoRequest.isValid()) { + throw new IllegalArgumentException("Given drmInfoRequest is invalid/null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _acquireDrmInfo(mUniqueId, drmInfoRequest); + } + + /** + * Retrieves the type of the protected object (content, rights, etc..) + * using specified path or mimetype. At least one parameter should be non null + * to retrieve DRM object type + * + * @param path Path of the content or null. + * @param mimeType Mimetype of the content or null. + * @return Type of the DRM content. + * @see DrmStore.DrmObjectType + */ + public int getDrmObjectType(String path, String mimeType) { + if ((null == path || path.equals("")) && (null == mimeType || mimeType.equals(""))) { + throw new IllegalArgumentException("Path or the mimetype should be non null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _getDrmObjectType(mUniqueId, path, mimeType); + } + + /** + * Retrieves the mime type embedded inside the original content + * + * @param path Path of the protected content + * @return Mimetype of the original content, such as "video/mpeg" + */ + public String getOriginalMimeType(String path) { + if (null == path || path.equals("")) { + throw new IllegalArgumentException("Given path should be non null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _getOriginalMimeType(mUniqueId, path); + } + + /** + * Check whether the given content has valid rights or not + * + * @param path Path of the protected content + * @return Status of the rights for the protected content + * @see DrmStore.RightsStatus + */ + public int checkRightsStatus(String path) { + return checkRightsStatus(path, DrmStore.Action.DEFAULT); + } + + /** + * Check whether the given content has valid rights or not for specified action. + * + * @param path Path of the protected content + * @param action Action to perform + * @return Status of the rights for the protected content + * @see DrmStore.RightsStatus + */ + public int checkRightsStatus(String path, int action) { + if (null == path || path.equals("") || !DrmStore.Action.isValid(action)) { + throw new IllegalArgumentException("Given path or action is not valid"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _checkRightsStatus(mUniqueId, path, action); + } + + /** + * Removes the rights associated with the given protected content + * + * @param path Path of the protected content + */ + public void removeRights(String path) { + if (null == path || path.equals("")) { + throw new IllegalArgumentException("Given path should be non null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + _removeRights(mUniqueId, path); + } + + /** + * Removes all the rights information of every plug-in associated with + * DRM framework. Will be used in master reset + */ + public void removeAllRights() { + if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + _removeAllRights(mUniqueId); + } + + /** + * This API is for Forward Lock based DRM scheme. + * Each time the application tries to download a new DRM file + * which needs to be converted, then the application has to + * begin with calling this API. + * + * @param mimeType Description/MIME type of the input data packet + * @return convert ID which will be used for maintaining convert session. + */ + public int openConvertSession(String mimeType) { + if (null == mimeType || mimeType.equals("")) { + throw new IllegalArgumentException("Path or the mimeType should be non null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _openConvertSession(mUniqueId, mimeType); + } + + /** + * Accepts and converts the input data which is part of DRM file. + * The resultant converted data and the status is returned in the DrmConvertedInfo + * object. This method will be called each time there are new block + * of data received by the application. + * + * @param convertId Handle for the convert session + * @param inputData Input Data which need to be converted + * @return Return object contains the status of the data conversion, + * the output converted data and offset. In this case the + * application will ignore the offset information. + */ + public DrmConvertedStatus convertData(int convertId, byte[] inputData) { + if (null == inputData || 0 >= inputData.length) { + throw new IllegalArgumentException("Given inputData should be non null"); + } else if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _convertData(mUniqueId, convertId, inputData); + } + + /** + * Informs the Drm Agent when there is no more data which need to be converted + * or when an error occurs. Upon successful conversion of the complete data, + * the agent will inform that where the header and body signature + * should be added. This signature appending is needed to integrity + * protect the converted file. + * + * @param convertId Handle for the convert session + * @return Return object contains the status of the data conversion, + * the header and body signature data. It also informs + * the application on which offset these signature data should be appended. + */ + public DrmConvertedStatus closeConvertSession(int convertId) { + if (getState() == STATE_UNINITIALIZED) { + throw new IllegalStateException("Not Initialized yet"); + } + return _closeConvertSession(mUniqueId, convertId); + } + + private int getState() { + return mCurrentState; + } + + // private native interfaces + private native void _loadPlugIns(int uniqueId, Object weak_this); + + private native void _unloadPlugIns(int uniqueId); + + private native void _installDrmEngine(int uniqueId, String engineFilepath); + + private native ContentValues _getConstraints(int uniqueId, String path, int usage); + + private native boolean _canHandle(int uniqueId, String path, String mimeType); + + private native DrmInfoStatus _processDrmInfo(int uniqueId, DrmInfo drmInfo); + + private native DrmInfo _acquireDrmInfo(int uniqueId, DrmInfoRequest drmInfoRequest); + + private native void _saveRights( + int uniqueId, DrmRights drmRights, String rightsPath, String contentPath); + + private native int _getDrmObjectType(int uniqueId, String path, String mimeType); + + private native String _getOriginalMimeType(int uniqueId, String path); + + private native int _checkRightsStatus(int uniqueId, String path, int action); + + private native void _removeRights(int uniqueId, String path); + + private native void _removeAllRights(int uniqueId); + + private native int _openConvertSession(int uniqueId, String mimeType); + + private native DrmConvertedStatus _convertData(int uniqueId, int convertId, byte[] inputData); + + private native DrmConvertedStatus _closeConvertSession(int uniqueId, int convertId); + + private native DrmSupportInfo[] _getAllSupportInfo(int uniqueId); +} + diff --git a/drm/java/android/drm/DrmRights.java b/drm/java/android/drm/DrmRights.java new file mode 100644 index 0000000000000000000000000000000000000000..103af0742f95a967c6aacbdc94e0fee32270f20c --- /dev/null +++ b/drm/java/android/drm/DrmRights.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +import java.io.File; +import java.io.IOException; + +/** + * This is an entity class which wraps the license information which was + * retrieved from the online DRM server. + * + * Caller can instantiate {@link DrmRights} by + * invoking {@link DrmRights#DrmRights(ProcessedData, String)} + * constructor by using the result of {@link DrmManagerClient#processDrmInfo(DrmInfo)} interface. + * Caller can also instantiate {@link DrmRights} using the file path + * which contains rights information. + * + */ +public class DrmRights { + private byte[] mData; + private String mMimeType; + private String mAccountId = "_NO_USER"; + private String mSubscriptionId = ""; + + /** + * constructor to create DrmRights object with given parameters + * + * @param rightsFilePath Path of the file containing rights data + * @param mimeType MIME type + */ + public DrmRights(String rightsFilePath, String mimeType) { + File file = new File(rightsFilePath); + instantiate(file, mimeType); + } + + /** + * constructor to create DrmRights object with given parameters + * + * @param rightsFilePath Path of the file containing rights data + * @param mimeType MIME type + * @param accountId Account Id of the user + */ + public DrmRights(String rightsFilePath, String mimeType, String accountId) { + this(rightsFilePath, mimeType); + + if (null != accountId && !accountId.equals("")) { + mAccountId = accountId; + } + } + + /** + * constructor to create DrmRights object with given parameters + * + * @param rightsFilePath Path of the file containing rights data + * @param mimeType MIME type + * @param accountId Account Id of the user + * @param subscriptionId Subscription Id of the user + */ + public DrmRights( + String rightsFilePath, String mimeType, String accountId, String subscriptionId) { + this(rightsFilePath, mimeType); + + if (null != accountId && !accountId.equals("")) { + mAccountId = accountId; + } + + if (null != subscriptionId && !subscriptionId.equals("")) { + mSubscriptionId = subscriptionId; + } + } + + /** + * constructor to create DrmRights object with given parameters + * + * @param rightsFile File containing rights data + * @param mimeType MIME type + */ + public DrmRights(File rightsFile, String mimeType) { + instantiate(rightsFile, mimeType); + } + + private void instantiate(File rightsFile, String mimeType) { + try { + mData = DrmUtils.readBytes(rightsFile); + } catch (IOException e) { + e.printStackTrace(); + } + + mMimeType = mimeType; + } + + /** + * constructor to create DrmRights object with given parameters + * The user can pass String or binary data

    + * Usage:

    + * i) String(e.g. data is instance of String):
    + * - new DrmRights(data.getBytes(), mimeType)

    + * ii) Binary data
    + * - new DrmRights(binaryData[], mimeType)
    + * + * @param data Processed data + * @param mimeType MIME type + */ + public DrmRights(ProcessedData data, String mimeType) { + mData = data.getData(); + + String accountId = data.getAccountId(); + if (null != accountId && !accountId.equals("")) { + mAccountId = accountId; + } + + String subscriptionId = data.getSubscriptionId(); + if (null != subscriptionId && !subscriptionId.equals("")) { + mSubscriptionId = subscriptionId; + } + + mMimeType = mimeType; + } + + /** + * Returns the rights data associated with this object + * + * @return Rights data + */ + public byte[] getData() { + return mData; + } + + /** + * Returns the mimetype associated with this object + * + * @return MIME type + */ + public String getMimeType() { + return mMimeType; + } + + /** + * Returns the account-id associated with this object + * + * @return Account Id + */ + public String getAccountId() { + return mAccountId; + } + + /** + * Returns the subscription-id associated with this object + * + * @return Subscription Id + */ + public String getSubscriptionId() { + return mSubscriptionId; + } + + /** + * Returns whether this instance is valid or not + * + * @return + * true if valid + * false if invalid + */ + /*package*/ boolean isValid() { + return (null != mMimeType && !mMimeType.equals("") + && null != mData && mData.length > 0); + } +} + diff --git a/drm/java/android/drm/DrmStore.java b/drm/java/android/drm/DrmStore.java new file mode 100644 index 0000000000000000000000000000000000000000..44df90c6687cea354c1b31af799f92bd89a978d9 --- /dev/null +++ b/drm/java/android/drm/DrmStore.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +/** + * This class defines all the constants used by DRM framework + * + */ +public class DrmStore { + /** + * Columns representing drm constraints + */ + public interface ConstraintsColumns { + /** + * The max repeat count + *

    Type: INTEGER

    + */ + public static final String MAX_REPEAT_COUNT = "max_repeat_count"; + + /** + * The remaining repeat count + *

    Type: INTEGER

    + */ + public static final String REMAINING_REPEAT_COUNT = "remaining_repeat_count"; + + /** + * The time before which the protected file can not be played/viewed + *

    Type: TEXT

    + */ + public static final String LICENSE_START_TIME = "license_start_time"; + + /** + * The time after which the protected file can not be played/viewed + *

    Type: TEXT

    + */ + public static final String LICENSE_EXPIRY_TIME = "license_expiry_time"; + + /** + * The available time for license + *

    Type: TEXT

    + */ + public static final String LICENSE_AVAILABLE_TIME = "license_available_time"; + + /** + * The data stream for extended metadata + *

    Type: TEXT

    + */ + public static final String EXTENDED_METADATA = "extended_metadata"; + } + + /** + * Defines constants related to DRM types + */ + public static class DrmObjectType { + /** + * Field specifies the unknown type + */ + public static final int UNKNOWN = 0x00; + /** + * Field specifies the protected content type + */ + public static final int CONTENT = 0x01; + /** + * Field specifies the rights information + */ + public static final int RIGHTS_OBJECT = 0x02; + /** + * Field specifies the trigger information + */ + public static final int TRIGGER_OBJECT = 0x03; + } + + /** + * Defines constants related to playback + */ + public static class Playback { + /** + * Constant field signifies playback start + */ + public static final int START = 0x00; + /** + * Constant field signifies playback stop + */ + public static final int STOP = 0x01; + /** + * Constant field signifies playback paused + */ + public static final int PAUSE = 0x02; + /** + * Constant field signifies playback resumed + */ + public static final int RESUME = 0x03; + + /* package */ static boolean isValid(int playbackStatus) { + boolean isValid = false; + + switch (playbackStatus) { + case START: + case STOP: + case PAUSE: + case RESUME: + isValid = true; + } + return isValid; + } + } + + /** + * Defines actions that can be performed on protected content + */ + public static class Action { + /** + * Constant field signifies that the default action + */ + public static final int DEFAULT = 0x00; + /** + * Constant field signifies that the content can be played + */ + public static final int PLAY = 0x01; + /** + * Constant field signifies that the content can be set as ring tone + */ + public static final int RINGTONE = 0x02; + /** + * Constant field signifies that the content can be transfered + */ + public static final int TRANSFER = 0x03; + /** + * Constant field signifies that the content can be set as output + */ + public static final int OUTPUT = 0x04; + /** + * Constant field signifies that preview is allowed + */ + public static final int PREVIEW = 0x05; + /** + * Constant field signifies that the content can be executed + */ + public static final int EXECUTE = 0x06; + /** + * Constant field signifies that the content can displayed + */ + public static final int DISPLAY = 0x07; + + /* package */ static boolean isValid(int action) { + boolean isValid = false; + + switch (action) { + case DEFAULT: + case PLAY: + case RINGTONE: + case TRANSFER: + case OUTPUT: + case PREVIEW: + case EXECUTE: + case DISPLAY: + isValid = true; + } + return isValid; + } + } + + /** + * Defines constants related to status of the rights + */ + public static class RightsStatus { + /** + * Constant field signifies that the rights are valid + */ + public static final int RIGHTS_VALID = 0x00; + /** + * Constant field signifies that the rights are invalid + */ + public static final int RIGHTS_INVALID = 0x01; + /** + * Constant field signifies that the rights are expired for the content + */ + public static final int RIGHTS_EXPIRED = 0x02; + /** + * Constant field signifies that the rights are not acquired for the content + */ + public static final int RIGHTS_NOT_ACQUIRED = 0x03; + } +} + diff --git a/drm/java/android/drm/DrmSupportInfo.java b/drm/java/android/drm/DrmSupportInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..0886af8e8da03594e373f65a5b39bbb25705bd56 --- /dev/null +++ b/drm/java/android/drm/DrmSupportInfo.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * This is an entity class which wraps the capability of each plug-in, + * such as mimetype's and file suffixes it could handle. + * + * Plug-in developer could return the capability of the plugin by passing + * {@link DrmSupportInfo} instance. + * + */ +public class DrmSupportInfo { + private final ArrayList mFileSuffixList = new ArrayList(); + private final ArrayList mMimeTypeList = new ArrayList(); + private String mDescription = ""; + + /** + * Add the mime-type to the support info such that respective plug-in is + * capable of handling the given mime-type. + * + * @param mimeType MIME type + */ + public void addMimeType(String mimeType) { + mMimeTypeList.add(mimeType); + } + + /** + * Add the file suffix to the support info such that respective plug-in is + * capable of handling the given file suffix. + * + * @param fileSuffix File suffix which can be handled + */ + public void addFileSuffix(String fileSuffix) { + mFileSuffixList.add(fileSuffix); + } + + /** + * Returns the iterator to walk to through mime types of this object + * + * @return Iterator object + */ + public Iterator getMimeTypeIterator() { + return mMimeTypeList.iterator(); + } + + /** + * Returns the iterator to walk to through file suffixes of this object + * + * @return Iterator object + */ + public Iterator getFileSuffixIterator() { + return mFileSuffixList.iterator(); + } + + /** + * Set the unique description about the plugin + * + * @param description Unique description + */ + public void setDescription(String description) { + if (null != description) { + mDescription = description; + } + } + + /** + * Returns the unique description associated with the plugin + * + * @return Unique description + */ + public String getDescriprition() { + return mDescription; + } + + /** + * Overridden hash code implementation + * + * @return Hash code value + */ + public int hashCode() { + return mFileSuffixList.hashCode() + mMimeTypeList.hashCode() + mDescription.hashCode(); + } + + /** + * Overridden equals implementation + * + * @param object The object to be compared + * @return + * true if equal + * false if not equal + */ + public boolean equals(Object object) { + boolean result = false; + + if (object instanceof DrmSupportInfo) { + result = mFileSuffixList.equals(((DrmSupportInfo) object).mFileSuffixList) && + mMimeTypeList.equals(((DrmSupportInfo) object).mMimeTypeList) && + mDescription.equals(((DrmSupportInfo) object).mDescription); + } + return result; + } + + /** + * Returns whether given mime-type is supported or not + * + * @param mimeType MIME type + * @return + * true if mime type is supported + * false if mime type is not supported + */ + /* package */ boolean isSupportedMimeType(String mimeType) { + if (null != mimeType && !mimeType.equals("")) { + for (int i = 0; i < mMimeTypeList.size(); i++) { + String completeMimeType = mMimeTypeList.get(i); + if (completeMimeType.startsWith(mimeType)) { + return true; + } + } + } + return false; + } + + /** + * Returns whether given file suffix is supported or not + * + * @param fileSuffix File suffix + * @return + * true - if file suffix is supported + * false - if file suffix is not supported + */ + /* package */ boolean isSupportedFileSuffix(String fileSuffix) { + return mFileSuffixList.contains(fileSuffix); + } +} + diff --git a/drm/java/android/drm/DrmUtils.java b/drm/java/android/drm/DrmUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..5e5397c45874cea0c6ee6ee53fc1d121a4f187bd --- /dev/null +++ b/drm/java/android/drm/DrmUtils.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Iterator; + +/** + * The utility class used in the DRM Framework. This inclueds APIs for file operations + * and ExtendedMetadataParser for parsing extended metadata BLOB in DRM constraints. + * + */ +public class DrmUtils { + /* Should be used when we need to read from local file */ + /* package */ static byte[] readBytes(String path) throws IOException { + File file = new File(path); + return readBytes(file); + } + + /* Should be used when we need to read from local file */ + /* package */ static byte[] readBytes(File file) throws IOException { + FileInputStream inputStream = new FileInputStream(file); + BufferedInputStream bufferedStream = new BufferedInputStream(inputStream); + byte[] data = null; + + try { + int length = bufferedStream.available(); + if (length > 0) { + data = new byte[length]; + // read the entire data + bufferedStream.read(data); + } + } finally { + quiteDispose(bufferedStream); + quiteDispose(inputStream); + } + return data; + } + + /* package */ static void writeToFile(final String path, byte[] data) throws IOException { + /* check for invalid inputs */ + FileOutputStream outputStream = null; + + if (null != path && null != data) { + try { + outputStream = new FileOutputStream(path); + outputStream.write(data); + } finally { + quiteDispose(outputStream); + } + } + } + + /* package */ static void removeFile(String path) throws IOException { + File file = new File(path); + file.delete(); + } + + private static void quiteDispose(InputStream stream) { + try { + if (null != stream) { + stream.close(); + } + } catch (IOException e) { + // no need to care, at least as of now + } + } + + private static void quiteDispose(OutputStream stream) { + try { + if (null != stream) { + stream.close(); + } + } catch (IOException e) { + // no need to care + } + } + + /** + * Get an instance of ExtendedMetadataParser to be used for parsing + * extended metadata BLOB in DRM constraints.
    + * + * extendedMetadata BLOB is retrieved by specifing + * key DrmStore.ConstraintsColumns.EXTENDED_METADATA. + * + * @param extendedMetadata BLOB in which key-value pairs of extended metadata are embedded. + * + */ + public static ExtendedMetadataParser getExtendedMetadataParser(byte[] extendedMetadata) { + return new ExtendedMetadataParser(extendedMetadata); + } + + /** + * Utility parser to parse the extended meta-data embedded inside DRM constraints

    + * + * Usage example
    + * byte[] extendedMetadata
    + *      = + * constraints.getAsByteArray(DrmStore.ConstraintsColumns.EXTENDED_METADATA);
    + * ExtendedMetadataParser parser = getExtendedMetadataParser(extendedMetadata);
    + * Iterator keyIterator = parser.keyIterator();
    + * while (keyIterator.hasNext()) {
    + *     String extendedMetadataKey = keyIterator.next();
    + *     String extendedMetadataValue = + * parser.get(extendedMetadataKey);
    + * } + */ + public static class ExtendedMetadataParser { + HashMap mMap = new HashMap(); + + private int readByte(byte[] constraintData, int arrayIndex) { + //Convert byte[] into int. + return (int)constraintData[arrayIndex]; + } + + private String readMultipleBytes( + byte[] constraintData, int numberOfBytes, int arrayIndex) { + byte[] returnBytes = new byte[numberOfBytes]; + for (int j = arrayIndex, i = 0; j < arrayIndex + numberOfBytes; j++,i++) { + returnBytes[i] = constraintData[j]; + } + return new String(returnBytes); + } + + /* + * This will parse the following format + * KeyLengthValueLengthKeyValueKeyLength1ValueLength1Key1Value1..\0 + */ + private ExtendedMetadataParser(byte[] constraintData) { + //Extract KeyValue Pair Info, till terminator occurs. + int index = 0; + + while (index < constraintData.length) { + //Parse Key Length + int keyLength = readByte(constraintData, index); + index++; + + //Parse Value Length + int valueLength = readByte(constraintData, index); + index++; + + //Fetch key + String strKey = readMultipleBytes(constraintData, keyLength, index); + index += keyLength; + + //Fetch Value + String strValue = readMultipleBytes(constraintData, valueLength, index); + index += valueLength; + mMap.put(strKey, strValue); + } + } + + public Iterator iterator() { + return mMap.values().iterator(); + } + + public Iterator keyIterator() { + return mMap.keySet().iterator(); + } + + public String get(String key) { + return mMap.get(key); + } + } +} + diff --git a/drm/java/android/drm/ProcessedData.java b/drm/java/android/drm/ProcessedData.java new file mode 100644 index 0000000000000000000000000000000000000000..579264f00ba2f2675d51765158ec296ddec9a7b8 --- /dev/null +++ b/drm/java/android/drm/ProcessedData.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.drm; + +/** + * This is an entity class which wraps the result of transaction between + * device and online DRM server by using {@link DrmManagerClient#processDrmInfo(DrmInfo)} + * + * In license acquisition scenario this class would hold the binary data + * of rights information. + * + */ +public class ProcessedData { + private final byte[] mData; + private String mAccountId = "_NO_USER"; + private String mSubscriptionId = ""; + + /** + * constructor to create ProcessedData object with given parameters + * + * @param data Rights data + * @param accountId Account Id of the user + */ + /* package */ ProcessedData(byte[] data, String accountId) { + mData = data; + mAccountId = accountId; + } + + /** + * constructor to create ProcessedData object with given parameters + * + * @param data Rights data + * @param accountId Account Id of the user + * @param subscriptionId Subscription Id of the user + */ + /* package */ ProcessedData(byte[] data, String accountId, String subscriptionId) { + mData = data; + mAccountId = accountId; + mSubscriptionId = subscriptionId; + } + + /** + * Returns the processed data as a result. + * + * @return Rights data associated + */ + public byte[] getData() { + return mData; + } + + /** + * Returns the account-id associated with this object + * + * @return Account Id associated + */ + public String getAccountId() { + return mAccountId; + } + + /** + * Returns the subscription-id associated with this object + * + * @return Subscription Id associated + */ + public String getSubscriptionId() { + return mSubscriptionId; + } +} + diff --git a/drm/jni/Android.mk b/drm/jni/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..b65e4daafe635c0370f42af157d8d9cd147d96eb --- /dev/null +++ b/drm/jni/Android.mk @@ -0,0 +1,50 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + android_drm_DrmManagerClient.cpp + +LOCAL_MODULE:= libdrmframework_jni + +LOCAL_SHARED_LIBRARIES := \ + libdrmframework \ + libutils \ + libandroid_runtime \ + libnativehelper \ + libbinder + +ifeq ($(TARGET_SIMULATOR),true) + LOCAL_LDLIBS += -ldl +else + LOCAL_SHARED_LIBRARIES += libdl +endif + +LOCAL_STATIC_LIBRARIES := + +LOCAL_C_INCLUDES += \ + $(JNI_H_INCLUDE) \ + $(TOP)/frameworks/base/drm/libdrmframework/include \ + $(TOP)/frameworks/base/drm/libdrmframework/plugins/common/include \ + $(TOP)/frameworks/base/include + +LOCAL_PRELINK_MODULE := false + +LOCAL_MODULE_TAGS := optional + +include $(BUILD_SHARED_LIBRARY) + diff --git a/drm/jni/android_drm_DrmManagerClient.cpp b/drm/jni/android_drm_DrmManagerClient.cpp new file mode 100644 index 0000000000000000000000000000000000000000..bc5a7bfcd400ade0fa9f47459a833db8c30ddd71 --- /dev/null +++ b/drm/jni/android_drm_DrmManagerClient.cpp @@ -0,0 +1,755 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmManager-JNI" +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace android; + +/** + * Utility class used to extract the value from the provided java object. + * May need to add some utility function to create java object. + */ +class Utility { +public: + static String8 getStringValue(JNIEnv* env, jobject object, const char* fieldName); + + static char* getByteArrayValue( + JNIEnv* env, jobject object, const char* fieldName, int* dataLength); + + static char* getByteArrayValue( + JNIEnv* env, jbyteArray byteArray, int* dataLength); + + static String8 getStringValue(JNIEnv* env, jstring string); + + static int getIntValue(JNIEnv* env, jobject object, const char* fieldName); +}; + +String8 Utility::getStringValue(JNIEnv* env, jobject object, const char* fieldName) { + String8 dataString(""); + + /* Look for the instance field with the name fieldName */ + jfieldID fieldID + = env->GetFieldID(env->GetObjectClass(object), fieldName , "Ljava/lang/String;"); + + if (NULL != fieldID) { + jstring valueString = (jstring) env->GetObjectField(object, fieldID); + + if (NULL != valueString && valueString != env->NewStringUTF("")) { + char* bytes = const_cast< char* > (env->GetStringUTFChars(valueString, NULL)); + + const int length = strlen(bytes) + 1; + char *data = new char[length]; + strncpy(data, bytes, length); + dataString = String8(data); + + env->ReleaseStringUTFChars(valueString, bytes); + delete [] data; data = NULL; + } else { + LOGD("Failed to retrieve the data from the field %s", fieldName); + } + } + return dataString; +} + +String8 Utility::getStringValue(JNIEnv* env, jstring string) { + String8 dataString(""); + + if (NULL != string && string != env->NewStringUTF("")) { + char* bytes = const_cast< char* > (env->GetStringUTFChars(string, NULL)); + + const int length = strlen(bytes) + 1; + char *data = new char[length]; + strncpy(data, bytes, length); + dataString = String8(data); + + env->ReleaseStringUTFChars(string, bytes); + delete [] data; data = NULL; + } + return dataString; +} + +char* Utility::getByteArrayValue( + JNIEnv* env, jobject object, const char* fieldName, int* dataLength) { + char* data = NULL; + *dataLength = 0; + + jfieldID fieldID = env->GetFieldID(env->GetObjectClass(object), fieldName , "[B"); + + if (NULL != fieldID) { + jbyteArray byteArray = (jbyteArray) env->GetObjectField(object, fieldID); + if (NULL != byteArray) { + jint length = env->GetArrayLength(byteArray); + + *dataLength = length; + if (0 < *dataLength) { + data = new char[length]; + env->GetByteArrayRegion(byteArray, (jint)0, length, (jbyte *) data); + } + } + } + return data; +} + +char* Utility::getByteArrayValue(JNIEnv* env, jbyteArray byteArray, int* dataLength) { + char* data = NULL; + if (NULL != byteArray) { + jint length = env->GetArrayLength(byteArray); + + *dataLength = length; + if (0 < *dataLength) { + data = new char[length]; + env->GetByteArrayRegion(byteArray, (jint)0, length, (jbyte *) data); + } + } + return data; +} + +int Utility::getIntValue(JNIEnv* env, jobject object, const char* fieldName) { + jfieldID fieldID; + int intValue = -1; + + /* Get a reference to obj’s class */ + jclass clazz = env->GetObjectClass(object); + /* Look for the instance field with the name fieldName */ + fieldID = env->GetFieldID(clazz, fieldName , "I"); + + if (NULL != fieldID) { + intValue = (int) env->GetIntField(object, fieldID); + } + + return intValue; +} + +class JNIOnInfoListener : public DrmManagerClient::OnInfoListener { +public: + JNIOnInfoListener(JNIEnv* env, jobject thiz, jobject weak_thiz); + + virtual ~JNIOnInfoListener(); + void onInfo(const DrmInfoEvent& event); + +private: + JNIOnInfoListener(); + jclass mClass; + jobject mObject; +}; + +JNIOnInfoListener::JNIOnInfoListener(JNIEnv* env, jobject thiz, jobject weak_thiz) { + jclass clazz = env->GetObjectClass(thiz); + + if (clazz == NULL) { + LOGE("Can't find android/drm/DrmManagerClient"); + jniThrowException(env, "java/lang/Exception", NULL); + return; + } + mClass = (jclass)env->NewGlobalRef(clazz); + mObject = env->NewGlobalRef(weak_thiz); +} + +JNIOnInfoListener::~JNIOnInfoListener() { + JNIEnv *env = AndroidRuntime::getJNIEnv(); + env->DeleteGlobalRef(mObject); + env->DeleteGlobalRef(mClass); +} + +void JNIOnInfoListener::onInfo(const DrmInfoEvent& event) { + jint uniqueId = event.getUniqueId(); + jint type = event.getType(); + JNIEnv *env = AndroidRuntime::getJNIEnv(); + jstring message = env->NewStringUTF(event.getMessage().string()); + LOGV("JNIOnInfoListener::onInfo => %d | %d | %s", uniqueId, type, event.getMessage().string()); + + env->CallStaticVoidMethod( + mClass, + env->GetStaticMethodID(mClass, "notify", "(Ljava/lang/Object;IILjava/lang/String;)V"), + mObject, uniqueId, type, message); +} + +static Mutex sLock; + +static sp setDrmManagerClientImpl( + JNIEnv* env, jobject thiz, const sp& client) { + Mutex::Autolock l(sLock); + jclass clazz = env->FindClass("android/drm/DrmManagerClient"); + jfieldID fieldId = env->GetFieldID(clazz, "mNativeContext", "I"); + + sp old = (DrmManagerClientImpl*)env->GetIntField(thiz, fieldId); + if (client.get()) { + client->incStrong(thiz); + } + if (old != 0) { + old->decStrong(thiz); + } + env->SetIntField(thiz, fieldId, (int)client.get()); + return old; +} + +static sp getDrmManagerClientImpl(JNIEnv* env, jobject thiz) { + Mutex::Autolock l(sLock); + jclass clazz = env->FindClass("android/drm/DrmManagerClient"); + jfieldID fieldId = env->GetFieldID(clazz, "mNativeContext", "I"); + + DrmManagerClientImpl* const client = (DrmManagerClientImpl*)env->GetIntField(thiz, fieldId); + return sp(client); +} + +static void android_drm_DrmManagerClient_loadPlugIns( + JNIEnv* env, jobject thiz, jint uniqueId, jobject weak_thiz) { + LOGV("load plugins - Enter"); + + sp drmManager = DrmManagerClientImpl::create(&uniqueId); + + // Set the listener to DrmManager + sp listener = new JNIOnInfoListener(env, thiz, weak_thiz); + drmManager->setOnInfoListener(uniqueId, listener); + + setDrmManagerClientImpl(env, thiz, drmManager); + + getDrmManagerClientImpl(env, thiz)->loadPlugIns(uniqueId); + LOGV("load plugins - Exit"); +} + +static void android_drm_DrmManagerClient_unloadPlugIns(JNIEnv* env, jobject thiz, jint uniqueId) { + LOGV("unload plugins - Enter"); + sp client = getDrmManagerClientImpl(env, thiz); + client->unloadPlugIns(uniqueId); + client->setOnInfoListener(uniqueId, NULL); + DrmManagerClientImpl::remove(uniqueId); + + sp oldClient = setDrmManagerClientImpl(env, thiz, NULL); + if (oldClient != NULL) { + oldClient->setOnInfoListener(uniqueId, NULL); + } + + LOGV("unload plugins - Exit"); +} + +static jobject android_drm_DrmManagerClient_getConstraintsFromContent( + JNIEnv* env, jobject thiz, jint uniqueId, jstring jpath, jint usage) { + LOGV("GetConstraints - Enter"); + + const String8 pathString = Utility::getStringValue(env, jpath); + DrmConstraints* pConstraints + = getDrmManagerClientImpl(env, thiz)->getConstraints(uniqueId, &pathString, usage); + + jclass localRef = env->FindClass("android/content/ContentValues"); + jobject constraints = NULL; + + if (NULL != localRef && NULL != pConstraints) { + // Get the constructor id + jmethodID constructorId = env->GetMethodID(localRef, "", "()V"); + // create the java DrmConstraints object + constraints = env->NewObject(localRef, constructorId); + + DrmConstraints::KeyIterator keyIt = pConstraints->keyIterator(); + + while (keyIt.hasNext()) { + String8 key = keyIt.next(); + + // insert the entry to newly created java object + if (DrmConstraints::EXTENDED_METADATA == key) { + const char* value = pConstraints->getAsByteArray(&key); + if (NULL != value) { + jbyteArray dataArray = env->NewByteArray(strlen(value)); + env->SetByteArrayRegion(dataArray, 0, strlen(value), (jbyte*)value); + env->CallVoidMethod( + constraints, env->GetMethodID(localRef, "put", "(Ljava/lang/String;[B)V"), + env->NewStringUTF(key.string()), dataArray); + } + } else { + String8 value = pConstraints->get(key); + env->CallVoidMethod( + constraints, + env->GetMethodID(localRef, "put", "(Ljava/lang/String;Ljava/lang/String;)V"), + env->NewStringUTF(key.string()), env->NewStringUTF(value.string())); + } + } + } + + delete pConstraints; pConstraints = NULL; + LOGV("GetConstraints - Exit"); + return constraints; +} + +static jobjectArray android_drm_DrmManagerClient_getAllSupportInfo( + JNIEnv* env, jobject thiz, jint uniqueId) { + LOGV("GetAllSupportInfo - Enter"); + DrmSupportInfo* drmSupportInfoArray = NULL; + + int length = 0; + getDrmManagerClientImpl(env, thiz)->getAllSupportInfo(uniqueId, &length, &drmSupportInfoArray); + + jclass clazz = env->FindClass("android/drm/DrmSupportInfo"); + + jobjectArray array = (jobjectArray)env->NewObjectArray(length, clazz, NULL); + + for (int i = 0; i < length; i++) { + DrmSupportInfo info = drmSupportInfoArray[i]; + + jobject drmSupportInfo = env->NewObject(clazz, env->GetMethodID(clazz, "", "()V")); + + jmethodID addMimeTypeId + = env->GetMethodID(clazz, "addMimeType", "(Ljava/lang/String;)V"); + jmethodID addFileSuffixId + = env->GetMethodID(clazz, "addFileSuffix", "(Ljava/lang/String;)V"); + + env->CallVoidMethod( + drmSupportInfo, env->GetMethodID(clazz, "setDescription", "(Ljava/lang/String;)V"), + env->NewStringUTF(info.getDescription().string())); + + DrmSupportInfo::MimeTypeIterator iterator = info.getMimeTypeIterator(); + while (iterator.hasNext()) { + String8 value = iterator.next(); + env->CallVoidMethod(drmSupportInfo, addMimeTypeId, env->NewStringUTF(value.string())); + } + + DrmSupportInfo::FileSuffixIterator it = info.getFileSuffixIterator(); + while (it.hasNext()) { + String8 value = it.next(); + env->CallVoidMethod( + drmSupportInfo, addFileSuffixId, env->NewStringUTF(value.string())); + } + + env->SetObjectArrayElement(array, i, drmSupportInfo); + } + + delete [] drmSupportInfoArray; drmSupportInfoArray = NULL; + LOGV("GetAllSupportInfo - Exit"); + return array; +} + +static void android_drm_DrmManagerClient_installDrmEngine( + JNIEnv* env, jobject thiz, jint uniqueId, jstring engineFilePath) { + LOGV("installDrmEngine - Enter"); + //getDrmManagerClient(env, thiz) + // ->installDrmEngine(uniqueId, Utility::getStringValue(env, engineFilePath)); + LOGV("installDrmEngine - Exit"); +} + +static void android_drm_DrmManagerClient_saveRights( + JNIEnv* env, jobject thiz, jint uniqueId, + jobject drmRights, jstring rightsPath, jstring contentPath) { + LOGV("saveRights - Enter"); + int dataLength = 0; + char* mData = Utility::getByteArrayValue(env, drmRights, "mData", &dataLength); + + if (NULL != mData) { + DrmRights rights(DrmBuffer(mData, dataLength), + Utility::getStringValue(env, drmRights, "mMimeType"), + Utility::getStringValue(env, drmRights, "mAccountId"), + Utility::getStringValue(env, drmRights, "mSubscriptionId")); + getDrmManagerClientImpl(env, thiz) + ->saveRights(uniqueId, rights, Utility::getStringValue(env, rightsPath), + Utility::getStringValue(env, contentPath)); + } + + delete mData; mData = NULL; + LOGV("saveRights - Exit"); +} + +static jboolean android_drm_DrmManagerClient_canHandle( + JNIEnv* env, jobject thiz, jint uniqueId, jstring path, jstring mimeType) { + LOGV("canHandle - Enter"); + jboolean result + = getDrmManagerClientImpl(env, thiz) + ->canHandle(uniqueId, Utility::getStringValue(env, path), + Utility::getStringValue(env, mimeType)); + LOGV("canHandle - Exit"); + return result; +} + +static jobject android_drm_DrmManagerClient_processDrmInfo( + JNIEnv* env, jobject thiz, jint uniqueId, jobject drmInfoObject) { + LOGV("processDrmInfo - Enter"); + int dataLength = 0; + const String8 mMimeType = Utility::getStringValue(env, drmInfoObject, "mMimeType"); + char* mData = Utility::getByteArrayValue(env, drmInfoObject, "mData", &dataLength); + int mInfoType = Utility::getIntValue(env, drmInfoObject, "mInfoType"); + + const DrmBuffer buffer(mData, dataLength); + DrmInfo drmInfo(mInfoType, buffer, mMimeType); + + jclass clazz = env->FindClass("android/drm/DrmInfo"); + jobject keyIterator + = env->CallObjectMethod(drmInfoObject, + env->GetMethodID(clazz, "keyIterator", "()Ljava/util/Iterator;")); + + jmethodID hasNextId = env->GetMethodID(env->FindClass("java/util/Iterator"), "hasNext", "()Z"); + + while (env->CallBooleanMethod(keyIterator, hasNextId)) { + jstring key = (jstring) env->CallObjectMethod(keyIterator, + env->GetMethodID(env->FindClass("java/util/Iterator"), + "next", "()Ljava/lang/Object;")); + + jobject valueObject = env->CallObjectMethod(drmInfoObject, + env->GetMethodID(clazz, "get", "(Ljava/lang/String;)Ljava/lang/Object;"), key); + + jstring valString = NULL; + if (NULL != valueObject) { + valString = (jstring) env->CallObjectMethod(valueObject, + env->GetMethodID(env->FindClass("java/lang/Object"), + "toString", "()Ljava/lang/String;")); + } + + String8 keyString = Utility::getStringValue(env, key); + String8 valueString = Utility::getStringValue(env, valString); + LOGD("Key: %s | Value: %s", keyString.string(), valueString.string()); + + drmInfo.put(keyString, valueString); + } + + DrmInfoStatus* pDrmInfoStatus + = getDrmManagerClientImpl(env, thiz)->processDrmInfo(uniqueId, &drmInfo); + + jclass localRef = env->FindClass("android/drm/DrmInfoStatus"); + jobject drmInfoStatus = NULL; + + if (NULL != localRef && NULL != pDrmInfoStatus) { + int statusCode = pDrmInfoStatus->statusCode; + + jbyteArray dataArray = NULL; + if (NULL != pDrmInfoStatus->drmBuffer) { + int length = pDrmInfoStatus->drmBuffer->length; + dataArray = env->NewByteArray(length); + env->SetByteArrayRegion( + dataArray, 0, length, (jbyte*) pDrmInfoStatus->drmBuffer->data); + + delete [] pDrmInfoStatus->drmBuffer->data; + delete pDrmInfoStatus->drmBuffer; pDrmInfoStatus->drmBuffer = NULL; + } + jclass clazz = env->FindClass("android/drm/ProcessedData"); + jmethodID constructorId + = env->GetMethodID(clazz, "", "([BLjava/lang/String;Ljava/lang/String;)V"); + jobject processedData = env->NewObject(clazz, constructorId, dataArray, + env->NewStringUTF((drmInfo.get(DrmInfoRequest::ACCOUNT_ID)).string()), + env->NewStringUTF((drmInfo.get(DrmInfoRequest::SUBSCRIPTION_ID)).string())); + + constructorId + = env->GetMethodID(localRef, + "", "(ILandroid/drm/ProcessedData;Ljava/lang/String;)V"); + + drmInfoStatus = env->NewObject(localRef, constructorId, statusCode, processedData, + env->NewStringUTF(pDrmInfoStatus->mimeType.string())); + } + + delete mData; mData = NULL; + delete pDrmInfoStatus; pDrmInfoStatus = NULL; + + LOGV("processDrmInfo - Exit"); + return drmInfoStatus; +} + +static jobject android_drm_DrmManagerClient_acquireDrmInfo( + JNIEnv* env, jobject thiz, jint uniqueId, jobject drmInfoRequest) { + LOGV("acquireDrmInfo Enter"); + const String8 mMimeType = Utility::getStringValue(env, drmInfoRequest, "mMimeType"); + int mInfoType = Utility::getIntValue(env, drmInfoRequest, "mInfoType"); + + DrmInfoRequest drmInfoReq(mInfoType, mMimeType); + + jclass clazz = env->FindClass("android/drm/DrmInfoRequest"); + jobject keyIterator + = env->CallObjectMethod(drmInfoRequest, + env->GetMethodID(clazz, "keyIterator", "()Ljava/util/Iterator;")); + + jmethodID hasNextId = env->GetMethodID(env->FindClass("java/util/Iterator"), "hasNext", "()Z"); + + while (env->CallBooleanMethod(keyIterator, hasNextId)) { + jstring key + = (jstring) env->CallObjectMethod(keyIterator, + env->GetMethodID(env->FindClass("java/util/Iterator"), + "next", "()Ljava/lang/Object;")); + + jstring value = (jstring) env->CallObjectMethod(drmInfoRequest, + env->GetMethodID(clazz, "get", "(Ljava/lang/String;)Ljava/lang/Object;"), key); + + String8 keyString = Utility::getStringValue(env, key); + String8 valueString = Utility::getStringValue(env, value); + LOGD("Key: %s | Value: %s", keyString.string(), valueString.string()); + + drmInfoReq.put(keyString, valueString); + } + + DrmInfo* pDrmInfo = getDrmManagerClientImpl(env, thiz)->acquireDrmInfo(uniqueId, &drmInfoReq); + + jobject drmInfoObject = NULL; + + if (NULL != pDrmInfo) { + jclass localRef = env->FindClass("android/drm/DrmInfo"); + + if (NULL != localRef) { + int length = pDrmInfo->getData().length; + + jbyteArray dataArray = env->NewByteArray(length); + env->SetByteArrayRegion(dataArray, 0, length, (jbyte*)pDrmInfo->getData().data); + + drmInfoObject + = env->NewObject(localRef, + env->GetMethodID(localRef, "", "(I[BLjava/lang/String;)V"), + mInfoType, dataArray, env->NewStringUTF(pDrmInfo->getMimeType().string())); + + DrmInfo::KeyIterator it = pDrmInfo->keyIterator(); + jmethodID putMethodId + = env->GetMethodID(localRef, "put", "(Ljava/lang/String;Ljava/lang/Object;)V"); + + while (it.hasNext()) { + String8 key = it.next(); + String8 value = pDrmInfo->get(key); + + env->CallVoidMethod(drmInfoObject, putMethodId, + env->NewStringUTF(key.string()), env->NewStringUTF(value.string())); + } + } + delete [] pDrmInfo->getData().data; + } + + delete pDrmInfo; pDrmInfo = NULL; + + LOGV("acquireDrmInfo Exit"); + return drmInfoObject; +} + +static jint android_drm_DrmManagerClient_getDrmObjectType( + JNIEnv* env, jobject thiz, jint uniqueId, jstring path, jstring mimeType) { + LOGV("getDrmObjectType Enter"); + int drmObjectType + = getDrmManagerClientImpl(env, thiz) + ->getDrmObjectType(uniqueId, Utility::getStringValue(env, path), + Utility::getStringValue(env, mimeType)); + LOGV("getDrmObjectType Exit"); + return drmObjectType; +} + +static jstring android_drm_DrmManagerClient_getOriginalMimeType( + JNIEnv* env, jobject thiz, jint uniqueId, jstring path) { + LOGV("getOriginalMimeType Enter"); + String8 mimeType + = getDrmManagerClientImpl(env, thiz) + ->getOriginalMimeType(uniqueId, Utility::getStringValue(env, path)); + LOGV("getOriginalMimeType Exit"); + return env->NewStringUTF(mimeType.string()); +} + +static jint android_drm_DrmManagerClient_checkRightsStatus( + JNIEnv* env, jobject thiz, jint uniqueId, jstring path, int action) { + LOGV("getOriginalMimeType Enter"); + int rightsStatus + = getDrmManagerClientImpl(env, thiz) + ->checkRightsStatus(uniqueId, Utility::getStringValue(env, path), action); + LOGV("getOriginalMimeType Exit"); + return rightsStatus; +} + +static void android_drm_DrmManagerClient_removeRights( + JNIEnv* env, jobject thiz, jint uniqueId, jstring path) { + LOGV("removeRights Enter"); + getDrmManagerClientImpl(env, thiz)->removeRights(uniqueId, Utility::getStringValue(env, path)); + LOGV("removeRights Exit"); +} + +static void android_drm_DrmManagerClient_removeAllRights( + JNIEnv* env, jobject thiz, jint uniqueId) { + LOGV("removeAllRights Enter"); + getDrmManagerClientImpl(env, thiz)->removeAllRights(uniqueId); + LOGV("removeAllRights Exit"); +} + +static jint android_drm_DrmManagerClient_openConvertSession( + JNIEnv* env, jobject thiz, jint uniqueId, jstring mimeType) { + LOGV("openConvertSession Enter"); + int convertId + = getDrmManagerClientImpl(env, thiz) + ->openConvertSession(uniqueId, Utility::getStringValue(env, mimeType)); + LOGV("openConvertSession Exit"); + return convertId; +} + +static jobject android_drm_DrmManagerClient_convertData( + JNIEnv* env, jobject thiz, jint uniqueId, jint convertId, jbyteArray inputData) { + LOGV("convertData Enter"); + + int dataLength = 0; + char* mData = Utility::getByteArrayValue(env, inputData, &dataLength); + const DrmBuffer buffer(mData, dataLength); + + DrmConvertedStatus* pDrmConvertedStatus + = getDrmManagerClientImpl(env, thiz)->convertData(uniqueId, convertId, &buffer); + + jclass localRef = env->FindClass("android/drm/DrmConvertedStatus"); + + jobject drmConvertedStatus = NULL; + + if (NULL != localRef && NULL != pDrmConvertedStatus) { + int statusCode = pDrmConvertedStatus->statusCode; + + jbyteArray dataArray = NULL; + if (NULL != pDrmConvertedStatus->convertedData) { + int length = pDrmConvertedStatus->convertedData->length; + dataArray = env->NewByteArray(length); + env->SetByteArrayRegion(dataArray, 0, length, + (jbyte*) pDrmConvertedStatus->convertedData->data); + + delete [] pDrmConvertedStatus->convertedData->data; + delete pDrmConvertedStatus->convertedData; pDrmConvertedStatus->convertedData = NULL; + } + jmethodID constructorId = env->GetMethodID(localRef, "", "(I[BI)V"); + drmConvertedStatus + = env->NewObject(localRef, constructorId, + statusCode, dataArray, pDrmConvertedStatus->offset); + } + + delete mData; mData = NULL; + delete pDrmConvertedStatus; pDrmConvertedStatus = NULL; + + LOGV("convertData - Exit"); + return drmConvertedStatus; +} + +static jobject android_drm_DrmManagerClient_closeConvertSession( + JNIEnv* env, jobject thiz, int uniqueId, jint convertId) { + + LOGV("closeConvertSession Enter"); + + DrmConvertedStatus* pDrmConvertedStatus + = getDrmManagerClientImpl(env, thiz)->closeConvertSession(uniqueId, convertId); + + jclass localRef = env->FindClass("android/drm/DrmConvertedStatus"); + + jobject drmConvertedStatus = NULL; + + if (NULL != localRef && NULL != pDrmConvertedStatus) { + int statusCode = pDrmConvertedStatus->statusCode; + + jbyteArray dataArray = NULL; + if (NULL != pDrmConvertedStatus->convertedData) { + int length = pDrmConvertedStatus->convertedData->length; + dataArray = env->NewByteArray(length); + env->SetByteArrayRegion( + dataArray, 0, length, (jbyte*) pDrmConvertedStatus->convertedData->data); + + delete [] pDrmConvertedStatus->convertedData->data; + delete pDrmConvertedStatus->convertedData; pDrmConvertedStatus->convertedData = NULL; + } + jmethodID constructorId = env->GetMethodID(localRef, "", "(I[BI)V"); + drmConvertedStatus + = env->NewObject(localRef, constructorId, + statusCode, dataArray, pDrmConvertedStatus->offset); + } + + delete pDrmConvertedStatus; pDrmConvertedStatus = NULL; + + LOGV("closeConvertSession - Exit"); + return drmConvertedStatus; +} + +static JNINativeMethod nativeMethods[] = { + + {"_loadPlugIns", "(ILjava/lang/Object;)V", + (void*)android_drm_DrmManagerClient_loadPlugIns}, + + {"_unloadPlugIns", "(I)V", + (void*)android_drm_DrmManagerClient_unloadPlugIns}, + + {"_getConstraints", "(ILjava/lang/String;I)Landroid/content/ContentValues;", + (void*)android_drm_DrmManagerClient_getConstraintsFromContent}, + + {"_getAllSupportInfo", "(I)[Landroid/drm/DrmSupportInfo;", + (void*)android_drm_DrmManagerClient_getAllSupportInfo}, + + {"_installDrmEngine", "(ILjava/lang/String;)V", + (void*)android_drm_DrmManagerClient_installDrmEngine}, + + {"_canHandle", "(ILjava/lang/String;Ljava/lang/String;)Z", + (void*)android_drm_DrmManagerClient_canHandle}, + + {"_processDrmInfo", "(ILandroid/drm/DrmInfo;)Landroid/drm/DrmInfoStatus;", + (void*)android_drm_DrmManagerClient_processDrmInfo}, + + {"_acquireDrmInfo", "(ILandroid/drm/DrmInfoRequest;)Landroid/drm/DrmInfo;", + (void*)android_drm_DrmManagerClient_acquireDrmInfo}, + + {"_saveRights", "(ILandroid/drm/DrmRights;Ljava/lang/String;Ljava/lang/String;)V", + (void*)android_drm_DrmManagerClient_saveRights}, + + {"_getDrmObjectType", "(ILjava/lang/String;Ljava/lang/String;)I", + (void*)android_drm_DrmManagerClient_getDrmObjectType}, + + {"_getOriginalMimeType", "(ILjava/lang/String;)Ljava/lang/String;", + (void*)android_drm_DrmManagerClient_getOriginalMimeType}, + + {"_checkRightsStatus", "(ILjava/lang/String;I)I", + (void*)android_drm_DrmManagerClient_checkRightsStatus}, + + {"_removeRights", "(ILjava/lang/String;)V", + (void*)android_drm_DrmManagerClient_removeRights}, + + {"_removeAllRights", "(I)V", + (void*)android_drm_DrmManagerClient_removeAllRights}, + + {"_openConvertSession", "(ILjava/lang/String;)I", + (void*)android_drm_DrmManagerClient_openConvertSession}, + + {"_convertData", "(II[B)Landroid/drm/DrmConvertedStatus;", + (void*)android_drm_DrmManagerClient_convertData}, + + {"_closeConvertSession", "(II)Landroid/drm/DrmConvertedStatus;", + (void*)android_drm_DrmManagerClient_closeConvertSession}, +}; + +static int registerNativeMethods(JNIEnv* env) { + int result = -1; + + /* look up the class */ + jclass clazz = env->FindClass("android/drm/DrmManagerClient"); + + if (NULL != clazz) { + if (env->RegisterNatives(clazz, nativeMethods, sizeof(nativeMethods) + / sizeof(nativeMethods[0])) == JNI_OK) { + result = 0; + } + } + return result; +} + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env = NULL; + jint result = -1; + + if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) == JNI_OK) { + if (NULL != env && registerNativeMethods(env) == 0) { + result = JNI_VERSION_1_4; + } + } + return result; +} + diff --git a/drm/libdrmframework/Android.mk b/drm/libdrmframework/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..99133ba28f2f88872e7def343641033d5eb13914 --- /dev/null +++ b/drm/libdrmframework/Android.mk @@ -0,0 +1,49 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + DrmManagerClientImpl.cpp \ + DrmManagerClient.cpp + +LOCAL_MODULE:= libdrmframework + +LOCAL_SHARED_LIBRARIES := \ + libutils \ + libbinder + +ifeq ($(TARGET_SIMULATOR),true) + LOCAL_LDLIBS += -ldl +else + LOCAL_SHARED_LIBRARIES += libdl +endif + +LOCAL_STATIC_LIBRARIES := \ + libdrmframeworkcommon + +LOCAL_C_INCLUDES += \ + $(TOP)/frameworks/base/drm/libdrmframework/include \ + $(TOP)/frameworks/base/include + +LOCAL_PRELINK_MODULE := false + +LOCAL_MODULE_TAGS := optional + +include $(BUILD_SHARED_LIBRARY) + +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/drm/libdrmframework/DrmManagerClient.cpp b/drm/libdrmframework/DrmManagerClient.cpp new file mode 100644 index 0000000000000000000000000000000000000000..06c7c50a214d11c054407648f2c2c818770c3315 --- /dev/null +++ b/drm/libdrmframework/DrmManagerClient.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmManagerClient(Native)" +#include + +#include +#include +#include + +#include "DrmManagerClientImpl.h" + +using namespace android; + +DrmManagerClient::DrmManagerClient() { + int uniqueId = 0; + mDrmManagerClientImpl = NULL; + + mDrmManagerClientImpl = DrmManagerClientImpl::create(&uniqueId); + mUniqueId = uniqueId; + + loadPlugIns(); +} + +DrmManagerClient::~DrmManagerClient() { + unloadPlugIns(); + DrmManagerClientImpl::remove(mUniqueId); + + delete mDrmManagerClientImpl; mDrmManagerClientImpl = NULL; +} + +status_t DrmManagerClient::loadPlugIns() { + return mDrmManagerClientImpl->loadPlugIns(mUniqueId); +} + +status_t DrmManagerClient::setOnInfoListener( + const sp& infoListener) { + return mDrmManagerClientImpl->setOnInfoListener(mUniqueId, infoListener); +} + +status_t DrmManagerClient::unloadPlugIns() { + return mDrmManagerClientImpl->unloadPlugIns(mUniqueId); +} + +DrmConstraints* DrmManagerClient::getConstraints(const String8* path, const int action) { + return mDrmManagerClientImpl->getConstraints(mUniqueId, path, action); +} + +bool DrmManagerClient::canHandle(const String8& path, const String8& mimeType) { + return mDrmManagerClientImpl->canHandle(mUniqueId, path, mimeType); +} + +DrmInfoStatus* DrmManagerClient::processDrmInfo(const DrmInfo* drmInfo) { + return mDrmManagerClientImpl->processDrmInfo(mUniqueId, drmInfo); +} + +DrmInfo* DrmManagerClient::acquireDrmInfo(const DrmInfoRequest* drmInfoRequest) { + return mDrmManagerClientImpl->acquireDrmInfo(mUniqueId, drmInfoRequest); +} + +void DrmManagerClient::saveRights( + const DrmRights& drmRights, const String8& rightsPath, const String8& contentPath) { + return mDrmManagerClientImpl->saveRights(mUniqueId, drmRights, rightsPath, contentPath); +} + +String8 DrmManagerClient::getOriginalMimeType(const String8& path) { + return mDrmManagerClientImpl->getOriginalMimeType(mUniqueId, path); +} + +int DrmManagerClient::getDrmObjectType(const String8& path, const String8& mimeType) { + return mDrmManagerClientImpl->getDrmObjectType( mUniqueId, path, mimeType); +} + +int DrmManagerClient::checkRightsStatus(const String8& path, int action) { + return mDrmManagerClientImpl->checkRightsStatus(mUniqueId, path, action); +} + +void DrmManagerClient::consumeRights(DecryptHandle* decryptHandle, int action, bool reserve) { + mDrmManagerClientImpl->consumeRights(mUniqueId, decryptHandle, action, reserve); +} + +void DrmManagerClient::setPlaybackStatus( + DecryptHandle* decryptHandle, int playbackStatus, int position) { + mDrmManagerClientImpl->setPlaybackStatus(mUniqueId, decryptHandle, playbackStatus, position); +} + +bool DrmManagerClient::validateAction( + const String8& path, int action, const ActionDescription& description) { + return mDrmManagerClientImpl->validateAction(mUniqueId, path, action, description); +} + +void DrmManagerClient::removeRights(const String8& path) { + mDrmManagerClientImpl->removeRights(mUniqueId, path); +} + +void DrmManagerClient::removeAllRights() { + mDrmManagerClientImpl->removeAllRights(mUniqueId); +} + +int DrmManagerClient::openConvertSession(const String8& mimeType) { + return mDrmManagerClientImpl->openConvertSession(mUniqueId, mimeType); +} + +DrmConvertedStatus* DrmManagerClient::convertData(int convertId, const DrmBuffer* inputData) { + return mDrmManagerClientImpl->convertData(mUniqueId, convertId, inputData); +} + +DrmConvertedStatus* DrmManagerClient::closeConvertSession(int convertId) { + return mDrmManagerClientImpl->closeConvertSession(mUniqueId, convertId); +} + +status_t DrmManagerClient::getAllSupportInfo(int* length, DrmSupportInfo** drmSupportInfoArray) { + return mDrmManagerClientImpl->getAllSupportInfo(mUniqueId, length, drmSupportInfoArray); +} + +DecryptHandle* DrmManagerClient::openDecryptSession(int fd, int offset, int length) { + return mDrmManagerClientImpl->openDecryptSession(mUniqueId, fd, offset, length); +} + +void DrmManagerClient::closeDecryptSession(DecryptHandle* decryptHandle) { + mDrmManagerClientImpl->closeDecryptSession(mUniqueId, decryptHandle); +} + +void DrmManagerClient::initializeDecryptUnit( + DecryptHandle* decryptHandle, int decryptUnitId, const DrmBuffer* headerInfo) { + mDrmManagerClientImpl->initializeDecryptUnit( + mUniqueId, decryptHandle, decryptUnitId, headerInfo); +} + +status_t DrmManagerClient::decrypt( + DecryptHandle* decryptHandle, int decryptUnitId, + const DrmBuffer* encBuffer, DrmBuffer** decBuffer) { + return mDrmManagerClientImpl->decrypt( + mUniqueId, decryptHandle, decryptUnitId, encBuffer, decBuffer); +} + +void DrmManagerClient::finalizeDecryptUnit(DecryptHandle* decryptHandle, int decryptUnitId) { + mDrmManagerClientImpl->finalizeDecryptUnit(mUniqueId, decryptHandle, decryptUnitId); +} + +ssize_t DrmManagerClient::pread( + DecryptHandle* decryptHandle, void* buffer, ssize_t numBytes, off_t offset) { + return mDrmManagerClientImpl->pread(mUniqueId, decryptHandle, buffer, numBytes, offset); +} + diff --git a/drm/libdrmframework/DrmManagerClientImpl.cpp b/drm/libdrmframework/DrmManagerClientImpl.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7274b49d12bd84563a5e18d8dd94474c5a912508 --- /dev/null +++ b/drm/libdrmframework/DrmManagerClientImpl.cpp @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmManagerClientImpl(Native)" +#include + +#include +#include +#include + +#include "DrmManagerClientImpl.h" + +using namespace android; + +#define INVALID_VALUE -1 + +Mutex DrmManagerClientImpl::mMutex; +Vector DrmManagerClientImpl::mUniqueIdVector; +sp DrmManagerClientImpl::mDrmManagerService; +const String8 DrmManagerClientImpl::EMPTY_STRING(""); + +DrmManagerClientImpl* DrmManagerClientImpl::create(int* pUniqueId) { + if (0 == *pUniqueId) { + int uniqueId = 0; + bool foundUniqueId = false; + srand(time(NULL)); + + while (!foundUniqueId) { + const int size = mUniqueIdVector.size(); + uniqueId = rand() % 100; + + int index = 0; + for (; index < size; ++index) { + if (mUniqueIdVector.itemAt(index) == uniqueId) { + foundUniqueId = false; + break; + } + } + if (index == size) { + foundUniqueId = true; + } + } + *pUniqueId = uniqueId; + } + mUniqueIdVector.push(*pUniqueId); + return new DrmManagerClientImpl(); +} + +void DrmManagerClientImpl::remove(int uniqueId) { + for (int i = 0; i < mUniqueIdVector.size(); i++) { + if (uniqueId == mUniqueIdVector.itemAt(i)) { + mUniqueIdVector.removeAt(i); + break; + } + } +} + +DrmManagerClientImpl::DrmManagerClientImpl() { + +} + +DrmManagerClientImpl::~DrmManagerClientImpl() { + +} + +const sp& DrmManagerClientImpl::getDrmManagerService() { + mMutex.lock(); + if (NULL == mDrmManagerService.get()) { + sp sm = defaultServiceManager(); + sp binder; + do { + binder = sm->getService(String16("drm.drmManager")); + if (binder != 0) { + break; + } + LOGW("DrmManagerService not published, waiting..."); + struct timespec reqt; + reqt.tv_sec = 0; + reqt.tv_nsec = 500000000; //0.5 sec + nanosleep(&reqt, NULL); + } while (true); + + mDrmManagerService = interface_cast(binder); + } + mMutex.unlock(); + return mDrmManagerService; +} + +status_t DrmManagerClientImpl::loadPlugIns(int uniqueId) { + return getDrmManagerService()->loadPlugIns(uniqueId); +} + +status_t DrmManagerClientImpl::loadPlugIns(int uniqueId, const String8& plugInDirPath) { + status_t status = DRM_ERROR_UNKNOWN; + if (EMPTY_STRING != plugInDirPath) { + status = getDrmManagerService()->loadPlugIns(uniqueId, plugInDirPath); + } + return status; +} + +status_t DrmManagerClientImpl::setOnInfoListener( + int uniqueId, const sp& infoListener) { + Mutex::Autolock _l(mLock); + mOnInfoListener = infoListener; + return getDrmManagerService()->setDrmServiceListener(uniqueId, this); +} + +status_t DrmManagerClientImpl::unloadPlugIns(int uniqueId) { + return getDrmManagerService()->unloadPlugIns(uniqueId); +} + +status_t DrmManagerClientImpl::installDrmEngine(int uniqueId, const String8& drmEngineFile) { + status_t status = DRM_ERROR_UNKNOWN; + if (EMPTY_STRING != drmEngineFile) { + status = getDrmManagerService()->installDrmEngine(uniqueId, drmEngineFile); + } + return status; +} + +DrmConstraints* DrmManagerClientImpl::getConstraints( + int uniqueId, const String8* path, const int action) { + DrmConstraints *drmConstraints = NULL; + if ((NULL != path) && (EMPTY_STRING != *path)) { + drmConstraints = getDrmManagerService()->getConstraints(uniqueId, path, action); + } + return drmConstraints; +} + +bool DrmManagerClientImpl::canHandle(int uniqueId, const String8& path, const String8& mimeType) { + bool retCode = false; + if ((EMPTY_STRING != path) || (EMPTY_STRING != mimeType)) { + retCode = getDrmManagerService()->canHandle(uniqueId, path, mimeType); + } + return retCode; +} + +DrmInfoStatus* DrmManagerClientImpl::processDrmInfo(int uniqueId, const DrmInfo* drmInfo) { + DrmInfoStatus *drmInfoStatus = NULL; + if (NULL != drmInfo) { + drmInfoStatus = getDrmManagerService()->processDrmInfo(uniqueId, drmInfo); + } + return drmInfoStatus; +} + +DrmInfo* DrmManagerClientImpl::acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest) { + DrmInfo* drmInfo = NULL; + if (NULL != drmInfoRequest) { + drmInfo = getDrmManagerService()->acquireDrmInfo(uniqueId, drmInfoRequest); + } + return drmInfo; +} + +void DrmManagerClientImpl::saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) { + if (EMPTY_STRING != contentPath) { + getDrmManagerService()->saveRights(uniqueId, drmRights, rightsPath, contentPath); + } +} + +String8 DrmManagerClientImpl::getOriginalMimeType(int uniqueId, const String8& path) { + String8 mimeType = EMPTY_STRING; + if (EMPTY_STRING != path) { + mimeType = getDrmManagerService()->getOriginalMimeType(uniqueId, path); + } + return mimeType; +} + +int DrmManagerClientImpl::getDrmObjectType( + int uniqueId, const String8& path, const String8& mimeType) { + int drmOjectType = DrmObjectType::UNKNOWN; + if ((EMPTY_STRING != path) || (EMPTY_STRING != mimeType)) { + drmOjectType = getDrmManagerService()->getDrmObjectType(uniqueId, path, mimeType); + } + return drmOjectType; +} + +int DrmManagerClientImpl::checkRightsStatus( + int uniqueId, const String8& path, int action) { + int rightsStatus = RightsStatus::RIGHTS_INVALID; + if (EMPTY_STRING != path) { + rightsStatus = getDrmManagerService()->checkRightsStatus(uniqueId, path, action); + } + return rightsStatus; +} + +void DrmManagerClientImpl::consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve) { + if (NULL != decryptHandle) { + getDrmManagerService()->consumeRights(uniqueId, decryptHandle, action, reserve); + } +} + +void DrmManagerClientImpl::setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position) { + if (NULL != decryptHandle) { + getDrmManagerService()->setPlaybackStatus( + uniqueId, decryptHandle, playbackStatus, position); + } +} + +bool DrmManagerClientImpl::validateAction( + int uniqueId, const String8& path, int action, const ActionDescription& description) { + bool retCode = false; + if (EMPTY_STRING != path) { + retCode = getDrmManagerService()->validateAction(uniqueId, path, action, description); + } + return retCode; +} + +void DrmManagerClientImpl::removeRights(int uniqueId, const String8& path) { + if (EMPTY_STRING != path) { + getDrmManagerService()->removeRights(uniqueId, path); + } +} + +void DrmManagerClientImpl::removeAllRights(int uniqueId) { + getDrmManagerService()->removeAllRights(uniqueId); +} + +int DrmManagerClientImpl::openConvertSession(int uniqueId, const String8& mimeType) { + int retCode = INVALID_VALUE; + if (EMPTY_STRING != mimeType) { + retCode = getDrmManagerService()->openConvertSession(uniqueId, mimeType); + } + return retCode; +} + +DrmConvertedStatus* DrmManagerClientImpl::convertData( + int uniqueId, int convertId, const DrmBuffer* inputData) { + DrmConvertedStatus* drmConvertedStatus = NULL; + if (NULL != inputData) { + drmConvertedStatus = getDrmManagerService()->convertData(uniqueId, convertId, inputData); + } + return drmConvertedStatus; +} + +DrmConvertedStatus* DrmManagerClientImpl::closeConvertSession(int uniqueId, int convertId) { + return getDrmManagerService()->closeConvertSession(uniqueId, convertId); +} + +status_t DrmManagerClientImpl::getAllSupportInfo( + int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray) { + status_t status = DRM_ERROR_UNKNOWN; + if ((NULL != drmSupportInfoArray) && (NULL != length)) { + status = getDrmManagerService()->getAllSupportInfo(uniqueId, length, drmSupportInfoArray); + } + return status; +} + +DecryptHandle* DrmManagerClientImpl::openDecryptSession( + int uniqueId, int fd, int offset, int length) { + LOGV("Entering DrmManagerClientImpl::openDecryptSession"); + return getDrmManagerService()->openDecryptSession(uniqueId, fd, offset, length); +} + +void DrmManagerClientImpl::closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) { + if (NULL != decryptHandle) { + getDrmManagerService()->closeDecryptSession( uniqueId, decryptHandle); + } +} + +void DrmManagerClientImpl::initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo) { + if ((NULL != decryptHandle) && (NULL != headerInfo)) { + getDrmManagerService()->initializeDecryptUnit( + uniqueId, decryptHandle, decryptUnitId, headerInfo); + } +} + +status_t DrmManagerClientImpl::decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer) { + status_t status = DRM_ERROR_UNKNOWN; + if ((NULL != decryptHandle) && (NULL != encBuffer) + && (NULL != decBuffer) && (NULL != *decBuffer)) { + status = getDrmManagerService()->decrypt( + uniqueId, decryptHandle, decryptUnitId, encBuffer, decBuffer); + } + return status; +} + +void DrmManagerClientImpl::finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) { + if (NULL != decryptHandle) { + getDrmManagerService()->finalizeDecryptUnit(uniqueId, decryptHandle, decryptUnitId); + } +} + +ssize_t DrmManagerClientImpl::pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset) { + ssize_t retCode = INVALID_VALUE; + if ((NULL != decryptHandle) && (NULL != buffer) && (0 < numBytes)) { + retCode = getDrmManagerService()->pread(uniqueId, decryptHandle, buffer, numBytes, offset); + } + return retCode; +} + +status_t DrmManagerClientImpl::notify(const DrmInfoEvent& event) { + if (NULL != mOnInfoListener.get()) { + Mutex::Autolock _l(mLock); + sp listener = mOnInfoListener; + listener->onInfo(event); + } + return DRM_NO_ERROR; +} + diff --git a/drm/libdrmframework/include/DrmIOService.h b/drm/libdrmframework/include/DrmIOService.h new file mode 100644 index 0000000000000000000000000000000000000000..244124e79124d8cbbe6c723e206531e185575291 --- /dev/null +++ b/drm/libdrmframework/include/DrmIOService.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_IO_SERVICE_H__ +#define __DRM_IO_SERVICE_H__ + +#include "IDrmIOService.h" + +namespace android { + +/** + * This is the implementation class for DRM IO service. + * + * The instance of this class is created while starting the DRM IO service. + * + */ +class DrmIOService : public BnDrmIOService { +public: + static void instantiate(); + +private: + DrmIOService(); + virtual ~DrmIOService(); + +public: + void writeToFile(const String8& filePath, const String8& dataBuffer); + String8 readFromFile(const String8& filePath); +}; + +}; + +#endif /* __DRM_IO_SERVICE_H__ */ + diff --git a/drm/libdrmframework/include/DrmManager.h b/drm/libdrmframework/include/DrmManager.h new file mode 100644 index 0000000000000000000000000000000000000000..2ba9e995e790a55cae9e41199af8450d03a582e6 --- /dev/null +++ b/drm/libdrmframework/include/DrmManager.h @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_MANAGER_H__ +#define __DRM_MANAGER_H__ + +#include +#include +#include +#include "IDrmEngine.h" +#include "PlugInManager.h" +#include "IDrmServiceListener.h" + +namespace android { + +class IDrmManager; +class DrmRegistrationInfo; +class DrmUnregistrationInfo; +class DrmRightsAcquisitionInfo; +class DrmContentIds; +class DrmConstraints; +class DrmRights; +class DrmInfo; +class DrmInfoStatus; +class DrmConvertedStatus; +class DrmInfoRequest; +class DrmSupportInfo; +class ActionDescription; + +/** + * This is implementation class for DRM Manager. This class delegates the + * functionality to corresponding DRM Engine. + * + * The DrmManagerService class creates an instance of this class. + * + */ +class DrmManager : public IDrmEngine::OnInfoListener { +public: + DrmManager(); + virtual ~DrmManager(); + +public: + + status_t loadPlugIns(int uniqueId); + + status_t loadPlugIns(int uniqueId, const String8& plugInDirPath); + + status_t setDrmServiceListener( + int uniqueId, const sp& drmServiceListener); + + status_t unloadPlugIns(int uniqueId); + + status_t installDrmEngine(int uniqueId, const String8& drmEngineFile); + + DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action); + + bool canHandle(int uniqueId, const String8& path, const String8& mimeType); + + DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo); + + DrmInfo* acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest); + + void saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath); + + String8 getOriginalMimeType(int uniqueId, const String8& path); + + int getDrmObjectType(int uniqueId, const String8& path, const String8& mimeType); + + int checkRightsStatus(int uniqueId, const String8& path, int action); + + void consumeRights(int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve); + + void setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position); + + bool validateAction( + int uniqueId, const String8& path, int action, const ActionDescription& description); + + void removeRights(int uniqueId, const String8& path); + + void removeAllRights(int uniqueId); + + int openConvertSession(int uniqueId, const String8& mimeType); + + DrmConvertedStatus* convertData(int uniqueId, int convertId, const DrmBuffer* inputData); + + DrmConvertedStatus* closeConvertSession(int uniqueId, int convertId); + + status_t getAllSupportInfo(int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray); + + DecryptHandle* openDecryptSession(int uniqueId, int fd, int offset, int length); + + void closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle); + + void initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo); + + status_t decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer,DrmBuffer** decBuffer); + + void finalizeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId); + + ssize_t pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset); + + void onInfo(const DrmInfoEvent& event); + +private: + String8 getSupportedPlugInId(int uniqueId, const String8& path, const String8& mimeType); + + String8 getSupportedPlugInId(const String8& mimeType); + + String8 getSupportedPlugInIdFromPath(int uniqueId, const String8& path); + + void populate(int uniqueId); + + bool canHandle(int uniqueId, const String8& path); + + void initializePlugIns(int uniqueId); + +private: + static const String8 EMPTY_STRING; + + int mDecryptSessionId; + int mConvertId; + Mutex mLock; + Mutex mDecryptLock; + Mutex mConvertLock; + TPlugInManager mPlugInManager; + KeyedVector< DrmSupportInfo, String8 > mSupportInfoToPlugInIdMap; + KeyedVector< int, IDrmEngine*> mConvertSessionMap; + KeyedVector< int, sp > mServiceListeners; + KeyedVector< int, IDrmEngine*> mDecryptSessionMap; +}; + +}; + +#endif /* __DRM_MANAGER_H__ */ + diff --git a/drm/libdrmframework/include/DrmManagerClientImpl.h b/drm/libdrmframework/include/DrmManagerClientImpl.h new file mode 100644 index 0000000000000000000000000000000000000000..e70e6e118bd6d0796105e503bdb87155cee8f256 --- /dev/null +++ b/drm/libdrmframework/include/DrmManagerClientImpl.h @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_MANAGER_CLIENT_IMPL_H__ +#define __DRM_MANAGER_CLIENT_IMPL_H__ + +#include +#include +#include + +#include "IDrmManagerService.h" + +namespace android { + +class DrmInfoEvent; +/** + * This is implementation class for DrmManagerClient class. + * + * Only the JNI layer creates an instance of this class to delegate + * functionality to Native later. + * + */ +class DrmManagerClientImpl : public BnDrmServiceListener { +private: + DrmManagerClientImpl(); + +public: + static DrmManagerClientImpl* create(int* pUniqueId); + + static void remove(int uniqueId); + + virtual ~DrmManagerClientImpl(); + +public: + /** + * Initialize DRM Manager + * load available plug-ins from default plugInDirPath + * + * @param[in] uniqueId Unique identifier for a session + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t loadPlugIns(int uniqueId); + + /** + * Finalize DRM Manager + * release resources associated with each plug-in + * unload all plug-ins and etc. + * + * @param[in] uniqueId Unique identifier for a session + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t unloadPlugIns(int uniqueId); + + /** + * Register a callback to be invoked when the caller required to + * receive necessary information + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] infoListener Listener + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t setOnInfoListener( + int uniqueId, const sp& infoListener); + + /** + * Get constraint information associated with input content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Actions defined such as, + * Action::DEFAULT, Action::PLAY, etc + * @return DrmConstraints + * key-value pairs of constraint are embedded in it + * @note + * In case of error, return NULL + */ + DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action); + + /** + * Check whether the given mimetype or path can be handled + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the content needs to be handled + * @param[in] mimetype Mimetype of the content needs to be handled + * @return + * True if DrmManager can handle given path or mime type. + */ + bool canHandle(int uniqueId, const String8& path, const String8& mimeType); + + /** + * Executes given drm information based on its type + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmInfo Information needs to be processed + * @return DrmInfoStatus + * instance as a result of processing given input + */ + DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo); + + /** + * Retrieves necessary information for registration, unregistration or rights + * acquisition information. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmInfoRequest Request information to retrieve drmInfo + * @return DrmInfo + * instance as a result of processing given input + */ + DrmInfo* acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest); + + /** + * Save DRM rights to specified rights path + * and make association with content path + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmRights DrmRights to be saved + * @param[in] rightsPath File path where rights to be saved + * @param[in] contentPath File path where content was saved + */ + void saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath); + + /** + * Retrieves the mime type embedded inside the original content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path the path of the protected content + * @return String8 + * Returns mime-type of the original content, such as "video/mpeg" + */ + String8 getOriginalMimeType(int uniqueId, const String8& path); + + /** + * Retrieves the type of the protected object (content, rights, etc..) + * using specified path or mimetype. At least one parameter should be non null + * to retrieve DRM object type + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the content or null. + * @param[in] mimeType Mime type of the content or null. + * @return type of the DRM content, + * such as DrmObjectType::CONTENT, DrmObjectType::RIGHTS_OBJECT + */ + int getDrmObjectType(int uniqueId, const String8& path, const String8& mimeType); + + /** + * Check whether the given content has valid rights or not + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Action to perform (Action::DEFAULT, Action::PLAY, etc) + * @return the status of the rights for the protected content, + * such as RightsStatus::RIGHTS_VALID, RightsStatus::RIGHTS_EXPIRED, etc. + */ + int checkRightsStatus(int uniqueId, const String8& path, int action); + + /** + * Consumes the rights for a content. + * If the reserve parameter is true the rights is reserved until the same + * application calls this api again with the reserve parameter set to false. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] action Action to perform. (Action::DEFAULT, Action::PLAY, etc) + * @param[in] reserve True if the rights should be reserved. + */ + void consumeRights(int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve); + + /** + * Informs the DRM engine about the playback actions performed on the DRM files. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] playbackStatus Playback action (Playback::START, Playback::STOP, Playback::PAUSE) + * @param[in] position Position in the file (in milliseconds) where the start occurs. + * Only valid together with Playback::START. + */ + void setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position); + + /** + * Validates whether an action on the DRM content is allowed or not. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Action to validate (Action::PLAY, Action::TRANSFER, etc) + * @param[in] description Detailed description of the action + * @return true if the action is allowed. + */ + bool validateAction( + int uniqueId, const String8& path, int action, const ActionDescription& description); + + /** + * Removes the rights associated with the given protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + */ + void removeRights(int uniqueId, const String8& path); + + /** + * Removes all the rights information of each plug-in associated with + * DRM framework. Will be used in master reset + * + * @param[in] uniqueId Unique identifier for a session + */ + void removeAllRights(int uniqueId); + + /** + * This API is for Forward Lock based DRM scheme. + * Each time the application tries to download a new DRM file + * which needs to be converted, then the application has to + * begin with calling this API. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] mimeType Description/MIME type of the input data packet + * @return Return handle for the convert session + */ + int openConvertSession(int uniqueId, const String8& mimeType); + + /** + * Accepts and converts the input data which is part of DRM file. + * The resultant converted data and the status is returned in the DrmConvertedInfo + * object. This method will be called each time there are new block + * of data received by the application. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + * @param[in] inputData Input Data which need to be converted + * @return Return object contains the status of the data conversion, + * the output converted data and offset. In this case the + * application will ignore the offset information. + */ + DrmConvertedStatus* convertData(int uniqueId, int convertId, const DrmBuffer* inputData); + + /** + * Informs the Drm Agent when there is no more data which need to be converted + * or when an error occurs. Upon successful conversion of the complete data, + * the agent will inform that where the header and body signature + * should be added. This signature appending is needed to integrity + * protect the converted file. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + * @return Return object contains the status of the data conversion, + * the header and body signature data. It also informs + * the application on which offset these signature data + * should be appended. + */ + DrmConvertedStatus* closeConvertSession(int uniqueId, int convertId); + + /** + * Retrieves all DrmSupportInfo instance that native DRM framework can handle. + * This interface is meant to be used by JNI layer + * + * @param[in] uniqueId Unique identifier for a session + * @param[out] length Number of elements in drmSupportInfoArray + * @param[out] drmSupportInfoArray Array contains all DrmSupportInfo + * that native DRM framework can handle + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t getAllSupportInfo(int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray); + + /** + * Open the decrypt session to decrypt the given protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] fd File descriptor of the protected content to be decrypted + * @param[in] offset Start position of the content + * @param[in] length The length of the protected content + * @return + * Handle for the decryption session + */ + DecryptHandle* openDecryptSession(int uniqueId, int fd, int offset, int length); + + /** + * Close the decrypt session for the given handle + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + */ + void closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle); + + /** + * Initialize decryption for the given unit of the protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + * @param[in] headerInfo Information for initializing decryption of this decrypUnit + */ + void initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo); + + /** + * Decrypt the protected content buffers for the given unit + * This method will be called any number of times, based on number of + * encrypted streams received from application. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + * @param[in] encBuffer Encrypted data block + * @param[out] decBuffer Decrypted data block + * @return status_t + * Returns the error code for this API + * DRM_NO_ERROR for success, and one of DRM_ERROR_UNKNOWN, DRM_ERROR_LICENSE_EXPIRED + * DRM_ERROR_SESSION_NOT_OPENED, DRM_ERROR_DECRYPT_UNIT_NOT_INITIALIZED, + * DRM_ERROR_DECRYPT for failure. + */ + status_t decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer); + + /** + * Finalize decryption for the given unit of the protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + */ + void finalizeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId); + + /** + * Reads the specified number of bytes from an open DRM file. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[out] buffer Reference to the buffer that should receive the read data. + * @param[in] numBytes Number of bytes to read. + * @param[in] offset Offset with which to update the file position. + * + * @return Number of bytes read. Returns -1 for Failure. + */ + ssize_t pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset); + + /** + * Notify the event to the registered listener + * + * @param[in] event The event to be notified + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t notify(const DrmInfoEvent& event); + +private: + /** + * Initialize DRM Manager + * load available plug-ins from plugInDirPath + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] plugInDirPath Directory from where to load plug-ins + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t loadPlugIns(int uniqueId, const String8& plugInDirPath); + + /** + * Install new DRM Engine Plug-in at the runtime + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmEngine Shared Object(so) File in which DRM Engine defined + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t installDrmEngine(int uniqueId, const String8& drmEngineFile); + +private: + Mutex mLock; + sp mOnInfoListener; + +private: + static Mutex mMutex; + static Vector mUniqueIdVector; + static sp mDrmManagerService; + static const sp& getDrmManagerService(); + static const String8 EMPTY_STRING; +}; + +}; + +#endif /* __DRM_MANAGER_CLIENT_IMPL_H__ */ + diff --git a/drm/libdrmframework/include/DrmManagerService.h b/drm/libdrmframework/include/DrmManagerService.h new file mode 100644 index 0000000000000000000000000000000000000000..fef121cd462ebde4ab9019b66fc3e585dec5c67d --- /dev/null +++ b/drm/libdrmframework/include/DrmManagerService.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_MANAGER_SERVICE_H__ +#define __DRM_MANAGER_SERVICE_H__ + +#include +#include +#include +#include +#include "IDrmManagerService.h" +#include "IDrmServiceListener.h" + +namespace android { + +class DrmManager; +class String8; +class Mutex; + +/** + * This is the implementation class for DRM manager service. This delegates + * the responsibility to DrmManager. + * + * The instance of this class is created while starting the DRM manager service. + * + */ +class DrmManagerService : public BnDrmManagerService { +public: + static void instantiate(); + +private: + DrmManagerService(); + virtual ~DrmManagerService(); + +public: + status_t loadPlugIns(int uniqueId); + + status_t loadPlugIns(int uniqueId, const String8& plugInDirPath); + + status_t setDrmServiceListener( + int uniqueId, const sp& drmServiceListener); + + status_t unloadPlugIns(int uniqueId); + + status_t installDrmEngine(int uniqueId, const String8& drmEngineFile); + + DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action); + + bool canHandle(int uniqueId, const String8& path, const String8& mimeType); + + DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo); + + DrmInfo* acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInforequest); + + void saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath); + + String8 getOriginalMimeType(int uniqueId, const String8& path); + + int getDrmObjectType(int uniqueId, const String8& path, const String8& mimeType); + + int checkRightsStatus(int uniqueId, const String8& path,int action); + + void consumeRights(int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve); + + void setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position); + + bool validateAction(int uniqueId, const String8& path, + int action, const ActionDescription& description); + + void removeRights(int uniqueId, const String8& path); + + void removeAllRights(int uniqueId); + + int openConvertSession(int uniqueId, const String8& mimeType); + + DrmConvertedStatus* convertData(int uniqueId, int convertId, const DrmBuffer* inputData); + + DrmConvertedStatus* closeConvertSession(int uniqueId, int convertId); + + status_t getAllSupportInfo(int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray); + + DecryptHandle* openDecryptSession(int uniqueId, int fd, int offset, int length); + + void closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle); + + void initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo); + + status_t decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer); + + void finalizeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId); + + ssize_t pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset); + +private: + DrmManager* mDrmManager; +}; + +}; + +#endif /* __DRM_MANAGER_SERVICE_H__ */ + diff --git a/drm/libdrmframework/include/IDrmIOService.h b/drm/libdrmframework/include/IDrmIOService.h new file mode 100644 index 0000000000000000000000000000000000000000..5e0d907e679476b4dec6887e4b9be1784e1abbca --- /dev/null +++ b/drm/libdrmframework/include/IDrmIOService.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __IDRM_IO_SERVICE_H__ +#define __IDRM_IO_SERVICE_H__ + +#include +#include +#include + +namespace android { + +/** + * This is the interface class for DRM IO service. + * + */ +class IDrmIOService : public IInterface +{ +public: + enum { + WRITE_TO_FILE = IBinder::FIRST_CALL_TRANSACTION, + READ_FROM_FILE + }; + +public: + DECLARE_META_INTERFACE(DrmIOService); + +public: + /** + * Writes the data into the file path provided + * + * @param[in] filePath Path of the file + * @param[in] dataBuffer Data to write + */ + virtual void writeToFile(const String8& filePath, const String8& dataBuffer) = 0; + + /** + * Reads the data from the file path provided + * + * @param[in] filePath Path of the file + * @return Data read from the file + */ + virtual String8 readFromFile(const String8& filePath) = 0; +}; + +/** + * This is the Binder implementation class for DRM IO service. + */ +class BpDrmIOService: public BpInterface +{ +public: + BpDrmIOService(const sp& impl) + : BpInterface(impl) {} + + virtual void writeToFile(const String8& filePath, const String8& dataBuffer); + + virtual String8 readFromFile(const String8& filePath); +}; + +/** + * This is the Binder implementation class for DRM IO service. + */ +class BnDrmIOService: public BnInterface +{ +public: + virtual status_t onTransact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0); +}; + +}; + +#endif /* __IDRM_IO_SERVICE_H__ */ + diff --git a/drm/libdrmframework/include/IDrmManagerService.h b/drm/libdrmframework/include/IDrmManagerService.h new file mode 100644 index 0000000000000000000000000000000000000000..a4d128af8f8ea299655cd1b482cdd06b7ccaabe5 --- /dev/null +++ b/drm/libdrmframework/include/IDrmManagerService.h @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __IDRM_MANAGER_SERVICE_H__ +#define __IDRM_MANAGER_SERVICE_H__ + +#include +#include +#include +#include +#include "IDrmServiceListener.h" + +namespace android { + +class DrmContentIds; +class DrmConstraints; +class DrmRights; +class DrmInfo; +class DrmInfoStatus; +class DrmInfoRequest; +class DrmSupportInfo; +class DrmConvertedStatus; +class String8; +class ActionDescription; + +/** + * This is the interface class for DRM Manager service. + * + */ +class IDrmManagerService : public IInterface +{ +public: + enum { + LOAD_PLUGINS = IBinder::FIRST_CALL_TRANSACTION, + LOAD_PLUGINS_FROM_PATH, + SET_DRM_SERVICE_LISTENER, + UNLOAD_PLUGINS, + INSTALL_DRM_ENGINE, + GET_CONSTRAINTS_FROM_CONTENT, + CAN_HANDLE, + PROCESS_DRM_INFO, + ACQUIRE_DRM_INFO, + SAVE_RIGHTS, + GET_ORIGINAL_MIMETYPE, + GET_DRM_OBJECT_TYPE, + CHECK_RIGHTS_STATUS, + CONSUME_RIGHTS, + SET_PLAYBACK_STATUS, + VALIDATE_ACTION, + REMOVE_RIGHTS, + REMOVE_ALL_RIGHTS, + OPEN_CONVERT_SESSION, + CONVERT_DATA, + CLOSE_CONVERT_SESSION, + GET_ALL_SUPPORT_INFO, + OPEN_DECRYPT_SESSION, + CLOSE_DECRYPT_SESSION, + INITIALIZE_DECRYPT_UNIT, + DECRYPT, + FINALIZE_DECRYPT_UNIT, + PREAD + }; + +public: + DECLARE_META_INTERFACE(DrmManagerService); + +public: + virtual status_t loadPlugIns(int uniqueId) = 0; + + virtual status_t loadPlugIns(int uniqueId, const String8& plugInDirPath) = 0; + + virtual status_t setDrmServiceListener( + int uniqueId, const sp& infoListener) = 0; + + virtual status_t unloadPlugIns(int uniqueId) = 0; + + virtual status_t installDrmEngine(int uniqueId, const String8& drmEngineFile) = 0; + + virtual DrmConstraints* getConstraints( + int uniqueId, const String8* path, const int action) = 0; + + virtual bool canHandle(int uniqueId, const String8& path, const String8& mimeType) = 0; + + virtual DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo) = 0; + + virtual DrmInfo* acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInforequest) = 0; + + virtual void saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) = 0; + + virtual String8 getOriginalMimeType(int uniqueId, const String8& path) = 0; + + virtual int getDrmObjectType( + int uniqueId, const String8& path, const String8& mimeType) = 0; + + virtual int checkRightsStatus(int uniqueId, const String8& path, int action) = 0; + + virtual void consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve) = 0; + + virtual void setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position) = 0; + + virtual bool validateAction( + int uniqueId, const String8& path, + int action, const ActionDescription& description) = 0; + + virtual void removeRights(int uniqueId, const String8& path) = 0; + + virtual void removeAllRights(int uniqueId) = 0; + + virtual int openConvertSession(int uniqueId, const String8& mimeType) = 0; + + virtual DrmConvertedStatus* convertData( + int uniqueId, int convertId, const DrmBuffer* inputData) = 0; + + virtual DrmConvertedStatus* closeConvertSession(int uniqueId, int convertId) = 0; + + virtual status_t getAllSupportInfo( + int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray) = 0; + + virtual DecryptHandle* openDecryptSession(int uniqueId, int fd, int offset, int length) = 0; + + virtual void closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) = 0; + + virtual void initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo) = 0; + + virtual status_t decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer) = 0; + + virtual void finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) = 0; + + virtual ssize_t pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes,off_t offset) = 0; +}; + +/** + * This is the Binder implementation class for DRM Manager service. + */ +class BpDrmManagerService: public BpInterface +{ +public: + BpDrmManagerService(const sp& impl) + : BpInterface(impl) {} + + virtual status_t loadPlugIns(int uniqueId); + + virtual status_t loadPlugIns(int uniqueId, const String8& plugInDirPath); + + virtual status_t setDrmServiceListener( + int uniqueId, const sp& infoListener); + + virtual status_t unloadPlugIns(int uniqueId); + + virtual status_t installDrmEngine(int uniqueId, const String8& drmEngineFile); + + virtual DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action); + + virtual bool canHandle(int uniqueId, const String8& path, const String8& mimeType); + + virtual DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo); + + virtual DrmInfo* acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInforequest); + + virtual void saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath); + + virtual String8 getOriginalMimeType(int uniqueId, const String8& path); + + virtual int getDrmObjectType(int uniqueId, const String8& path, const String8& mimeType); + + virtual int checkRightsStatus(int uniqueId, const String8& path, int action); + + virtual void consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve); + + virtual void setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position); + + virtual bool validateAction( + int uniqueId, const String8& path, int action, const ActionDescription& description); + + virtual void removeRights(int uniqueId, const String8& path); + + virtual void removeAllRights(int uniqueId); + + virtual int openConvertSession(int uniqueId, const String8& mimeType); + + virtual DrmConvertedStatus* convertData( + int uniqueId, int convertId, const DrmBuffer* inputData); + + virtual DrmConvertedStatus* closeConvertSession(int uniqueId, int convertId); + + virtual status_t getAllSupportInfo( + int uniqueId, int* length, DrmSupportInfo** drmSupportInfoArray); + + virtual DecryptHandle* openDecryptSession(int uniqueId, int fd, int offset, int length); + + virtual void closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle); + + virtual void initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo); + + virtual status_t decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer); + + virtual void finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId); + + virtual ssize_t pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset); +}; + +/** + * This is the Binder implementation class for DRM Manager service. + */ +class BnDrmManagerService: public BnInterface +{ +public: + virtual status_t onTransact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0); +}; + +}; + +#endif /* __IDRM_MANAGER_SERVICE_H__ */ + diff --git a/drm/libdrmframework/include/IDrmServiceListener.h b/drm/libdrmframework/include/IDrmServiceListener.h new file mode 100644 index 0000000000000000000000000000000000000000..7f7109f29b88ac91b9072d440fbf32bcf19e7c6f --- /dev/null +++ b/drm/libdrmframework/include/IDrmServiceListener.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __IDRM_SERVICE_LISTENER_H__ +#define __IDRM_SERVICE_LISTENER_H__ + +#include +#include +#include + +namespace android { + +class DrmInfoEvent; + +/** + * This is the interface class for DRM service listener. + * + */ +class IDrmServiceListener : public IInterface +{ +public: + enum { + NOTIFY = IBinder::FIRST_CALL_TRANSACTION, + }; + +public: + DECLARE_META_INTERFACE(DrmServiceListener); + +public: + virtual status_t notify(const DrmInfoEvent& event) = 0; +}; + +/** + * This is the Binder implementation class for DRM service listener. + */ +class BpDrmServiceListener: public BpInterface +{ +public: + BpDrmServiceListener(const sp& impl) + : BpInterface(impl) {} + + virtual status_t notify(const DrmInfoEvent& event); +}; + +/** + * This is the Binder implementation class for DRM service listener. + */ +class BnDrmServiceListener: public BnInterface +{ +public: + virtual status_t onTransact( + uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0); +}; + +}; + +#endif /* __IDRM_SERVICE_LISTENER_H__ */ + diff --git a/drm/libdrmframework/include/PlugInManager.h b/drm/libdrmframework/include/PlugInManager.h new file mode 100644 index 0000000000000000000000000000000000000000..9ad195fd80b077ea761cb34d0d42c0d362af8b35 --- /dev/null +++ b/drm/libdrmframework/include/PlugInManager.h @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __PLUGIN_MANAGER_H__ +#define __PLUGIN_MANAGER_H__ + +#include +#include +#include + +#include +#include +#include + +namespace android { + +const char* const PLUGIN_MANAGER_CREATE = "create"; +const char* const PLUGIN_MANAGER_DESTROY = "destroy"; +const char* const PLUGIN_EXTENSION = ".so"; + +/** + * This is the template class for Plugin manager. + * + * The DrmManager uses this class to handle the plugins. + * + */ +template +class TPlugInManager { +private: + typedef void* HANDLE; + typedef Type* create_t(void); + typedef void destroy_t(Type*); + typedef create_t* FPCREATE; + typedef destroy_t* FPDESTORY; + + typedef struct _PlugInContainer { + String8 sPath; + HANDLE hHandle; + FPCREATE fpCreate; + FPDESTORY fpDestory; + Type* pInstance; + + _PlugInContainer(): + sPath("") + ,hHandle(NULL) + ,fpCreate(NULL) + ,fpDestory(NULL) + ,pInstance(NULL) + {} + } PlugInContainer; + + typedef KeyedVector PlugInMap; + PlugInMap m_plugInMap; + + typedef Vector PlugInIdList; + PlugInIdList m_plugInIdList; + +public: + /** + * Load all the plug-ins in the specified directory + * + * @param[in] rsPlugInDirPath + * Directory path which plug-ins (dynamic library) are stored + * @note Plug-ins should be implemented according to the specification + */ + void loadPlugIns(const String8& rsPlugInDirPath) { + Vector plugInFileList = getPlugInPathList(rsPlugInDirPath); + + if (!plugInFileList.isEmpty()) { + for (unsigned int i = 0; i < plugInFileList.size(); ++i) { + loadPlugIn(plugInFileList[i]); + } + } + } + + /** + * Unload all the plug-ins + * + */ + void unloadPlugIns() { + for (unsigned int i = 0; i < m_plugInIdList.size(); ++i) { + unloadPlugIn(m_plugInIdList[i]); + } + m_plugInIdList.clear(); + } + + /** + * Get all the IDs of available plug-ins + * + * @return[in] plugInIdList + * String type Vector in which all plug-in IDs are stored + */ + Vector getPlugInIdList() const { + return m_plugInIdList; + } + + /** + * Get a plug-in reference of specified ID + * + * @param[in] rsPlugInId + * Plug-in ID to be used + * @return plugIn + * Reference of specified plug-in instance + */ + Type& getPlugIn(const String8& rsPlugInId) { + if (!contains(rsPlugInId)) { + // This error case never happens + } + return *(m_plugInMap.valueFor(rsPlugInId)->pInstance); + } + +public: + /** + * Load a plug-in stored in the specified path + * + * @param[in] rsPlugInPath + * Plug-in (dynamic library) file path + * @note Plug-in should be implemented according to the specification + */ + void loadPlugIn(const String8& rsPlugInPath) { + if (contains(rsPlugInPath)) { + return; + } + + PlugInContainer* pPlugInContainer = new PlugInContainer(); + + pPlugInContainer->hHandle = dlopen(rsPlugInPath.string(), RTLD_LAZY); + + if (NULL == pPlugInContainer->hHandle) { + delete pPlugInContainer; + pPlugInContainer = NULL; + return; + } + + pPlugInContainer->sPath = rsPlugInPath; + pPlugInContainer->fpCreate + = (FPCREATE)dlsym(pPlugInContainer->hHandle, PLUGIN_MANAGER_CREATE); + pPlugInContainer->fpDestory + = (FPDESTORY)dlsym(pPlugInContainer->hHandle, PLUGIN_MANAGER_DESTROY); + + if (NULL != pPlugInContainer->fpCreate && NULL != pPlugInContainer->fpDestory) { + pPlugInContainer->pInstance = (Type*)pPlugInContainer->fpCreate(); + m_plugInIdList.add(rsPlugInPath); + m_plugInMap.add(rsPlugInPath, pPlugInContainer); + } else { + dlclose(pPlugInContainer->hHandle); + delete pPlugInContainer; + pPlugInContainer = NULL; + return; + } + } + + /** + * Unload a plug-in stored in the specified path + * + * @param[in] rsPlugInPath + * Plug-in (dynamic library) file path + */ + void unloadPlugIn(const String8& rsPlugInPath) { + if (!contains(rsPlugInPath)) { + return; + } + + PlugInContainer* pPlugInContainer = m_plugInMap.valueFor(rsPlugInPath); + pPlugInContainer->fpDestory(pPlugInContainer->pInstance); + dlclose(pPlugInContainer->hHandle); + + m_plugInMap.removeItem(rsPlugInPath); + delete pPlugInContainer; + pPlugInContainer = NULL; + } + +private: + /** + * True if TPlugInManager contains rsPlugInId + */ + bool contains(const String8& rsPlugInId) { + return m_plugInMap.indexOfKey(rsPlugInId) != NAME_NOT_FOUND; + } + + /** + * Return file path list of plug-ins stored in the specified directory + * + * @param[in] rsDirPath + * Directory path in which plug-ins are stored + * @return plugInFileList + * String type Vector in which file path of plug-ins are stored + */ + Vector getPlugInPathList(const String8& rsDirPath) { + Vector fileList; + DIR* pDir = opendir(rsDirPath.string()); + struct dirent* pEntry = new dirent(); + + while (NULL != pDir && NULL != (pEntry = readdir(pDir))) { + if (!isPlugIn(pEntry)) { + continue; + } + String8 plugInPath; + plugInPath += rsDirPath; + plugInPath += "/"; + plugInPath += pEntry->d_name; + + fileList.add(plugInPath); + } + + if (NULL != pDir) { + closedir(pDir); + } + delete pEntry; + pEntry = NULL; + + return fileList; + } + + /** + * True if the input name denotes plug-in + */ + bool isPlugIn(const struct dirent* pEntry) const { + String8 sName(pEntry->d_name); + int extentionPos = sName.size() - String8(PLUGIN_EXTENSION).size(); + if (extentionPos < 0) { + return false; + } + return extentionPos == (int)sName.find(PLUGIN_EXTENSION); + } + + /** + * True if the input entry is "." or ".." + */ + bool isDotOrDDot(const struct dirent* pEntry) const { + String8 sName(pEntry->d_name); + return "." == sName || ".." == sName; + } + + /** + * True if input entry is directory + */ + bool isDirectory(const struct dirent* pEntry) const { + return DT_DIR == pEntry->d_type; + } + + /** + * True if input entry is regular file + */ + bool isRegularFile(const struct dirent* pEntry) const { + return DT_REG == pEntry->d_type; + } + + /** + * True if input entry is link + */ + bool isLink(const struct dirent* pEntry) const { + return DT_LNK == pEntry->d_type; + } +}; + +}; + +#endif /* __PLUGIN_MANAGER_H__ */ + diff --git a/drm/libdrmframework/include/ReadWriteUtils.h b/drm/libdrmframework/include/ReadWriteUtils.h new file mode 100644 index 0000000000000000000000000000000000000000..022149ee20f3c325b7bf524989bf0aa861adaa8c --- /dev/null +++ b/drm/libdrmframework/include/ReadWriteUtils.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __READ_WRITE_UTILS_H__ +#define __READ_WRITE_UTILS_H__ + +#include +#include + +namespace android { + +/** + * This is an utility class which performs IO operations. + * + */ +class ReadWriteUtils { +public: + /** + * Constructor for ReadWriteUtils + */ + ReadWriteUtils() {} + + /** + * Destructor for ReadWriteUtils + */ + virtual ~ReadWriteUtils(); + +public: + /** + * Reads the data from the file path provided + * + * @param[in] filePath Path of the file + * @return Data read from the file + */ + static String8 readBytes(const String8& filePath); + /** + * Writes the data into the file path provided + * + * @param[in] filePath Path of the file + * @param[in] dataBuffer Data to write + */ + static void writeToFile(const String8& filePath, const String8& data); + /** + * Appends the data into the file path provided + * + * @param[in] filePath Path of the file + * @param[in] dataBuffer Data to append + */ + static void appendToFile(const String8& filePath, const String8& data); + +private: + FileMap* mFileMap; +}; + +}; + +#endif /* __READ_WRITE_UTILS_H__ */ + diff --git a/drm/libdrmframework/include/StringTokenizer.h b/drm/libdrmframework/include/StringTokenizer.h new file mode 100644 index 0000000000000000000000000000000000000000..70e75582240358dcbecae5d8793f872e912e0bd6 --- /dev/null +++ b/drm/libdrmframework/include/StringTokenizer.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __STRING_TOKENIZER_H__ +#define __STRING_TOKENIZER_H__ + +#include + +namespace android { + +/** + * This is an utility class for String manipulation. + * + */ +class StringTokenizer { +public: + /** + * Iterator for string tokens + */ + class Iterator { + friend class StringTokenizer; + private: + Iterator(StringTokenizer* StringTokenizer) + : mStringTokenizer(StringTokenizer), mIndex(0) {} + + public: + Iterator(const Iterator& iterator); + Iterator& operator=(const Iterator& iterator); + virtual ~Iterator() {} + + public: + bool hasNext(); + String8& next(); + + private: + StringTokenizer* mStringTokenizer; + unsigned int mIndex; + }; + +public: + /** + * Constructor for StringTokenizer + * + * @param[in] string Complete string data + * @param[in] delimeter Delimeter used to split the string + */ + StringTokenizer(const String8& string, const String8& delimeter); + + /** + * Destructor for StringTokenizer + */ + ~StringTokenizer() {} + +private: + /** + * Splits the string according to the delimeter + */ + void splitString(const String8& string, const String8& delimeter); + +public: + /** + * Returns Iterator object to walk through the split string values + * + * @return Iterator object + */ + Iterator iterator(); + +private: + Vector mStringTokenizerVector; +}; + +}; +#endif /* __STRING_TOKENIZER_H__ */ + diff --git a/drm/libdrmframework/plugins/Android.mk b/drm/libdrmframework/plugins/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..9ee79618c203737d83fea3965f36fa14be6d82d0 --- /dev/null +++ b/drm/libdrmframework/plugins/Android.mk @@ -0,0 +1,16 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +include $(call all-subdir-makefiles) diff --git a/drm/libdrmframework/plugins/common/include/DrmEngineBase.h b/drm/libdrmframework/plugins/common/include/DrmEngineBase.h new file mode 100644 index 0000000000000000000000000000000000000000..667958ae8b72151ea4cb0b51948b47c892022209 --- /dev/null +++ b/drm/libdrmframework/plugins/common/include/DrmEngineBase.h @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_ENGINE_BASE_H__ +#define __DRM_ENGINE_BASE_H__ + +#include +#include "IDrmEngine.h" + +namespace android { + +/** + * This class is an interface for plug-in developers + * + * Responsibility of this class is control the sequence of actual plug-in. + * All each plug-in developer has to do is implement onXXX() type virtual interfaces. + */ +class DrmEngineBase : public IDrmEngine { +public: + DrmEngineBase(); + virtual ~DrmEngineBase(); + +public: + DrmConstraints* getConstraints(int uniqueId, const String8* path, int action); + + status_t initialize(int uniqueId); + + status_t setOnInfoListener(int uniqueId, const IDrmEngine::OnInfoListener* infoListener); + + status_t terminate(int uniqueId); + + bool canHandle(int uniqueId, const String8& path); + + DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo); + + void saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath); + + DrmInfo* acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest); + + String8 getOriginalMimeType(int uniqueId, const String8& path); + + int getDrmObjectType(int uniqueId, const String8& path, const String8& mimeType); + + int checkRightsStatus(int uniqueId, const String8& path, int action); + + void consumeRights(int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve); + + void setPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position); + + bool validateAction( + int uniqueId, const String8& path, int action, const ActionDescription& description); + + void removeRights(int uniqueId, const String8& path); + + void removeAllRights(int uniqueId); + + void openConvertSession(int uniqueId, int convertId); + + DrmConvertedStatus* convertData(int uniqueId, int convertId, const DrmBuffer* inputData); + + DrmConvertedStatus* closeConvertSession(int uniqueId, int convertId); + + DrmSupportInfo* getSupportInfo(int uniqueId); + + status_t openDecryptSession( + int uniqueId, DecryptHandle* decryptHandle, int fd, int offset, int length); + + void closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle); + + void initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo); + + status_t decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer); + + void finalizeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId); + + ssize_t pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset); + +protected: + ///////////////////////////////////////////////////// + // Interface for plug-in developers // + // each plug-in has to implement following method // + ///////////////////////////////////////////////////// + /** + * Get constraint information associated with input content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Actions defined such as, + * Action::DEFAULT, Action::PLAY, etc + * @return DrmConstraints + * key-value pairs of constraint are embedded in it + * @note + * In case of error, return NULL + */ + virtual DrmConstraints* onGetConstraints( + int uniqueId, const String8* path, int action) = 0; + + /** + * Initialize plug-in + * + * @param[in] uniqueId Unique identifier for a session + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + virtual status_t onInitialize(int uniqueId) = 0; + + /** + * Register a callback to be invoked when the caller required to + * receive necessary information + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] infoListener Listener + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + virtual status_t onSetOnInfoListener( + int uniqueId, const IDrmEngine::OnInfoListener* infoListener) = 0; + + /** + * Terminate the plug-in + * and release resource bound to plug-in + * + * @param[in] uniqueId Unique identifier for a session + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + virtual status_t onTerminate(int uniqueId) = 0; + + /** + * Get whether the given content can be handled by this plugin or not + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path the protected object + * @return bool + * Returns true if this plugin can handle , false in case of not able to handle + */ + virtual bool onCanHandle(int uniqueId, const String8& path) = 0; + + /** + * Executes given drm information based on its type + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmInfo Information needs to be processed + * @return DrmInfoStatus + * instance as a result of processing given input + */ + virtual DrmInfoStatus* onProcessDrmInfo(int uniqueId, const DrmInfo* drmInfo) = 0; + + /** + * Save DRM rights to specified rights path + * and make association with content path + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmRights DrmRights to be saved + * @param[in] rightsPath File path where rights to be saved + * @param[in] contentPath File path where content was saved + */ + virtual void onSaveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightspath, const String8& contentPath) = 0; + + /** + * Retrieves necessary information for registration, unregistration or rights + * acquisition information. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmInfoRequest Request information to retrieve drmInfo + * @return DrmInfo + * instance as a result of processing given input + */ + virtual DrmInfo* onAcquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInforequest) = 0; + + /** + * Retrieves the mime type embedded inside the original content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @return String8 + * Returns mime-type of the original content, such as "video/mpeg" + */ + virtual String8 onGetOriginalMimeType(int uniqueId, const String8& path) = 0; + + /** + * Retrieves the type of the protected object (content, rights, etc..) + * using specified path or mimetype. At least one parameter should be non null + * to retrieve DRM object type + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the content or null. + * @param[in] mimeType Mime type of the content or null. + * @return type of the DRM content, + * such as DrmObjectType::CONTENT, DrmObjectType::RIGHTS_OBJECT + */ + virtual int onGetDrmObjectType( + int uniqueId, const String8& path, const String8& mimeType) = 0; + + /** + * Check whether the given content has valid rights or not + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Action to perform (Action::DEFAULT, Action::PLAY, etc) + * @return the status of the rights for the protected content, + * such as RightsStatus::RIGHTS_VALID, RightsStatus::RIGHTS_EXPIRED, etc. + */ + virtual int onCheckRightsStatus(int uniqueId, const String8& path, int action) = 0; + + /** + * Consumes the rights for a content. + * If the reserve parameter is true the rights is reserved until the same + * application calls this api again with the reserve parameter set to false. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] action Action to perform. (Action::DEFAULT, Action::PLAY, etc) + * @param[in] reserve True if the rights should be reserved. + */ + virtual void onConsumeRights(int uniqueId, DecryptHandle* decryptHandle, + int action, bool reserve) = 0; + + /** + * Informs the DRM Engine about the playback actions performed on the DRM files. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] playbackStatus Playback action (Playback::START, Playback::STOP, Playback::PAUSE) + * @param[in] position Position in the file (in milliseconds) where the start occurs. + * Only valid together with Playback::START. + */ + virtual void onSetPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position) = 0; + + /** + * Validates whether an action on the DRM content is allowed or not. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Action to validate (Action::PLAY, Action::TRANSFER, etc) + * @param[in] description Detailed description of the action + * @return true if the action is allowed. + */ + virtual bool onValidateAction(int uniqueId, const String8& path, + int action, const ActionDescription& description) = 0; + + /** + * Removes the rights associated with the given protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + */ + virtual void onRemoveRights(int uniqueId, const String8& path) = 0; + + /** + * Removes all the rights information of each plug-in associated with + * DRM framework. Will be used in master reset + * + * @param[in] uniqueId Unique identifier for a session + */ + virtual void onRemoveAllRights(int uniqueId) = 0; + + /** + * This API is for Forward Lock based DRM scheme. + * Each time the application tries to download a new DRM file + * which needs to be converted, then the application has to + * begin with calling this API. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + */ + virtual void onOpenConvertSession(int uniqueId, int convertId) = 0; + + /** + * Accepts and converts the input data which is part of DRM file. + * The resultant converted data and the status is returned in the DrmConvertedInfo + * object. This method will be called each time there are new block + * of data received by the application. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + * @param[in] inputData Input Data which need to be converted + * @return Return object contains the status of the data conversion, + * the output converted data and offset. In this case the + * application will ignore the offset information. + */ + virtual DrmConvertedStatus* onConvertData( + int uniqueId, int convertId, const DrmBuffer* inputData) = 0; + + /** + * Informs the Drm Agent when there is no more data which need to be converted + * or when an error occurs. Upon successful conversion of the complete data, + * the agent will inform that where the header and body signature + * should be added. This signature appending is needed to integrity + * protect the converted file. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + * @return Return object contains the status of the data conversion, + * the header and body signature data. It also informs + * the application on which offset these signature data + * should be appended. + */ + virtual DrmConvertedStatus* onCloseConvertSession(int uniqueId, int convertId) = 0; + + /** + * Returns the information about the Drm Engine capabilities which includes + * supported MimeTypes and file suffixes. + * + * @param[in] uniqueId Unique identifier for a session + * @return DrmSupportInfo + * instance which holds the capabilities of a plug-in + */ + virtual DrmSupportInfo* onGetSupportInfo(int uniqueId) = 0; + + /** + * Open the decrypt session to decrypt the given protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the current decryption session + * @param[in] fd File descriptor of the protected content to be decrypted + * @param[in] offset Start position of the content + * @param[in] length The length of the protected content + * @return + * DRM_ERROR_CANNOT_HANDLE for failure and DRM_NO_ERROR for success + */ + virtual status_t onOpenDecryptSession( + int uniqueId, DecryptHandle* decryptHandle, int fd, int offset, int length) = 0; + + /** + * Close the decrypt session for the given handle + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + */ + virtual void onCloseDecryptSession(int uniqueId, DecryptHandle* decryptHandle) = 0; + + /** + * Initialize decryption for the given unit of the protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptId Handle for the decryption session + * @param[in] decryptUnitId ID Specifies decryption unit, such as track ID + * @param[in] headerInfo Information for initializing decryption of this decrypUnit + */ + virtual void onInitializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo) = 0; + + /** + * Decrypt the protected content buffers for the given unit + * This method will be called any number of times, based on number of + * encrypted streams received from application. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptId Handle for the decryption session + * @param[in] decryptUnitId ID Specifies decryption unit, such as track ID + * @param[in] encBuffer Encrypted data block + * @param[out] decBuffer Decrypted data block + * @return status_t + * Returns the error code for this API + * DRM_NO_ERROR for success, and one of DRM_ERROR_UNKNOWN, DRM_ERROR_LICENSE_EXPIRED + * DRM_ERROR_SESSION_NOT_OPENED, DRM_ERROR_DECRYPT_UNIT_NOT_INITIALIZED, + * DRM_ERROR_DECRYPT for failure. + */ + virtual status_t onDecrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer) = 0; + + /** + * Finalize decryption for the given unit of the protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID Specifies decryption unit, such as track ID + */ + virtual void onFinalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) = 0; + + /** + * Reads the specified number of bytes from an open DRM file. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[out] buffer Reference to the buffer that should receive the read data. + * @param[in] numBytes Number of bytes to read. + * @param[in] offset Offset with which to update the file position. + * + * @return Number of bytes read. Returns -1 for Failure. + */ + virtual ssize_t onPread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset) = 0; +}; + +}; + +#endif /* __DRM_ENGINE_BASE_H__ */ + diff --git a/drm/libdrmframework/plugins/common/include/IDrmEngine.h b/drm/libdrmframework/plugins/common/include/IDrmEngine.h new file mode 100644 index 0000000000000000000000000000000000000000..0d52f6650a6ea751f2c1674b1ce2c097268f6c56 --- /dev/null +++ b/drm/libdrmframework/plugins/common/include/IDrmEngine.h @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __IDRM_ENGINE_H__ +#define __IDRM_ENGINE_H__ + +#include + +namespace android { + +class DrmContentIds; +class DrmConstraints; +class DrmRights; +class DrmInfo; +class DrmInfoStatus; +class DrmConvertedStatus; +class DrmInfoRequest; +class DrmSupportInfo; +class DrmInfoEvent; + +/** + * This class is an interface for plug-in user + * + * Responsibility of this class is provide generic interface to DRM Engine Manager. + * Each interface need to be as abstract as possible. + */ +class IDrmEngine { +public: + virtual ~IDrmEngine() { + } + +public: + class OnInfoListener { + + public: + virtual void onInfo(const DrmInfoEvent& event) = 0; + + virtual ~OnInfoListener() { } + }; + +public: + + ////////////////////////////////// + // Implementation of IDrmEngine // + ////////////////////////////////// + + /** + * Initialize plug-in + * + * @param[in] uniqueId Unique identifier for a session + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + virtual status_t initialize(int uniqueId) = 0; + + /** + * Register a callback to be invoked when the caller required to + * receive necessary information + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] infoListener Listener + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + virtual status_t setOnInfoListener( + int uniqueId, const IDrmEngine::OnInfoListener* infoListener) = 0; + + /** + * Terminate the plug-in + * and release resource bound to plug-in + * e.g.) release native resource + * + * @param[in] uniqueId Unique identifier for a session + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + virtual status_t terminate(int uniqueId) = 0; + + /** + * Get constraint information associated with input content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Actions defined such as, + * Action::DEFAULT, Action::PLAY, etc + * @return DrmConstraints + * key-value pairs of constraint are embedded in it + * @note + * In case of error, return NULL + */ + virtual DrmConstraints* getConstraints( + int uniqueId, const String8* path, int action) = 0; + + /** + * Get whether the given content can be handled by this plugin or not + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path the protected object + * @return bool + * true if this plugin can handle , false in case of not able to handle + */ + virtual bool canHandle(int uniqueId, const String8& path) = 0; + + /** + * Executes given drm information based on its type + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmInfo Information needs to be processed + * @return DrmInfoStatus + * instance as a result of processing given input + */ + virtual DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo) = 0; + + /** + * Retrieves necessary information for registration, unregistration or rights + * acquisition information. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmInfoRequest Request information to retrieve drmInfo + * @return DrmInfo + * instance as a result of processing given input + */ + virtual DrmInfo* acquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest) = 0; + + /** + * Save DRM rights to specified rights path + * and make association with content path + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] drmRights DrmRights to be saved + * @param[in] rightsPath File path where rights to be saved + * @param[in] contentPath File path where content was saved + */ + virtual void saveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) = 0; + + /** + * Retrieves the mime type embedded inside the original content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @return String8 + * Returns mime-type of the original content, such as "video/mpeg" + */ + virtual String8 getOriginalMimeType(int uniqueId, const String8& path) = 0; + + /** + * Retrieves the type of the protected object (content, rights, etc..) + * using specified path or mimetype. At least one parameter should be non null + * to retrieve DRM object type + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the content or null. + * @param[in] mimeType Mime type of the content or null. + * @return type of the DRM content, + * such as DrmObjectType::CONTENT, DrmObjectType::RIGHTS_OBJECT + */ + virtual int getDrmObjectType( + int uniqueId, const String8& path, const String8& mimeType) = 0; + + /** + * Check whether the given content has valid rights or not + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Action to perform (Action::DEFAULT, Action::PLAY, etc) + * @return the status of the rights for the protected content, + * such as RightsStatus::RIGHTS_VALID, RightsStatus::RIGHTS_EXPIRED, etc. + */ + virtual int checkRightsStatus(int uniqueId, const String8& path, int action) = 0; + + /** + * Consumes the rights for a content. + * If the reserve parameter is true the rights is reserved until the same + * application calls this api again with the reserve parameter set to false. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] action Action to perform. (Action::DEFAULT, Action::PLAY, etc) + * @param[in] reserve True if the rights should be reserved. + */ + virtual void consumeRights( + int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve) = 0; + + /** + * Informs the DRM Engine about the playback actions performed on the DRM files. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] playbackStatus Playback action (Playback::START, Playback::STOP, Playback::PAUSE) + * @param[in] position Position in the file (in milliseconds) where the start occurs. + * Only valid together with Playback::START. + */ + virtual void setPlaybackStatus(int uniqueId, DecryptHandle* decryptHandle, + int playbackStatus, int position) = 0; + + /** + * Validates whether an action on the DRM content is allowed or not. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + * @param[in] action Action to validate (Action::PLAY, Action::TRANSFER, etc) + * @param[in] description Detailed description of the action + * @return true if the action is allowed. + */ + virtual bool validateAction(int uniqueId, const String8& path, + int action, const ActionDescription& description) = 0; + + /** + * Removes the rights associated with the given protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] path Path of the protected content + */ + virtual void removeRights(int uniqueId, const String8& path) = 0; + + /** + * Removes all the rights information of each plug-in associated with + * DRM framework. Will be used in master reset + * + * @param[in] uniqueId Unique identifier for a session + */ + virtual void removeAllRights(int uniqueId) = 0; + + /** + * This API is for Forward Lock based DRM scheme. + * Each time the application tries to download a new DRM file + * which needs to be converted, then the application has to + * begin with calling this API. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + */ + virtual void openConvertSession(int uniqueId, int convertId) = 0; + + /** + * Accepts and converts the input data which is part of DRM file. + * The resultant converted data and the status is returned in the DrmConvertedInfo + * object. This method will be called each time there are new block + * of data received by the application. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + * @param[in] inputData Input Data which need to be converted + * @return Return object contains the status of the data conversion, + * the output converted data and offset. In this case the + * application will ignore the offset information. + */ + virtual DrmConvertedStatus* convertData( + int uniqueId, int convertId, const DrmBuffer* inputData) = 0; + + /** + * Informs the Drm Agent when there is no more data which need to be converted + * or when an error occurs. Upon successful conversion of the complete data, + * the agent will inform that where the header and body signature + * should be added. This signature appending is needed to integrity + * protect the converted file. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] convertId Handle for the convert session + * @return Return object contains the status of the data conversion, + * the header and body signature data. It also informs + * the application on which offset these signature data + * should be appended. + */ + virtual DrmConvertedStatus* closeConvertSession( int uniqueId, int convertId) = 0; + + /** + * Returns the information about the Drm Engine capabilities which includes + * supported MimeTypes and file suffixes. + * + * @param[in] uniqueId Unique identifier for a session + * @return DrmSupportInfo + * instance which holds the capabilities of a plug-in + */ + virtual DrmSupportInfo* getSupportInfo(int uniqueId) = 0; + + /** + * Open the decrypt session to decrypt the given protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the current decryption session + * @param[in] fd File descriptor of the protected content to be decrypted + * @param[in] offset Start position of the content + * @param[in] length The length of the protected content + * @return + * DRM_ERROR_CANNOT_HANDLE for failure and DRM_NO_ERROR for success + */ + virtual status_t openDecryptSession( + int uniqueId, DecryptHandle* decryptHandle, int fd, int offset, int length) = 0; + + /** + * Close the decrypt session for the given handle + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + */ + virtual void closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) = 0; + + /** + * Initialize decryption for the given unit of the protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + * @param[in] headerInfo Information for initializing decryption of this decrypUnit + */ + virtual void initializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo) = 0; + + /** + * Decrypt the protected content buffers for the given unit + * This method will be called any number of times, based on number of + * encrypted streams received from application. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + * @param[in] encBuffer Encrypted data block + * @param[out] decBuffer Decrypted data block + * @return status_t + * Returns the error code for this API + * DRM_NO_ERROR for success, and one of DRM_ERROR_UNKNOWN, DRM_ERROR_LICENSE_EXPIRED + * DRM_ERROR_SESSION_NOT_OPENED, DRM_ERROR_DECRYPT_UNIT_NOT_INITIALIZED, + * DRM_ERROR_DECRYPT for failure. + */ + virtual status_t decrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer) = 0; + + /** + * Finalize decryption for the given unit of the protected content + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + */ + virtual void finalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) = 0; + + /** + * Reads the specified number of bytes from an open DRM file. + * + * @param[in] uniqueId Unique identifier for a session + * @param[in] decryptHandle Handle for the decryption session + * @param[out] buffer Reference to the buffer that should receive the read data. + * @param[in] numBytes Number of bytes to read. + * @param[in] offset Offset with which to update the file position. + * + * @return Number of bytes read. Returns -1 for Failure. + */ + virtual ssize_t pread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset) = 0; +}; + +}; + +#endif /* __IDRM_ENGINE_H__ */ + diff --git a/drm/libdrmframework/plugins/passthru/Android.mk b/drm/libdrmframework/plugins/passthru/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..7856d3730bad688eefac5f4fde1d74aee0d7571d --- /dev/null +++ b/drm/libdrmframework/plugins/passthru/Android.mk @@ -0,0 +1,48 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + src/DrmPassthruPlugIn.cpp + +LOCAL_MODULE := libdrmpassthruplugin + +LOCAL_STATIC_LIBRARIES := libdrmframeworkcommon + +LOCAL_SHARED_LIBRARIES := \ + libutils + +ifeq ($(TARGET_SIMULATOR),true) + LOCAL_LDLIBS += -ldl +else + LOCAL_SHARED_LIBRARIES += libdl +endif + +LOCAL_PRELINK_MODULE := false + +LOCAL_C_INCLUDES += \ + $(TOP)/frameworks/base/drm/libdrmframework/include \ + $(TOP)/frameworks/base/drm/libdrmframework/plugins/passthru/include \ + $(TOP)/frameworks/base/drm/libdrmframework/plugins/common/include \ + $(TOP)/frameworks/base/include + +# Set the following flag to enable the decryption passthru flow +#LOCAL_CFLAGS += -DENABLE_PASSTHRU_DECRYPTION + +LOCAL_MODULE_TAGS := optional + +include $(BUILD_SHARED_LIBRARY) diff --git a/drm/libdrmframework/plugins/passthru/include/DrmPassthruPlugIn.h b/drm/libdrmframework/plugins/passthru/include/DrmPassthruPlugIn.h new file mode 100644 index 0000000000000000000000000000000000000000..d2c7852954100bfb1e59e6ff347c7a5bb2891b8a --- /dev/null +++ b/drm/libdrmframework/plugins/passthru/include/DrmPassthruPlugIn.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_PASSTHRU_PLUGIN_H__ +#define __DRM_PASSTHRU_PLUGIN_H__ + +#include + +namespace android { + +class DrmPassthruPlugIn : public DrmEngineBase { + +public: + DrmPassthruPlugIn(); + virtual ~DrmPassthruPlugIn(); + +protected: + DrmConstraints* onGetConstraints(int uniqueId, const String8* path, int action); + + status_t onInitialize(int uniqueId); + + status_t onSetOnInfoListener(int uniqueId, const IDrmEngine::OnInfoListener* infoListener); + + status_t onTerminate(int uniqueId); + + bool onCanHandle(int uniqueId, const String8& path); + + DrmInfoStatus* onProcessDrmInfo(int uniqueId, const DrmInfo* drmInfo); + + void onSaveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath); + + DrmInfo* onAcquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest); + + String8 onGetOriginalMimeType(int uniqueId, const String8& path); + + int onGetDrmObjectType(int uniqueId, const String8& path, const String8& mimeType); + + int onCheckRightsStatus(int uniqueId, const String8& path, int action); + + void onConsumeRights(int uniqueId, DecryptHandle* decryptHandle, int action, bool reserve); + + void onSetPlaybackStatus( + int uniqueId, DecryptHandle* decryptHandle, int playbackStatus, int position); + + bool onValidateAction( + int uniqueId, const String8& path, int action, const ActionDescription& description); + + void onRemoveRights(int uniqueId, const String8& path); + + void onRemoveAllRights(int uniqueId); + + void onOpenConvertSession(int uniqueId, int convertId); + + DrmConvertedStatus* onConvertData(int uniqueId, int convertId, const DrmBuffer* inputData); + + DrmConvertedStatus* onCloseConvertSession(int uniqueId, int convertId); + + DrmSupportInfo* onGetSupportInfo(int uniqueId); + + status_t onOpenDecryptSession( + int uniqueId, DecryptHandle* decryptHandle, int fd, int offset, int length); + + void onCloseDecryptSession(int uniqueId, DecryptHandle* decryptHandle); + + void onInitializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo); + + status_t onDecrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer); + + void onFinalizeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId); + + ssize_t onPread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset); + +private: + DecryptHandle* openDecryptSessionImpl(); +}; + +}; + +#endif /* __DRM_PASSTHRU_PLUGIN_H__ */ + diff --git a/drm/libdrmframework/plugins/passthru/src/DrmPassthruPlugIn.cpp b/drm/libdrmframework/plugins/passthru/src/DrmPassthruPlugIn.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2655d0b4c351e16cfecfb7c98e4c991185f63669 --- /dev/null +++ b/drm/libdrmframework/plugins/passthru/src/DrmPassthruPlugIn.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_NDEBUG 0 +#define LOG_TAG "DrmPassthruPlugIn" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace android; + + +// This extern "C" is mandatory to be managed by TPlugInManager +extern "C" IDrmEngine* create() { + return new DrmPassthruPlugIn(); +} + +// This extern "C" is mandatory to be managed by TPlugInManager +extern "C" void destroy(IDrmEngine* pPlugIn) { + delete pPlugIn; + pPlugIn = NULL; +} + +DrmPassthruPlugIn::DrmPassthruPlugIn() + : DrmEngineBase() { + +} + +DrmPassthruPlugIn::~DrmPassthruPlugIn() { + +} + +DrmConstraints* DrmPassthruPlugIn::onGetConstraints( + int uniqueId, const String8* path, int action) { + LOGD("DrmPassthruPlugIn::onGetConstraints From Path: %d", uniqueId); + DrmConstraints* drmConstraints = new DrmConstraints(); + + String8 value("dummy_available_time"); + char* charValue = NULL; + charValue = new char[value.length() + 1]; + strncpy(charValue, value.string(), value.length()); + + //Just add dummy available time for verification + drmConstraints->put(&(DrmConstraints::LICENSE_AVAILABLE_TIME), charValue); + + return drmConstraints; +} + +DrmInfoStatus* DrmPassthruPlugIn::onProcessDrmInfo(int uniqueId, const DrmInfo* drmInfo) { + LOGD("DrmPassthruPlugIn::onProcessDrmInfo - Enter : %d", uniqueId); + DrmInfoStatus* drmInfoStatus = NULL; + if (NULL != drmInfo) { + switch (drmInfo->getInfoType()) { + case DrmInfoRequest::TYPE_REGISTRATION_INFO: { + const DrmBuffer* emptyBuffer = new DrmBuffer(); + drmInfoStatus + = new DrmInfoStatus(DrmInfoStatus::STATUS_OK, emptyBuffer, drmInfo->getMimeType()); + break; + } + case DrmInfoRequest::TYPE_UNREGISTRATION_INFO: { + const DrmBuffer* emptyBuffer = new DrmBuffer(); + drmInfoStatus + = new DrmInfoStatus(DrmInfoStatus::STATUS_OK, emptyBuffer, drmInfo->getMimeType()); + break; + } + case DrmInfoRequest::TYPE_RIGHTS_ACQUISITION_INFO: { + String8 licenseString("dummy_license_string"); + const int bufferSize = licenseString.size(); + char* data = NULL; + data = new char[bufferSize]; + memcpy(data, licenseString.string(), bufferSize); + const DrmBuffer* buffer = new DrmBuffer(data, bufferSize); + drmInfoStatus + = new DrmInfoStatus(DrmInfoStatus::STATUS_OK, buffer, drmInfo->getMimeType()); + break; + } + } + } + LOGD("DrmPassthruPlugIn::onProcessDrmInfo - Exit"); + return drmInfoStatus; +} + +status_t DrmPassthruPlugIn::onSetOnInfoListener( + int uniqueId, const IDrmEngine::OnInfoListener* infoListener) { + LOGD("DrmPassthruPlugIn::onSetOnInfoListener : %d", uniqueId); + return DRM_NO_ERROR; +} + +status_t DrmPassthruPlugIn::onInitialize(int uniqueId) { + LOGD("DrmPassthruPlugIn::onInitialize : %d", uniqueId); + return DRM_NO_ERROR; +} + +status_t DrmPassthruPlugIn::onTerminate(int uniqueId) { + LOGD("DrmPassthruPlugIn::onTerminate : %d", uniqueId); + return DRM_NO_ERROR; +} + +DrmSupportInfo* DrmPassthruPlugIn::onGetSupportInfo(int uniqueId) { + LOGD("DrmPassthruPlugIn::onGetSupportInfo : %d", uniqueId); + DrmSupportInfo* drmSupportInfo = new DrmSupportInfo(); + // Add mimetype's + drmSupportInfo->addMimeType(String8("application/vnd.passthru.drm")); + // Add File Suffixes + drmSupportInfo->addFileSuffix(String8(".passthru")); + // Add plug-in description + drmSupportInfo->setDescription(String8("Passthru plug-in")); + return drmSupportInfo; +} + +void DrmPassthruPlugIn::onSaveRights(int uniqueId, const DrmRights& drmRights, + const String8& rightsPath, const String8& contentPath) { + LOGD("DrmPassthruPlugIn::onSaveRights : %d", uniqueId); +} + +DrmInfo* DrmPassthruPlugIn::onAcquireDrmInfo(int uniqueId, const DrmInfoRequest* drmInfoRequest) { + LOGD("DrmPassthruPlugIn::onAcquireDrmInfo : %d", uniqueId); + DrmInfo* drmInfo = NULL; + + if (NULL != drmInfoRequest) { + String8 dataString("dummy_acquistion_string"); + int length = dataString.length(); + char* data = NULL; + data = new char[length]; + memcpy(data, dataString.string(), length); + drmInfo = new DrmInfo(drmInfoRequest->getInfoType(), + DrmBuffer(data, length), drmInfoRequest->getMimeType()); + } + return drmInfo; +} + +bool DrmPassthruPlugIn::onCanHandle(int uniqueId, const String8& path) { + LOGD("DrmPassthruPlugIn::canHandle: %s ", path.string()); + String8 extension = path.getPathExtension(); + extension.toLower(); + return (String8(".passthru") == extension); +} + +String8 DrmPassthruPlugIn::onGetOriginalMimeType(int uniqueId, const String8& path) { + LOGD("DrmPassthruPlugIn::onGetOriginalMimeType() : %d", uniqueId); + return String8("video/passthru"); +} + +int DrmPassthruPlugIn::onGetDrmObjectType( + int uniqueId, const String8& path, const String8& mimeType) { + LOGD("DrmPassthruPlugIn::onGetDrmObjectType() : %d", uniqueId); + return DrmObjectType::UNKNOWN; +} + +int DrmPassthruPlugIn::onCheckRightsStatus(int uniqueId, const String8& path, int action) { + LOGD("DrmPassthruPlugIn::onCheckRightsStatus() : %d", uniqueId); + int rightsStatus = RightsStatus::RIGHTS_VALID; + return rightsStatus; +} + +void DrmPassthruPlugIn::onConsumeRights(int uniqueId, DecryptHandle* decryptHandle, + int action, bool reserve) { + LOGD("DrmPassthruPlugIn::onConsumeRights() : %d", uniqueId); +} + +void DrmPassthruPlugIn::onSetPlaybackStatus(int uniqueId, DecryptHandle* decryptHandle, + int playbackStatus, int position) { + LOGD("DrmPassthruPlugIn::onSetPlaybackStatus() : %d", uniqueId); +} + +bool DrmPassthruPlugIn::onValidateAction(int uniqueId, const String8& path, + int action, const ActionDescription& description) { + LOGD("DrmPassthruPlugIn::onValidateAction() : %d", uniqueId); + return true; +} + +void DrmPassthruPlugIn::onRemoveRights(int uniqueId, const String8& path) { + LOGD("DrmPassthruPlugIn::onRemoveRights() : %d", uniqueId); +} + +void DrmPassthruPlugIn::onRemoveAllRights(int uniqueId) { + LOGD("DrmPassthruPlugIn::onRemoveAllRights() : %d", uniqueId); +} + +void DrmPassthruPlugIn::onOpenConvertSession(int uniqueId, int convertId) { + LOGD("DrmPassthruPlugIn::onOpenConvertSession() : %d", uniqueId); +} + +DrmConvertedStatus* DrmPassthruPlugIn::onConvertData( + int uniqueId, int convertId, const DrmBuffer* inputData) { + LOGD("DrmPassthruPlugIn::onConvertData() : %d", uniqueId); + DrmBuffer* convertedData = NULL; + + if (NULL != inputData && 0 < inputData->length) { + int length = inputData->length; + char* data = NULL; + data = new char[length]; + convertedData = new DrmBuffer(data, length); + memcpy(convertedData->data, inputData->data, length); + } + return new DrmConvertedStatus(DrmConvertedStatus::STATUS_OK, convertedData, 0 /*offset*/); +} + +DrmConvertedStatus* DrmPassthruPlugIn::onCloseConvertSession(int uniqueId, int convertId) { + LOGD("DrmPassthruPlugIn::onCloseConvertSession() : %d", uniqueId); + return new DrmConvertedStatus(DrmConvertedStatus::STATUS_OK, NULL, 0 /*offset*/); +} + +status_t DrmPassthruPlugIn::onOpenDecryptSession( + int uniqueId, DecryptHandle* decryptHandle, int fd, int offset, int length) { + LOGD("DrmPassthruPlugIn::onOpenDecryptSession() : %d", uniqueId); + +#ifdef ENABLE_PASSTHRU_DECRYPTION + decryptHandle->mimeType = String8("video/passthru"); + decryptHandle->decryptApiType = DecryptApiType::ELEMENTARY_STREAM_BASED; + decryptHandle->status = DRM_NO_ERROR; + decryptHandle->decryptInfo = NULL; + return DRM_NO_ERROR; +#endif + + return DRM_ERROR_CANNOT_HANDLE; +} + +void DrmPassthruPlugIn::onCloseDecryptSession(int uniqueId, DecryptHandle* decryptHandle) { + LOGD("DrmPassthruPlugIn::onCloseDecryptSession() : %d", uniqueId); + if (NULL != decryptHandle) { + if (NULL != decryptHandle->decryptInfo) { + delete decryptHandle->decryptInfo; decryptHandle->decryptInfo = NULL; + } + delete decryptHandle; decryptHandle = NULL; + } +} + +void DrmPassthruPlugIn::onInitializeDecryptUnit(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* headerInfo) { + LOGD("DrmPassthruPlugIn::onInitializeDecryptUnit() : %d", uniqueId); +} + +status_t DrmPassthruPlugIn::onDecrypt(int uniqueId, DecryptHandle* decryptHandle, + int decryptUnitId, const DrmBuffer* encBuffer, DrmBuffer** decBuffer) { + LOGD("DrmPassthruPlugIn::onDecrypt() : %d", uniqueId); + /** + * As a workaround implementation passthru would copy the given + * encrypted buffer as it is to decrypted buffer. Note, decBuffer + * memory has to be allocated by the caller. + */ + if (NULL != (*decBuffer) && 0 < (*decBuffer)->length) { + memcpy((*decBuffer)->data, encBuffer->data, encBuffer->length); + (*decBuffer)->length = encBuffer->length; + } + return DRM_NO_ERROR; +} + +void DrmPassthruPlugIn::onFinalizeDecryptUnit( + int uniqueId, DecryptHandle* decryptHandle, int decryptUnitId) { + LOGD("DrmPassthruPlugIn::onFinalizeDecryptUnit() : %d", uniqueId); +} + +ssize_t DrmPassthruPlugIn::onPread(int uniqueId, DecryptHandle* decryptHandle, + void* buffer, ssize_t numBytes, off_t offset) { + LOGD("DrmPassthruPlugIn::onPread() : %d", uniqueId); + return 0; +} + diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 7ca374164096b55ce609d79c1914efc7a5575722..9a19056dcdd086c448566b9d72d64a5ac4ef0586 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -19,12 +19,16 @@ package android.graphics; import android.os.Parcel; import android.os.Parcelable; import android.util.DisplayMetrics; +import android.util.Finalizers; import java.io.OutputStream; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; public final class Bitmap implements Parcelable { /** @@ -35,9 +39,13 @@ public final class Bitmap implements Parcelable { */ public static final int DENSITY_NONE = 0; - // Note: mNativeBitmap is used by FaceDetector_jni.cpp - // Don't change/rename without updating FaceDetector_jni.cpp - private final int mNativeBitmap; + /** + * Note: mNativeBitmap is used by FaceDetector_jni.cpp + * Don't change/rename without updating FaceDetector_jni.cpp + * + * @hide + */ + public final int mNativeBitmap; private final boolean mIsMutable; private byte[] mNinePatchChunk; // may be null @@ -51,7 +59,7 @@ public final class Bitmap implements Parcelable { private static volatile Matrix sScaleMatrix; private static volatile int sDefaultDensity = -1; - + /** * For backwards compatibility, allows the app layer to change the default * density when running old apps. @@ -77,8 +85,7 @@ public final class Bitmap implements Parcelable { This can be called from JNI code. */ - private Bitmap(int nativeBitmap, boolean isMutable, byte[] ninePatchChunk, - int density) { + private Bitmap(int nativeBitmap, boolean isMutable, byte[] ninePatchChunk, int density) { if (nativeBitmap == 0) { throw new RuntimeException("internal error: native bitmap is 0"); } @@ -90,6 +97,13 @@ public final class Bitmap implements Parcelable { if (density >= 0) { mDensity = density; } + + // If the finalizers queue is null, we are running in zygote and the + // bitmap will never be reclaimed, so we don't need to run our native + // destructor + if (Finalizers.getQueue() != null) { + new BitmapFinalizer(this); + } } /** @@ -171,6 +185,19 @@ public final class Bitmap implements Parcelable { return mRecycled; } + /** + * Returns the generation ID of this bitmap. The generation ID changes + * whenever the bitmap is modified. This can be used as an efficient way to + * check if a bitmap has changed. + * + * @return The current generation ID for this bitmap. + * + * @hide + */ + public int getGenerationId() { + return nativeGenerationId(mNativeBitmap); + } + /** * This is called by methods that want to throw an exception if the bitmap * has already been recycled. @@ -419,28 +446,30 @@ public final class Bitmap implements Parcelable { Rect srcR = new Rect(x, y, x + width, y + height); RectF dstR = new RectF(0, 0, width, height); + final Config newConfig = source.getConfig() == Config.ARGB_8888 ? + Config.ARGB_8888 : Config.RGB_565; + if (m == null || m.isIdentity()) { - bitmap = createBitmap(neww, newh, - source.hasAlpha() ? Config.ARGB_8888 : Config.RGB_565); + bitmap = createBitmap(neww, newh, newConfig, source.hasAlpha()); paint = null; // not needed } else { - /* the dst should have alpha if the src does, or if our matrix - doesn't preserve rectness - */ - boolean hasAlpha = source.hasAlpha() || !m.rectStaysRect(); + final boolean transformed = !m.rectStaysRect(); + RectF deviceR = new RectF(); m.mapRect(deviceR, dstR); + neww = Math.round(deviceR.width()); newh = Math.round(deviceR.height()); - bitmap = createBitmap(neww, newh, hasAlpha ? Config.ARGB_8888 : Config.RGB_565); - if (hasAlpha) { - bitmap.eraseColor(0); - } + + bitmap = createBitmap(neww, newh, transformed ? Config.ARGB_8888 : newConfig, + transformed || source.hasAlpha()); + canvas.translate(-deviceR.left, -deviceR.top); canvas.concat(m); + paint = new Paint(); paint.setFilterBitmap(filter); - if (!m.rectStaysRect()) { + if (transformed) { paint.setAntiAlias(true); } } @@ -465,8 +494,30 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0 */ public static Bitmap createBitmap(int width, int height, Config config) { + return createBitmap(width, height, config, true); + } + + /** + * Returns a mutable bitmap with the specified width and height. Its + * initial density is as per {@link #getDensity}. + * + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param config The bitmap config to create. + * @param hasAlpha If the bitmap is ARGB_8888 this flag can be used to mark the + * bitmap as opaque. Doing so will clear the bitmap in black + * instead of transparent. + * + * @throws IllegalArgumentException if the width or height are <= 0 + */ + private static Bitmap createBitmap(int width, int height, Config config, boolean hasAlpha) { Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true); - bm.eraseColor(0); // start with black/transparent pixels + if (config == Config.ARGB_8888 && !hasAlpha) { + bm.eraseColor(0xff000000); + nativeSetHasAlpha(bm.mNativeBitmap, hasAlpha); + } else { + bm.eraseColor(0); + } return bm; } @@ -999,12 +1050,22 @@ public final class Bitmap implements Parcelable { nativePrepareToDraw(mNativeBitmap); } - @Override - protected void finalize() throws Throwable { - try { + private static class BitmapFinalizer extends Finalizers.ReclaimableReference { + private static final Set sFinalizers = Collections.synchronizedSet( + new HashSet()); + + private int mNativeBitmap; + + BitmapFinalizer(Bitmap b) { + super(b, Finalizers.getQueue()); + mNativeBitmap = b.mNativeBitmap; + sFinalizers.add(this); + } + + @Override + public void reclaim() { nativeDestructor(mNativeBitmap); - } finally { - super.finalize(); + sFinalizers.remove(this); } } @@ -1041,6 +1102,7 @@ public final class Bitmap implements Parcelable { private static native void nativeCopyPixelsToBuffer(int nativeBitmap, Buffer dst); private static native void nativeCopyPixelsFromBuffer(int nb, Buffer src); + private static native int nativeGenerationId(int nativeBitmap); private static native Bitmap nativeCreateFromParcel(Parcel p); // returns true on success @@ -1056,7 +1118,7 @@ public final class Bitmap implements Parcelable { private static native void nativePrepareToDraw(int nativeBitmap); private static native void nativeSetHasAlpha(int nBitmap, boolean hasAlpha); private static native boolean nativeSameAs(int nb0, int nb1); - + /* package */ final int ni() { return mNativeBitmap; } diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java index 6234f2c1c3d1b5a0f6e116cae8115c02952b02d3..dc21a721a894986525e53918e7c3fa948bc8047e 100644 --- a/graphics/java/android/graphics/BitmapFactory.java +++ b/graphics/java/android/graphics/BitmapFactory.java @@ -18,7 +18,6 @@ package android.graphics; import android.content.res.AssetManager; import android.content.res.Resources; -import android.os.MemoryFile; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -84,7 +83,7 @@ public class BitmapFactory { /** * The pixel density to use for the bitmap. This will always result * in the returned bitmap having a density set for it (see - * {@link Bitmap#setDensity(int) Bitmap.setDensity(int)). In addition, + * {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition, * if {@link #inScaled} is set (which it is by default} and this * density does not match {@link #inTargetDensity}, then the bitmap * will be scaled to the target density before being returned. @@ -507,9 +506,7 @@ public class BitmapFactory { * * @param is The input stream that holds the raw data to be decoded into a * bitmap. - * @return The decoded bitmap, or null if the image data could not be - * decoded, or, if opts is non-null, if opts requested only the - * size be returned (in opts.outWidth and opts.outHeight) + * @return The decoded bitmap, or null if the image data could not be decoded. */ public static Bitmap decodeStream(InputStream is) { return decodeStream(is, null, null); @@ -530,18 +527,6 @@ public class BitmapFactory { * @return the decoded bitmap, or null */ public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts) { - try { - if (MemoryFile.isMemoryFile(fd)) { - int mappedlength = MemoryFile.getSize(fd); - MemoryFile file = new MemoryFile(fd, mappedlength, "r"); - InputStream is = file.getInputStream(); - Bitmap bm = decodeStream(is, outPadding, opts); - return finishDecode(bm, outPadding, opts); - } - } catch (IOException ex) { - // invalid filedescriptor, no need to call nativeDecodeFileDescriptor() - return null; - } Bitmap bm = nativeDecodeFileDescriptor(fd, outPadding, opts); return finishDecode(bm, outPadding, opts); } diff --git a/graphics/java/android/graphics/BitmapShader.java b/graphics/java/android/graphics/BitmapShader.java index 612b0ab717e41db64122a5dc74aaa72481da5a99..4ba679b2e14aee2c7a85a7dd13cc970f935d1d18 100644 --- a/graphics/java/android/graphics/BitmapShader.java +++ b/graphics/java/android/graphics/BitmapShader.java @@ -16,10 +16,16 @@ package android.graphics; +/** + * Shader used to draw a bitmap as a texture. The bitmap can be repeated or + * mirrored by setting the tiling mode. + */ public class BitmapShader extends Shader { - - // we hold on just for the GC, since our native counterpart is using it - private Bitmap mBitmap; + /** + * Prevent garbage collection. + */ + @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) + private final Bitmap mBitmap; /** * Call this to create a new shader that will draw with a bitmap. @@ -30,12 +36,13 @@ public class BitmapShader extends Shader { */ public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY) { mBitmap = bitmap; - native_instance = nativeCreate(bitmap.ni(), - tileX.nativeInt, tileY.nativeInt); + final int b = bitmap.ni(); + native_instance = nativeCreate(b, tileX.nativeInt, tileY.nativeInt); + native_shader = nativePostCreate(native_instance, b, tileX.nativeInt, tileY.nativeInt); } - private static native int nativeCreate(int native_bitmap, - int shaderTileModeX, - int shaderTileModeY); + private static native int nativeCreate(int native_bitmap, int shaderTileModeX, + int shaderTileModeY); + private static native int nativePostCreate(int native_shader, int native_bitmap, + int shaderTileModeX, int shaderTileModeY); } - diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index 345f810ecf2a983ff6e127efda0c7d2e8e91893f..e75a19e5c016a202509f6fc46c6cb441dafb3881 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -16,11 +16,10 @@ package android.graphics; -import android.text.TextUtils; -import android.text.SpannedString; -import android.text.SpannableString; import android.text.GraphicsOperations; -import android.util.DisplayMetrics; +import android.text.SpannableString; +import android.text.SpannedString; +import android.text.TextUtils; import javax.microedition.khronos.opengles.GL; @@ -43,21 +42,38 @@ public class Canvas { for both to be null. */ private Bitmap mBitmap; // if not null, mGL must be null - private GL mGL; // if not null, mBitmap must be null // optional field set by the caller - private DrawFilter mDrawFilter; + private DrawFilter mDrawFilter; - // Package-scoped for quick access. - /*package*/ int mDensity = Bitmap.DENSITY_NONE; + /** + * @hide + */ + protected int mDensity = Bitmap.DENSITY_NONE; - // Used to determine when compatibility scaling is in effect. - private int mScreenDensity = Bitmap.DENSITY_NONE; + /** + * Used to determine when compatibility scaling is in effect. + * + * @hide + */ + protected int mScreenDensity = Bitmap.DENSITY_NONE; // Used by native code @SuppressWarnings({"UnusedDeclaration"}) private int mSurfaceFormat; + /** + * Flag for drawTextRun indicating left-to-right run direction. + * @hide + */ + public static final int DIRECTION_LTR = 0; + + /** + * Flag for drawTextRun indicating right-to-left run direction. + * @hide + */ + public static final int DIRECTION_RTL = 1; + /** * Construct an empty raster canvas. Use setBitmap() to specify a bitmap to * draw into. The initial target density is {@link Bitmap#DENSITY_NONE}; @@ -89,47 +105,37 @@ public class Canvas { mDensity = bitmap.mDensity; } - /*package*/ Canvas(int nativeCanvas) { + Canvas(int nativeCanvas) { if (nativeCanvas == 0) { throw new IllegalStateException(); } mNativeCanvas = nativeCanvas; mDensity = Bitmap.getDefaultDensity(); } - + /** - * Construct a canvas with the specified gl context. All drawing through - * this canvas will be redirected to OpenGL. Note: some features may not - * be supported in this mode (e.g. some GL implementations may not support - * antialiasing or certain effects like ColorMatrix or certain Xfermodes). - * However, no exception will be thrown in those cases. + * Returns null. * - *

    The initial target density of the canvas is the same as the initial - * density of bitmaps as per {@link Bitmap#getDensity() Bitmap.getDensity()}. - */ - public Canvas(GL gl) { - mNativeCanvas = initGL(); - mGL = gl; - mDensity = Bitmap.getDefaultDensity(); - } - - /** - * Return the GL object associated with this canvas, or null if it is not - * backed by GL. + * @deprecated This method is not supported and should not be invoked. */ - public GL getGL() { - return mGL; + @Deprecated + protected GL getGL() { + return null; } - + /** - * Call this to free up OpenGL resources that may be cached or allocated - * on behalf of the Canvas. Any subsequent drawing with a GL-backed Canvas - * will have to recreate those resources. + * Indicates whether this Canvas uses hardware acceleration. + * + * Note that this method does not define what type of hardware acceleration + * may or may not be used. + * + * @return True if drawing operations are hardware accelerated, + * false otherwise. */ - public static void freeGlCaches() { - freeCaches(); + public boolean isHardwareAccelerated() { + return false; } - + /** * Specify a bitmap for the canvas to draw into. As a side-effect, also * updates the canvas's target density to match that of the bitmap. @@ -143,7 +149,7 @@ public class Canvas { if (!bitmap.isMutable()) { throw new IllegalStateException(); } - if (mGL != null) { + if (isHardwareAccelerated()) { throw new RuntimeException("Can't set a bitmap device on a GL canvas"); } throwIfRecycled(bitmap); @@ -157,13 +163,12 @@ public class Canvas { * Set the viewport dimensions if this canvas is GL based. If it is not, * this method is ignored and no exception is thrown. * - * @param width The width of the viewport - * @param height The height of the viewport + * @param width The width of the viewport + * @param height The height of the viewport + * + * @hide */ public void setViewport(int width, int height) { - if (mGL != null) { - nativeSetViewport(mNativeCanvas, width, height); - } } /** @@ -377,8 +382,8 @@ public class Canvas { * * @param sx The amount to scale in X * @param sy The amount to scale in Y - * @param px The x-coord for the pivot point (unchanged by the rotation) - * @param py The y-coord for the pivot point (unchanged by the rotation) + * @param px The x-coord for the pivot point (unchanged by the scale) + * @param py The y-coord for the pivot point (unchanged by the scale) */ public final void scale(float sx, float sy, float px, float py) { translate(px, py); @@ -621,7 +626,11 @@ public class Canvas { EdgeType(int nativeInt) { this.nativeInt = nativeInt; } - final int nativeInt; + + /** + * @hide + */ + public final int nativeInt; } /** @@ -957,6 +966,21 @@ public class Canvas { } } + /** + * Draws the specified bitmap as an N-patch (most often, a 9-patches.) + * + * Note: Only supported by hardware accelerated canvas at the moment. + * + * @param bitmap The bitmap to draw as an N-patch + * @param chunks The patches information (matches the native struct Res_png_9patch) + * @param dst The destination rectangle. + * @param paint The paint to draw the bitmap with. may be null + * + * @hide + */ + public void drawPatch(Bitmap bitmap, byte[] chunks, RectF dst, Paint paint) { + } + /** * Draw the specified bitmap, with its top/left corner at (x,y), using * the specified paint, transformed by the current matrix. @@ -1221,7 +1245,7 @@ public class Canvas { checkRange(texs.length, texOffset, vertexCount); } if (colors != null) { - checkRange(colors.length, colorOffset, vertexCount); + checkRange(colors.length, colorOffset, vertexCount / 2); } if (indices != null) { checkRange(indices.length, indexOffset, indexCount); @@ -1246,8 +1270,8 @@ public class Canvas { (text.length - index - count)) < 0) { throw new IndexOutOfBoundsException(); } - native_drawText(mNativeCanvas, text, index, count, x, y, - paint.mNativePaint); + native_drawText(mNativeCanvas, text, index, count, x, y, paint.mBidiFlags, + paint.mNativePaint); } /** @@ -1259,7 +1283,10 @@ public class Canvas { * @param y The y-coordinate of the origin of the text being drawn * @param paint The paint used for the text (e.g. color, size, style) */ - public native void drawText(String text, float x, float y, Paint paint); + public void drawText(String text, float x, float y, Paint paint) { + native_drawText(mNativeCanvas, text, 0, text.length(), x, y, paint.mBidiFlags, + paint.mNativePaint); + } /** * Draw the text, with origin at (x,y), using the specified paint. @@ -1277,8 +1304,8 @@ public class Canvas { if ((start | end | (end - start) | (text.length() - end)) < 0) { throw new IndexOutOfBoundsException(); } - native_drawText(mNativeCanvas, text, start, end, x, y, - paint.mNativePaint); + native_drawText(mNativeCanvas, text, start, end, x, y, paint.mBidiFlags, + paint.mNativePaint); } /** @@ -1299,16 +1326,108 @@ public class Canvas { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { native_drawText(mNativeCanvas, text.toString(), start, end, x, y, - paint.mNativePaint); - } - else if (text instanceof GraphicsOperations) { + paint.mBidiFlags, paint.mNativePaint); + } else if (text instanceof GraphicsOperations) { ((GraphicsOperations) text).drawText(this, start, end, x, y, paint); - } - else { + } else { char[] buf = TemporaryBuffer.obtain(end - start); TextUtils.getChars(text, start, end, buf, 0); - drawText(buf, 0, end - start, x, y, paint); + native_drawText(mNativeCanvas, buf, 0, end - start, x, y, + paint.mBidiFlags, paint.mNativePaint); + TemporaryBuffer.recycle(buf); + } + } + + /** + * Render a run of all LTR or all RTL text, with shaping. This does not run + * bidi on the provided text, but renders it as a uniform right-to-left or + * left-to-right run, as indicated by dir. Alignment of the text is as + * determined by the Paint's TextAlign value. + * + * @param text the text to render + * @param index the start of the text to render + * @param count the count of chars to render + * @param contextIndex the start of the context for shaping. Must be + * no greater than index. + * @param contextCount the number of characters in the context for shaping. + * ContexIndex + contextCount must be no less than index + * + count. + * @param x the x position at which to draw the text + * @param y the y position at which to draw the text + * @param dir the run direction, either {@link #DIRECTION_LTR} or + * {@link #DIRECTION_RTL}. + * @param paint the paint + * @hide + */ + public void drawTextRun(char[] text, int index, int count, + int contextIndex, int contextCount, float x, float y, int dir, + Paint paint) { + + if (text == null) { + throw new NullPointerException("text is null"); + } + if (paint == null) { + throw new NullPointerException("paint is null"); + } + if ((index | count | text.length - index - count) < 0) { + throw new IndexOutOfBoundsException(); + } + if (dir != DIRECTION_LTR && dir != DIRECTION_RTL) { + throw new IllegalArgumentException("unknown dir: " + dir); + } + + native_drawTextRun(mNativeCanvas, text, index, count, + contextIndex, contextCount, x, y, dir, paint.mNativePaint); + } + + /** + * Render a run of all LTR or all RTL text, with shaping. This does not run + * bidi on the provided text, but renders it as a uniform right-to-left or + * left-to-right run, as indicated by dir. Alignment of the text is as + * determined by the Paint's TextAlign value. + * + * @param text the text to render + * @param start the start of the text to render. Data before this position + * can be used for shaping context. + * @param end the end of the text to render. Data at or after this + * position can be used for shaping context. + * @param x the x position at which to draw the text + * @param y the y position at which to draw the text + * @param dir the run direction, either 0 for LTR or 1 for RTL. + * @param paint the paint + * @hide + */ + public void drawTextRun(CharSequence text, int start, int end, + int contextStart, int contextEnd, float x, float y, int dir, + Paint paint) { + + if (text == null) { + throw new NullPointerException("text is null"); + } + if (paint == null) { + throw new NullPointerException("paint is null"); + } + if ((start | end | end - start | text.length() - end) < 0) { + throw new IndexOutOfBoundsException(); + } + + int flags = dir == 0 ? 0 : 1; + + if (text instanceof String || text instanceof SpannedString || + text instanceof SpannableString) { + native_drawTextRun(mNativeCanvas, text.toString(), start, end, + contextStart, contextEnd, x, y, flags, paint.mNativePaint); + } else if (text instanceof GraphicsOperations) { + ((GraphicsOperations) text).drawTextRun(this, start, end, + contextStart, contextEnd, x, y, flags, paint); + } else { + int contextLen = contextEnd - contextStart; + int len = end - start; + char[] buf = TemporaryBuffer.obtain(contextLen); + TextUtils.getChars(text, contextStart, contextEnd, buf, 0); + native_drawTextRun(mNativeCanvas, buf, start - contextStart, len, + 0, contextLen, x, y, flags, paint.mNativePaint); TemporaryBuffer.recycle(buf); } } @@ -1368,7 +1487,7 @@ public class Canvas { } native_drawTextOnPath(mNativeCanvas, text, index, count, path.ni(), hOffset, vOffset, - paint.mNativePaint); + paint.mBidiFlags, paint.mNativePaint); } /** @@ -1388,7 +1507,8 @@ public class Canvas { float vOffset, Paint paint) { if (text.length() > 0) { native_drawTextOnPath(mNativeCanvas, text, path.ni(), - hOffset, vOffset, paint.mNativePaint); + hOffset, vOffset, paint.mBidiFlags, + paint.mNativePaint); } } @@ -1431,28 +1551,74 @@ public class Canvas { drawPicture(picture); restore(); } - + + /** + *

    Acquires the Canvas context. After invoking this method, the Canvas + * context can be modified by the caller. For instance, if you acquire + * the context of an OpenGL Canvas you can reset the GL viewport, scissor, + * etc.

    + * + *

    A call to {@link #acquireContext()} should aways be followed by + * a call to {@link #releaseContext()}, preferrably using a try block:

    + * + *
    +     * try {
    +     *     if (canvas.acquireContext()) {
    +     *         // Use the canvas and/or its context
    +     *     }
    +     * } finally {
    +     *     canvas.releaseContext();
    +     * }
    +     * 
    + * + *

    Acquiring the context can be an expensive operation and should not + * be done unless absolutely necessary.

    + * + *

    Applications should never invoke this method directly.

    + * + * @return True if the context could be acquired successfully, false + * otherwise (if the context is already acquired for instance.) + * + * @see #releaseContext() + * + * @hide + */ + public boolean acquireContext() { + return false; + } + + /** + *

    Release the context acquired with {@link #acquireContext()}.

    + * + * @see #acquireContext() + * + * @hide + */ + public void releaseContext() { + } + + @Override protected void finalize() throws Throwable { - super.finalize(); - // If the constructor threw an exception before setting mNativeCanvas, the native finalizer - // must not be invoked. - if (mNativeCanvas != 0) { - finalizer(mNativeCanvas); + try { + super.finalize(); + } finally { + // If the constructor threw an exception before setting mNativeCanvas, + // the native finalizer must not be invoked. + if (mNativeCanvas != 0) { + finalizer(mNativeCanvas); + } } } /** - * Free up as much memory as possible from private caches (e.g. fonts, - * images) + * Free up as much memory as possible from private caches (e.g. fonts, images) * - * @hide - for now + * @hide */ public static native void freeCaches(); private static native int initRaster(int nativeBitmapOrZero); - private static native int initGL(); private static native void native_setBitmap(int nativeCanvas, int bitmap); - private static native void nativeSetViewport(int nCanvas, int w, int h); private static native int native_saveLayer(int nativeCanvas, RectF bounds, int paint, int layerFlags); private static native int native_saveLayer(int nativeCanvas, float l, @@ -1555,10 +1721,19 @@ public class Canvas { private static native void native_drawText(int nativeCanvas, char[] text, int index, int count, float x, - float y, int paint); + float y, int flags, int paint); private static native void native_drawText(int nativeCanvas, String text, int start, int end, float x, - float y, int paint); + float y, int flags, int paint); + + private static native void native_drawTextRun(int nativeCanvas, String text, + int start, int end, int contextStart, int contextEnd, + float x, float y, int flags, int paint); + + private static native void native_drawTextRun(int nativeCanvas, char[] text, + int start, int count, int contextStart, int contextCount, + float x, float y, int flags, int paint); + private static native void native_drawPosText(int nativeCanvas, char[] text, int index, int count, float[] pos, @@ -1570,11 +1745,13 @@ public class Canvas { char[] text, int index, int count, int path, float hOffset, - float vOffset, int paint); + float vOffset, int bidiFlags, + int paint); private static native void native_drawTextOnPath(int nativeCanvas, String text, int path, - float hOffset, - float vOffset, int paint); + float hOffset, + float vOffset, + int flags, int paint); private static native void native_drawPicture(int nativeCanvas, int nativePicture); private static native void finalizer(int nativeCanvas); diff --git a/graphics/java/android/graphics/ColorFilter.java b/graphics/java/android/graphics/ColorFilter.java index 76f2c7f92b6ae2a798b550b623ed511217995150..e5cf8301b0e39d4b07cc77c27b0c80cacd42798c 100644 --- a/graphics/java/android/graphics/ColorFilter.java +++ b/graphics/java/android/graphics/ColorFilter.java @@ -23,12 +23,20 @@ package android.graphics; public class ColorFilter { + int native_instance; + + /** + * @hide + */ + public int nativeColorFilter; protected void finalize() throws Throwable { - finalizer(native_instance); + try { + super.finalize(); + } finally { + finalizer(native_instance, nativeColorFilter); + } } - private static native void finalizer(int native_instance); - - int native_instance; + private static native void finalizer(int native_instance, int nativeColorFilter); } diff --git a/graphics/java/android/graphics/ColorMatrixColorFilter.java b/graphics/java/android/graphics/ColorMatrixColorFilter.java index 5d73cff77e0acdc184f7e69fdb4ba337a8e1a896..245c61597e7394031976e9b8a600c1722d7fcc06 100644 --- a/graphics/java/android/graphics/ColorMatrixColorFilter.java +++ b/graphics/java/android/graphics/ColorMatrixColorFilter.java @@ -25,7 +25,9 @@ public class ColorMatrixColorFilter extends ColorFilter { * is constructed will not be reflected in the filter. */ public ColorMatrixColorFilter(ColorMatrix matrix) { - native_instance = nativeColorMatrixFilter(matrix.getArray()); + final float[] colorMatrix = matrix.getArray(); + native_instance = nativeColorMatrixFilter(colorMatrix); + nativeColorFilter = nColorMatrixFilter(colorMatrix); } /** @@ -40,7 +42,9 @@ public class ColorMatrixColorFilter extends ColorFilter { throw new ArrayIndexOutOfBoundsException(); } native_instance = nativeColorMatrixFilter(array); + nativeColorFilter = nColorMatrixFilter(array); } private static native int nativeColorMatrixFilter(float[] array); + private static native int nColorMatrixFilter(float[] array); } diff --git a/graphics/java/android/graphics/ComposeShader.java b/graphics/java/android/graphics/ComposeShader.java index a06d30b101713bd1a251571416beea491fd75517..8d5c913d1b6948eb1dd83145a9e360f1fe8550d5 100644 --- a/graphics/java/android/graphics/ComposeShader.java +++ b/graphics/java/android/graphics/ComposeShader.java @@ -20,6 +20,14 @@ package android.graphics; an {@link android.graphics.Xfermode} subclass. */ public class ComposeShader extends Shader { + /** + * Hold onto the shaders to avoid GC. + */ + @SuppressWarnings({"UnusedDeclaration"}) + private final Shader mShaderA; + @SuppressWarnings({"UnusedDeclaration"}) + private final Shader mShaderB; + /** Create a new compose shader, given shaders A, B, and a combining mode. When the mode is applied, it will be given the result from shader A as its "dst", and the result of from shader B as its "src". @@ -29,8 +37,18 @@ public class ComposeShader extends Shader { is null, then SRC_OVER is assumed. */ public ComposeShader(Shader shaderA, Shader shaderB, Xfermode mode) { + mShaderA = shaderA; + mShaderB = shaderB; native_instance = nativeCreate1(shaderA.native_instance, shaderB.native_instance, - (mode != null) ? mode.native_instance : 0); + (mode != null) ? mode.native_instance : 0); + if (mode instanceof PorterDuffXfermode) { + PorterDuff.Mode pdMode = ((PorterDuffXfermode) mode).mode; + native_shader = nativePostCreate2(native_instance, shaderA.native_shader, + shaderB.native_shader, pdMode != null ? pdMode.nativeInt : 0); + } else { + native_shader = nativePostCreate1(native_instance, shaderA.native_shader, + shaderB.native_shader, mode != null ? mode.native_instance : 0); + } } /** Create a new compose shader, given shaders A, B, and a combining PorterDuff mode. @@ -41,11 +59,20 @@ public class ComposeShader extends Shader { @param mode The PorterDuff mode that combines the colors from the two shaders. */ public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode) { + mShaderA = shaderA; + mShaderB = shaderB; native_instance = nativeCreate2(shaderA.native_instance, shaderB.native_instance, - mode.nativeInt); + mode.nativeInt); + native_shader = nativePostCreate2(native_instance, shaderA.native_shader, + shaderB.native_shader, mode.nativeInt); } - private static native int nativeCreate1(int native_shaderA, int native_shaderB, int native_mode); - private static native int nativeCreate2(int native_shaderA, int native_shaderB, int porterDuffMode); + private static native int nativeCreate1(int native_shaderA, int native_shaderB, + int native_mode); + private static native int nativeCreate2(int native_shaderA, int native_shaderB, + int porterDuffMode); + private static native int nativePostCreate1(int native_shader, int native_skiaShaderA, + int native_skiaShaderB, int native_mode); + private static native int nativePostCreate2(int native_shader, int native_skiaShaderA, + int native_skiaShaderB, int porterDuffMode); } - diff --git a/graphics/java/android/graphics/LightingColorFilter.java b/graphics/java/android/graphics/LightingColorFilter.java index 55623892527e41632e2853d3dfc7817774673b23..715ce86503a1c16050f8dbedc19c96ba419169be 100644 --- a/graphics/java/android/graphics/LightingColorFilter.java +++ b/graphics/java/android/graphics/LightingColorFilter.java @@ -30,7 +30,9 @@ public class LightingColorFilter extends ColorFilter { */ public LightingColorFilter(int mul, int add) { native_instance = native_CreateLightingFilter(mul, add); + nativeColorFilter = nCreateLightingFilter(mul, add); } private static native int native_CreateLightingFilter(int mul, int add); + private static native int nCreateLightingFilter(int mul, int add); } diff --git a/graphics/java/android/graphics/LinearGradient.java b/graphics/java/android/graphics/LinearGradient.java index e3db1059512697d34110564f6f37e96a4b671db5..82ed1992bac3c09ea5156a221182825c71320fdb 100644 --- a/graphics/java/android/graphics/LinearGradient.java +++ b/graphics/java/android/graphics/LinearGradient.java @@ -17,7 +17,6 @@ package android.graphics; public class LinearGradient extends Shader { - /** Create a shader that draws a linear gradient along a line. @param x0 The x-coordinate for the start of the gradient line @param y0 The y-coordinate for the start of the gradient line @@ -38,6 +37,8 @@ public class LinearGradient extends Shader { throw new IllegalArgumentException("color and position arrays must be of equal length"); } native_instance = nativeCreate1(x0, y0, x1, y1, colors, positions, tile.nativeInt); + native_shader = nativePostCreate1(native_instance, x0, y0, x1, y1, colors, positions, + tile.nativeInt); } /** Create a shader that draws a linear gradient along a line. @@ -52,12 +53,16 @@ public class LinearGradient extends Shader { public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile) { native_instance = nativeCreate2(x0, y0, x1, y1, color0, color1, tile.nativeInt); + native_shader = nativePostCreate2(native_instance, x0, y0, x1, y1, color0, color1, + tile.nativeInt); } - - private static native int nativeCreate1(float x0, float y0, float x1, float y1, - int colors[], float positions[], int tileMode); - private static native int nativeCreate2(float x0, float y0, float x1, float y1, - int color0, int color1, int tileMode); + private native int nativeCreate1(float x0, float y0, float x1, float y1, + int colors[], float positions[], int tileMode); + private native int nativeCreate2(float x0, float y0, float x1, float y1, + int color0, int color1, int tileMode); + private native int nativePostCreate1(int native_shader, float x0, float y0, float x1, float y1, + int colors[], float positions[], int tileMode); + private native int nativePostCreate2(int native_shader, float x0, float y0, float x1, float y1, + int color0, int color1, int tileMode); } - diff --git a/graphics/java/android/graphics/Matrix.java b/graphics/java/android/graphics/Matrix.java index f549900b030ca727f105e8113feadf219f296969..66ed104ad2459d1c2dacde767622c6e9a3f22168 100644 --- a/graphics/java/android/graphics/Matrix.java +++ b/graphics/java/android/graphics/Matrix.java @@ -37,7 +37,10 @@ public class Matrix { public static final int MPERSP_1 = 7; //!< use with getValues/setValues public static final int MPERSP_2 = 8; //!< use with getValues/setValues - /* package */ int native_instance; + /** + * @hide + */ + public int native_instance; /** * Create an identity matrix @@ -419,6 +422,10 @@ public class Matrix { * the transformed vectors into the array of vectors specified by dst. The * two arrays represent their "vectors" as pairs of floats [x, y]. * + * Note: this method does not apply the translation associated with the matrix. Use + * {@link Matrix#mapPoints(float[], int, float[], int, int)} if you want the translation + * to be applied. + * * @param dst The array of dst vectors (x,y pairs) * @param dstIndex The index of the first [x,y] pair of dst floats * @param src The array of src vectors (x,y pairs) @@ -452,6 +459,9 @@ public class Matrix { * the transformed vectors into the array of vectors specified by dst. The * two arrays represent their "vectors" as pairs of floats [x, y]. * + * Note: this method does not apply the translation associated with the matrix. Use + * {@link Matrix#mapPoints(float[], float[])} if you want the translation to be applied. + * * @param dst The array of dst vectors (x,y pairs) * @param src The array of src vectors (x,y pairs) */ @@ -475,6 +485,10 @@ public class Matrix { /** * Apply this matrix to the array of 2D vectors, and write the transformed * vectors back into the array. + * + * Note: this method does not apply the translation associated with the matrix. Use + * {@link Matrix#mapPoints(float[])} if you want the translation to be applied. + * * @param vecs The array [x0, y0, x1, y1, ...] of vectors to transform. */ public void mapVectors(float[] vecs) { diff --git a/graphics/java/android/graphics/NinePatch.java b/graphics/java/android/graphics/NinePatch.java index 88dfd6790f3a2c4279e3902f05bdca3fb042b8fe..df6feba47de943977a525e770e750b2b34bf9e86 100644 --- a/graphics/java/android/graphics/NinePatch.java +++ b/graphics/java/android/graphics/NinePatch.java @@ -35,6 +35,12 @@ package android.graphics; *

    */ public class NinePatch { + private final Bitmap mBitmap; + private final byte[] mChunk; + private Paint mPaint; + private String mSrcName; // Useful for debugging + private final RectF mRect = new RectF(); + /** * Create a drawable projection from a bitmap to nine patches. * @@ -74,10 +80,14 @@ public class NinePatch { * @param location Where to draw the bitmap. */ public void draw(Canvas canvas, RectF location) { - nativeDraw(canvas.mNativeCanvas, location, - mBitmap.ni(), mChunk, - mPaint != null ? mPaint.mNativePaint : 0, - canvas.mDensity, mBitmap.mDensity); + if (!canvas.isHardwareAccelerated()) { + nativeDraw(canvas.mNativeCanvas, location, + mBitmap.ni(), mChunk, + mPaint != null ? mPaint.mNativePaint : 0, + canvas.mDensity, mBitmap.mDensity); + } else { + canvas.drawPatch(mBitmap, mChunk, location, null); + } } /** @@ -87,10 +97,15 @@ public class NinePatch { * @param location Where to draw the bitmap. */ public void draw(Canvas canvas, Rect location) { - nativeDraw(canvas.mNativeCanvas, location, - mBitmap.ni(), mChunk, - mPaint != null ? mPaint.mNativePaint : 0, - canvas.mDensity, mBitmap.mDensity); + if (!canvas.isHardwareAccelerated()) { + nativeDraw(canvas.mNativeCanvas, location, + mBitmap.ni(), mChunk, + mPaint != null ? mPaint.mNativePaint : 0, + canvas.mDensity, mBitmap.mDensity); + } else { + mRect.set(location); + canvas.drawPatch(mBitmap, mChunk, mRect, null); + } } /** @@ -101,9 +116,14 @@ public class NinePatch { * @param paint The Paint to draw through. */ public void draw(Canvas canvas, Rect location, Paint paint) { - nativeDraw(canvas.mNativeCanvas, location, - mBitmap.ni(), mChunk, paint != null ? paint.mNativePaint : 0, - canvas.mDensity, mBitmap.mDensity); + if (!canvas.isHardwareAccelerated()) { + nativeDraw(canvas.mNativeCanvas, location, + mBitmap.ni(), mChunk, paint != null ? paint.mNativePaint : 0, + canvas.mDensity, mBitmap.mDensity); + } else { + mRect.set(location); + canvas.drawPatch(mBitmap, mChunk, mRect, paint); + } } /** @@ -133,11 +153,6 @@ public class NinePatch { public native static boolean isNinePatchChunk(byte[] chunk); - private final Bitmap mBitmap; - private final byte[] mChunk; - private Paint mPaint; - private String mSrcName; // Useful for debugging - private static native void validateNinePatchChunk(int bitmap, byte[] chunk); private static native void nativeDraw(int canvas_instance, RectF loc, int bitmap_instance, byte[] c, int paint_instance_or_null, diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 3e3f87b570e5eee7e94e2e230b94b4b515febb4a..62fbfb45668a8cee5f63c1e7a59db79dcc03037e 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -16,10 +16,10 @@ package android.graphics; -import android.text.TextUtils; +import android.text.GraphicsOperations; import android.text.SpannableString; import android.text.SpannedString; -import android.text.GraphicsOperations; +import android.text.TextUtils; /** * The Paint class holds the style and color information about how to draw @@ -27,7 +27,11 @@ import android.text.GraphicsOperations; */ public class Paint { - /*package*/ int mNativePaint; + /** + * @hide + */ + public int mNativePaint; + private ColorFilter mColorFilter; private MaskFilter mMaskFilter; private PathEffect mPathEffect; @@ -39,6 +43,32 @@ public class Paint { private boolean mHasCompatScaling; private float mCompatScaling; private float mInvCompatScaling; + + /** + * @hide + */ + public boolean hasShadow; + /** + * @hide + */ + public float shadowDx; + /** + * @hide + */ + public float shadowDy; + /** + * @hide + */ + public float shadowRadius; + /** + * @hide + */ + public int shadowColor; + + /** + * @hide + */ + public int mBidiFlags = BIDI_DEFAULT_LTR; private static final Style[] sStyleArray = { Style.FILL, Style.STROKE, Style.FILL_AND_STROKE @@ -76,8 +106,116 @@ public class Paint { private static final int DEFAULT_PAINT_FLAGS = DEV_KERN_TEXT_FLAG; /** - * The Style specifies if the primitive being drawn is filled, - * stroked, or both (in the same color). The default is FILL. + * Bidi flag to set LTR paragraph direction. + * + * @hide + */ + public static final int BIDI_LTR = 0x0; + + /** + * Bidi flag to set RTL paragraph direction. + * + * @hide + */ + public static final int BIDI_RTL = 0x1; + + /** + * Bidi flag to detect paragraph direction via heuristics, defaulting to + * LTR. + * + * @hide + */ + public static final int BIDI_DEFAULT_LTR = 0x2; + + /** + * Bidi flag to detect paragraph direction via heuristics, defaulting to + * RTL. + * + * @hide + */ + public static final int BIDI_DEFAULT_RTL = 0x3; + + /** + * Bidi flag to override direction to all LTR (ignore bidi). + * + * @hide + */ + public static final int BIDI_FORCE_LTR = 0x4; + + /** + * Bidi flag to override direction to all RTL (ignore bidi). + * + * @hide + */ + public static final int BIDI_FORCE_RTL = 0x5; + + /** + * Maximum Bidi flag value. + * @hide + */ + private static final int BIDI_MAX_FLAG_VALUE = BIDI_FORCE_RTL; + + /** + * Mask for bidi flags. + * @hide + */ + private static final int BIDI_FLAG_MASK = 0x7; + + /** + * Flag for getTextRunAdvances indicating left-to-right run direction. + * @hide + */ + public static final int DIRECTION_LTR = 0; + + /** + * Flag for getTextRunAdvances indicating right-to-left run direction. + * @hide + */ + public static final int DIRECTION_RTL = 1; + + /** + * Option for getTextRunCursor to compute the valid cursor after + * offset or the limit of the context, whichever is less. + * @hide + */ + public static final int CURSOR_AFTER = 0; + + /** + * Option for getTextRunCursor to compute the valid cursor at or after + * the offset or the limit of the context, whichever is less. + * @hide + */ + public static final int CURSOR_AT_OR_AFTER = 1; + + /** + * Option for getTextRunCursor to compute the valid cursor before + * offset or the start of the context, whichever is greater. + * @hide + */ + public static final int CURSOR_BEFORE = 2; + + /** + * Option for getTextRunCursor to compute the valid cursor at or before + * offset or the start of the context, whichever is greater. + * @hide + */ + public static final int CURSOR_AT_OR_BEFORE = 3; + + /** + * Option for getTextRunCursor to return offset if the cursor at offset + * is valid, or -1 if it isn't. + * @hide + */ + public static final int CURSOR_AT = 4; + + /** + * Maximum cursor option value. + */ + private static final int CURSOR_OPT_MAX_VALUE = CURSOR_AT; + + /** + * The Style specifies if the primitive being drawn is filled, stroked, or + * both (in the same color). The default is FILL. */ public enum Style { /** @@ -93,7 +231,9 @@ public class Paint { /** * Geometry and text drawn with this style will be both filled and * stroked at the same time, respecting the stroke-related fields on - * the paint. + * the paint. This mode can give unexpected results if the geometry + * is oriented counter-clockwise. This restriction does not apply to + * either FILL or STROKE. */ FILL_AND_STROKE (2); @@ -208,6 +348,7 @@ public class Paint { mHasCompatScaling = paint.mHasCompatScaling; mCompatScaling = paint.mCompatScaling; mInvCompatScaling = paint.mInvCompatScaling; + mBidiFlags = paint.mBidiFlags; } /** Restores the paint to its default settings. */ @@ -216,6 +357,7 @@ public class Paint { setFlags(DEFAULT_PAINT_FLAGS); mHasCompatScaling = false; mCompatScaling = mInvCompatScaling = 1; + mBidiFlags = BIDI_DEFAULT_LTR; } /** @@ -238,6 +380,7 @@ public class Paint { mHasCompatScaling = src.mHasCompatScaling; mCompatScaling = src.mCompatScaling; mInvCompatScaling = src.mInvCompatScaling; + mBidiFlags = src.mBidiFlags; } } @@ -252,10 +395,33 @@ public class Paint { mInvCompatScaling = 1.0f/factor; } } - + + /** + * Return the bidi flags on the paint. + * + * @return the bidi flags on the paint + * @hide + */ + public int getBidiFlags() { + return mBidiFlags; + } + + /** + * Set the bidi flags on the paint. + * @hide + */ + public void setBidiFlags(int flags) { + // only flag value is the 3-bit BIDI control setting + flags &= BIDI_FLAG_MASK; + if (flags > BIDI_MAX_FLAG_VALUE) { + throw new IllegalArgumentException("unknown bidi flag: " + flags); + } + mBidiFlags = flags; + } + /** * Return the paint's flags. Use the Flag enum to test flag values. - * + * * @return the paint's flags (see enums ending in _Flag for bit masks) */ public native int getFlags(); @@ -787,18 +953,27 @@ public class Paint { } /** - * Temporary API to expose layer drawing. This draws a shadow layer below - * the main layer, with the specified offset and color, and blur radius. - * If radius is 0, then the shadow layer is removed. + * This draws a shadow layer below the main layer, with the specified + * offset and color, and blur radius. If radius is 0, then the shadow + * layer is removed. */ - public native void setShadowLayer(float radius, float dx, float dy, - int color); + public void setShadowLayer(float radius, float dx, float dy, int color) { + hasShadow = radius > 0.0f; + shadowRadius = radius; + shadowDx = dx; + shadowDy = dy; + shadowColor = color; + nSetShadowLayer(radius, dx, dy, color); + } + + private native void nSetShadowLayer(float radius, float dx, float dy, int color); /** - * Temporary API to clear the shadow layer. + * Clear the shadow layer. */ public void clearShadowLayer() { - setShadowLayer(0, 0, 0, 0); + hasShadow = false; + nSetShadowLayer(0, 0, 0, 0); } /** @@ -1232,10 +1407,10 @@ public class Paint { } char[] buf = TemporaryBuffer.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - int result = getTextWidths(buf, 0, end - start, widths); + TextUtils.getChars(text, start, end, buf, 0); + int result = getTextWidths(buf, 0, end - start, widths); TemporaryBuffer.recycle(buf); - return result; + return result; } /** @@ -1281,6 +1456,284 @@ public class Paint { return getTextWidths(text, 0, text.length(), widths); } + /** + * Convenience overload that takes a char array instead of a + * String. + * + * @see #getTextRunAdvances(String, int, int, int, int, int, float[], int) + * @hide + */ + public float getTextRunAdvances(char[] chars, int index, int count, + int contextIndex, int contextCount, int flags, float[] advances, + int advancesIndex) { + + if ((index | count | contextIndex | contextCount | advancesIndex + | (index - contextIndex) + | ((contextIndex + contextCount) - (index + count)) + | (chars.length - (contextIndex + contextCount)) + | (advances == null ? 0 : + (advances.length - (advancesIndex + count)))) < 0) { + throw new IndexOutOfBoundsException(); + } + if (flags != DIRECTION_LTR && flags != DIRECTION_RTL) { + throw new IllegalArgumentException("unknown flags value: " + flags); + } + + if (!mHasCompatScaling) { + return native_getTextRunAdvances(mNativePaint, chars, index, count, + contextIndex, contextCount, flags, advances, advancesIndex); + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); + float res = native_getTextRunAdvances(mNativePaint, chars, index, count, + contextIndex, contextCount, flags, advances, advancesIndex); + setTextSize(oldSize); + + if (advances != null) { + for (int i = advancesIndex, e = i + count; i < e; i++) { + advances[i] *= mInvCompatScaling; + } + } + return res * mInvCompatScaling; // assume errors are not significant + } + + /** + * Convenience overload that takes a CharSequence instead of a + * String. + * + * @see #getTextRunAdvances(String, int, int, int, int, int, float[], int) + * @hide + */ + public float getTextRunAdvances(CharSequence text, int start, int end, + int contextStart, int contextEnd, int flags, float[] advances, + int advancesIndex) { + + if (text instanceof String) { + return getTextRunAdvances((String) text, start, end, + contextStart, contextEnd, flags, advances, advancesIndex); + } + if (text instanceof SpannedString || + text instanceof SpannableString) { + return getTextRunAdvances(text.toString(), start, end, + contextStart, contextEnd, flags, advances, advancesIndex); + } + if (text instanceof GraphicsOperations) { + return ((GraphicsOperations) text).getTextRunAdvances(start, end, + contextStart, contextEnd, flags, advances, advancesIndex, this); + } + + int contextLen = contextEnd - contextStart; + int len = end - start; + char[] buf = TemporaryBuffer.obtain(contextLen); + TextUtils.getChars(text, start, end, buf, 0); + float result = getTextRunAdvances(buf, start - contextStart, len, + 0, contextLen, flags, advances, advancesIndex); + TemporaryBuffer.recycle(buf); + return result; + } + + /** + * Returns the total advance width for the characters in the run + * between start and end, and if advances is not null, the advance + * assigned to each of these characters (java chars). + * + *

    The trailing surrogate in a valid surrogate pair is assigned + * an advance of 0. Thus the number of returned advances is + * always equal to count, not to the number of unicode codepoints + * represented by the run. + * + *

    In the case of conjuncts or combining marks, the total + * advance is assigned to the first logical character, and the + * following characters are assigned an advance of 0. + * + *

    This generates the sum of the advances of glyphs for + * characters in a reordered cluster as the width of the first + * logical character in the cluster, and 0 for the widths of all + * other characters in the cluster. In effect, such clusters are + * treated like conjuncts. + * + *

    The shaping bounds limit the amount of context available + * outside start and end that can be used for shaping analysis. + * These bounds typically reflect changes in bidi level or font + * metrics across which shaping does not occur. + * + * @param text the text to measure + * @param start the index of the first character to measure + * @param end the index past the last character to measure + * @param contextStart the index of the first character to use for shaping context, + * must be <= start + * @param contextEnd the index past the last character to use for shaping context, + * must be >= end + * @param flags the flags to control the advances, either {@link #DIRECTION_LTR} + * or {@link #DIRECTION_RTL} + * @param advances array to receive the advances, must have room for all advances, + * can be null if only total advance is needed + * @param advancesIndex the position in advances at which to put the + * advance corresponding to the character at start + * @return the total advance + * + * @hide + */ + public float getTextRunAdvances(String text, int start, int end, int contextStart, + int contextEnd, int flags, float[] advances, int advancesIndex) { + + if ((start | end | contextStart | contextEnd | advancesIndex | (end - start) + | (start - contextStart) | (contextEnd - end) + | (text.length() - contextEnd) + | (advances == null ? 0 : + (advances.length - advancesIndex - (end - start)))) < 0) { + throw new IndexOutOfBoundsException(); + } + if (flags != DIRECTION_LTR && flags != DIRECTION_RTL) { + throw new IllegalArgumentException("unknown flags value: " + flags); + } + + if (!mHasCompatScaling) { + return native_getTextRunAdvances(mNativePaint, text, start, end, + contextStart, contextEnd, flags, advances, advancesIndex); + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); + float totalAdvance = native_getTextRunAdvances(mNativePaint, text, start, end, + contextStart, contextEnd, flags, advances, advancesIndex); + setTextSize(oldSize); + + if (advances != null) { + for (int i = advancesIndex, e = i + (end - start); i < e; i++) { + advances[i] *= mInvCompatScaling; + } + } + return totalAdvance * mInvCompatScaling; // assume errors are insignificant + } + + /** + * Returns the next cursor position in the run. This avoids placing the + * cursor between surrogates, between characters that form conjuncts, + * between base characters and combining marks, or within a reordering + * cluster. + * + *

    ContextStart and offset are relative to the start of text. + * The context is the shaping context for cursor movement, generally + * the bounds of the metric span enclosing the cursor in the direction of + * movement. + * + *

    If cursorOpt is {@link #CURSOR_AT} and the offset is not a valid + * cursor position, this returns -1. Otherwise this will never return a + * value before contextStart or after contextStart + contextLength. + * + * @param text the text + * @param contextStart the start of the context + * @param contextLength the length of the context + * @param flags either {@link #DIRECTION_RTL} or {@link #DIRECTION_LTR} + * @param offset the cursor position to move from + * @param cursorOpt how to move the cursor, one of {@link #CURSOR_AFTER}, + * {@link #CURSOR_AT_OR_AFTER}, {@link #CURSOR_BEFORE}, + * {@link #CURSOR_AT_OR_BEFORE}, or {@link #CURSOR_AT} + * @return the offset of the next position, or -1 + * @hide + */ + public int getTextRunCursor(char[] text, int contextStart, int contextLength, + int flags, int offset, int cursorOpt) { + int contextEnd = contextStart + contextLength; + if (((contextStart | contextEnd | offset | (contextEnd - contextStart) + | (offset - contextStart) | (contextEnd - offset) + | (text.length - contextEnd) | cursorOpt) < 0) + || cursorOpt > CURSOR_OPT_MAX_VALUE) { + throw new IndexOutOfBoundsException(); + } + + return native_getTextRunCursor(mNativePaint, text, + contextStart, contextLength, flags, offset, cursorOpt); + } + + /** + * Returns the next cursor position in the run. This avoids placing the + * cursor between surrogates, between characters that form conjuncts, + * between base characters and combining marks, or within a reordering + * cluster. + * + *

    ContextStart, contextEnd, and offset are relative to the start of + * text. The context is the shaping context for cursor movement, generally + * the bounds of the metric span enclosing the cursor in the direction of + * movement. + * + *

    If cursorOpt is {@link #CURSOR_AT} and the offset is not a valid + * cursor position, this returns -1. Otherwise this will never return a + * value before contextStart or after contextEnd. + * + * @param text the text + * @param contextStart the start of the context + * @param contextEnd the end of the context + * @param flags either {@link #DIRECTION_RTL} or {@link #DIRECTION_LTR} + * @param offset the cursor position to move from + * @param cursorOpt how to move the cursor, one of {@link #CURSOR_AFTER}, + * {@link #CURSOR_AT_OR_AFTER}, {@link #CURSOR_BEFORE}, + * {@link #CURSOR_AT_OR_BEFORE}, or {@link #CURSOR_AT} + * @return the offset of the next position, or -1 + * @hide + */ + public int getTextRunCursor(CharSequence text, int contextStart, + int contextEnd, int flags, int offset, int cursorOpt) { + + if (text instanceof String || text instanceof SpannedString || + text instanceof SpannableString) { + return getTextRunCursor(text.toString(), contextStart, contextEnd, + flags, offset, cursorOpt); + } + if (text instanceof GraphicsOperations) { + return ((GraphicsOperations) text).getTextRunCursor( + contextStart, contextEnd, flags, offset, cursorOpt, this); + } + + int contextLen = contextEnd - contextStart; + char[] buf = TemporaryBuffer.obtain(contextLen); + TextUtils.getChars(text, contextStart, contextEnd, buf, 0); + int result = getTextRunCursor(buf, 0, contextLen, flags, offset, cursorOpt); + TemporaryBuffer.recycle(buf); + return result; + } + + /** + * Returns the next cursor position in the run. This avoids placing the + * cursor between surrogates, between characters that form conjuncts, + * between base characters and combining marks, or within a reordering + * cluster. + * + *

    ContextStart, contextEnd, and offset are relative to the start of + * text. The context is the shaping context for cursor movement, generally + * the bounds of the metric span enclosing the cursor in the direction of + * movement. + * + *

    If cursorOpt is {@link #CURSOR_AT} and the offset is not a valid + * cursor position, this returns -1. Otherwise this will never return a + * value before contextStart or after contextEnd. + * + * @param text the text + * @param contextStart the start of the context + * @param contextEnd the end of the context + * @param flags either {@link #DIRECTION_RTL} or {@link #DIRECTION_LTR} + * @param offset the cursor position to move from + * @param cursorOpt how to move the cursor, one of {@link #CURSOR_AFTER}, + * {@link #CURSOR_AT_OR_AFTER}, {@link #CURSOR_BEFORE}, + * {@link #CURSOR_AT_OR_BEFORE}, or {@link #CURSOR_AT} + * @return the offset of the next position, or -1 + * @hide + */ + public int getTextRunCursor(String text, int contextStart, int contextEnd, + int flags, int offset, int cursorOpt) { + if (((contextStart | contextEnd | offset | (contextEnd - contextStart) + | (offset - contextStart) | (contextEnd - offset) + | (text.length() - contextEnd) | cursorOpt) < 0) + || cursorOpt > CURSOR_OPT_MAX_VALUE) { + throw new IndexOutOfBoundsException(); + } + + return native_getTextRunCursor(mNativePaint, text, + contextStart, contextEnd, flags, offset, cursorOpt); + } + /** * Return the path (outline) for the specified text. * Note: just like Canvas.drawText, this will respect the Align setting in @@ -1299,7 +1752,8 @@ public class Paint { if ((index | count) < 0 || index + count > text.length) { throw new ArrayIndexOutOfBoundsException(); } - native_getTextPath(mNativePaint, text, index, count, x, y, path.ni()); + native_getTextPath(mNativePaint, mBidiFlags, text, index, count, x, y, + path.ni()); } /** @@ -1320,7 +1774,8 @@ public class Paint { if ((start | end | (end - start) | (text.length() - end)) < 0) { throw new IndexOutOfBoundsException(); } - native_getTextPath(mNativePaint, text, start, end, x, y, path.ni()); + native_getTextPath(mNativePaint, mBidiFlags, text, start, end, x, y, + path.ni()); } /** @@ -1404,9 +1859,22 @@ public class Paint { char[] text, int index, int count, float[] widths); private static native int native_getTextWidths(int native_object, String text, int start, int end, float[] widths); - private static native void native_getTextPath(int native_object, + + private static native float native_getTextRunAdvances(int native_object, + char[] text, int index, int count, int contextIndex, int contextCount, + int flags, float[] advances, int advancesIndex); + private static native float native_getTextRunAdvances(int native_object, + String text, int start, int end, int contextStart, int contextEnd, + int flags, float[] advances, int advancesIndex); + + private native int native_getTextRunCursor(int native_object, char[] text, + int contextStart, int contextLength, int flags, int offset, int cursorOpt); + private native int native_getTextRunCursor(int native_object, String text, + int contextStart, int contextEnd, int flags, int offset, int cursorOpt); + + private static native void native_getTextPath(int native_object, int bidiFlags, char[] text, int index, int count, float x, float y, int path); - private static native void native_getTextPath(int native_object, + private static native void native_getTextPath(int native_object, int bidiFlags, String text, int start, int end, float x, float y, int path); private static native void nativeGetStringBounds(int nativePaint, String text, int start, int end, Rect bounds); @@ -1414,4 +1882,3 @@ public class Paint { char[] text, int index, int count, Rect bounds); private static native void finalizer(int nativePaint); } - diff --git a/graphics/java/android/graphics/Path.java b/graphics/java/android/graphics/Path.java index 281823a284f9bc48e449ac3e00621d671558c70e..c3416a01f571783e57352d34d6ff27eeddf3cca3 100644 --- a/graphics/java/android/graphics/Path.java +++ b/graphics/java/android/graphics/Path.java @@ -16,6 +16,8 @@ package android.graphics; +import android.view.HardwareRenderer; + /** * The Path class encapsulates compound (multiple contour) geometric paths * consisting of straight line segments, quadratic curves, and cubic curves. @@ -24,12 +26,27 @@ package android.graphics; * text on a path. */ public class Path { + /** + * @hide + */ + public final int mNativePath; + + /** + * @hide + */ + public boolean isSimplePath = true; + /** + * @hide + */ + public Region rects; + private boolean mDetectSimplePaths; /** * Create an empty path */ public Path() { mNativePath = init1(); + mDetectSimplePaths = HardwareRenderer.isAvailable(); } /** @@ -43,6 +60,7 @@ public class Path { valNative = src.mNativePath; } mNativePath = init2(valNative); + mDetectSimplePaths = HardwareRenderer.isAvailable(); } /** @@ -50,6 +68,10 @@ public class Path { * This does NOT change the fill-type setting. */ public void reset() { + isSimplePath = true; + if (mDetectSimplePaths) { + if (rects != null) rects.setEmpty(); + } native_reset(mNativePath); } @@ -58,6 +80,10 @@ public class Path { * keeps the internal data structure for faster reuse. */ public void rewind() { + isSimplePath = true; + if (mDetectSimplePaths) { + if (rects != null) rects.setEmpty(); + } native_rewind(mNativePath); } @@ -65,6 +91,7 @@ public class Path { */ public void set(Path src) { if (this != src) { + isSimplePath = src.isSimplePath; native_set(mNativePath, src.mNativePath); } } @@ -160,6 +187,7 @@ public class Path { * @param bounds Returns the computed bounds of the path's control points. * @param exact This parameter is no longer used. */ + @SuppressWarnings({"UnusedDeclaration"}) public void computeBounds(RectF bounds, boolean exact) { native_computeBounds(mNativePath, bounds); } @@ -236,6 +264,7 @@ public class Path { * @param y2 The y-coordinate of the end point on a quadratic curve */ public void quadTo(float x1, float y1, float x2, float y2) { + isSimplePath = false; native_quadTo(mNativePath, x1, y1, x2, y2); } @@ -254,6 +283,7 @@ public class Path { * this contour, for the end point of a quadratic curve */ public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { + isSimplePath = false; native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2); } @@ -271,6 +301,7 @@ public class Path { */ public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { + isSimplePath = false; native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3); } @@ -281,6 +312,7 @@ public class Path { */ public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { + isSimplePath = false; native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3); } @@ -299,6 +331,7 @@ public class Path { */ public void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) { + isSimplePath = false; native_arcTo(mNativePath, oval, startAngle, sweepAngle, forceMoveTo); } @@ -314,6 +347,7 @@ public class Path { * @param sweepAngle Sweep angle (in degrees) measured clockwise */ public void arcTo(RectF oval, float startAngle, float sweepAngle) { + isSimplePath = false; native_arcTo(mNativePath, oval, startAngle, sweepAngle, false); } @@ -322,6 +356,7 @@ public class Path { * first point of the contour, a line segment is automatically added. */ public void close() { + isSimplePath = false; native_close(mNativePath); } @@ -351,6 +386,11 @@ public class Path { if (rect == null) { throw new NullPointerException("need rect parameter"); } + if (mDetectSimplePaths) { + if (rects == null) rects = new Region(); + rects.op((int) rect.left, (int) rect.top, (int) rect.right, (int) rect.bottom, + Region.Op.UNION); + } native_addRect(mNativePath, rect, dir.nativeInt); } @@ -363,8 +403,11 @@ public class Path { * @param bottom The bottom of a rectangle to add to the path * @param dir The direction to wind the rectangle's contour */ - public void addRect(float left, float top, float right, float bottom, - Direction dir) { + public void addRect(float left, float top, float right, float bottom, Direction dir) { + if (mDetectSimplePaths) { + if (rects == null) rects = new Region(); + rects.op((int) left, (int) top, (int) right, (int) bottom, Region.Op.UNION); + } native_addRect(mNativePath, left, top, right, bottom, dir.nativeInt); } @@ -378,6 +421,7 @@ public class Path { if (oval == null) { throw new NullPointerException("need oval parameter"); } + isSimplePath = false; native_addOval(mNativePath, oval, dir.nativeInt); } @@ -390,6 +434,7 @@ public class Path { * @param dir The direction to wind the circle's contour */ public void addCircle(float x, float y, float radius, Direction dir) { + isSimplePath = false; native_addCircle(mNativePath, x, y, radius, dir.nativeInt); } @@ -404,6 +449,7 @@ public class Path { if (oval == null) { throw new NullPointerException("need oval parameter"); } + isSimplePath = false; native_addArc(mNativePath, oval, startAngle, sweepAngle); } @@ -419,6 +465,7 @@ public class Path { if (rect == null) { throw new NullPointerException("need rect parameter"); } + isSimplePath = false; native_addRoundRect(mNativePath, rect, rx, ry, dir.nativeInt); } @@ -438,6 +485,7 @@ public class Path { if (radii.length < 8) { throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values"); } + isSimplePath = false; native_addRoundRect(mNativePath, rect, radii, dir.nativeInt); } @@ -448,6 +496,7 @@ public class Path { * @param dx The amount to translate the path in X as it is added */ public void addPath(Path src, float dx, float dy) { + isSimplePath = false; native_addPath(mNativePath, src.mNativePath, dx, dy); } @@ -457,6 +506,7 @@ public class Path { * @param src The path that is appended to the current path */ public void addPath(Path src) { + isSimplePath = false; native_addPath(mNativePath, src.mNativePath); } @@ -466,6 +516,7 @@ public class Path { * @param src The path to add as a new contour */ public void addPath(Path src, Matrix matrix) { + if (!src.isSimplePath) isSimplePath = false; native_addPath(mNativePath, src.mNativePath, matrix.native_instance); } @@ -502,6 +553,7 @@ public class Path { * @param dy The new Y coordinate for the last point */ public void setLastPoint(float dx, float dy) { + isSimplePath = false; native_setLastPoint(mNativePath, dx, dy); } @@ -537,8 +589,8 @@ public class Path { super.finalize(); } } - - /*package*/ final int ni() { + + final int ni() { return mNativePath; } @@ -592,6 +644,4 @@ public class Path { int dst_path); private static native void native_transform(int nPath, int matrix); private static native void finalizer(int nPath); - - private final int mNativePath; } diff --git a/graphics/java/android/graphics/PorterDuff.java b/graphics/java/android/graphics/PorterDuff.java index 39042346d036647ab6624b38d12c6b7cf7c1bfcc..2ef166213d2ee40a2943e27e69f601663fc44af9 100644 --- a/graphics/java/android/graphics/PorterDuff.java +++ b/graphics/java/android/graphics/PorterDuff.java @@ -53,11 +53,18 @@ public class PorterDuff { /** [Sa * Da, Sc * Dc] */ MULTIPLY (14), /** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */ - SCREEN (15); + SCREEN (15), + /** Saturate(S + D) */ + ADD (16), + OVERLAY (17); Mode(int nativeInt) { this.nativeInt = nativeInt; } - final int nativeInt; + + /** + * @hide + */ + public final int nativeInt; } } diff --git a/graphics/java/android/graphics/PorterDuffColorFilter.java b/graphics/java/android/graphics/PorterDuffColorFilter.java index 06724bd95865fce87079ed8c0d2e343cacedb925..b02dab15f999891d9c65aa18d982a255d2c9456e 100644 --- a/graphics/java/android/graphics/PorterDuffColorFilter.java +++ b/graphics/java/android/graphics/PorterDuffColorFilter.java @@ -25,10 +25,10 @@ public class PorterDuffColorFilter extends ColorFilter { * @param mode The porter-duff mode that is applied */ public PorterDuffColorFilter(int srcColor, PorterDuff.Mode mode) { - native_instance = native_CreatePorterDuffFilter(srcColor, - mode.nativeInt); + native_instance = native_CreatePorterDuffFilter(srcColor, mode.nativeInt); + nativeColorFilter = nCreatePorterDuffFilter(srcColor, mode.nativeInt); } - private static native int native_CreatePorterDuffFilter(int srcColor, - int porterDuffMode); + private static native int native_CreatePorterDuffFilter(int srcColor, int porterDuffMode); + private static native int nCreatePorterDuffFilter(int srcColor, int porterDuffMode); } diff --git a/graphics/java/android/graphics/PorterDuffXfermode.java b/graphics/java/android/graphics/PorterDuffXfermode.java index cb127fdaa3fb41f7bdf530825d0a7668eb1c71ac..6ba064cc05efcc8065731f9b8ae1ede2ea7797c3 100644 --- a/graphics/java/android/graphics/PorterDuffXfermode.java +++ b/graphics/java/android/graphics/PorterDuffXfermode.java @@ -17,12 +17,18 @@ package android.graphics; public class PorterDuffXfermode extends Xfermode { + /** + * @hide + */ + public final PorterDuff.Mode mode; + /** * Create an xfermode that uses the specified porter-duff mode. * * @param mode The porter-duff mode that is applied */ public PorterDuffXfermode(PorterDuff.Mode mode) { + this.mode = mode; native_instance = nativeCreateXfermode(mode.nativeInt); } diff --git a/graphics/java/android/graphics/RadialGradient.java b/graphics/java/android/graphics/RadialGradient.java index b4e902dc6ec79cf6098909d5d116bcad2d556538..897762cba7618f9db7e3739b09b8f5ff756ff19c 100644 --- a/graphics/java/android/graphics/RadialGradient.java +++ b/graphics/java/android/graphics/RadialGradient.java @@ -40,6 +40,8 @@ public class RadialGradient extends Shader { throw new IllegalArgumentException("color and position arrays must be of equal length"); } native_instance = nativeCreate1(x, y, radius, colors, positions, tile.nativeInt); + native_shader = nativePostCreate1(native_instance, x, y, radius, colors, positions, + tile.nativeInt); } /** Create a shader that draws a radial gradient given the center and radius. @@ -56,11 +58,18 @@ public class RadialGradient extends Shader { throw new IllegalArgumentException("radius must be > 0"); } native_instance = nativeCreate2(x, y, radius, color0, color1, tile.nativeInt); + native_shader = nativePostCreate2(native_instance, x, y, radius, color0, color1, + tile.nativeInt); } private static native int nativeCreate1(float x, float y, float radius, - int colors[], float positions[], int tileMode); + int colors[], float positions[], int tileMode); private static native int nativeCreate2(float x, float y, float radius, - int color0, int color1, int tileMode); + int color0, int color1, int tileMode); + + private static native int nativePostCreate1(int native_shader, float x, float y, float radius, + int colors[], float positions[], int tileMode); + private static native int nativePostCreate2(int native_shader, float x, float y, float radius, + int color0, int color1, int tileMode); } diff --git a/graphics/java/android/graphics/Region.java b/graphics/java/android/graphics/Region.java index 2b080aaa6be5b8327ae641b97e23b3c81c874dcc..e5408068e64ff67bf430bb282eede7e465c71d98 100644 --- a/graphics/java/android/graphics/Region.java +++ b/graphics/java/android/graphics/Region.java @@ -20,6 +20,10 @@ import android.os.Parcel; import android.os.Parcelable; public class Region implements Parcelable { + /** + * @hide + */ + public final int mNativeRegion; // the native values for these must match up with the enum in SkRegion.h public enum Op { @@ -33,7 +37,11 @@ public class Region implements Parcelable { Op(int nativeInt) { this.nativeInt = nativeInt; } - final int nativeInt; + + /** + * @hide + */ + public final int nativeInt; } /** Create an empty region @@ -325,10 +333,14 @@ public class Region implements Parcelable { } protected void finalize() throws Throwable { - nativeDestructor(mNativeRegion); + try { + nativeDestructor(mNativeRegion); + } finally { + super.finalize(); + } } - /*package*/ Region(int ni) { + Region(int ni) { if (ni == 0) { throw new RuntimeException(); } @@ -341,7 +353,7 @@ public class Region implements Parcelable { this(ni); } - /*package*/ final int ni() { + final int ni() { return mNativeRegion; } @@ -370,6 +382,4 @@ public class Region implements Parcelable { Parcel p); private static native boolean nativeEquals(int native_r1, int native_r2); - - private final int mNativeRegion; } diff --git a/graphics/java/android/graphics/Shader.java b/graphics/java/android/graphics/Shader.java index ae0304e1a0c0760d588eafeb89432b6d4bbaafd6..0400b5c9e4aaae926bf4932feeb291b5f4aaa6f5 100644 --- a/graphics/java/android/graphics/Shader.java +++ b/graphics/java/android/graphics/Shader.java @@ -23,9 +23,16 @@ package android.graphics; * drawn with that paint will get its color(s) from the shader. */ public class Shader { - - // this is set by subclasses, but don't make it public - /* package */ int native_instance; + /** + * This is set by subclasses, but don't make it public. + * + * @hide + */ + public int native_instance; + /** + * @hide + */ + public int native_shader; public enum TileMode { /** @@ -64,17 +71,21 @@ public class Shader { * @param localM The shader's new local matrix, or null to specify identity */ public void setLocalMatrix(Matrix localM) { - nativeSetLocalMatrix(native_instance, - localM != null ? localM.native_instance : 0); + nativeSetLocalMatrix(native_instance, native_shader, + localM == null ? 0 : localM.native_instance); } protected void finalize() throws Throwable { - nativeDestructor(native_instance); + try { + super.finalize(); + } finally { + nativeDestructor(native_instance, native_shader); + } } - private static native void nativeDestructor(int native_shader); + private static native void nativeDestructor(int native_shader, int native_skiaShader); private static native boolean nativeGetLocalMatrix(int native_shader, - int matrix_instance); + int matrix_instance); private static native void nativeSetLocalMatrix(int native_shader, - int matrix_instance); + int native_skiaShader, int matrix_instance); } diff --git a/graphics/java/android/graphics/SweepGradient.java b/graphics/java/android/graphics/SweepGradient.java index 7456993d40173eb3029386029f8c1268d865e386..2afdd4dd583597dad47246789b1a8a1a6329417b 100644 --- a/graphics/java/android/graphics/SweepGradient.java +++ b/graphics/java/android/graphics/SweepGradient.java @@ -42,6 +42,7 @@ public class SweepGradient extends Shader { "color and position arrays must be of equal length"); } native_instance = nativeCreate1(cx, cy, colors, positions); + native_shader = nativePostCreate1(native_instance, cx, cy, colors, positions); } /** @@ -54,11 +55,15 @@ public class SweepGradient extends Shader { */ public SweepGradient(float cx, float cy, int color0, int color1) { native_instance = nativeCreate2(cx, cy, color0, color1); + native_shader = nativePostCreate2(native_instance, cx, cy, color0, color1); } - private static native int nativeCreate1(float x, float y, - int colors[], float positions[]); - private static native int nativeCreate2(float x, float y, - int color0, int color1); + private static native int nativeCreate1(float x, float y, int colors[], float positions[]); + private static native int nativeCreate2(float x, float y, int color0, int color1); + + private static native int nativePostCreate1(int native_shader, float cx, float cy, + int[] colors, float[] positions); + private static native int nativePostCreate2(int native_shader, float cx, float cy, + int color0, int color1); } diff --git a/graphics/java/android/graphics/TemporaryBuffer.java b/graphics/java/android/graphics/TemporaryBuffer.java index 1d7fe011df466ea88b88ae393e68b96ff4645db8..c5b8143dab9e9e156625e1fb7c21d117a2b505f1 100644 --- a/graphics/java/android/graphics/TemporaryBuffer.java +++ b/graphics/java/android/graphics/TemporaryBuffer.java @@ -18,9 +18,11 @@ package android.graphics; import com.android.internal.util.ArrayUtils; -/* package */ class TemporaryBuffer -{ - /* package */ static char[] obtain(int len) { +/** + * @hide + */ +public class TemporaryBuffer { + public static char[] obtain(int len) { char[] buf; synchronized (TemporaryBuffer.class) { @@ -28,15 +30,15 @@ import com.android.internal.util.ArrayUtils; sTemp = null; } - if (buf == null || buf.length < len) + if (buf == null || buf.length < len) { buf = new char[ArrayUtils.idealCharArraySize(len)]; + } return buf; } - /* package */ static void recycle(char[] temp) { - if (temp.length > 1000) - return; + public static void recycle(char[] temp) { + if (temp.length > 1000) return; synchronized (TemporaryBuffer.class) { sTemp = temp; diff --git a/graphics/java/android/graphics/Xfermode.java b/graphics/java/android/graphics/Xfermode.java index 42c410e58b3aa71f45082d2b388007171317b860..2467bdc29bcc376e4e1dc1fb664b91ed2f19d5b6 100644 --- a/graphics/java/android/graphics/Xfermode.java +++ b/graphics/java/android/graphics/Xfermode.java @@ -31,7 +31,11 @@ package android.graphics; public class Xfermode { protected void finalize() throws Throwable { - finalizer(native_instance); + try { + finalizer(native_instance); + } finally { + super.finalize(); + } } private static native void finalizer(int native_instance); diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index 31253215f90830c14daa0b6febb8491f90a6fe19..7b2d9d7a33647323d3f5ae6dc590bb5e303cacd8 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -16,21 +16,30 @@ package android.graphics.drawable; -import java.io.InputStream; -import java.io.IOException; -import java.util.Arrays; - import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.*; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.NinePatch; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.Region; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.StateSet; -import android.util.Xml; import android.util.TypedValue; +import android.util.Xml; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; /** * A Drawable is a general abstraction for "something that can be drawn." Most @@ -645,6 +654,8 @@ public abstract class Drawable { * Calling this method on a mutable Drawable will have no effect. * * @return This drawable. + * @see ConstantState + * @see #getConstantState() */ public Drawable mutate() { return this; @@ -749,6 +760,8 @@ public abstract class Drawable { drawable = new StateListDrawable(); } else if (name.equals("level-list")) { drawable = new LevelListDrawable(); + } else if (name.equals("mipmap")) { + drawable = new MipmapDrawable(); } else if (name.equals("layer-list")) { drawable = new LayerDrawable(); } else if (name.equals("transition")) { @@ -770,7 +783,7 @@ public abstract class Drawable { } else if (name.equals("inset")) { drawable = new InsetDrawable(); } else if (name.equals("bitmap")) { - drawable = new BitmapDrawable(); + drawable = new BitmapDrawable(r); if (r != null) { ((BitmapDrawable) drawable).setTargetDensity(r.getDisplayMetrics()); } @@ -805,6 +818,9 @@ public abstract class Drawable { return null; } + /** + * Inflate this Drawable from an XML resource. + */ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { @@ -813,6 +829,12 @@ public abstract class Drawable { a.recycle(); } + /** + * Inflate a Drawable from an XML resource. + * + * @throws XmlPullParserException + * @throws IOException + */ void inflateWithAttributes(Resources r, XmlPullParser parser, TypedArray attrs, int visibleAttr) throws XmlPullParserException, IOException { @@ -820,12 +842,27 @@ public abstract class Drawable { mVisible = attrs.getBoolean(visibleAttr, mVisible); } + /** + * This abstract class is used by {@link Drawable}s to store shared constant state and data + * between Drawables. {@link BitmapDrawable}s created from the same resource will for instance + * share a unique bitmap stored in their ConstantState. + * + *

    + * {@link #newDrawable(Resources)} can be used as a factory to create new Drawable instances + * from this ConstantState. + *

    + * + * Use {@link Drawable#getConstantState()} to retrieve the ConstantState of a Drawable. Calling + * {@link Drawable#mutate()} on a Drawable should typically create a new ConstantState for that + * Drawable. + */ public static abstract class ConstantState { /** * Create a new drawable without supplying resources the caller * is running in. Note that using this means the density-dependent * drawables (like bitmaps) will not be able to update their target - * density correctly. + * density correctly. One should use {@link #newDrawable(Resources)} + * instead to provide a resource. */ public abstract Drawable newDrawable(); /** @@ -844,6 +881,13 @@ public abstract class Drawable { public abstract int getChangingConfigurations(); } + /** + * Return a {@link ConstantState} instance that holds the shared state of this Drawable. + *q + * @return The ConstantState associated to that Drawable. + * @see ConstantState + * @see Drawable#mutate() + */ public ConstantState getConstantState() { return null; } diff --git a/graphics/java/android/graphics/drawable/DrawableContainer.java b/graphics/java/android/graphics/drawable/DrawableContainer.java index c6f57d4c482ae3ef416a4beee042be739cddaa30..124d90707ecd1ba39c0dcfd73f08704ed38fec4c 100644 --- a/graphics/java/android/graphics/drawable/DrawableContainer.java +++ b/graphics/java/android/graphics/drawable/DrawableContainer.java @@ -17,8 +17,16 @@ package android.graphics.drawable; import android.content.res.Resources; -import android.graphics.*; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +/** + * A helper class that contains several {@link Drawable}s and selects which one to use. + * + * You can subclass it to create your own DrawableContainers or directly use one its child classes. + */ public class DrawableContainer extends Drawable implements Drawable.Callback { /** @@ -196,8 +204,7 @@ public class DrawableContainer extends Drawable implements Drawable.Callback { mDrawableContainerState.getOpacity(); } - public boolean selectDrawable(int idx) - { + public boolean selectDrawable(int idx) { if (idx == mCurIndex) { return false; } @@ -255,6 +262,12 @@ public class DrawableContainer extends Drawable implements Drawable.Callback { return this; } + /** + * A ConstantState that can contain several {@link Drawable}s. + * + * This class was made public to enable testing, and its visibility may change in a future + * release. + */ public abstract static class DrawableContainerState extends ConstantState { final DrawableContainer mOwner; @@ -443,12 +456,12 @@ public class DrawableContainer extends Drawable implements Drawable.Callback { return mConstantMinimumHeight; } - private void computeConstantSize() { + protected void computeConstantSize() { mComputedConstantSize = true; final int N = getChildCount(); final Drawable[] drawables = mDrawables; - mConstantWidth = mConstantHeight = 0; + mConstantWidth = mConstantHeight = -1; mConstantMinimumWidth = mConstantMinimumHeight = 0; for (int i = 0; i < N; i++) { Drawable dr = drawables[i]; diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java index 33ecbea910ed34823e9c5e402bc154f62b67a809..88f6d43c834c1aa73f866b471a6ec1ad0f74bb10 100644 --- a/graphics/java/android/graphics/drawable/GradientDrawable.java +++ b/graphics/java/android/graphics/drawable/GradientDrawable.java @@ -313,18 +313,16 @@ public class GradientDrawable extends Drawable { case RECTANGLE: if (st.mRadiusArray != null) { mPath.reset(); - mPath.addRoundRect(mRect, st.mRadiusArray, - Path.Direction.CW); + mPath.addRoundRect(mRect, st.mRadiusArray, Path.Direction.CW); canvas.drawPath(mPath, mFillPaint); if (haveStroke) { canvas.drawPath(mPath, mStrokePaint); } - } - else { + } else if (st.mRadius > 0.0f) { // since the caller is only giving us 1 value, we will force // it to be square if the rect is too small in one dimension // to show it. If we did nothing, Skia would clamp the rad - // independently along each axis, giving us a thin ellips + // independently along each axis, giving us a thin ellipse // if the rect were very wide but not very tall float rad = st.mRadius; float r = Math.min(mRect.width(), mRect.height()) * 0.5f; @@ -335,6 +333,11 @@ public class GradientDrawable extends Drawable { if (haveStroke) { canvas.drawRoundRect(mRect, rad, rad, mStrokePaint); } + } else { + canvas.drawRect(mRect, mFillPaint); + if (haveStroke) { + canvas.drawRect(mRect, mStrokePaint); + } } break; case OVAL: diff --git a/graphics/java/android/graphics/drawable/LayerDrawable.java b/graphics/java/android/graphics/drawable/LayerDrawable.java index 8047dd4cb08cb52583aa42c3bf81782b0bb9bc4c..501cca9ca76feddca54c64b10157ce6f788d017a 100644 --- a/graphics/java/android/graphics/drawable/LayerDrawable.java +++ b/graphics/java/android/graphics/drawable/LayerDrawable.java @@ -266,6 +266,7 @@ public class LayerDrawable extends Drawable implements Drawable.Callback { */ public boolean setDrawableByLayerId(int id, Drawable drawable) { final ChildDrawable[] layers = mLayerState.mChildren; + drawable.setCallback(this); for (int i = mLayerState.mNum - 1; i >= 0; i--) { if (layers[i].mId == id) { diff --git a/graphics/java/android/graphics/drawable/MipmapDrawable.java b/graphics/java/android/graphics/drawable/MipmapDrawable.java new file mode 100644 index 0000000000000000000000000000000000000000..75fdeed4bda01b31b35ca6cf1b677147e706d1e8 --- /dev/null +++ b/graphics/java/android/graphics/drawable/MipmapDrawable.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics.drawable; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; + +import java.io.IOException; + +/** + * A resource that manages a number of alternate Drawables, and which actually draws the one which + * size matches the most closely the drawing bounds. Providing several pre-scaled version of the + * drawable helps minimizing the aliasing artifacts that can be introduced by the scaling. + * + *

    + * Use {@link #addDrawable(Drawable)} to define the different Drawables that will represent the + * mipmap levels of this MipmapDrawable. The mipmap Drawable that will actually be used when this + * MipmapDrawable is drawn is the one which has the smallest intrinsic height greater or equal than + * the bounds' height. This selection ensures that the best available mipmap level is scaled down to + * draw this MipmapDrawable. + *

    + * + * If the bounds' height is larger than the largest mipmap, the largest mipmap will be scaled up. + * Note that Drawables without intrinsic height (i.e. with a negative value, such as Color) will + * only be used if no other mipmap Drawable are provided. The Drawables' intrinsic heights should + * not be changed after the Drawable has been added to this MipmapDrawable. + * + *

    + * The different mipmaps' parameters (opacity, padding, color filter, gravity...) should typically + * be similar to ensure a continuous visual appearance when the MipmapDrawable is scaled. The aspect + * ratio of the different mipmaps should especially be equal. + *

    + * + * A typical example use of a MipmapDrawable would be for an image which is intended to be scaled at + * various sizes, and for which one wants to provide pre-scaled versions to precisely control its + * appearance. + * + *

    + * The intrinsic size of a MipmapDrawable are inferred from those of the largest mipmap (in terms of + * {@link Drawable#getIntrinsicHeight()}). On the opposite, its minimum + * size is defined by the smallest provided mipmap. + *

    + + * It can be defined in an XML file with the <mipmap> element. + * Each mipmap Drawable is defined in a nested <item>. For example: + *
    + * <mipmap xmlns:android="http://schemas.android.com/apk/res/android">
    + *  <item android:drawable="@drawable/my_image_8" />
    + *  <item android:drawable="@drawable/my_image_32" />
    + *  <item android:drawable="@drawable/my_image_128" />
    + * </mipmap>
    + *
    + *

    + * With this XML saved into the res/drawable/ folder of the project, it can be referenced as + * the drawable for an {@link android.widget.ImageView}. Assuming that the heights of the provided + * drawables are respectively 8, 32 and 128 pixels, the first one will be scaled down when the + * bounds' height is lower or equal than 8 pixels. The second drawable will then be used up to a + * height of 32 pixels and the largest drawable will be used for greater heights. + *

    + * @attr ref android.R.styleable#MipmapDrawableItem_drawable + */ +public class MipmapDrawable extends DrawableContainer { + private final MipmapContainerState mMipmapContainerState; + private boolean mMutated; + + public MipmapDrawable() { + this(null, null); + } + + /** + * Adds a Drawable to the list of available mipmap Drawables. The Drawable actually used when + * this MipmapDrawable is drawn is determined from its bounds. + * + * This method has no effect if drawable is null. + * + * @param drawable The Drawable that will be added to list of available mipmap Drawables. + */ + + public void addDrawable(Drawable drawable) { + if (drawable != null) { + mMipmapContainerState.addDrawable(drawable); + onDrawableAdded(); + } + } + + private void onDrawableAdded() { + // selectDrawable assumes that the container content does not change. + // When a Drawable is added, the same index can correspond to a new Drawable, and since + // selectDrawable has a fast exit case when oldIndex==newIndex, the new drawable could end + // up not being used in place of the previous one if they happen to share the same index. + // This make sure the new computed index can actually replace the previous one. + selectDrawable(-1); + onBoundsChange(getBounds()); + } + + // overrides from Drawable + + @Override + protected void onBoundsChange(Rect bounds) { + final int index = mMipmapContainerState.indexForBounds(bounds); + + // Will call invalidateSelf() if needed + selectDrawable(index); + + super.onBoundsChange(bounds); + } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + + super.inflate(r, parser, attrs); + + int type; + + final int innerDepth = parser.getDepth() + 1; + int depth; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth + || type != XmlPullParser.END_TAG)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + if (depth > innerDepth || !parser.getName().equals("item")) { + continue; + } + + TypedArray a = r.obtainAttributes(attrs, + com.android.internal.R.styleable.MipmapDrawableItem); + + int drawableRes = a.getResourceId( + com.android.internal.R.styleable.MipmapDrawableItem_drawable, 0); + + a.recycle(); + + Drawable dr; + if (drawableRes != 0) { + dr = r.getDrawable(drawableRes); + } else { + while ((type = parser.next()) == XmlPullParser.TEXT) { + } + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException( + parser.getPositionDescription() + + ": tag requires a 'drawable' attribute or " + + "child tag defining a drawable"); + } + dr = Drawable.createFromXmlInner(r, parser, attrs); + } + + mMipmapContainerState.addDrawable(dr); + } + + onDrawableAdded(); + } + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mMipmapContainerState.mMipmapHeights = mMipmapContainerState.mMipmapHeights.clone(); + mMutated = true; + } + return this; + } + + private final static class MipmapContainerState extends DrawableContainerState { + private int[] mMipmapHeights; + + MipmapContainerState(MipmapContainerState orig, MipmapDrawable owner, Resources res) { + super(orig, owner, res); + + if (orig != null) { + mMipmapHeights = orig.mMipmapHeights; + } else { + mMipmapHeights = new int[getChildren().length]; + } + + // Change the default value + setConstantSize(true); + } + + /** + * Returns the index of the child mipmap drawable that will best fit the provided bounds. + * This index is determined by comparing bounds' height and children intrinsic heights. + * The returned mipmap index is the smallest mipmap which height is greater or equal than + * the bounds' height. If the bounds' height is larger than the largest mipmap, the largest + * mipmap index is returned. + * + * @param bounds The bounds of the MipMapDrawable. + * @return The index of the child Drawable that will best fit these bounds, or -1 if there + * are no children mipmaps. + */ + public int indexForBounds(Rect bounds) { + final int boundsHeight = bounds.height(); + final int N = getChildCount(); + for (int i = 0; i < N; i++) { + if (boundsHeight <= mMipmapHeights[i]) { + return i; + } + } + + // No mipmap larger than bounds found. Use largest one which will be scaled up. + if (N > 0) { + return N - 1; + } + // No Drawable mipmap at all + return -1; + } + + /** + * Adds a Drawable to the list of available mipmap Drawables. This list can be retrieved + * using {@link DrawableContainer.DrawableContainerState#getChildren()} and this method + * ensures that it is always sorted by increasing {@link Drawable#getIntrinsicHeight()}. + * + * @param drawable The Drawable that will be added to children list + */ + public void addDrawable(Drawable drawable) { + // Insert drawable in last position, correctly resetting cached values and + // especially mComputedConstantSize + int pos = addChild(drawable); + + // Bubble sort the last drawable to restore the sort by intrinsic height + final int drawableHeight = drawable.getIntrinsicHeight(); + + while (pos > 0) { + final Drawable previousDrawable = mDrawables[pos-1]; + final int previousIntrinsicHeight = previousDrawable.getIntrinsicHeight(); + + if (drawableHeight < previousIntrinsicHeight) { + mDrawables[pos] = previousDrawable; + mMipmapHeights[pos] = previousIntrinsicHeight; + + mDrawables[pos-1] = drawable; + mMipmapHeights[pos-1] = drawableHeight; + pos--; + } else { + break; + } + } + } + + /** + * Intrinsic sizes are those of the largest available mipmap. + * Minimum sizes are those of the smallest available mipmap. + */ + @Override + protected void computeConstantSize() { + final int N = getChildCount(); + if (N > 0) { + final Drawable smallestDrawable = mDrawables[0]; + mConstantMinimumWidth = smallestDrawable.getMinimumWidth(); + mConstantMinimumHeight = smallestDrawable.getMinimumHeight(); + + final Drawable largestDrawable = mDrawables[N-1]; + mConstantWidth = largestDrawable.getIntrinsicWidth(); + mConstantHeight = largestDrawable.getIntrinsicHeight(); + } else { + mConstantWidth = mConstantHeight = -1; + mConstantMinimumWidth = mConstantMinimumHeight = 0; + } + mComputedConstantSize = true; + } + + @Override + public Drawable newDrawable() { + return new MipmapDrawable(this, null); + } + + @Override + public Drawable newDrawable(Resources res) { + return new MipmapDrawable(this, res); + } + + @Override + public void growArray(int oldSize, int newSize) { + super.growArray(oldSize, newSize); + int[] newInts = new int[newSize]; + System.arraycopy(mMipmapHeights, 0, newInts, 0, oldSize); + mMipmapHeights = newInts; + } + } + + private MipmapDrawable(MipmapContainerState state, Resources res) { + MipmapContainerState as = new MipmapContainerState(state, this, res); + mMipmapContainerState = as; + setConstantState(as); + onDrawableAdded(); + } +} diff --git a/graphics/java/android/graphics/drawable/NinePatchDrawable.java b/graphics/java/android/graphics/drawable/NinePatchDrawable.java index bad94fb771d015874b797ec93d4445ebd5cd8a81..9a98d53953093a0ee5998a74535e4929be01bc08 100644 --- a/graphics/java/android/graphics/drawable/NinePatchDrawable.java +++ b/graphics/java/android/graphics/drawable/NinePatchDrawable.java @@ -175,16 +175,9 @@ public class NinePatchDrawable extends Drawable { dest.bottom = Bitmap.scaleFromDensity(src.bottom, sdensity, tdensity); } } - - // overrides @Override public void draw(Canvas canvas) { - if (false) { - float[] pts = new float[2]; - canvas.getMatrix().mapPoints(pts); - Log.v("9patch", "Drawing 9-patch @ " + pts[0] + "," + pts[1] + ": " + getBounds()); - } mNinePatch.draw(canvas, getBounds(), mPaint); } diff --git a/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java b/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java index f4cf15c7469884dd1ad7b42cab969a2451d9ad2b..b469d2a094b2f789e10b454e74507523a1718dad 100644 --- a/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java +++ b/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java @@ -57,13 +57,11 @@ public class RoundRectShape extends RectShape { */ public RoundRectShape(float[] outerRadii, RectF inset, float[] innerRadii) { - if (outerRadii.length < 8) { - throw new ArrayIndexOutOfBoundsException( - "outer radii must have >= 8 values"); + if (outerRadii != null && outerRadii.length < 8) { + throw new ArrayIndexOutOfBoundsException("outer radii must have >= 8 values"); } if (innerRadii != null && innerRadii.length < 8) { - throw new ArrayIndexOutOfBoundsException( - "inner radii must have >= 8 values"); + throw new ArrayIndexOutOfBoundsException("inner radii must have >= 8 values"); } mOuterRadii = outerRadii; mInset = inset; @@ -97,8 +95,7 @@ public class RoundRectShape extends RectShape { r.right - mInset.right, r.bottom - mInset.bottom); if (mInnerRect.width() < w && mInnerRect.height() < h) { if (mInnerRadii != null) { - mPath.addRoundRect(mInnerRect, mInnerRadii, - Path.Direction.CCW); + mPath.addRoundRect(mInnerRect, mInnerRadii, Path.Direction.CCW); } else { mPath.addRect(mInnerRect, Path.Direction.CCW); } @@ -109,8 +106,8 @@ public class RoundRectShape extends RectShape { @Override public RoundRectShape clone() throws CloneNotSupportedException { RoundRectShape shape = (RoundRectShape) super.clone(); - shape.mOuterRadii = mOuterRadii.clone(); - shape.mInnerRadii = mInnerRadii.clone(); + shape.mOuterRadii = mOuterRadii != null ? mOuterRadii.clone() : null; + shape.mInnerRadii = mInnerRadii != null ? mInnerRadii.clone() : null; shape.mInset = new RectF(mInset); shape.mInnerRect = new RectF(mInnerRect); shape.mPath = new Path(mPath); diff --git a/graphics/java/android/renderscript/Allocation.java b/graphics/java/android/renderscript/Allocation.java index b27c7f502e96c324238ab021e3d6bdf6627551a1..985d700147dc8008a20bff49f8d6766301492f44 100644 --- a/graphics/java/android/renderscript/Allocation.java +++ b/graphics/java/android/renderscript/Allocation.java @@ -35,11 +35,25 @@ public class Allocation extends BaseObj { Bitmap mBitmap; Allocation(int id, RenderScript rs, Type t) { - super(rs); - mID = id; + super(id, rs); mType = t; } + Allocation(int id, RenderScript rs) { + super(id, rs); + } + + @Override + void updateFromNative() { + mRS.validate(); + mName = mRS.nGetName(mID); + int typeID = mRS.nAllocationGetType(mID); + if(typeID != 0) { + mType = new Type(typeID, mRS); + mType.updateFromNative(); + } + } + public Type getType() { return mType; } @@ -76,10 +90,50 @@ public class Allocation extends BaseObj { subData1D(0, mType.getElementCount(), d); } + public void subData(int xoff, FieldPacker fp) { + int eSize = mType.mElement.getSizeBytes(); + final byte[] data = fp.getData(); + + int count = data.length / eSize; + if ((eSize * count) != data.length) { + throw new IllegalArgumentException("Field packer length " + data.length + + " not divisible by element size " + eSize + "."); + } + data1DChecks(xoff, count, data.length, data.length); + mRS.nAllocationSubData1D(mID, xoff, count, data, data.length); + } + + + public void subElementData(int xoff, int component_number, FieldPacker fp) { + if (component_number >= mType.mElement.mElements.length) { + throw new IllegalArgumentException("Component_number " + component_number + " out of range."); + } + if(xoff < 0) { + throw new IllegalArgumentException("Offset must be >= 0."); + } + + final byte[] data = fp.getData(); + int eSize = mType.mElement.mElements[component_number].getSizeBytes(); + + if (data.length != eSize) { + throw new IllegalArgumentException("Field packer sizelength " + data.length + + " does not match component size " + eSize + "."); + } + + mRS.nAllocationSubElementData1D(mID, xoff, component_number, data, data.length); + } + private void data1DChecks(int off, int count, int len, int dataSize) { mRS.validate(); - if((off < 0) || (count < 1) || ((off + count) > mType.getElementCount())) { - throw new IllegalArgumentException("Offset or Count out of bounds."); + if(off < 0) { + throw new IllegalArgumentException("Offset must be >= 0."); + } + if(count < 1) { + throw new IllegalArgumentException("Count must be >= 1."); + } + if((off + count) > mType.getElementCount()) { + throw new IllegalArgumentException("Overflow, Available count " + mType.getElementCount() + + ", got " + count + " at offset " + off + "."); } if((len) < dataSize) { throw new IllegalArgumentException("Array too small for allocation type."); @@ -108,7 +162,6 @@ public class Allocation extends BaseObj { } - public void subData2D(int xoff, int yoff, int w, int h, int[] d) { mRS.validate(); mRS.nAllocationSubData2D(mID, xoff, yoff, w, h, d, d.length * 4); @@ -129,25 +182,10 @@ public class Allocation extends BaseObj { mRS.nAllocationRead(mID, d); } - public void data(Object o) { - mRS.validate(); - mRS.nAllocationSubDataFromObject(mID, mType, 0, o); - } - - public void read(Object o) { - mRS.validate(); - mRS.nAllocationSubReadFromObject(mID, mType, 0, o); - } - - public void subData(int offset, Object o) { - mRS.validate(); - mRS.nAllocationSubDataFromObject(mID, mType, offset, o); - } public class Adapter1D extends BaseObj { Adapter1D(int id, RenderScript rs) { - super(rs); - mID = id; + super(id, rs); } public void setConstraint(Dimension dim, int value) { @@ -189,8 +227,7 @@ public class Allocation extends BaseObj { public class Adapter2D extends BaseObj { Adapter2D(int id, RenderScript rs) { - super(rs); - mID = id; + super(id, rs); } public void setConstraint(Dimension dim, int value) { @@ -377,6 +414,21 @@ public class Allocation extends BaseObj { Bitmap b = BitmapFactory.decodeResource(res, id, mBitmapOptions); return createFromBitmapBoxed(rs, b, dstFmt, genMips); } + + static public Allocation createFromString(RenderScript rs, String str) + throws IllegalArgumentException { + byte[] allocArray = null; + try { + allocArray = str.getBytes("UTF-8"); + Allocation alloc = Allocation.createSized(rs, Element.U8(rs), allocArray.length); + alloc.data(allocArray); + return alloc; + } + catch (Exception e) { + Log.e("rs", "could not convert string to utf-8"); + } + return null; + } } diff --git a/graphics/java/android/renderscript/BaseObj.java b/graphics/java/android/renderscript/BaseObj.java index 002fc788326a3315307f9b1a7eaf441f14fb2ad7..715e3fbcbdc7d5fd5aa9db869f1b8ba45b24c846 100644 --- a/graphics/java/android/renderscript/BaseObj.java +++ b/graphics/java/android/renderscript/BaseObj.java @@ -24,14 +24,17 @@ import android.util.Log; **/ class BaseObj { - BaseObj(RenderScript rs) { + BaseObj(int id, RenderScript rs) { rs.validate(); mRS = rs; - mID = 0; + mID = id; mDestroyed = false; } public int getID() { + if (mDestroyed) { + throw new IllegalStateException("using a destroyed object."); + } return mID; } @@ -62,7 +65,7 @@ class BaseObj { { if (!mDestroyed) { if(mID != 0 && mRS.isAlive()) { - mRS.nObjDestroyOOB(mID); + mRS.nObjDestroy(mID); } mRS = null; mID = 0; @@ -81,5 +84,10 @@ class BaseObj { mRS.nObjDestroy(mID); } + // If an object came from an a3d file, java fields need to be + // created with objects from the native layer + void updateFromNative() { + } + } diff --git a/graphics/java/android/renderscript/Vector2f.java b/graphics/java/android/renderscript/Byte2.java similarity index 89% rename from graphics/java/android/renderscript/Vector2f.java rename to graphics/java/android/renderscript/Byte2.java index 567d57fad2c430b5a9a2dd852010892e7aa3abbb..95cf88cf300cdc4c10f06426c1e185b3f676f84c 100644 --- a/graphics/java/android/renderscript/Vector2f.java +++ b/graphics/java/android/renderscript/Byte2.java @@ -24,12 +24,12 @@ import android.util.Log; * @hide * **/ -public class Vector2f { - public Vector2f() { +public class Byte2 { + public Byte2() { } - public float x; - public float y; + public byte x; + public byte y; } diff --git a/graphics/java/android/renderscript/Vector3f.java b/graphics/java/android/renderscript/Byte3.java similarity index 87% rename from graphics/java/android/renderscript/Vector3f.java rename to graphics/java/android/renderscript/Byte3.java index f2842f3b4909b9ffdec0084f2d97f7cf601f59cd..a6c0ca9383e194f20692ec80fd578774938567bf 100644 --- a/graphics/java/android/renderscript/Vector3f.java +++ b/graphics/java/android/renderscript/Byte3.java @@ -24,13 +24,13 @@ import android.util.Log; * @hide * **/ -public class Vector3f { - public Vector3f() { +public class Byte3 { + public Byte3() { } - public float x; - public float y; - public float z; + public byte x; + public byte y; + public byte z; } diff --git a/graphics/java/android/renderscript/Vector4f.java b/graphics/java/android/renderscript/Byte4.java similarity index 85% rename from graphics/java/android/renderscript/Vector4f.java rename to graphics/java/android/renderscript/Byte4.java index fabd9593db6b5720c9ed8514a7d40394297657c9..a5bfc611c0e15f1131ef9dbeddfef525421f2ee5 100644 --- a/graphics/java/android/renderscript/Vector4f.java +++ b/graphics/java/android/renderscript/Byte4.java @@ -24,14 +24,14 @@ import android.util.Log; * @hide * **/ -public class Vector4f { - public Vector4f() { +public class Byte4 { + public Byte4() { } - public float x; - public float y; - public float z; - public float w; + public byte x; + public byte y; + public byte z; + public byte w; } diff --git a/graphics/java/android/renderscript/Element.java b/graphics/java/android/renderscript/Element.java index 10ef05a378b11647a43d3d4e7c9581d30c91f5bf..05b2d6019f32f2a792ac6d3774d570e51f51bdbd 100644 --- a/graphics/java/android/renderscript/Element.java +++ b/graphics/java/android/renderscript/Element.java @@ -17,6 +17,7 @@ package android.renderscript; import java.lang.reflect.Field; +import android.util.Log; /** * @hide @@ -26,6 +27,7 @@ public class Element extends BaseObj { int mSize; Element[] mElements; String[] mElementNames; + int[] mArraySizes; DataType mType; DataKind mKind; @@ -47,20 +49,26 @@ public class Element extends BaseObj { UNSIGNED_32 (10, 4), //UNSIGNED_64 (11, 8), - UNSIGNED_5_6_5 (12, 2), - UNSIGNED_5_5_5_1 (13, 2), - UNSIGNED_4_4_4_4 (14, 2), - - RS_ELEMENT (15, 4), - RS_TYPE (16, 4), - RS_ALLOCATION (17, 4), - RS_SAMPLER (18, 4), - RS_SCRIPT (19, 4), - RS_MESH (20, 4), - RS_PROGRAM_FRAGMENT (21, 4), - RS_PROGRAM_VERTEX (22, 4), - RS_PROGRAM_RASTER (23, 4), - RS_PROGRAM_STORE (24, 4); + BOOLEAN(12, 1), + + UNSIGNED_5_6_5 (13, 2), + UNSIGNED_5_5_5_1 (14, 2), + UNSIGNED_4_4_4_4 (15, 2), + + MATRIX_4X4 (16, 64), + MATRIX_3X3 (17, 36), + MATRIX_2X2 (18, 16), + + RS_ELEMENT (1000, 4), + RS_TYPE (1001, 4), + RS_ALLOCATION (1002, 4), + RS_SAMPLER (1003, 4), + RS_SCRIPT (1004, 4), + RS_MESH (1005, 4), + RS_PROGRAM_FRAGMENT (1006, 4), + RS_PROGRAM_VERTEX (1007, 4), + RS_PROGRAM_RASTER (1008, 4), + RS_PROGRAM_STORE (1009, 4); int mID; int mSize; @@ -72,12 +80,6 @@ public class Element extends BaseObj { public enum DataKind { USER (0), - COLOR (1), - POSITION (2), - TEXTURE (3), - NORMAL (4), - INDEX (5), - POINT_SIZE(6), PIXEL_L (7), PIXEL_A (8), @@ -91,41 +93,133 @@ public class Element extends BaseObj { } } - public static Element USER_U8(RenderScript rs) { - if(rs.mElement_USER_U8 == null) { - rs.mElement_USER_U8 = createUser(rs, DataType.UNSIGNED_8); + public static Element BOOLEAN(RenderScript rs) { + if(rs.mElement_BOOLEAN == null) { + rs.mElement_BOOLEAN = createUser(rs, DataType.BOOLEAN); + } + return rs.mElement_BOOLEAN; + } + + public static Element U8(RenderScript rs) { + if(rs.mElement_U8 == null) { + rs.mElement_U8 = createUser(rs, DataType.UNSIGNED_8); + } + return rs.mElement_U8; + } + + public static Element I8(RenderScript rs) { + if(rs.mElement_I8 == null) { + rs.mElement_I8 = createUser(rs, DataType.SIGNED_8); + } + return rs.mElement_I8; + } + + public static Element U16(RenderScript rs) { + if(rs.mElement_U16 == null) { + rs.mElement_U16 = createUser(rs, DataType.UNSIGNED_16); + } + return rs.mElement_U16; + } + + public static Element I16(RenderScript rs) { + if(rs.mElement_I16 == null) { + rs.mElement_I16 = createUser(rs, DataType.SIGNED_16); } - return rs.mElement_USER_U8; + return rs.mElement_I16; } - public static Element USER_I8(RenderScript rs) { - if(rs.mElement_USER_I8 == null) { - rs.mElement_USER_I8 = createUser(rs, DataType.SIGNED_8); + public static Element U32(RenderScript rs) { + if(rs.mElement_U32 == null) { + rs.mElement_U32 = createUser(rs, DataType.UNSIGNED_32); } - return rs.mElement_USER_I8; + return rs.mElement_U32; } - public static Element USER_U32(RenderScript rs) { - if(rs.mElement_USER_U32 == null) { - rs.mElement_USER_U32 = createUser(rs, DataType.UNSIGNED_32); + public static Element I32(RenderScript rs) { + if(rs.mElement_I32 == null) { + rs.mElement_I32 = createUser(rs, DataType.SIGNED_32); } - return rs.mElement_USER_U32; + return rs.mElement_I32; } - public static Element USER_I32(RenderScript rs) { - if(rs.mElement_USER_I32 == null) { - rs.mElement_USER_I32 = createUser(rs, DataType.SIGNED_32); + public static Element F32(RenderScript rs) { + if(rs.mElement_F32 == null) { + rs.mElement_F32 = createUser(rs, DataType.FLOAT_32); } - return rs.mElement_USER_I32; + return rs.mElement_F32; } - public static Element USER_F32(RenderScript rs) { - if(rs.mElement_USER_F32 == null) { - rs.mElement_USER_F32 = createUser(rs, DataType.FLOAT_32); + public static Element ELEMENT(RenderScript rs) { + if(rs.mElement_ELEMENT == null) { + rs.mElement_ELEMENT = createUser(rs, DataType.RS_ELEMENT); } - return rs.mElement_USER_F32; + return rs.mElement_ELEMENT; } + public static Element TYPE(RenderScript rs) { + if(rs.mElement_TYPE == null) { + rs.mElement_TYPE = createUser(rs, DataType.RS_TYPE); + } + return rs.mElement_TYPE; + } + + public static Element ALLOCATION(RenderScript rs) { + if(rs.mElement_ALLOCATION == null) { + rs.mElement_ALLOCATION = createUser(rs, DataType.RS_ALLOCATION); + } + return rs.mElement_ALLOCATION; + } + + public static Element SAMPLER(RenderScript rs) { + if(rs.mElement_SAMPLER == null) { + rs.mElement_SAMPLER = createUser(rs, DataType.RS_SAMPLER); + } + return rs.mElement_SAMPLER; + } + + public static Element SCRIPT(RenderScript rs) { + if(rs.mElement_SCRIPT == null) { + rs.mElement_SCRIPT = createUser(rs, DataType.RS_SCRIPT); + } + return rs.mElement_SCRIPT; + } + + public static Element MESH(RenderScript rs) { + if(rs.mElement_MESH == null) { + rs.mElement_MESH = createUser(rs, DataType.RS_MESH); + } + return rs.mElement_MESH; + } + + public static Element PROGRAM_FRAGMENT(RenderScript rs) { + if(rs.mElement_PROGRAM_FRAGMENT == null) { + rs.mElement_PROGRAM_FRAGMENT = createUser(rs, DataType.RS_PROGRAM_FRAGMENT); + } + return rs.mElement_PROGRAM_FRAGMENT; + } + + public static Element PROGRAM_VERTEX(RenderScript rs) { + if(rs.mElement_PROGRAM_VERTEX == null) { + rs.mElement_PROGRAM_VERTEX = createUser(rs, DataType.RS_PROGRAM_VERTEX); + } + return rs.mElement_PROGRAM_VERTEX; + } + + public static Element PROGRAM_RASTER(RenderScript rs) { + if(rs.mElement_PROGRAM_RASTER == null) { + rs.mElement_PROGRAM_RASTER = createUser(rs, DataType.RS_PROGRAM_RASTER); + } + return rs.mElement_PROGRAM_RASTER; + } + + public static Element PROGRAM_STORE(RenderScript rs) { + if(rs.mElement_PROGRAM_STORE == null) { + rs.mElement_PROGRAM_STORE = createUser(rs, DataType.RS_PROGRAM_STORE); + } + return rs.mElement_PROGRAM_STORE; + } + + public static Element A_8(RenderScript rs) { if(rs.mElement_A_8 == null) { rs.mElement_A_8 = createPixel(rs, DataType.UNSIGNED_8, DataKind.PIXEL_A); @@ -168,168 +262,141 @@ public class Element extends BaseObj { return rs.mElement_RGBA_8888; } - public static Element INDEX_16(RenderScript rs) { - if(rs.mElement_INDEX_16 == null) { - rs.mElement_INDEX_16 = createIndex(rs); + public static Element F32_2(RenderScript rs) { + if(rs.mElement_FLOAT_2 == null) { + rs.mElement_FLOAT_2 = createVector(rs, DataType.FLOAT_32, 2); } - return rs.mElement_INDEX_16; + return rs.mElement_FLOAT_2; } - public static Element ATTRIB_POSITION_2(RenderScript rs) { - if(rs.mElement_POSITION_2 == null) { - rs.mElement_POSITION_2 = createAttrib(rs, DataType.FLOAT_32, DataKind.POSITION, 2); + public static Element F32_3(RenderScript rs) { + if(rs.mElement_FLOAT_3 == null) { + rs.mElement_FLOAT_3 = createVector(rs, DataType.FLOAT_32, 3); } - return rs.mElement_POSITION_2; + return rs.mElement_FLOAT_3; } - public static Element ATTRIB_POSITION_3(RenderScript rs) { - if(rs.mElement_POSITION_3 == null) { - rs.mElement_POSITION_3 = createAttrib(rs, DataType.FLOAT_32, DataKind.POSITION, 3); + public static Element F32_4(RenderScript rs) { + if(rs.mElement_FLOAT_4 == null) { + rs.mElement_FLOAT_4 = createVector(rs, DataType.FLOAT_32, 4); } - return rs.mElement_POSITION_3; + return rs.mElement_FLOAT_4; } - public static Element ATTRIB_TEXTURE_2(RenderScript rs) { - if(rs.mElement_TEXTURE_2 == null) { - rs.mElement_TEXTURE_2 = createAttrib(rs, DataType.FLOAT_32, DataKind.TEXTURE, 2); + public static Element U8_4(RenderScript rs) { + if(rs.mElement_UCHAR_4 == null) { + rs.mElement_UCHAR_4 = createVector(rs, DataType.UNSIGNED_8, 4); } - return rs.mElement_TEXTURE_2; + return rs.mElement_UCHAR_4; } - public static Element ATTRIB_NORMAL_3(RenderScript rs) { - if(rs.mElement_NORMAL_3 == null) { - rs.mElement_NORMAL_3 = createAttrib(rs, DataType.FLOAT_32, DataKind.NORMAL, 3); + public static Element MATRIX_4X4(RenderScript rs) { + if(rs.mElement_MATRIX_4X4 == null) { + rs.mElement_MATRIX_4X4 = createUser(rs, DataType.MATRIX_4X4); } - return rs.mElement_NORMAL_3; + return rs.mElement_MATRIX_4X4; + } + public static Element MATRIX4X4(RenderScript rs) { + return MATRIX_4X4(rs); } - public static Element ATTRIB_COLOR_U8_4(RenderScript rs) { - if(rs.mElement_COLOR_U8_4 == null) { - rs.mElement_COLOR_U8_4 = createAttrib(rs, DataType.UNSIGNED_8, DataKind.COLOR, 4); + public static Element MATRIX_3X3(RenderScript rs) { + if(rs.mElement_MATRIX_3X3 == null) { + rs.mElement_MATRIX_3X3 = createUser(rs, DataType.MATRIX_3X3); } - return rs.mElement_COLOR_U8_4; + return rs.mElement_MATRIX_4X4; } - public static Element ATTRIB_COLOR_F32_4(RenderScript rs) { - if(rs.mElement_COLOR_F32_4 == null) { - rs.mElement_COLOR_F32_4 = createAttrib(rs, DataType.FLOAT_32, DataKind.COLOR, 4); + public static Element MATRIX_2X2(RenderScript rs) { + if(rs.mElement_MATRIX_2X2 == null) { + rs.mElement_MATRIX_2X2 = createUser(rs, DataType.MATRIX_2X2); } - return rs.mElement_COLOR_F32_4; + return rs.mElement_MATRIX_2X2; } - Element(RenderScript rs, Element[] e, String[] n) { - super(rs); + Element(int id, RenderScript rs, Element[] e, String[] n, int[] as) { + super(id, rs); mSize = 0; mElements = e; mElementNames = n; - int[] ids = new int[mElements.length]; + mArraySizes = as; for (int ct = 0; ct < mElements.length; ct++ ) { mSize += mElements[ct].mSize; - ids[ct] = mElements[ct].mID; } - mID = rs.nElementCreate2(ids, mElementNames); } - Element(RenderScript rs, DataType dt, DataKind dk, boolean norm, int size) { - super(rs); + Element(int id, RenderScript rs, DataType dt, DataKind dk, boolean norm, int size) { + super(id, rs); mSize = dt.mSize * size; mType = dt; mKind = dk; mNormalized = norm; mVectorSize = size; - mID = rs.nElementCreate(dt.mID, dk.mID, norm, size); } - public void destroy() throws IllegalStateException { - super.destroy(); + Element(int id, RenderScript rs) { + super(id, rs); } - public static Element createFromClass(RenderScript rs, Class c) { - rs.validate(); - Field[] fields = c.getFields(); - Builder b = new Builder(rs); - - for(Field f: fields) { - Class fc = f.getType(); - if(fc == int.class) { - b.add(createUser(rs, DataType.SIGNED_32), f.getName()); - } else if(fc == short.class) { - b.add(createUser(rs, DataType.SIGNED_16), f.getName()); - } else if(fc == byte.class) { - b.add(createUser(rs, DataType.SIGNED_8), f.getName()); - } else if(fc == float.class) { - b.add(createUser(rs, DataType.FLOAT_32), f.getName()); - } else { - throw new IllegalArgumentException("Unkown field type"); + @Override + void updateFromNative() { + + // we will pack mType; mKind; mNormalized; mVectorSize; NumSubElements + int[] dataBuffer = new int[5]; + mRS.nElementGetNativeData(mID, dataBuffer); + + mNormalized = dataBuffer[2] == 1 ? true : false; + mVectorSize = dataBuffer[3]; + mSize = 0; + for (DataType dt: DataType.values()) { + if(dt.mID == dataBuffer[0]){ + mType = dt; + mSize = mType.mSize * mVectorSize; } } - return b.create(); + for (DataKind dk: DataKind.values()) { + if(dk.mID == dataBuffer[1]){ + mKind = dk; + } + } + + int numSubElements = dataBuffer[4]; + if(numSubElements > 0) { + mElements = new Element[numSubElements]; + mElementNames = new String[numSubElements]; + + int[] subElementIds = new int[numSubElements]; + mRS.nElementGetSubElements(mID, subElementIds, mElementNames); + for(int i = 0; i < numSubElements; i ++) { + mElements[i] = new Element(subElementIds[i], mRS); + mElements[i].updateFromNative(); + mSize += mElements[i].mSize; + } + } + } + public void destroy() throws IllegalStateException { + super.destroy(); + } ///////////////////////////////////////// public static Element createUser(RenderScript rs, DataType dt) { - return new Element(rs, dt, DataKind.USER, false, 1); + DataKind dk = DataKind.USER; + boolean norm = false; + int vecSize = 1; + int id = rs.nElementCreate(dt.mID, dk.mID, norm, vecSize); + return new Element(id, rs, dt, dk, norm, vecSize); } public static Element createVector(RenderScript rs, DataType dt, int size) { if (size < 2 || size > 4) { throw new IllegalArgumentException("Bad size"); } - return new Element(rs, dt, DataKind.USER, false, size); - } - - public static Element createIndex(RenderScript rs) { - return new Element(rs, DataType.UNSIGNED_16, DataKind.INDEX, false, 1); - } - - public static Element createAttrib(RenderScript rs, DataType dt, DataKind dk, int size) { - if (!(dt == DataType.FLOAT_32 || - dt == DataType.UNSIGNED_8 || - dt == DataType.UNSIGNED_16 || - dt == DataType.UNSIGNED_32 || - dt == DataType.SIGNED_8 || - dt == DataType.SIGNED_16 || - dt == DataType.SIGNED_32)) { - throw new IllegalArgumentException("Unsupported DataType"); - } - - if (!(dk == DataKind.COLOR || - dk == DataKind.POSITION || - dk == DataKind.TEXTURE || - dk == DataKind.NORMAL || - dk == DataKind.POINT_SIZE || - dk == DataKind.USER)) { - throw new IllegalArgumentException("Unsupported DataKind"); - } - - if (dk == DataKind.COLOR && - ((dt != DataType.FLOAT_32 && dt != DataType.UNSIGNED_8) || - size < 3 || size > 4)) { - throw new IllegalArgumentException("Bad combo"); - } - if (dk == DataKind.POSITION && (size < 1 || size > 4)) { - throw new IllegalArgumentException("Bad combo"); - } - if (dk == DataKind.TEXTURE && - (dt != DataType.FLOAT_32 || size < 1 || size > 4)) { - throw new IllegalArgumentException("Bad combo"); - } - if (dk == DataKind.NORMAL && - (dt != DataType.FLOAT_32 || size != 3)) { - throw new IllegalArgumentException("Bad combo"); - } - if (dk == DataKind.POINT_SIZE && - (dt != DataType.FLOAT_32 || size != 1)) { - throw new IllegalArgumentException("Bad combo"); - } - + DataKind dk = DataKind.USER; boolean norm = false; - if (dk == DataKind.COLOR && dt == DataType.UNSIGNED_8) { - norm = true; - } - - return new Element(rs, dt, dk, norm, size); + int id = rs.nElementCreate(dt.mID, dk.mID, norm, size); + return new Element(id, rs, dt, dk, norm, size); } public static Element createPixel(RenderScript rs, DataType dt, DataKind dk) { @@ -367,13 +434,16 @@ public class Element extends BaseObj { size = 4; } - return new Element(rs, dt, dk, true, size); + boolean norm = true; + int id = rs.nElementCreate(dt.mID, dk.mID, norm, size); + return new Element(id, rs, dt, dk, norm, size); } public static class Builder { RenderScript mRS; Element[] mElements; String[] mElementNames; + int[] mArraySizes; int mCount; public Builder(RenderScript rs) { @@ -381,29 +451,49 @@ public class Element extends BaseObj { mCount = 0; mElements = new Element[8]; mElementNames = new String[8]; + mArraySizes = new int[8]; } - public void add(Element element, String name) { + public void add(Element element, String name, int arraySize) { + if (arraySize < 1) { + throw new IllegalArgumentException("Array size cannot be less than 1."); + } if(mCount == mElements.length) { Element[] e = new Element[mCount + 8]; String[] s = new String[mCount + 8]; + int[] as = new int[mCount + 8]; System.arraycopy(mElements, 0, e, 0, mCount); System.arraycopy(mElementNames, 0, s, 0, mCount); + System.arraycopy(mArraySizes, 0, as, 0, mCount); mElements = e; mElementNames = s; + mArraySizes = as; } mElements[mCount] = element; mElementNames[mCount] = name; + mArraySizes[mCount] = arraySize; mCount++; } + public void add(Element element, String name) { + add(element, name, 1); + } + public Element create() { mRS.validate(); Element[] ein = new Element[mCount]; String[] sin = new String[mCount]; + int[] asin = new int[mCount]; java.lang.System.arraycopy(mElements, 0, ein, 0, mCount); java.lang.System.arraycopy(mElementNames, 0, sin, 0, mCount); - return new Element(mRS, ein, sin); + java.lang.System.arraycopy(mArraySizes, 0, asin, 0, mCount); + + int[] ids = new int[ein.length]; + for (int ct = 0; ct < ein.length; ct++ ) { + ids[ct] = ein[ct].mID; + } + int id = mRS.nElementCreate2(ids, sin, asin); + return new Element(id, mRS, ein, sin, asin); } } diff --git a/graphics/java/android/renderscript/FieldPacker.java b/graphics/java/android/renderscript/FieldPacker.java index b26e47d068fb20f81ccbce0d0073db9df6f37c90..b6f88bee982b2afff485e686efb54de3cc3f9702 100644 --- a/graphics/java/android/renderscript/FieldPacker.java +++ b/graphics/java/android/renderscript/FieldPacker.java @@ -33,21 +33,28 @@ public class FieldPacker { } } - void reset() { + public void reset() { mPos = 0; } + public void reset(int i) { + mPos = i; + } + + public void skip(int i) { + mPos += i; + } - void addI8(byte v) { + public void addI8(byte v) { mData[mPos++] = v; } - void addI16(short v) { + public void addI16(short v) { align(2); mData[mPos++] = (byte)(v & 0xff); mData[mPos++] = (byte)(v >> 8); } - void addI32(int v) { + public void addI32(int v) { align(4); mData[mPos++] = (byte)(v & 0xff); mData[mPos++] = (byte)((v >> 8) & 0xff); @@ -55,7 +62,7 @@ public class FieldPacker { mData[mPos++] = (byte)((v >> 24) & 0xff); } - void addI64(long v) { + public void addI64(long v) { align(8); mData[mPos++] = (byte)(v & 0xff); mData[mPos++] = (byte)((v >> 8) & 0xff); @@ -67,15 +74,17 @@ public class FieldPacker { mData[mPos++] = (byte)((v >> 56) & 0xff); } - void addU8(short v) { + public void addU8(short v) { if ((v < 0) || (v > 0xff)) { + android.util.Log.e("rs", "FieldPacker.addU8( " + v + " )"); throw new IllegalArgumentException("Saving value out of range for type"); } mData[mPos++] = (byte)v; } - void addU16(int v) { + public void addU16(int v) { if ((v < 0) || (v > 0xffff)) { + android.util.Log.e("rs", "FieldPacker.addU16( " + v + " )"); throw new IllegalArgumentException("Saving value out of range for type"); } align(2); @@ -83,8 +92,9 @@ public class FieldPacker { mData[mPos++] = (byte)(v >> 8); } - void addU32(long v) { - if ((v < 0) || (v > 0xffffffff)) { + public void addU32(long v) { + if ((v < 0) || (v > 0xffffffffL)) { + android.util.Log.e("rs", "FieldPacker.addU32( " + v + " )"); throw new IllegalArgumentException("Saving value out of range for type"); } align(4); @@ -94,8 +104,9 @@ public class FieldPacker { mData[mPos++] = (byte)((v >> 24) & 0xff); } - void addU64(long v) { + public void addU64(long v) { if (v < 0) { + android.util.Log.e("rs", "FieldPacker.addU64( " + v + " )"); throw new IllegalArgumentException("Saving value out of range for type"); } align(8); @@ -109,15 +120,157 @@ public class FieldPacker { mData[mPos++] = (byte)((v >> 56) & 0xff); } - void addF32(float v) { + public void addF32(float v) { addI32(Float.floatToRawIntBits(v)); } - void addF64(float v) { + public void addF64(float v) { addI64(Double.doubleToRawLongBits(v)); } - final byte[] getData() { + public void addObj(BaseObj obj) { + if (obj != null) { + addI32(obj.getID()); + } else { + addI32(0); + } + } + + public void addF32(Float2 v) { + addF32(v.x); + addF32(v.y); + } + public void addF32(Float3 v) { + addF32(v.x); + addF32(v.y); + addF32(v.z); + } + public void addF32(Float4 v) { + addF32(v.x); + addF32(v.y); + addF32(v.z); + addF32(v.w); + } + + public void addI8(Byte2 v) { + addI8(v.x); + addI8(v.y); + } + public void addI8(Byte3 v) { + addI8(v.x); + addI8(v.y); + addI8(v.z); + } + public void addI8(Byte4 v) { + addI8(v.x); + addI8(v.y); + addI8(v.z); + addI8(v.w); + } + + public void addU8(Short2 v) { + addU8(v.x); + addU8(v.y); + } + public void addU8(Short3 v) { + addU8(v.x); + addU8(v.y); + addU8(v.z); + } + public void addU8(Short4 v) { + addU8(v.x); + addU8(v.y); + addU8(v.z); + addU8(v.w); + } + + public void addI16(Short2 v) { + addI16(v.x); + addI16(v.y); + } + public void addI16(Short3 v) { + addI16(v.x); + addI16(v.y); + addI16(v.z); + } + public void addI16(Short4 v) { + addI16(v.x); + addI16(v.y); + addI16(v.z); + addI16(v.w); + } + + public void addU16(Int2 v) { + addU16(v.x); + addU16(v.y); + } + public void addU16(Int3 v) { + addU16(v.x); + addU16(v.y); + addU16(v.z); + } + public void addU16(Int4 v) { + addU16(v.x); + addU16(v.y); + addU16(v.z); + addU16(v.w); + } + + public void addI32(Int2 v) { + addI32(v.x); + addI32(v.y); + } + public void addI32(Int3 v) { + addI32(v.x); + addI32(v.y); + addI32(v.z); + } + public void addI32(Int4 v) { + addI32(v.x); + addI32(v.y); + addI32(v.z); + addI32(v.w); + } + + public void addU32(Int2 v) { + addU32(v.x); + addU32(v.y); + } + public void addU32(Int3 v) { + addU32(v.x); + addU32(v.y); + addU32(v.z); + } + public void addU32(Int4 v) { + addU32(v.x); + addU32(v.y); + addU32(v.z); + addU32(v.w); + } + + public void addObj(Matrix4f v) { + for (int i=0; i < v.mMat.length; i++) { + addF32(v.mMat[i]); + } + } + + public void addObj(Matrix3f v) { + for (int i=0; i < v.mMat.length; i++) { + addF32(v.mMat[i]); + } + } + + public void addObj(Matrix2f v) { + for (int i=0; i < v.mMat.length; i++) { + addF32(v.mMat[i]); + } + } + + public void addBoolean(boolean v) { + addI8((byte)(v ? 1 : 0)); + } + + public final byte[] getData() { return mData; } diff --git a/graphics/java/android/renderscript/FileA3D.java b/graphics/java/android/renderscript/FileA3D.java new file mode 100644 index 0000000000000000000000000000000000000000..24140620b15090a0e10a0281f5a66668d249e52d --- /dev/null +++ b/graphics/java/android/renderscript/FileA3D.java @@ -0,0 +1,217 @@ +/* + * 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.renderscript; + +import java.io.IOException; +import java.io.InputStream; + +import android.content.res.Resources; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; +import android.util.TypedValue; + +/** + * @hide + * + **/ +public class FileA3D extends BaseObj { + + public enum ClassID { + + UNKNOWN, + MESH, + TYPE, + ELEMENT, + ALLOCATION, + PROGRAM_VERTEX, + PROGRAM_RASTER, + PROGRAM_FRAGMENT, + PROGRAM_STORE, + SAMPLER, + ANIMATION, + LIGHT, + ADAPTER_1D, + ADAPTER_2D, + SCRIPT_C; + + public static ClassID toClassID(int intID) { + return ClassID.values()[intID]; + } + } + + // Read only class with index entries + public static class IndexEntry { + RenderScript mRS; + int mIndex; + int mID; + String mName; + ClassID mClassID; + BaseObj mLoadedObj; + + public String getName() { + return mName; + } + + public ClassID getClassID() { + return mClassID; + } + + public BaseObj getObject() { + mRS.validate(); + BaseObj obj = internalCreate(mRS, this); + return obj; + } + + static synchronized BaseObj internalCreate(RenderScript rs, IndexEntry entry) { + if(entry.mLoadedObj != null) { + return entry.mLoadedObj; + } + + if(entry.mClassID == ClassID.UNKNOWN) { + return null; + } + + int objectID = rs.nFileA3DGetEntryByIndex(entry.mID, entry.mIndex); + if(objectID == 0) { + return null; + } + + switch (entry.mClassID) { + case MESH: + entry.mLoadedObj = new Mesh(objectID, rs); + break; + case TYPE: + entry.mLoadedObj = new Type(objectID, rs); + break; + case ELEMENT: + entry.mLoadedObj = null; + break; + case ALLOCATION: + entry.mLoadedObj = null; + break; + case PROGRAM_VERTEX: + entry.mLoadedObj = new ProgramVertex(objectID, rs); + break; + case PROGRAM_RASTER: + break; + case PROGRAM_FRAGMENT: + break; + case PROGRAM_STORE: + break; + case SAMPLER: + break; + case ANIMATION: + break; + case LIGHT: + break; + case ADAPTER_1D: + break; + case ADAPTER_2D: + break; + case SCRIPT_C: + break; + } + + entry.mLoadedObj.updateFromNative(); + + return entry.mLoadedObj; + } + + IndexEntry(RenderScript rs, int index, int id, String name, ClassID classID) { + mRS = rs; + mIndex = index; + mID = id; + mName = name; + mClassID = classID; + mLoadedObj = null; + } + } + + IndexEntry[] mFileEntries; + + FileA3D(int id, RenderScript rs) { + super(id, rs); + } + + private void initEntries() { + int numFileEntries = mRS.nFileA3DGetNumIndexEntries(mID); + if(numFileEntries <= 0) { + return; + } + + mFileEntries = new IndexEntry[numFileEntries]; + int[] ids = new int[numFileEntries]; + String[] names = new String[numFileEntries]; + + mRS.nFileA3DGetIndexEntries(mID, numFileEntries, ids, names); + + for(int i = 0; i < numFileEntries; i ++) { + mFileEntries[i] = new IndexEntry(mRS, i, mID, names[i], ClassID.toClassID(ids[i])); + } + } + + public int getNumIndexEntries() { + if(mFileEntries == null) { + return 0; + } + return mFileEntries.length; + } + + public IndexEntry getIndexEntry(int index) { + if(getNumIndexEntries() == 0 || index < 0 || index >= mFileEntries.length) { + return null; + } + return mFileEntries[index]; + } + + static public FileA3D createFromResource(RenderScript rs, Resources res, int id) + throws IllegalArgumentException { + + rs.validate(); + InputStream is = null; + try { + final TypedValue value = new TypedValue(); + is = res.openRawResource(id, value); + + int asset = ((AssetManager.AssetInputStream) is).getAssetInt(); + + int fileId = rs.nFileA3DCreateFromAssetStream(asset); + + if(fileId == 0) { + throw new IllegalStateException("Load failed."); + } + FileA3D fa3d = new FileA3D(fileId, rs); + fa3d.initEntries(); + return fa3d; + + } catch (Exception e) { + // Ignore + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Ignore + } + } + } + + return null; + } +} diff --git a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java b/graphics/java/android/renderscript/Float2.java similarity index 69% rename from core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java rename to graphics/java/android/renderscript/Float2.java index e72c7df3da51bd52159a80740eb2d3123a198bf4..889bf7bbe2fdb2ab0aa3238bbcf9e498019f0fdc 100644 --- a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java +++ b/graphics/java/android/renderscript/Float2.java @@ -13,15 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.pim.vcard.exception; -public class VCardAgentNotSupportedException extends VCardNotSupportedException { - public VCardAgentNotSupportedException() { - super(); +package android.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Float2 { + public Float2() { } - public VCardAgentNotSupportedException(String message) { - super(message); + public Float2(float initX, float initY) { + x = initX; + y = initY; } -} \ No newline at end of file + public float x; + public float y; +} + + + + diff --git a/core/java/android/pim/vcard/exception/VCardNestedException.java b/graphics/java/android/renderscript/Float3.java similarity index 66% rename from core/java/android/pim/vcard/exception/VCardNestedException.java rename to graphics/java/android/renderscript/Float3.java index 503c2fbcf536a26046909363a8ee3518773ce015..ebe140d48c1e77f38e008ce687cd696e8f49db54 100644 --- a/core/java/android/pim/vcard/exception/VCardNestedException.java +++ b/graphics/java/android/renderscript/Float3.java @@ -14,16 +14,30 @@ * limitations under the License. */ -package android.pim.vcard.exception; +package android.renderscript; + +import java.lang.Math; +import android.util.Log; + /** - * VCardException thrown when VCard is nested without VCardParser's being notified. - */ -public class VCardNestedException extends VCardNotSupportedException { - public VCardNestedException() { - super(); + * @hide + * + **/ +public class Float3 { + public Float3() { } - public VCardNestedException(String message) { - super(message); + public Float3(float initX, float initY, float initZ) { + x = initX; + y = initY; + z = initZ; } + + public float x; + public float y; + public float z; } + + + + diff --git a/graphics/java/android/renderscript/Float4.java b/graphics/java/android/renderscript/Float4.java new file mode 100644 index 0000000000000000000000000000000000000000..847732f9f60da971ae6ecc2ccd174ed138c80066 --- /dev/null +++ b/graphics/java/android/renderscript/Float4.java @@ -0,0 +1,45 @@ +/* + * 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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Float4 { + public Float4() { + } + + public Float4(float initX, float initY, float initZ, float initW) { + x = initX; + y = initY; + z = initZ; + w = initW; + } + + public float x; + public float y; + public float z; + public float w; +} + + + diff --git a/graphics/java/android/renderscript/Font.java b/graphics/java/android/renderscript/Font.java new file mode 100644 index 0000000000000000000000000000000000000000..de250146289d0ef132ed903622b74086c0f467b6 --- /dev/null +++ b/graphics/java/android/renderscript/Font.java @@ -0,0 +1,165 @@ +/* + * 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.renderscript; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.HashMap; + +import android.content.res.Resources; +import android.content.res.AssetManager; +import android.util.Log; +import android.util.TypedValue; + +/** + * @hide + * + **/ +public class Font extends BaseObj { + + //These help us create a font by family name + private static final String[] sSansNames = { + "sans-serif", "arial", "helvetica", "tahoma", "verdana" + }; + + private static final String[] sSerifNames = { + "serif", "times", "times new roman", "palatino", "georgia", "baskerville", + "goudy", "fantasy", "cursive", "ITC Stone Serif" + }; + + private static final String[] sMonoNames = { + "monospace", "courier", "courier new", "monaco" + }; + + private static class FontFamily { + String[] mNames; + String mNormalFileName; + String mBoldFileName; + String mItalicFileName; + String mBoldItalicFileName; + } + + private static Map sFontFamilyMap; + + public enum Style { + NORMAL, + BOLD, + ITALIC, + BOLD_ITALIC; + } + + private static void addFamilyToMap(FontFamily family) { + for(int i = 0; i < family.mNames.length; i ++) { + sFontFamilyMap.put(family.mNames[i], family); + } + } + + private static void initFontFamilyMap() { + sFontFamilyMap = new HashMap(); + + FontFamily sansFamily = new FontFamily(); + sansFamily.mNames = sSansNames; + sansFamily.mNormalFileName = "DroidSans.ttf"; + sansFamily.mBoldFileName = "DroidSans-Bold.ttf"; + sansFamily.mItalicFileName = "DroidSans.ttf"; + sansFamily.mBoldItalicFileName = "DroidSans-Bold.ttf"; + addFamilyToMap(sansFamily); + + FontFamily serifFamily = new FontFamily(); + serifFamily.mNames = sSerifNames; + serifFamily.mNormalFileName = "DroidSerif-Regular.ttf"; + serifFamily.mBoldFileName = "DroidSerif-Bold.ttf"; + serifFamily.mItalicFileName = "DroidSerif-Italic.ttf"; + serifFamily.mBoldItalicFileName = "DroidSerif-BoldItalic.ttf"; + addFamilyToMap(serifFamily); + + FontFamily monoFamily = new FontFamily(); + monoFamily.mNames = sMonoNames; + monoFamily.mNormalFileName = "DroidSansMono.ttf"; + monoFamily.mBoldFileName = "DroidSansMono.ttf"; + monoFamily.mItalicFileName = "DroidSansMono.ttf"; + monoFamily.mBoldItalicFileName = "DroidSansMono.ttf"; + addFamilyToMap(monoFamily); + } + + static { + initFontFamilyMap(); + } + + static String getFontFileName(String familyName, Style style) { + FontFamily family = sFontFamilyMap.get(familyName); + if(family != null) { + switch(style) { + case NORMAL: + return family.mNormalFileName; + case BOLD: + return family.mBoldFileName; + case ITALIC: + return family.mItalicFileName; + case BOLD_ITALIC: + return family.mBoldItalicFileName; + } + } + // Fallback if we could not find the desired family + return "DroidSans.ttf"; + } + + Font(int id, RenderScript rs) { + super(id, rs); + } + + /** + * Takes a specific file name as an argument + */ + static public Font create(RenderScript rs, Resources res, String fileName, int size) + throws IllegalArgumentException { + + rs.validate(); + try { + int dpi = res.getDisplayMetrics().densityDpi; + int fontId = rs.nFontCreateFromFile(fileName, size, dpi); + + if(fontId == 0) { + throw new IllegalStateException("Failed loading a font"); + } + Font rsFont = new Font(fontId, rs); + + return rsFont; + + } catch (Exception e) { + // Ignore + } + + return null; + } + + /** + * Accepts one of the following family names as an argument + * and will attemp to produce the best match with a system font + * "sans-serif" "arial" "helvetica" "tahoma" "verdana" + * "serif" "times" "times new roman" "palatino" "georgia" "baskerville" + * "goudy" "fantasy" "cursive" "ITC Stone Serif" + * "monospace" "courier" "courier new" "monaco" + * Returns default font if no match could be found + */ + static public Font createFromFamily(RenderScript rs, Resources res, String familyName, Style fontStyle, int size) + throws IllegalArgumentException { + String fileName = getFontFileName(familyName, fontStyle); + return create(rs, res, fileName, size); + } +} diff --git a/graphics/java/android/renderscript/Int2.java b/graphics/java/android/renderscript/Int2.java new file mode 100644 index 0000000000000000000000000000000000000000..56e2fe9e6ab10f88cbc5946951f353000a996b09 --- /dev/null +++ b/graphics/java/android/renderscript/Int2.java @@ -0,0 +1,37 @@ +/* + * 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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Int2 { + public Int2() { + } + + public int x; + public int y; +} + + + + diff --git a/graphics/java/android/renderscript/Int3.java b/graphics/java/android/renderscript/Int3.java new file mode 100644 index 0000000000000000000000000000000000000000..1b27509de1c0dec42a83809835ba74d68cc74d8e --- /dev/null +++ b/graphics/java/android/renderscript/Int3.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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Int3 { + public Int3() { + } + + public int x; + public int y; + public int z; +} + + + + diff --git a/graphics/java/android/renderscript/Int4.java b/graphics/java/android/renderscript/Int4.java new file mode 100644 index 0000000000000000000000000000000000000000..3d6f3f5f88df6b695a8e11dd87e17c9d393cd0a3 --- /dev/null +++ b/graphics/java/android/renderscript/Int4.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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Int4 { + public Int4() { + } + + public int x; + public int y; + public int z; + public int w; +} + + + diff --git a/graphics/java/android/renderscript/Light.java b/graphics/java/android/renderscript/Light.java deleted file mode 100644 index aab656f608440902dad008b63449c2e3934d7853..0000000000000000000000000000000000000000 --- a/graphics/java/android/renderscript/Light.java +++ /dev/null @@ -1,76 +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.renderscript; - -import android.util.Config; -import android.util.Log; - -/** - * @hide - * - **/ -public class Light extends BaseObj { - Light(int id, RenderScript rs) { - super(rs); - mID = id; - } - - public void setColor(float r, float g, float b) { - mRS.validate(); - mRS.nLightSetColor(mID, r, g, b); - } - - public void setPosition(float x, float y, float z) { - mRS.validate(); - mRS.nLightSetPosition(mID, x, y, z); - } - - public static class Builder { - RenderScript mRS; - boolean mIsMono; - boolean mIsLocal; - - public Builder(RenderScript rs) { - mRS = rs; - mIsMono = false; - mIsLocal = false; - } - - public void lightSetIsMono(boolean isMono) { - mIsMono = isMono; - } - - public void lightSetIsLocal(boolean isLocal) { - mIsLocal = isLocal; - } - - static synchronized Light internalCreate(RenderScript rs, Builder b) { - rs.nSamplerBegin(); - rs.nLightSetIsMono(b.mIsMono); - rs.nLightSetIsLocal(b.mIsLocal); - int id = rs.nLightCreate(); - return new Light(id, rs); - } - - public Light create() { - mRS.validate(); - return internalCreate(mRS, this); - } - } - -} - diff --git a/graphics/java/android/renderscript/Long2.java b/graphics/java/android/renderscript/Long2.java new file mode 100644 index 0000000000000000000000000000000000000000..11ead2fd5cde4f6449ec7590d7bb21722191d2d8 --- /dev/null +++ b/graphics/java/android/renderscript/Long2.java @@ -0,0 +1,37 @@ +/* + * 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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Long2 { + public Long2() { + } + + public long x; + public long y; +} + + + + diff --git a/graphics/java/android/renderscript/Long3.java b/graphics/java/android/renderscript/Long3.java new file mode 100644 index 0000000000000000000000000000000000000000..16045324dcd17226dd095bddf121641d24a9daac --- /dev/null +++ b/graphics/java/android/renderscript/Long3.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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Long3 { + public Long3() { + } + + public long x; + public long y; + public long z; +} + + + + diff --git a/graphics/java/android/renderscript/Long4.java b/graphics/java/android/renderscript/Long4.java new file mode 100644 index 0000000000000000000000000000000000000000..2fd274796b134cca793dfcc01fababe201172aa6 --- /dev/null +++ b/graphics/java/android/renderscript/Long4.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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Long4 { + public Long4() { + } + + public long x; + public long y; + public long z; + public long w; +} + + + diff --git a/graphics/java/android/renderscript/Matrix2f.java b/graphics/java/android/renderscript/Matrix2f.java index 4b5e61ba1eaf467e86108bdaab8e99530b5c5e96..99d23db860b6ade83166048a129753559adb9c9f 100644 --- a/graphics/java/android/renderscript/Matrix2f.java +++ b/graphics/java/android/renderscript/Matrix2f.java @@ -51,6 +51,57 @@ public class Matrix2f { System.arraycopy(mMat, 0, src, 0, 4); } + public void loadRotate(float rot) { + float c, s; + rot *= (float)(java.lang.Math.PI / 180.0f); + c = (float)java.lang.Math.cos(rot); + s = (float)java.lang.Math.sin(rot); + mMat[0] = c; + mMat[1] = -s; + mMat[2] = s; + mMat[3] = c; + } + + public void loadScale(float x, float y) { + loadIdentity(); + mMat[0] = x; + mMat[3] = y; + } + public void loadMultiply(Matrix2f lhs, Matrix2f rhs) { + for (int i=0 ; i<2 ; i++) { + float ri0 = 0; + float ri1 = 0; + for (int j=0 ; j<2 ; j++) { + float rhs_ij = rhs.get(i,j); + ri0 += lhs.get(j,0) * rhs_ij; + ri1 += lhs.get(j,1) * rhs_ij; + } + set(i,0, ri0); + set(i,1, ri1); + } + } + + public void multiply(Matrix2f rhs) { + Matrix2f tmp = new Matrix2f(); + tmp.loadMultiply(this, rhs); + load(tmp); + } + public void rotate(float rot) { + Matrix2f tmp = new Matrix2f(); + tmp.loadRotate(rot); + multiply(tmp); + } + public void scale(float x, float y) { + Matrix2f tmp = new Matrix2f(); + tmp.loadScale(x, y); + multiply(tmp); + } + public void transpose() { + float temp = mMat[1]; + mMat[1] = mMat[2]; + mMat[2] = temp; + } + final float[] mMat; } diff --git a/graphics/java/android/renderscript/Matrix3f.java b/graphics/java/android/renderscript/Matrix3f.java index 19d7b4353e0df7aa1b67721c036e6f07c6e7b38b..961bc5dd45a61da191807be2b52f29e370edbcaa 100644 --- a/graphics/java/android/renderscript/Matrix3f.java +++ b/graphics/java/android/renderscript/Matrix3f.java @@ -57,6 +57,124 @@ public class Matrix3f { System.arraycopy(mMat, 0, src, 0, 9); } + public void loadRotate(float rot, float x, float y, float z) { + float c, s; + rot *= (float)(java.lang.Math.PI / 180.0f); + c = (float)java.lang.Math.cos(rot); + s = (float)java.lang.Math.sin(rot); + + float len = (float)java.lang.Math.sqrt(x*x + y*y + z*z); + if (!(len != 1)) { + float recipLen = 1.f / len; + x *= recipLen; + y *= recipLen; + z *= recipLen; + } + float nc = 1.0f - c; + float xy = x * y; + float yz = y * z; + float zx = z * x; + float xs = x * s; + float ys = y * s; + float zs = z * s; + mMat[0] = x*x*nc + c; + mMat[3] = xy*nc - zs; + mMat[6] = zx*nc + ys; + mMat[1] = xy*nc + zs; + mMat[4] = y*y*nc + c; + mMat[9] = yz*nc - xs; + mMat[2] = zx*nc - ys; + mMat[6] = yz*nc + xs; + mMat[8] = z*z*nc + c; + } + + public void loadRotate(float rot) { + float c, s; + rot *= (float)(java.lang.Math.PI / 180.0f); + c = (float)java.lang.Math.cos(rot); + s = (float)java.lang.Math.sin(rot); + mMat[0] = c; + mMat[1] = -s; + mMat[3] = s; + mMat[4] = c; + } + + public void loadScale(float x, float y) { + loadIdentity(); + mMat[0] = x; + mMat[4] = y; + } + + public void loadScale(float x, float y, float z) { + loadIdentity(); + mMat[0] = x; + mMat[4] = y; + mMat[8] = z; + } + + public void loadTranslate(float x, float y) { + loadIdentity(); + mMat[6] = x; + mMat[7] = y; + } + + public void loadMultiply(Matrix3f lhs, Matrix3f rhs) { + for (int i=0 ; i<3 ; i++) { + float ri0 = 0; + float ri1 = 0; + float ri2 = 0; + for (int j=0 ; j<3 ; j++) { + float rhs_ij = rhs.get(i,j); + ri0 += lhs.get(j,0) * rhs_ij; + ri1 += lhs.get(j,1) * rhs_ij; + ri2 += lhs.get(j,2) * rhs_ij; + } + set(i,0, ri0); + set(i,1, ri1); + set(i,2, ri2); + } + } + + public void multiply(Matrix3f rhs) { + Matrix3f tmp = new Matrix3f(); + tmp.loadMultiply(this, rhs); + load(tmp); + } + public void rotate(float rot, float x, float y, float z) { + Matrix3f tmp = new Matrix3f(); + tmp.loadRotate(rot, x, y, z); + multiply(tmp); + } + public void rotate(float rot) { + Matrix3f tmp = new Matrix3f(); + tmp.loadRotate(rot); + multiply(tmp); + } + public void scale(float x, float y) { + Matrix3f tmp = new Matrix3f(); + tmp.loadScale(x, y); + multiply(tmp); + } + public void scale(float x, float y, float z) { + Matrix3f tmp = new Matrix3f(); + tmp.loadScale(x, y, z); + multiply(tmp); + } + public void translate(float x, float y) { + Matrix3f tmp = new Matrix3f(); + tmp.loadTranslate(x, y); + multiply(tmp); + } + public void transpose() { + for(int i = 0; i < 2; ++i) { + for(int j = i + 1; j < 3; ++j) { + float temp = mMat[i*3 + j]; + mMat[i*3 + j] = mMat[j*3 + i]; + mMat[j*3 + i] = temp; + } + } + } + final float[] mMat; } diff --git a/graphics/java/android/renderscript/Matrix4f.java b/graphics/java/android/renderscript/Matrix4f.java index ebd5bdedfe68304edaab2174274a6da7e7f35992..5ffc21a589394ecd2231be76c4fde538c74b32fd 100644 --- a/graphics/java/android/renderscript/Matrix4f.java +++ b/graphics/java/android/renderscript/Matrix4f.java @@ -179,6 +179,85 @@ public class Matrix4f { tmp.loadTranslate(x, y, z); multiply(tmp); } + private float computeCofactor(int i, int j) { + int c0 = (i+1) % 4; + int c1 = (i+2) % 4; + int c2 = (i+3) % 4; + int r0 = (j+1) % 4; + int r1 = (j+2) % 4; + int r2 = (j+3) % 4; + + float minor = (mMat[c0 + 4*r0] * (mMat[c1 + 4*r1] * mMat[c2 + 4*r2] - + mMat[c1 + 4*r2] * mMat[c2 + 4*r1])) + - (mMat[c0 + 4*r1] * (mMat[c1 + 4*r0] * mMat[c2 + 4*r2] - + mMat[c1 + 4*r2] * mMat[c2 + 4*r0])) + + (mMat[c0 + 4*r2] * (mMat[c1 + 4*r0] * mMat[c2 + 4*r1] - + mMat[c1 + 4*r1] * mMat[c2 + 4*r0])); + + float cofactor = ((i+j) & 1) != 0 ? -minor : minor; + return cofactor; + } + + public boolean inverse() { + + Matrix4f result = new Matrix4f(); + + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result.mMat[4*i + j] = computeCofactor(i, j); + } + } + + // Dot product of 0th column of source and 0th row of result + float det = mMat[0]*result.mMat[0] + mMat[4]*result.mMat[1] + + mMat[8]*result.mMat[2] + mMat[12]*result.mMat[3]; + + if (Math.abs(det) < 1e-6) { + return false; + } + + det = 1.0f / det; + for (int i = 0; i < 16; ++i) { + mMat[i] = result.mMat[i] * det; + } + + return true; + } + + public boolean inverseTranspose() { + + Matrix4f result = new Matrix4f(); + + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result.mMat[4*j + i] = computeCofactor(i, j); + } + } + + float det = mMat[0]*result.mMat[0] + mMat[4]*result.mMat[4] + + mMat[8]*result.mMat[8] + mMat[12]*result.mMat[12]; + + if (Math.abs(det) < 1e-6) { + return false; + } + + det = 1.0f / det; + for (int i = 0; i < 16; ++i) { + mMat[i] = result.mMat[i] * det; + } + + return true; + } + + public void transpose() { + for(int i = 0; i < 3; ++i) { + for(int j = i + 1; j < 4; ++j) { + float temp = mMat[i*4 + j]; + mMat[i*4 + j] = mMat[j*4 + i]; + mMat[j*4 + i] = temp; + } + } + } final float[] mMat; } diff --git a/graphics/java/android/renderscript/Mesh.java b/graphics/java/android/renderscript/Mesh.java new file mode 100644 index 0000000000000000000000000000000000000000..b74c1f82409a4c06bf2dbed11725cf47421c980f --- /dev/null +++ b/graphics/java/android/renderscript/Mesh.java @@ -0,0 +1,479 @@ +/* + * 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.renderscript; + +import java.util.Vector; + +import android.util.Config; +import android.util.Log; + +/** + * @hide + * + **/ +public class Mesh extends BaseObj { + + Allocation[] mVertexBuffers; + Allocation[] mIndexBuffers; + Primitive[] mPrimitives; + + Mesh(int id, RenderScript rs) { + super(id, rs); + } + + public int getVertexAllocationCount() { + if(mVertexBuffers == null) { + return 0; + } + return mVertexBuffers.length; + } + public Allocation getVertexAllocation(int slot) { + return mVertexBuffers[slot]; + } + + public int getPrimitiveCount() { + if(mIndexBuffers == null) { + return 0; + } + return mIndexBuffers.length; + } + public Allocation getIndexAllocation(int slot) { + return mIndexBuffers[slot]; + } + public Primitive getPrimitive(int slot) { + return mPrimitives[slot]; + } + + @Override + void updateFromNative() { + mName = mRS.nGetName(mID); + int vtxCount = mRS.nMeshGetVertexBufferCount(mID); + int idxCount = mRS.nMeshGetIndexCount(mID); + + int[] vtxIDs = new int[vtxCount]; + int[] idxIDs = new int[idxCount]; + int[] primitives = new int[idxCount]; + + mRS.nMeshGetVertices(mID, vtxIDs, vtxCount); + mRS.nMeshGetIndices(mID, idxIDs, primitives, vtxCount); + + mVertexBuffers = new Allocation[vtxCount]; + mIndexBuffers = new Allocation[idxCount]; + mPrimitives = new Primitive[idxCount]; + + for(int i = 0; i < vtxCount; i ++) { + if(vtxIDs[i] != 0) { + mVertexBuffers[i] = new Allocation(vtxIDs[i], mRS); + mVertexBuffers[i].updateFromNative(); + } + } + + for(int i = 0; i < idxCount; i ++) { + if(idxIDs[i] != 0) { + mIndexBuffers[i] = new Allocation(idxIDs[i], mRS); + mIndexBuffers[i].updateFromNative(); + } + mPrimitives[i] = Primitive.values()[primitives[i]]; + } + } + + public static class Builder { + RenderScript mRS; + + class Entry { + Type t; + Element e; + int size; + Primitive prim; + } + + int mVertexTypeCount; + Entry[] mVertexTypes; + Vector mIndexTypes; + + public Builder(RenderScript rs) { + mRS = rs; + mVertexTypeCount = 0; + mVertexTypes = new Entry[16]; + mIndexTypes = new Vector(); + } + + public int addVertexType(Type t) throws IllegalStateException { + if (mVertexTypeCount >= mVertexTypes.length) { + throw new IllegalStateException("Max vertex types exceeded."); + } + + int addedIndex = mVertexTypeCount; + mVertexTypes[mVertexTypeCount] = new Entry(); + mVertexTypes[mVertexTypeCount].t = t; + mVertexTypes[mVertexTypeCount].e = null; + mVertexTypeCount++; + return addedIndex; + } + + public int addVertexType(Element e, int size) throws IllegalStateException { + if (mVertexTypeCount >= mVertexTypes.length) { + throw new IllegalStateException("Max vertex types exceeded."); + } + + int addedIndex = mVertexTypeCount; + mVertexTypes[mVertexTypeCount] = new Entry(); + mVertexTypes[mVertexTypeCount].t = null; + mVertexTypes[mVertexTypeCount].e = e; + mVertexTypes[mVertexTypeCount].size = size; + mVertexTypeCount++; + return addedIndex; + } + + public int addIndexType(Type t, Primitive p) { + int addedIndex = mIndexTypes.size(); + Entry indexType = new Entry(); + indexType.t = t; + indexType.e = null; + indexType.size = 0; + indexType.prim = p; + mIndexTypes.addElement(indexType); + return addedIndex; + } + + public int addIndexType(Primitive p) { + int addedIndex = mIndexTypes.size(); + Entry indexType = new Entry(); + indexType.t = null; + indexType.e = null; + indexType.size = 0; + indexType.prim = p; + mIndexTypes.addElement(indexType); + return addedIndex; + } + + public int addIndexType(Element e, int size, Primitive p) { + int addedIndex = mIndexTypes.size(); + Entry indexType = new Entry(); + indexType.t = null; + indexType.e = e; + indexType.size = size; + indexType.prim = p; + mIndexTypes.addElement(indexType); + return addedIndex; + } + + Type newType(Element e, int size) { + Type.Builder tb = new Type.Builder(mRS, e); + tb.add(Dimension.X, size); + return tb.create(); + } + + static synchronized Mesh internalCreate(RenderScript rs, Builder b) { + + int id = rs.nMeshCreate(b.mVertexTypeCount, b.mIndexTypes.size()); + Mesh newMesh = new Mesh(id, rs); + newMesh.mIndexBuffers = new Allocation[b.mIndexTypes.size()]; + newMesh.mPrimitives = new Primitive[b.mIndexTypes.size()]; + newMesh.mVertexBuffers = new Allocation[b.mVertexTypeCount]; + + for(int ct = 0; ct < b.mIndexTypes.size(); ct ++) { + Allocation alloc = null; + Entry entry = (Entry)b.mIndexTypes.elementAt(ct); + if (entry.t != null) { + alloc = Allocation.createTyped(rs, entry.t); + } + else if(entry.e != null) { + alloc = Allocation.createSized(rs, entry.e, entry.size); + } + int allocID = (alloc == null) ? 0 : alloc.getID(); + rs.nMeshBindIndex(id, allocID, entry.prim.mID, ct); + newMesh.mIndexBuffers[ct] = alloc; + newMesh.mPrimitives[ct] = entry.prim; + } + + for(int ct = 0; ct < b.mVertexTypeCount; ct ++) { + Allocation alloc = null; + Entry entry = b.mVertexTypes[ct]; + if (entry.t != null) { + alloc = Allocation.createTyped(rs, entry.t); + } else if(entry.e != null) { + alloc = Allocation.createSized(rs, entry.e, entry.size); + } + rs.nMeshBindVertex(id, alloc.getID(), ct); + newMesh.mVertexBuffers[ct] = alloc; + } + + return newMesh; + } + + public Mesh create() { + mRS.validate(); + Mesh sm = internalCreate(mRS, this); + return sm; + } + } + + public static class AllocationBuilder { + RenderScript mRS; + + class Entry { + Allocation a; + Primitive prim; + } + + int mVertexTypeCount; + Entry[] mVertexTypes; + + Vector mIndexTypes; + + public AllocationBuilder(RenderScript rs) { + mRS = rs; + mVertexTypeCount = 0; + mVertexTypes = new Entry[16]; + mIndexTypes = new Vector(); + } + + public int addVertexAllocation(Allocation a) throws IllegalStateException { + if (mVertexTypeCount >= mVertexTypes.length) { + throw new IllegalStateException("Max vertex types exceeded."); + } + + int addedIndex = mVertexTypeCount; + mVertexTypes[mVertexTypeCount] = new Entry(); + mVertexTypes[mVertexTypeCount].a = a; + mVertexTypeCount++; + return addedIndex; + } + + public int addIndexAllocation(Allocation a, Primitive p) { + int addedIndex = mIndexTypes.size(); + Entry indexType = new Entry(); + indexType.a = a; + indexType.prim = p; + mIndexTypes.addElement(indexType); + return addedIndex; + } + + public int addIndexType(Primitive p) { + int addedIndex = mIndexTypes.size(); + Entry indexType = new Entry(); + indexType.a = null; + indexType.prim = p; + mIndexTypes.addElement(indexType); + return addedIndex; + } + + static synchronized Mesh internalCreate(RenderScript rs, AllocationBuilder b) { + + int id = rs.nMeshCreate(b.mVertexTypeCount, b.mIndexTypes.size()); + Mesh newMesh = new Mesh(id, rs); + newMesh.mIndexBuffers = new Allocation[b.mIndexTypes.size()]; + newMesh.mPrimitives = new Primitive[b.mIndexTypes.size()]; + newMesh.mVertexBuffers = new Allocation[b.mVertexTypeCount]; + + for(int ct = 0; ct < b.mIndexTypes.size(); ct ++) { + Entry entry = (Entry)b.mIndexTypes.elementAt(ct); + int allocID = (entry.a == null) ? 0 : entry.a.getID(); + rs.nMeshBindIndex(id, allocID, entry.prim.mID, ct); + newMesh.mIndexBuffers[ct] = entry.a; + newMesh.mPrimitives[ct] = entry.prim; + } + + for(int ct = 0; ct < b.mVertexTypeCount; ct ++) { + Entry entry = b.mVertexTypes[ct]; + rs.nMeshBindVertex(id, entry.a.mID, ct); + newMesh.mVertexBuffers[ct] = entry.a; + } + + return newMesh; + } + + public Mesh create() { + mRS.validate(); + Mesh sm = internalCreate(mRS, this); + return sm; + } + } + + + public static class TriangleMeshBuilder { + float mVtxData[]; + int mVtxCount; + short mIndexData[]; + int mIndexCount; + RenderScript mRS; + Element mElement; + + float mNX = 0; + float mNY = 0; + float mNZ = -1; + float mS0 = 0; + float mT0 = 0; + float mR = 1; + float mG = 1; + float mB = 1; + float mA = 1; + + int mVtxSize; + int mFlags; + + public static final int COLOR = 0x0001; + public static final int NORMAL = 0x0002; + public static final int TEXTURE_0 = 0x0100; + + public TriangleMeshBuilder(RenderScript rs, int vtxSize, int flags) { + mRS = rs; + mVtxCount = 0; + mIndexCount = 0; + mVtxData = new float[128]; + mIndexData = new short[128]; + mVtxSize = vtxSize; + mFlags = flags; + + if (vtxSize < 2 || vtxSize > 3) { + throw new IllegalArgumentException("Vertex size out of range."); + } + } + + private void makeSpace(int count) { + if ((mVtxCount + count) >= mVtxData.length) { + float t[] = new float[mVtxData.length * 2]; + System.arraycopy(mVtxData, 0, t, 0, mVtxData.length); + mVtxData = t; + } + } + + private void latch() { + if ((mFlags & COLOR) != 0) { + makeSpace(4); + mVtxData[mVtxCount++] = mR; + mVtxData[mVtxCount++] = mG; + mVtxData[mVtxCount++] = mB; + mVtxData[mVtxCount++] = mA; + } + if ((mFlags & TEXTURE_0) != 0) { + makeSpace(2); + mVtxData[mVtxCount++] = mS0; + mVtxData[mVtxCount++] = mT0; + } + if ((mFlags & NORMAL) != 0) { + makeSpace(3); + mVtxData[mVtxCount++] = mNX; + mVtxData[mVtxCount++] = mNY; + mVtxData[mVtxCount++] = mNZ; + } + } + + public void addVertex(float x, float y) { + if (mVtxSize != 2) { + throw new IllegalStateException("add mistmatch with declared components."); + } + makeSpace(2); + mVtxData[mVtxCount++] = x; + mVtxData[mVtxCount++] = y; + latch(); + } + + public void addVertex(float x, float y, float z) { + if (mVtxSize != 3) { + throw new IllegalStateException("add mistmatch with declared components."); + } + makeSpace(3); + mVtxData[mVtxCount++] = x; + mVtxData[mVtxCount++] = y; + mVtxData[mVtxCount++] = z; + latch(); + } + + public void setTexture(float s, float t) { + if ((mFlags & TEXTURE_0) == 0) { + throw new IllegalStateException("add mistmatch with declared components."); + } + mS0 = s; + mT0 = t; + } + + public void setNormal(float x, float y, float z) { + if ((mFlags & NORMAL) == 0) { + throw new IllegalStateException("add mistmatch with declared components."); + } + mNX = x; + mNY = y; + mNZ = z; + } + + public void setColor(float r, float g, float b, float a) { + if ((mFlags & COLOR) == 0) { + throw new IllegalStateException("add mistmatch with declared components."); + } + mR = r; + mG = g; + mB = b; + mA = a; + } + + public void addTriangle(int idx1, int idx2, int idx3) { + if((idx1 >= mVtxCount) || (idx1 < 0) || + (idx2 >= mVtxCount) || (idx2 < 0) || + (idx3 >= mVtxCount) || (idx3 < 0)) { + throw new IllegalStateException("Index provided greater than vertex count."); + } + if ((mIndexCount + 3) >= mIndexData.length) { + short t[] = new short[mIndexData.length * 2]; + System.arraycopy(mIndexData, 0, t, 0, mIndexData.length); + mIndexData = t; + } + mIndexData[mIndexCount++] = (short)idx1; + mIndexData[mIndexCount++] = (short)idx2; + mIndexData[mIndexCount++] = (short)idx3; + } + + public Mesh create(boolean uploadToBufferObject) { + Element.Builder b = new Element.Builder(mRS); + int floatCount = mVtxSize; + b.add(Element.createVector(mRS, + Element.DataType.FLOAT_32, + mVtxSize), "position"); + if ((mFlags & COLOR) != 0) { + floatCount += 4; + b.add(Element.F32_4(mRS), "color"); + } + if ((mFlags & TEXTURE_0) != 0) { + floatCount += 2; + b.add(Element.F32_2(mRS), "texture0"); + } + if ((mFlags & NORMAL) != 0) { + floatCount += 3; + b.add(Element.F32_3(mRS), "normal"); + } + mElement = b.create(); + + Builder smb = new Builder(mRS); + smb.addVertexType(mElement, mVtxCount / floatCount); + smb.addIndexType(Element.U16(mRS), mIndexCount, Primitive.TRIANGLE); + + Mesh sm = smb.create(); + + sm.getVertexAllocation(0).data(mVtxData); + if(uploadToBufferObject) { + sm.getVertexAllocation(0).uploadToBufferObject(); + } + + sm.getIndexAllocation(0).data(mIndexData); + sm.getIndexAllocation(0).uploadToBufferObject(); + + return sm; + } + } +} + diff --git a/graphics/java/android/renderscript/Program.java b/graphics/java/android/renderscript/Program.java index 1614ec590cbd0ca702a32522d91381e3d6675f9d..c6ed72a53d84ef9c69331ab861af59557d18c27f 100644 --- a/graphics/java/android/renderscript/Program.java +++ b/graphics/java/android/renderscript/Program.java @@ -17,6 +17,11 @@ package android.renderscript; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +import android.content.res.Resources; import android.util.Config; import android.util.Log; @@ -38,8 +43,7 @@ public class Program extends BaseObj { String mShader; Program(int id, RenderScript rs) { - super(rs); - mID = id; + super(id, rs); } public void bindConstants(Allocation a, int slot) { @@ -91,8 +95,47 @@ public class Program extends BaseObj { mTextureCount = 0; } - public void setShader(String s) { + public BaseProgramBuilder setShader(String s) { mShader = s; + return this; + } + + public BaseProgramBuilder setShader(Resources resources, int resourceID) { + byte[] str; + int strLength; + InputStream is = resources.openRawResource(resourceID); + try { + try { + str = new byte[1024]; + strLength = 0; + while(true) { + int bytesLeft = str.length - strLength; + if (bytesLeft == 0) { + byte[] buf2 = new byte[str.length * 2]; + System.arraycopy(str, 0, buf2, 0, str.length); + str = buf2; + bytesLeft = str.length - strLength; + } + int bytesRead = is.read(str, strLength, bytesLeft); + if (bytesRead <= 0) { + break; + } + strLength += bytesRead; + } + } finally { + is.close(); + } + } catch(IOException e) { + throw new Resources.NotFoundException(); + } + + try { + mShader = new String(str, 0, strLength, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e("Renderscript shader creation", "Could not decode shader string"); + } + + return this; } public void addInput(Element e) throws IllegalStateException { @@ -120,12 +163,13 @@ public class Program extends BaseObj { return mConstantCount++; } - public void setTextureCount(int count) throws IllegalArgumentException { + public BaseProgramBuilder setTextureCount(int count) throws IllegalArgumentException { // Should check for consistant and non-conflicting names... if(count >= MAX_CONSTANT) { throw new IllegalArgumentException("Max texture count exceeded."); } mTextureCount = count; + return this; } protected void initProgram(Program p) { diff --git a/graphics/java/android/renderscript/ProgramFragment.java b/graphics/java/android/renderscript/ProgramFragment.java index 5e04f0c57526f765de620ce6d80aa6bdd0289bd5..00c5cf1f18b93c5593263294dd74d8b6b681ffcf 100644 --- a/graphics/java/android/renderscript/ProgramFragment.java +++ b/graphics/java/android/renderscript/ProgramFragment.java @@ -62,10 +62,11 @@ public class ProgramFragment extends Program { } } - public static class Builder { + public static class Builder extends ShaderBuilder { public static final int MAX_TEXTURE = 2; - RenderScript mRS; + int mNumTextures; boolean mPointSpriteEnable; + boolean mVaryingColorEnable; public enum EnvMode { REPLACE (1), @@ -100,39 +101,125 @@ public class ProgramFragment extends Program { } Slot[] mSlots; + private void buildShaderString() { + mShader = "//rs_shader_internal\n"; + mShader += "varying lowp vec4 varColor;\n"; + mShader += "varying vec4 varTex0;\n"; + + mShader += "void main() {\n"; + if (mVaryingColorEnable) { + mShader += " lowp vec4 col = varColor;\n"; + } else { + mShader += " lowp vec4 col = UNI_Color;\n"; + } + + if (mNumTextures != 0) { + if (mPointSpriteEnable) { + mShader += " vec2 t0 = gl_PointCoord;\n"; + } else { + mShader += " vec2 t0 = varTex0.xy;\n"; + } + } + + for(int i = 0; i < mNumTextures; i ++) { + switch(mSlots[i].env) { + case REPLACE: + switch (mSlots[i].format) { + case ALPHA: + mShader += " col.a = texture2D(UNI_Tex0, t0).a;\n"; + break; + case LUMINANCE_ALPHA: + mShader += " col.rgba = texture2D(UNI_Tex0, t0).rgba;\n"; + break; + case RGB: + mShader += " col.rgb = texture2D(UNI_Tex0, t0).rgb;\n"; + break; + case RGBA: + mShader += " col.rgba = texture2D(UNI_Tex0, t0).rgba;\n"; + break; + } + break; + case MODULATE: + switch (mSlots[i].format) { + case ALPHA: + mShader += " col.a *= texture2D(UNI_Tex0, t0).a;\n"; + break; + case LUMINANCE_ALPHA: + mShader += " col.rgba *= texture2D(UNI_Tex0, t0).rgba;\n"; + break; + case RGB: + mShader += " col.rgb *= texture2D(UNI_Tex0, t0).rgb;\n"; + break; + case RGBA: + mShader += " col.rgba *= texture2D(UNI_Tex0, t0).rgba;\n"; + break; + } + break; + case DECAL: + mShader += " col = texture2D(UNI_Tex0, t0);\n"; + break; + } + } + + mShader += " gl_FragColor = col;\n"; + mShader += "}\n"; + } + public Builder(RenderScript rs) { + super(rs); mRS = rs; mSlots = new Slot[MAX_TEXTURE]; mPointSpriteEnable = false; } - public void setTexture(EnvMode env, Format fmt, int slot) + public Builder setTexture(EnvMode env, Format fmt, int slot) throws IllegalArgumentException { if((slot < 0) || (slot >= MAX_TEXTURE)) { throw new IllegalArgumentException("MAX_TEXTURE exceeded."); } mSlots[slot] = new Slot(env, fmt); + return this; } - public void setPointSpriteTexCoordinateReplacement(boolean enable) { + public Builder setPointSpriteTexCoordinateReplacement(boolean enable) { mPointSpriteEnable = enable; + return this; + } + + public Builder setVaryingColor(boolean enable) { + mVaryingColorEnable = enable; + return this; } + @Override public ProgramFragment create() { - mRS.validate(); - int[] tmp = new int[MAX_TEXTURE * 2 + 1]; - if (mSlots[0] != null) { - tmp[0] = mSlots[0].env.mID; - tmp[1] = mSlots[0].format.mID; + mNumTextures = 0; + for(int i = 0; i < MAX_TEXTURE; i ++) { + if(mSlots[i] != null) { + mNumTextures ++; + } } - if (mSlots[1] != null) { - tmp[2] = mSlots[1].env.mID; - tmp[3] = mSlots[1].format.mID; + buildShaderString(); + Type constType = null; + if (!mVaryingColorEnable) { + Element.Builder b = new Element.Builder(mRS); + b.add(Element.F32_4(mRS), "Color"); + Type.Builder typeBuilder = new Type.Builder(mRS, b.create()); + typeBuilder.add(Dimension.X, 1); + constType = typeBuilder.create(); + addConstant(constType); } - tmp[4] = mPointSpriteEnable ? 1 : 0; - int id = mRS.nProgramFragmentCreate(tmp); - ProgramFragment pf = new ProgramFragment(id, mRS); + setTextureCount(mNumTextures); + + ProgramFragment pf = super.create(); pf.mTextureCount = MAX_TEXTURE; + if (!mVaryingColorEnable) { + Allocation constantData = Allocation.createTyped(mRS,constType); + float[] data = new float[4]; + data[0] = data[1] = data[2] = data[3] = 1.0f; + constantData.data(data); + pf.bindConstants(constantData, 0); + } return pf; } } diff --git a/graphics/java/android/renderscript/ProgramRaster.java b/graphics/java/android/renderscript/ProgramRaster.java index 56f9bf443f1180f447356ca0b33cb5bb7b8765c1..791dac8ca641bca12d8d9a552f54bb844e3419fd 100644 --- a/graphics/java/android/renderscript/ProgramRaster.java +++ b/graphics/java/android/renderscript/ProgramRaster.java @@ -26,76 +26,113 @@ import android.util.Log; * **/ public class ProgramRaster extends BaseObj { + + public enum CullMode { + BACK (0), + FRONT (1), + NONE (2); + + int mID; + CullMode(int id) { + mID = id; + } + } + boolean mPointSmooth; boolean mLineSmooth; boolean mPointSprite; - float mPointSize; float mLineWidth; - Element mIn; - Element mOut; + CullMode mCullMode; ProgramRaster(int id, RenderScript rs) { - super(rs); - mID = id; + super(id, rs); - mPointSize = 1.0f; mLineWidth = 1.0f; mPointSmooth = false; mLineSmooth = false; mPointSprite = false; + + mCullMode = CullMode.BACK; } - public void setLineWidth(float w) { + void setLineWidth(float w) { mRS.validate(); mLineWidth = w; mRS.nProgramRasterSetLineWidth(mID, w); } - public void setPointSize(float s) { + void setCullMode(CullMode m) { mRS.validate(); - mPointSize = s; - mRS.nProgramRasterSetPointSize(mID, s); + mCullMode = m; + mRS.nProgramRasterSetCullMode(mID, m.mID); } - void internalInit() { - int inID = 0; - int outID = 0; - if (mIn != null) { - inID = mIn.mID; + public static ProgramRaster CULL_BACK(RenderScript rs) { + if(rs.mProgramRaster_CULL_BACK == null) { + ProgramRaster.Builder builder = new ProgramRaster.Builder(rs); + builder.setCullMode(CullMode.BACK); + rs.mProgramRaster_CULL_BACK = builder.create(); } - if (mOut != null) { - outID = mOut.mID; + return rs.mProgramRaster_CULL_BACK; + } + + public static ProgramRaster CULL_FRONT(RenderScript rs) { + if(rs.mProgramRaster_CULL_FRONT == null) { + ProgramRaster.Builder builder = new ProgramRaster.Builder(rs); + builder.setCullMode(CullMode.FRONT); + rs.mProgramRaster_CULL_FRONT = builder.create(); } - mID = mRS.nProgramRasterCreate(inID, outID, mPointSmooth, mLineSmooth, mPointSprite); + return rs.mProgramRaster_CULL_FRONT; } + public static ProgramRaster CULL_NONE(RenderScript rs) { + if(rs.mProgramRaster_CULL_NONE == null) { + ProgramRaster.Builder builder = new ProgramRaster.Builder(rs); + builder.setCullMode(CullMode.NONE); + rs.mProgramRaster_CULL_NONE = builder.create(); + } + return rs.mProgramRaster_CULL_NONE; + } public static class Builder { RenderScript mRS; - ProgramRaster mPR; + boolean mPointSprite; + boolean mPointSmooth; + boolean mLineSmooth; + CullMode mCullMode; - public Builder(RenderScript rs, Element in, Element out) { + public Builder(RenderScript rs) { mRS = rs; - mPR = new ProgramRaster(0, rs); + mPointSmooth = false; + mLineSmooth = false; + mPointSprite = false; + mCullMode = CullMode.BACK; } - public void setPointSpriteEnable(boolean enable) { - mPR.mPointSprite = enable; + public Builder setPointSpriteEnable(boolean enable) { + mPointSprite = enable; + return this; } - public void setPointSmoothEnable(boolean enable) { - mPR.mPointSmooth = enable; + public Builder setPointSmoothEnable(boolean enable) { + mPointSmooth = enable; + return this; } - public void setLineSmoothEnable(boolean enable) { - mPR.mLineSmooth = enable; + public Builder setLineSmoothEnable(boolean enable) { + mLineSmooth = enable; + return this; } + public Builder setCullMode(CullMode m) { + mCullMode = m; + return this; + } static synchronized ProgramRaster internalCreate(RenderScript rs, Builder b) { - b.mPR.internalInit(); - ProgramRaster pr = b.mPR; - b.mPR = new ProgramRaster(0, b.mRS); + int id = rs.nProgramRasterCreate(b.mPointSmooth, b.mLineSmooth, b.mPointSprite); + ProgramRaster pr = new ProgramRaster(id, rs); + pr.setCullMode(b.mCullMode); return pr; } @@ -111,3 +148,4 @@ public class ProgramRaster extends BaseObj { + diff --git a/graphics/java/android/renderscript/ProgramStore.java b/graphics/java/android/renderscript/ProgramStore.java index 69be245483e9cc07c6966ce156b00e58541a6c67..32c0d013da701a7c0da3c699bd1186a1d8ba4274 100644 --- a/graphics/java/android/renderscript/ProgramStore.java +++ b/graphics/java/android/renderscript/ProgramStore.java @@ -76,11 +76,143 @@ public class ProgramStore extends BaseObj { ProgramStore(int id, RenderScript rs) { - super(rs); - mID = id; + super(id, rs); } + public static ProgramStore BLEND_NONE_DEPTH_TEST(RenderScript rs) { + if(rs.mProgramStore_BLEND_NONE_DEPTH_TEST == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.LESS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ZERO); + builder.setDitherEnable(false); + builder.setDepthMask(true); + rs.mProgramStore_BLEND_NONE_DEPTH_TEST = builder.create(); + } + return rs.mProgramStore_BLEND_NONE_DEPTH_TEST; + } + public static ProgramStore BLEND_NONE_DEPTH_NO_DEPTH(RenderScript rs) { + if(rs.mProgramStore_BLEND_NONE_DEPTH_NO_DEPTH == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.ALWAYS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ZERO); + builder.setDitherEnable(false); + builder.setDepthMask(false); + rs.mProgramStore_BLEND_NONE_DEPTH_NO_DEPTH = builder.create(); + } + return rs.mProgramStore_BLEND_NONE_DEPTH_NO_DEPTH; + } + public static ProgramStore BLEND_NONE_DEPTH_NO_TEST(RenderScript rs) { + if(rs.mProgramStore_BLEND_NONE_DEPTH_NO_TEST == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.ALWAYS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ZERO); + builder.setDitherEnable(false); + builder.setDepthMask(true); + rs.mProgramStore_BLEND_NONE_DEPTH_NO_TEST = builder.create(); + } + return rs.mProgramStore_BLEND_NONE_DEPTH_NO_TEST; + } + public static ProgramStore BLEND_NONE_DEPTH_NO_WRITE(RenderScript rs) { + if(rs.mProgramStore_BLEND_NONE_DEPTH_NO_WRITE == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.LESS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ZERO); + builder.setDitherEnable(false); + builder.setDepthMask(false); + rs.mProgramStore_BLEND_NONE_DEPTH_NO_WRITE = builder.create(); + } + return rs.mProgramStore_BLEND_NONE_DEPTH_NO_WRITE; + } + + public static ProgramStore BLEND_ALPHA_DEPTH_TEST(RenderScript rs) { + if(rs.mProgramStore_BLEND_ALPHA_DEPTH_TEST == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.LESS); + builder.setBlendFunc(BlendSrcFunc.SRC_ALPHA, BlendDstFunc.ONE_MINUS_SRC_ALPHA); + builder.setDitherEnable(false); + builder.setDepthMask(true); + rs.mProgramStore_BLEND_ALPHA_DEPTH_TEST = builder.create(); + } + return rs.mProgramStore_BLEND_ALPHA_DEPTH_TEST; + } + public static ProgramStore BLEND_ALPHA_DEPTH_NO_DEPTH(RenderScript rs) { + if(rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_DEPTH == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.ALWAYS); + builder.setBlendFunc(BlendSrcFunc.SRC_ALPHA, BlendDstFunc.ONE_MINUS_SRC_ALPHA); + builder.setDitherEnable(false); + builder.setDepthMask(false); + rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_DEPTH = builder.create(); + } + return rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_DEPTH; + } + public static ProgramStore BLEND_ALPHA_DEPTH_NO_TEST(RenderScript rs) { + if(rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_TEST == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.ALWAYS); + builder.setBlendFunc(BlendSrcFunc.SRC_ALPHA, BlendDstFunc.ONE_MINUS_SRC_ALPHA); + builder.setDitherEnable(false); + builder.setDepthMask(true); + rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_TEST = builder.create(); + } + return rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_TEST; + } + public static ProgramStore BLEND_ALPHA_DEPTH_NO_WRITE(RenderScript rs) { + if(rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_WRITE == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.LESS); + builder.setBlendFunc(BlendSrcFunc.SRC_ALPHA, BlendDstFunc.ONE_MINUS_SRC_ALPHA); + builder.setDitherEnable(false); + builder.setDepthMask(false); + rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_WRITE = builder.create(); + } + return rs.mProgramStore_BLEND_ALPHA_DEPTH_NO_WRITE; + } + public static ProgramStore BLEND_ADD_DEPTH_TEST(RenderScript rs) { + if(rs.mProgramStore_BLEND_ADD_DEPTH_TEST == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.LESS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ONE); + builder.setDitherEnable(false); + builder.setDepthMask(true); + rs.mProgramStore_BLEND_ADD_DEPTH_TEST = builder.create(); + } + return rs.mProgramStore_BLEND_ADD_DEPTH_TEST; + } + public static ProgramStore BLEND_ADD_DEPTH_NO_DEPTH(RenderScript rs) { + if(rs.mProgramStore_BLEND_ADD_DEPTH_NO_DEPTH == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.ALWAYS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ONE); + builder.setDitherEnable(false); + builder.setDepthMask(false); + rs.mProgramStore_BLEND_ADD_DEPTH_NO_DEPTH = builder.create(); + } + return rs.mProgramStore_BLEND_ADD_DEPTH_NO_DEPTH; + } + public static ProgramStore BLEND_ADD_DEPTH_NO_TEST(RenderScript rs) { + if(rs.mProgramStore_BLEND_ADD_DEPTH_NO_TEST == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.ALWAYS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ONE); + builder.setDitherEnable(false); + builder.setDepthMask(true); + rs.mProgramStore_BLEND_ADD_DEPTH_NO_DEPTH = builder.create(); + } + return rs.mProgramStore_BLEND_ADD_DEPTH_NO_TEST; + } + public static ProgramStore BLEND_ADD_DEPTH_NO_WRITE(RenderScript rs) { + if(rs.mProgramStore_BLEND_ADD_DEPTH_NO_WRITE == null) { + ProgramStore.Builder builder = new ProgramStore.Builder(rs); + builder.setDepthFunc(ProgramStore.DepthFunc.ALWAYS); + builder.setBlendFunc(BlendSrcFunc.ONE, BlendDstFunc.ONE); + builder.setDitherEnable(false); + builder.setDepthMask(false); + rs.mProgramStore_BLEND_ADD_DEPTH_NO_WRITE = builder.create(); + } + return rs.mProgramStore_BLEND_ADD_DEPTH_NO_WRITE; + } public static class Builder { RenderScript mRS; @@ -110,32 +242,49 @@ public class ProgramStore extends BaseObj { mColorMaskA = true; mBlendSrc = BlendSrcFunc.ONE; mBlendDst = BlendDstFunc.ZERO; + } - + public Builder(RenderScript rs) { + mRS = rs; + mIn = null; + mOut = null; + mDepthFunc = DepthFunc.ALWAYS; + mDepthMask = false; + mColorMaskR = true; + mColorMaskG = true; + mColorMaskB = true; + mColorMaskA = true; + mBlendSrc = BlendSrcFunc.ONE; + mBlendDst = BlendDstFunc.ZERO; } - public void setDepthFunc(DepthFunc func) { + public Builder setDepthFunc(DepthFunc func) { mDepthFunc = func; + return this; } - public void setDepthMask(boolean enable) { + public Builder setDepthMask(boolean enable) { mDepthMask = enable; + return this; } - public void setColorMask(boolean r, boolean g, boolean b, boolean a) { + public Builder setColorMask(boolean r, boolean g, boolean b, boolean a) { mColorMaskR = r; mColorMaskG = g; mColorMaskB = b; mColorMaskA = a; + return this; } - public void setBlendFunc(BlendSrcFunc src, BlendDstFunc dst) { + public Builder setBlendFunc(BlendSrcFunc src, BlendDstFunc dst) { mBlendSrc = src; mBlendDst = dst; + return this; } - public void setDitherEnable(boolean enable) { + public Builder setDitherEnable(boolean enable) { mDither = enable; + return this; } static synchronized ProgramStore internalCreate(RenderScript rs, Builder b) { @@ -147,17 +296,17 @@ public class ProgramStore extends BaseObj { if (b.mOut != null) { outID = b.mOut.mID; } - rs.nProgramFragmentStoreBegin(inID, outID); - rs.nProgramFragmentStoreDepthFunc(b.mDepthFunc.mID); - rs.nProgramFragmentStoreDepthMask(b.mDepthMask); - rs.nProgramFragmentStoreColorMask(b.mColorMaskR, + rs.nProgramStoreBegin(inID, outID); + rs.nProgramStoreDepthFunc(b.mDepthFunc.mID); + rs.nProgramStoreDepthMask(b.mDepthMask); + rs.nProgramStoreColorMask(b.mColorMaskR, b.mColorMaskG, b.mColorMaskB, b.mColorMaskA); - rs.nProgramFragmentStoreBlendFunc(b.mBlendSrc.mID, b.mBlendDst.mID); - rs.nProgramFragmentStoreDither(b.mDither); + rs.nProgramStoreBlendFunc(b.mBlendSrc.mID, b.mBlendDst.mID); + rs.nProgramStoreDither(b.mDither); - int id = rs.nProgramFragmentStoreCreate(); + int id = rs.nProgramStoreCreate(); return new ProgramStore(id, rs); } diff --git a/graphics/java/android/renderscript/ProgramVertex.java b/graphics/java/android/renderscript/ProgramVertex.java index 1b155d7c4bf833d66fb3c4167b76472b1e9350f3..119db69598e1f7695d53cb57f8b1dc4d7d6596cc 100644 --- a/graphics/java/android/renderscript/ProgramVertex.java +++ b/graphics/java/android/renderscript/ProgramVertex.java @@ -17,6 +17,7 @@ package android.renderscript; +import android.graphics.Matrix; import android.util.Config; import android.util.Log; @@ -38,25 +39,6 @@ public class ProgramVertex extends Program { bindConstants(va.mAlloc, 0); } - - public static class Builder { - RenderScript mRS; - boolean mTextureMatrixEnable; - - public Builder(RenderScript rs, Element in, Element out) { - mRS = rs; - } - - public void setTextureMatrixEnable(boolean enable) { - mTextureMatrixEnable = enable; - } - - public ProgramVertex create() { - int id = mRS.nProgramVertexCreate(mTextureMatrixEnable); - return new ProgramVertex(id, mRS); - } - } - public static class ShaderBuilder extends BaseProgramBuilder { public ShaderBuilder(RenderScript rs) { super(rs); @@ -89,6 +71,68 @@ public class ProgramVertex extends Program { } } + public static class Builder extends ShaderBuilder { + boolean mTextureMatrixEnable; + + public Builder(RenderScript rs, Element in, Element out) { + super(rs); + } + public Builder(RenderScript rs) { + super(rs); + } + + public Builder setTextureMatrixEnable(boolean enable) { + mTextureMatrixEnable = enable; + return this; + } + static Type getConstantInputType(RenderScript rs) { + Element.Builder b = new Element.Builder(rs); + b.add(Element.MATRIX4X4(rs), "MV"); + b.add(Element.MATRIX4X4(rs), "P"); + b.add(Element.MATRIX4X4(rs), "TexMatrix"); + b.add(Element.MATRIX4X4(rs), "MVP"); + + Type.Builder typeBuilder = new Type.Builder(rs, b.create()); + typeBuilder.add(Dimension.X, 1); + return typeBuilder.create(); + } + + private void buildShaderString() { + + mShader = "//rs_shader_internal\n"; + mShader += "varying vec4 varColor;\n"; + mShader += "varying vec4 varTex0;\n"; + + mShader += "void main() {\n"; + mShader += " gl_Position = UNI_MVP * ATTRIB_position;\n"; + mShader += " gl_PointSize = 1.0;\n"; + + mShader += " varColor = ATTRIB_color;\n"; + if (mTextureMatrixEnable) { + mShader += " varTex0 = UNI_TexMatrix * ATTRIB_texture0;\n"; + } else { + mShader += " varTex0 = ATTRIB_texture0;\n"; + } + mShader += "}\n"; + } + + @Override + public ProgramVertex create() { + buildShaderString(); + + addConstant(getConstantInputType(mRS)); + + Element.Builder b = new Element.Builder(mRS); + b.add(Element.F32_4(mRS), "position"); + b.add(Element.F32_4(mRS), "color"); + b.add(Element.F32_3(mRS), "normal"); + b.add(Element.F32_4(mRS), "texture0"); + addInput(b.create()); + + return super.create(); + } + } + public static class MatrixAllocation { @@ -101,16 +145,17 @@ public class ProgramVertex extends Program { Matrix4f mTexture; public Allocation mAlloc; + private FieldPacker mIOBuffer; public MatrixAllocation(RenderScript rs) { - mModel = new Matrix4f(); - mProjection = new Matrix4f(); - mTexture = new Matrix4f(); - - mAlloc = Allocation.createSized(rs, Element.createUser(rs, Element.DataType.FLOAT_32), 48); - mAlloc.subData1D(MODELVIEW_OFFSET, 16, mModel.mMat); - mAlloc.subData1D(PROJECTION_OFFSET, 16, mProjection.mMat); - mAlloc.subData1D(TEXTURE_OFFSET, 16, mTexture.mMat); + Type constInputType = ProgramVertex.Builder.getConstantInputType(rs); + mAlloc = Allocation.createTyped(rs, constInputType); + int bufferSize = constInputType.getElement().getSizeBytes()* + constInputType.getElementCount(); + mIOBuffer = new FieldPacker(bufferSize); + loadModelview(new Matrix4f()); + loadProjection(new Matrix4f()); + loadTexture(new Matrix4f()); } public void destroy() { @@ -118,24 +163,32 @@ public class ProgramVertex extends Program { mAlloc = null; } + private void addToBuffer(int offset, Matrix4f m) { + mIOBuffer.reset(offset); + for(int i = 0; i < 16; i ++) { + mIOBuffer.addF32(m.mMat[i]); + } + mAlloc.data(mIOBuffer.getData()); + } + public void loadModelview(Matrix4f m) { mModel = m; - mAlloc.subData1D(MODELVIEW_OFFSET, 16, m.mMat); + addToBuffer(MODELVIEW_OFFSET*4, m); } public void loadProjection(Matrix4f m) { mProjection = m; - mAlloc.subData1D(PROJECTION_OFFSET, 16, m.mMat); + addToBuffer(PROJECTION_OFFSET*4, m); } public void loadTexture(Matrix4f m) { mTexture = m; - mAlloc.subData1D(TEXTURE_OFFSET, 16, m.mMat); + addToBuffer(TEXTURE_OFFSET*4, m); } public void setupOrthoWindow(int w, int h) { mProjection.loadOrtho(0,w, h,0, -1,1); - mAlloc.subData1D(PROJECTION_OFFSET, 16, mProjection.mMat); + addToBuffer(PROJECTION_OFFSET*4, mProjection); } public void setupOrthoNormalized(int w, int h) { @@ -147,7 +200,7 @@ public class ProgramVertex extends Program { float aspect = ((float)h) / w; mProjection.loadOrtho(-1,1, -aspect,aspect, -1,1); } - mAlloc.subData1D(PROJECTION_OFFSET, 16, mProjection.mMat); + addToBuffer(PROJECTION_OFFSET*4, mProjection); } public void setupProjectionNormalized(int w, int h) { @@ -173,7 +226,7 @@ public class ProgramVertex extends Program { m1.loadMultiply(m1, m2); mProjection = m1; - mAlloc.subData1D(PROJECTION_OFFSET, 16, mProjection.mMat); + addToBuffer(PROJECTION_OFFSET*4, mProjection); } } diff --git a/graphics/java/android/renderscript/RenderScript.java b/graphics/java/android/renderscript/RenderScript.java index a9352436925d3527ff68729c7b14970dc36d515d..159e070681e6587fff60d04cf9d4063484fdd159 100644 --- a/graphics/java/android/renderscript/RenderScript.java +++ b/graphics/java/android/renderscript/RenderScript.java @@ -57,151 +57,470 @@ public class RenderScript { } } + // Non-threadsafe functions. native void nInitElements(int a8, int rgba4444, int rgba8888, int rgb565); - native int nDeviceCreate(); native void nDeviceDestroy(int dev); native void nDeviceSetConfig(int dev, int param, int value); - native int nContextCreateGL(int dev, int ver, boolean useDepth); - native int nContextCreate(int dev, int ver); - native void nContextDestroy(int con); - native void nContextSetSurface(int w, int h, Surface sur); - native void nContextSetPriority(int p); - native void nContextDump(int bits); - - native void nContextBindRootScript(int script); - native void nContextBindSampler(int sampler, int slot); - native void nContextBindProgramFragmentStore(int pfs); - native void nContextBindProgramFragment(int pf); - native void nContextBindProgramVertex(int pf); - native void nContextBindProgramRaster(int pr); - native void nContextPause(); - native void nContextResume(); - native int nContextGetMessage(int[] data, boolean wait); - native void nContextInitToClient(); - native void nContextDeinitToClient(); - - native void nAssignName(int obj, byte[] name); - native void nObjDestroy(int id); - native void nObjDestroyOOB(int id); - native int nFileOpen(byte[] name); - - - native int nElementCreate(int type, int kind, boolean norm, int vecSize); - native int nElementCreate2(int[] elements, String[] names); - - native void nTypeBegin(int elementID); - native void nTypeAdd(int dim, int val); - native int nTypeCreate(); - native void nTypeFinalDestroy(Type t); - native void nTypeSetupFields(Type t, int[] types, int[] bits, Field[] IDs); - - native int nAllocationCreateTyped(int type); - native int nAllocationCreateFromBitmap(int dstFmt, boolean genMips, Bitmap bmp); - native int nAllocationCreateBitmapRef(int type, Bitmap bmp); - native int nAllocationCreateFromBitmapBoxed(int dstFmt, boolean genMips, Bitmap bmp); - native int nAllocationCreateFromAssetStream(int dstFmt, boolean genMips, int assetStream); - - native void nAllocationUploadToTexture(int alloc, boolean genMips, int baseMioLevel); - native void nAllocationUploadToBufferObject(int alloc); - - native void nAllocationSubData1D(int id, int off, int count, int[] d, int sizeBytes); - native void nAllocationSubData1D(int id, int off, int count, short[] d, int sizeBytes); - native void nAllocationSubData1D(int id, int off, int count, byte[] d, int sizeBytes); - native void nAllocationSubData1D(int id, int off, int count, float[] d, int sizeBytes); - - native void nAllocationSubData2D(int id, int xoff, int yoff, int w, int h, int[] d, int sizeBytes); - native void nAllocationSubData2D(int id, int xoff, int yoff, int w, int h, float[] d, int sizeBytes); - native void nAllocationRead(int id, int[] d); - native void nAllocationRead(int id, float[] d); - native void nAllocationSubDataFromObject(int id, Type t, int offset, Object o); - native void nAllocationSubReadFromObject(int id, Type t, int offset, Object o); - - native void nAdapter1DBindAllocation(int ad, int alloc); - native void nAdapter1DSetConstraint(int ad, int dim, int value); - native void nAdapter1DData(int ad, int[] d); - native void nAdapter1DData(int ad, float[] d); - native void nAdapter1DSubData(int ad, int off, int count, int[] d); - native void nAdapter1DSubData(int ad, int off, int count, float[] d); - native int nAdapter1DCreate(); - - native void nAdapter2DBindAllocation(int ad, int alloc); - native void nAdapter2DSetConstraint(int ad, int dim, int value); - native void nAdapter2DData(int ad, int[] d); - native void nAdapter2DData(int ad, float[] d); - native void nAdapter2DSubData(int ad, int xoff, int yoff, int w, int h, int[] d); - native void nAdapter2DSubData(int ad, int xoff, int yoff, int w, int h, float[] d); - native int nAdapter2DCreate(); - - native void nScriptBindAllocation(int script, int alloc, int slot); - native void nScriptSetClearColor(int script, float r, float g, float b, float a); - native void nScriptSetClearDepth(int script, float depth); - native void nScriptSetClearStencil(int script, int stencil); - native void nScriptSetTimeZone(int script, byte[] timeZone); - native void nScriptSetType(int type, boolean writable, String name, int slot); - native void nScriptSetRoot(boolean isRoot); - native void nScriptSetInvokable(String name, int slot); - native void nScriptInvoke(int id, int slot); - - native void nScriptCBegin(); - native void nScriptCSetScript(byte[] script, int offset, int length); - native int nScriptCCreate(); - native void nScriptCAddDefineI32(String name, int value); - native void nScriptCAddDefineF(String name, float value); - - native void nSamplerBegin(); - native void nSamplerSet(int param, int value); - native int nSamplerCreate(); - - native void nProgramFragmentStoreBegin(int in, int out); - native void nProgramFragmentStoreDepthFunc(int func); - native void nProgramFragmentStoreDepthMask(boolean enable); - native void nProgramFragmentStoreColorMask(boolean r, boolean g, boolean b, boolean a); - native void nProgramFragmentStoreBlendFunc(int src, int dst); - native void nProgramFragmentStoreDither(boolean enable); - native int nProgramFragmentStoreCreate(); - - native int nProgramRasterCreate(int in, int out, boolean pointSmooth, boolean lineSmooth, boolean pointSprite); - native void nProgramRasterSetLineWidth(int pr, float v); - native void nProgramRasterSetPointSize(int pr, float v); - - native void nProgramBindConstants(int pv, int slot, int mID); - native void nProgramBindTexture(int vpf, int slot, int a); - native void nProgramBindSampler(int vpf, int slot, int s); - - native int nProgramFragmentCreate(int[] params); - native int nProgramFragmentCreate2(String shader, int[] params); - - native int nProgramVertexCreate(boolean texMat); - native int nProgramVertexCreate2(String shader, int[] params); - - native void nLightBegin(); - native void nLightSetIsMono(boolean isMono); - native void nLightSetIsLocal(boolean isLocal); - native int nLightCreate(); - native void nLightSetColor(int l, float r, float g, float b); - native void nLightSetPosition(int l, float x, float y, float z); - - native int nSimpleMeshCreate(int batchID, int idxID, int[] vtxID, int prim); - native void nSimpleMeshBindVertex(int id, int alloc, int slot); - native void nSimpleMeshBindIndex(int id, int alloc); - - native void nAnimationBegin(int attribCount, int keyframeCount); - native void nAnimationAdd(float time, float[] attribs); - native int nAnimationCreate(); + native int nContextGetMessage(int con, int[] data, boolean wait); + native void nContextInitToClient(int con); + native void nContextDeinitToClient(int con); + + + // Methods below are wrapped to protect the non-threadsafe + // lockless fifo. + native int rsnContextCreateGL(int dev, int ver, boolean useDepth); + synchronized int nContextCreateGL(int dev, int ver, boolean useDepth) { + return rsnContextCreateGL(dev, ver, useDepth); + } + native int rsnContextCreate(int dev, int ver); + synchronized int nContextCreate(int dev, int ver) { + return rsnContextCreate(dev, ver); + } + native void rsnContextDestroy(int con); + synchronized void nContextDestroy() { + rsnContextDestroy(mContext); + } + native void rsnContextSetSurface(int con, int w, int h, Surface sur); + synchronized void nContextSetSurface(int w, int h, Surface sur) { + rsnContextSetSurface(mContext, w, h, sur); + } + native void rsnContextSetPriority(int con, int p); + synchronized void nContextSetPriority(int p) { + rsnContextSetPriority(mContext, p); + } + native void rsnContextDump(int con, int bits); + synchronized void nContextDump(int bits) { + rsnContextDump(mContext, bits); + } + native void rsnContextFinish(int con); + synchronized void nContextFinish() { + rsnContextFinish(mContext); + } + + native void rsnContextBindRootScript(int con, int script); + synchronized void nContextBindRootScript(int script) { + rsnContextBindRootScript(mContext, script); + } + native void rsnContextBindSampler(int con, int sampler, int slot); + synchronized void nContextBindSampler(int sampler, int slot) { + rsnContextBindSampler(mContext, sampler, slot); + } + native void rsnContextBindProgramStore(int con, int pfs); + synchronized void nContextBindProgramStore(int pfs) { + rsnContextBindProgramStore(mContext, pfs); + } + native void rsnContextBindProgramFragment(int con, int pf); + synchronized void nContextBindProgramFragment(int pf) { + rsnContextBindProgramFragment(mContext, pf); + } + native void rsnContextBindProgramVertex(int con, int pv); + synchronized void nContextBindProgramVertex(int pv) { + rsnContextBindProgramVertex(mContext, pv); + } + native void rsnContextBindProgramRaster(int con, int pr); + synchronized void nContextBindProgramRaster(int pr) { + rsnContextBindProgramRaster(mContext, pr); + } + native void rsnContextPause(int con); + synchronized void nContextPause() { + rsnContextPause(mContext); + } + native void rsnContextResume(int con); + synchronized void nContextResume() { + rsnContextResume(mContext); + } + + native void rsnAssignName(int con, int obj, byte[] name); + synchronized void nAssignName(int obj, byte[] name) { + rsnAssignName(mContext, obj, name); + } + native String rsnGetName(int con, int obj); + synchronized String nGetName(int obj) { + return rsnGetName(mContext, obj); + } + native void rsnObjDestroy(int con, int id); + synchronized void nObjDestroy(int id) { + rsnObjDestroy(mContext, id); + } + native int rsnFileOpen(int con, byte[] name); + synchronized int nFileOpen(byte[] name) { + return rsnFileOpen(mContext, name); + } + + native int rsnElementCreate(int con, int type, int kind, boolean norm, int vecSize); + synchronized int nElementCreate(int type, int kind, boolean norm, int vecSize) { + return rsnElementCreate(mContext, type, kind, norm, vecSize); + } + native int rsnElementCreate2(int con, int[] elements, String[] names, int[] arraySizes); + synchronized int nElementCreate2(int[] elements, String[] names, int[] arraySizes) { + return rsnElementCreate2(mContext, elements, names, arraySizes); + } + native void rsnElementGetNativeData(int con, int id, int[] elementData); + synchronized void nElementGetNativeData(int id, int[] elementData) { + rsnElementGetNativeData(mContext, id, elementData); + } + native void rsnElementGetSubElements(int con, int id, int[] IDs, String[] names); + synchronized void nElementGetSubElements(int id, int[] IDs, String[] names) { + rsnElementGetSubElements(mContext, id, IDs, names); + } + + native void rsnTypeBegin(int con, int elementID); + synchronized void nTypeBegin(int elementID) { + rsnTypeBegin(mContext, elementID); + } + native void rsnTypeAdd(int con, int dim, int val); + synchronized void nTypeAdd(int dim, int val) { + rsnTypeAdd(mContext, dim, val); + } + native int rsnTypeCreate(int con); + synchronized int nTypeCreate() { + return rsnTypeCreate(mContext); + } + native void rsnTypeGetNativeData(int con, int id, int[] typeData); + synchronized void nTypeGetNativeData(int id, int[] typeData) { + rsnTypeGetNativeData(mContext, id, typeData); + } + + native int rsnAllocationCreateTyped(int con, int type); + synchronized int nAllocationCreateTyped(int type) { + return rsnAllocationCreateTyped(mContext, type); + } + native int rsnAllocationCreateFromBitmap(int con, int dstFmt, boolean genMips, Bitmap bmp); + synchronized int nAllocationCreateFromBitmap(int dstFmt, boolean genMips, Bitmap bmp) { + return rsnAllocationCreateFromBitmap(mContext, dstFmt, genMips, bmp); + } + native int rsnAllocationCreateBitmapRef(int con, int type, Bitmap bmp); + synchronized int nAllocationCreateBitmapRef(int type, Bitmap bmp) { + return rsnAllocationCreateBitmapRef(mContext, type, bmp); + } + native int rsnAllocationCreateFromBitmapBoxed(int con, int dstFmt, boolean genMips, Bitmap bmp); + synchronized int nAllocationCreateFromBitmapBoxed(int dstFmt, boolean genMips, Bitmap bmp) { + return rsnAllocationCreateFromBitmapBoxed(mContext, dstFmt, genMips, bmp); + } + native int rsnAllocationCreateFromAssetStream(int con, int dstFmt, boolean genMips, int assetStream); + synchronized int nAllocationCreateFromAssetStream(int dstFmt, boolean genMips, int assetStream) { + return rsnAllocationCreateFromAssetStream(mContext, dstFmt, genMips, assetStream); + } + + native void rsnAllocationUploadToTexture(int con, int alloc, boolean genMips, int baseMioLevel); + synchronized void nAllocationUploadToTexture(int alloc, boolean genMips, int baseMioLevel) { + rsnAllocationUploadToTexture(mContext, alloc, genMips, baseMioLevel); + } + native void rsnAllocationUploadToBufferObject(int con, int alloc); + synchronized void nAllocationUploadToBufferObject(int alloc) { + rsnAllocationUploadToBufferObject(mContext, alloc); + } + + native void rsnAllocationSubData1D(int con, int id, int off, int count, int[] d, int sizeBytes); + synchronized void nAllocationSubData1D(int id, int off, int count, int[] d, int sizeBytes) { + rsnAllocationSubData1D(mContext, id, off, count, d, sizeBytes); + } + native void rsnAllocationSubData1D(int con, int id, int off, int count, short[] d, int sizeBytes); + synchronized void nAllocationSubData1D(int id, int off, int count, short[] d, int sizeBytes) { + rsnAllocationSubData1D(mContext, id, off, count, d, sizeBytes); + } + native void rsnAllocationSubData1D(int con, int id, int off, int count, byte[] d, int sizeBytes); + synchronized void nAllocationSubData1D(int id, int off, int count, byte[] d, int sizeBytes) { + rsnAllocationSubData1D(mContext, id, off, count, d, sizeBytes); + } + native void rsnAllocationSubElementData1D(int con, int id, int xoff, int compIdx, byte[] d, int sizeBytes); + synchronized void nAllocationSubElementData1D(int id, int xoff, int compIdx, byte[] d, int sizeBytes) { + rsnAllocationSubElementData1D(mContext, id, xoff, compIdx, d, sizeBytes); + } + native void rsnAllocationSubData1D(int con, int id, int off, int count, float[] d, int sizeBytes); + synchronized void nAllocationSubData1D(int id, int off, int count, float[] d, int sizeBytes) { + rsnAllocationSubData1D(mContext, id, off, count, d, sizeBytes); + } + + native void rsnAllocationSubData2D(int con, int id, int xoff, int yoff, int w, int h, int[] d, int sizeBytes); + synchronized void nAllocationSubData2D(int id, int xoff, int yoff, int w, int h, int[] d, int sizeBytes) { + rsnAllocationSubData2D(mContext, id, xoff, yoff, w, h, d, sizeBytes); + } + native void rsnAllocationSubData2D(int con, int id, int xoff, int yoff, int w, int h, float[] d, int sizeBytes); + synchronized void nAllocationSubData2D(int id, int xoff, int yoff, int w, int h, float[] d, int sizeBytes) { + rsnAllocationSubData2D(mContext, id, xoff, yoff, w, h, d, sizeBytes); + } + native void rsnAllocationRead(int con, int id, int[] d); + synchronized void nAllocationRead(int id, int[] d) { + rsnAllocationRead(mContext, id, d); + } + native void rsnAllocationRead(int con, int id, float[] d); + synchronized void nAllocationRead(int id, float[] d) { + rsnAllocationRead(mContext, id, d); + } + native int rsnAllocationGetType(int con, int id); + synchronized int nAllocationGetType(int id) { + return rsnAllocationGetType(mContext, id); + } + + native int rsnFileA3DCreateFromAssetStream(int con, int assetStream); + synchronized int nFileA3DCreateFromAssetStream(int assetStream) { + return rsnFileA3DCreateFromAssetStream(mContext, assetStream); + } + native int rsnFileA3DGetNumIndexEntries(int con, int fileA3D); + synchronized int nFileA3DGetNumIndexEntries(int fileA3D) { + return rsnFileA3DGetNumIndexEntries(mContext, fileA3D); + } + native void rsnFileA3DGetIndexEntries(int con, int fileA3D, int numEntries, int[] IDs, String[] names); + synchronized void nFileA3DGetIndexEntries(int fileA3D, int numEntries, int[] IDs, String[] names) { + rsnFileA3DGetIndexEntries(mContext, fileA3D, numEntries, IDs, names); + } + native int rsnFileA3DGetEntryByIndex(int con, int fileA3D, int index); + synchronized int nFileA3DGetEntryByIndex(int fileA3D, int index) { + return rsnFileA3DGetEntryByIndex(mContext, fileA3D, index); + } + + native int rsnFontCreateFromFile(int con, String fileName, int size, int dpi); + synchronized int nFontCreateFromFile(String fileName, int size, int dpi) { + return rsnFontCreateFromFile(mContext, fileName, size, dpi); + } + + native void rsnAdapter1DBindAllocation(int con, int ad, int alloc); + synchronized void nAdapter1DBindAllocation(int ad, int alloc) { + rsnAdapter1DBindAllocation(mContext, ad, alloc); + } + native void rsnAdapter1DSetConstraint(int con, int ad, int dim, int value); + synchronized void nAdapter1DSetConstraint(int ad, int dim, int value) { + rsnAdapter1DSetConstraint(mContext, ad, dim, value); + } + native void rsnAdapter1DData(int con, int ad, int[] d); + synchronized void nAdapter1DData(int ad, int[] d) { + rsnAdapter1DData(mContext, ad, d); + } + native void rsnAdapter1DData(int con, int ad, float[] d); + synchronized void nAdapter1DData(int ad, float[] d) { + rsnAdapter1DData(mContext, ad, d); + } + native void rsnAdapter1DSubData(int con, int ad, int off, int count, int[] d); + synchronized void nAdapter1DSubData(int ad, int off, int count, int[] d) { + rsnAdapter1DSubData(mContext, ad, off, count, d); + } + native void rsnAdapter1DSubData(int con, int ad, int off, int count, float[] d); + synchronized void nAdapter1DSubData(int ad, int off, int count, float[] d) { + rsnAdapter1DSubData(mContext, ad, off, count, d); + } + native int rsnAdapter1DCreate(int con); + synchronized int nAdapter1DCreate() { + return rsnAdapter1DCreate(mContext); + } + + native void rsnAdapter2DBindAllocation(int con, int ad, int alloc); + synchronized void nAdapter2DBindAllocation(int ad, int alloc) { + rsnAdapter2DBindAllocation(mContext, ad, alloc); + } + native void rsnAdapter2DSetConstraint(int con, int ad, int dim, int value); + synchronized void nAdapter2DSetConstraint(int ad, int dim, int value) { + rsnAdapter2DSetConstraint(mContext, ad, dim, value); + } + native void rsnAdapter2DData(int con, int ad, int[] d); + synchronized void nAdapter2DData(int ad, int[] d) { + rsnAdapter2DData(mContext, ad, d); + } + native void rsnAdapter2DData(int con, int ad, float[] d); + synchronized void nAdapter2DData(int ad, float[] d) { + rsnAdapter2DData(mContext, ad, d); + } + native void rsnAdapter2DSubData(int con, int ad, int xoff, int yoff, int w, int h, int[] d); + synchronized void nAdapter2DSubData(int ad, int xoff, int yoff, int w, int h, int[] d) { + rsnAdapter2DSubData(mContext, ad, xoff, yoff, w, h, d); + } + native void rsnAdapter2DSubData(int con, int ad, int xoff, int yoff, int w, int h, float[] d); + synchronized void nAdapter2DSubData(int ad, int xoff, int yoff, int w, int h, float[] d) { + rsnAdapter2DSubData(mContext, ad, xoff, yoff, w, h, d); + } + native int rsnAdapter2DCreate(int con); + synchronized int nAdapter2DCreate() { + return rsnAdapter2DCreate(mContext); + } + + native void rsnScriptBindAllocation(int con, int script, int alloc, int slot); + synchronized void nScriptBindAllocation(int script, int alloc, int slot) { + rsnScriptBindAllocation(mContext, script, alloc, slot); + } + native void rsnScriptSetTimeZone(int con, int script, byte[] timeZone); + synchronized void nScriptSetTimeZone(int script, byte[] timeZone) { + rsnScriptSetTimeZone(mContext, script, timeZone); + } + native void rsnScriptInvoke(int con, int id, int slot); + synchronized void nScriptInvoke(int id, int slot) { + rsnScriptInvoke(mContext, id, slot); + } + native void rsnScriptInvokeV(int con, int id, int slot, byte[] params); + synchronized void nScriptInvokeV(int id, int slot, byte[] params) { + rsnScriptInvokeV(mContext, id, slot, params); + } + native void rsnScriptSetVarI(int con, int id, int slot, int val); + synchronized void nScriptSetVarI(int id, int slot, int val) { + rsnScriptSetVarI(mContext, id, slot, val); + } + native void rsnScriptSetVarF(int con, int id, int slot, float val); + synchronized void nScriptSetVarF(int id, int slot, float val) { + rsnScriptSetVarF(mContext, id, slot, val); + } + native void rsnScriptSetVarD(int con, int id, int slot, double val); + synchronized void nScriptSetVarD(int id, int slot, double val) { + rsnScriptSetVarD(mContext, id, slot, val); + } + native void rsnScriptSetVarV(int con, int id, int slot, byte[] val); + synchronized void nScriptSetVarV(int id, int slot, byte[] val) { + rsnScriptSetVarV(mContext, id, slot, val); + } + + native void rsnScriptCBegin(int con); + synchronized void nScriptCBegin() { + rsnScriptCBegin(mContext); + } + native void rsnScriptCSetScript(int con, byte[] script, int offset, int length); + synchronized void nScriptCSetScript(byte[] script, int offset, int length) { + rsnScriptCSetScript(mContext, script, offset, length); + } + native int rsnScriptCCreate(int con); + synchronized int nScriptCCreate() { + return rsnScriptCCreate(mContext); + } + + native void rsnSamplerBegin(int con); + synchronized void nSamplerBegin() { + rsnSamplerBegin(mContext); + } + native void rsnSamplerSet(int con, int param, int value); + synchronized void nSamplerSet(int param, int value) { + rsnSamplerSet(mContext, param, value); + } + native int rsnSamplerCreate(int con); + synchronized int nSamplerCreate() { + return rsnSamplerCreate(mContext); + } + + native void rsnProgramStoreBegin(int con, int in, int out); + synchronized void nProgramStoreBegin(int in, int out) { + rsnProgramStoreBegin(mContext, in, out); + } + native void rsnProgramStoreDepthFunc(int con, int func); + synchronized void nProgramStoreDepthFunc(int func) { + rsnProgramStoreDepthFunc(mContext, func); + } + native void rsnProgramStoreDepthMask(int con, boolean enable); + synchronized void nProgramStoreDepthMask(boolean enable) { + rsnProgramStoreDepthMask(mContext, enable); + } + native void rsnProgramStoreColorMask(int con, boolean r, boolean g, boolean b, boolean a); + synchronized void nProgramStoreColorMask(boolean r, boolean g, boolean b, boolean a) { + rsnProgramStoreColorMask(mContext, r, g, b, a); + } + native void rsnProgramStoreBlendFunc(int con, int src, int dst); + synchronized void nProgramStoreBlendFunc(int src, int dst) { + rsnProgramStoreBlendFunc(mContext, src, dst); + } + native void rsnProgramStoreDither(int con, boolean enable); + synchronized void nProgramStoreDither(boolean enable) { + rsnProgramStoreDither(mContext, enable); + } + native int rsnProgramStoreCreate(int con); + synchronized int nProgramStoreCreate() { + return rsnProgramStoreCreate(mContext); + } + + native int rsnProgramRasterCreate(int con, boolean pointSmooth, boolean lineSmooth, boolean pointSprite); + synchronized int nProgramRasterCreate(boolean pointSmooth, boolean lineSmooth, boolean pointSprite) { + return rsnProgramRasterCreate(mContext, pointSmooth, lineSmooth, pointSprite); + } + native void rsnProgramRasterSetLineWidth(int con, int pr, float v); + synchronized void nProgramRasterSetLineWidth(int pr, float v) { + rsnProgramRasterSetLineWidth(mContext, pr, v); + } + native void rsnProgramRasterSetCullMode(int con, int pr, int mode); + synchronized void nProgramRasterSetCullMode(int pr, int mode) { + rsnProgramRasterSetCullMode(mContext, pr, mode); + } + + native void rsnProgramBindConstants(int con, int pv, int slot, int mID); + synchronized void nProgramBindConstants(int pv, int slot, int mID) { + rsnProgramBindConstants(mContext, pv, slot, mID); + } + native void rsnProgramBindTexture(int con, int vpf, int slot, int a); + synchronized void nProgramBindTexture(int vpf, int slot, int a) { + rsnProgramBindTexture(mContext, vpf, slot, a); + } + native void rsnProgramBindSampler(int con, int vpf, int slot, int s); + synchronized void nProgramBindSampler(int vpf, int slot, int s) { + rsnProgramBindSampler(mContext, vpf, slot, s); + } + + native int rsnProgramFragmentCreate(int con, int[] params); + synchronized int nProgramFragmentCreate(int[] params) { + return rsnProgramFragmentCreate(mContext, params); + } + native int rsnProgramFragmentCreate2(int con, String shader, int[] params); + synchronized int nProgramFragmentCreate2(String shader, int[] params) { + return rsnProgramFragmentCreate2(mContext, shader, params); + } + + native int rsnProgramVertexCreate(int con, boolean texMat); + synchronized int nProgramVertexCreate(boolean texMat) { + return rsnProgramVertexCreate(mContext, texMat); + } + native int rsnProgramVertexCreate2(int con, String shader, int[] params); + synchronized int nProgramVertexCreate2(String shader, int[] params) { + return rsnProgramVertexCreate2(mContext, shader, params); + } + + + native int rsnMeshCreate(int con, int vtxCount, int indexCount); + synchronized int nMeshCreate(int vtxCount, int indexCount) { + return rsnMeshCreate(mContext, vtxCount, indexCount); + } + native void rsnMeshBindVertex(int con, int id, int alloc, int slot); + synchronized void nMeshBindVertex(int id, int alloc, int slot) { + rsnMeshBindVertex(mContext, id, alloc, slot); + } + native void rsnMeshBindIndex(int con, int id, int alloc, int prim, int slot); + synchronized void nMeshBindIndex(int id, int alloc, int prim, int slot) { + rsnMeshBindIndex(mContext, id, alloc, prim, slot); + } + native int rsnMeshGetVertexBufferCount(int con, int id); + synchronized int nMeshGetVertexBufferCount(int id) { + return rsnMeshGetVertexBufferCount(mContext, id); + } + native int rsnMeshGetIndexCount(int con, int id); + synchronized int nMeshGetIndexCount(int id) { + return rsnMeshGetIndexCount(mContext, id); + } + native void rsnMeshGetVertices(int con, int id, int[] vtxIds, int vtxIdCount); + synchronized void nMeshGetVertices(int id, int[] vtxIds, int vtxIdCount) { + rsnMeshGetVertices(mContext, id, vtxIds, vtxIdCount); + } + native void rsnMeshGetIndices(int con, int id, int[] idxIds, int[] primitives, int vtxIdCount); + synchronized void nMeshGetIndices(int id, int[] idxIds, int[] primitives, int vtxIdCount) { + rsnMeshGetIndices(mContext, id, idxIds, primitives, vtxIdCount); + } + protected int mDev; protected int mContext; @SuppressWarnings({"FieldCanBeLocal"}) protected MessageThread mMessageThread; - Element mElement_USER_U8; - Element mElement_USER_I8; - Element mElement_USER_U16; - Element mElement_USER_I16; - Element mElement_USER_U32; - Element mElement_USER_I32; - Element mElement_USER_F32; + Element mElement_U8; + Element mElement_I8; + Element mElement_U16; + Element mElement_I16; + Element mElement_U32; + Element mElement_I32; + Element mElement_F32; + Element mElement_BOOLEAN; + + Element mElement_ELEMENT; + Element mElement_TYPE; + Element mElement_ALLOCATION; + Element mElement_SAMPLER; + Element mElement_SCRIPT; + Element mElement_MESH; + Element mElement_PROGRAM_FRAGMENT; + Element mElement_PROGRAM_VERTEX; + Element mElement_PROGRAM_RASTER; + Element mElement_PROGRAM_STORE; Element mElement_A_8; Element mElement_RGB_565; @@ -210,13 +529,38 @@ public class RenderScript { Element mElement_RGBA_4444; Element mElement_RGBA_8888; - Element mElement_INDEX_16; - Element mElement_POSITION_2; - Element mElement_POSITION_3; - Element mElement_TEXTURE_2; - Element mElement_NORMAL_3; - Element mElement_COLOR_U8_4; - Element mElement_COLOR_F32_4; + Element mElement_FLOAT_2; + Element mElement_FLOAT_3; + Element mElement_FLOAT_4; + Element mElement_UCHAR_4; + + Element mElement_MATRIX_4X4; + Element mElement_MATRIX_3X3; + Element mElement_MATRIX_2X2; + + Sampler mSampler_CLAMP_NEAREST; + Sampler mSampler_CLAMP_LINEAR; + Sampler mSampler_CLAMP_LINEAR_MIP_LINEAR; + Sampler mSampler_WRAP_NEAREST; + Sampler mSampler_WRAP_LINEAR; + Sampler mSampler_WRAP_LINEAR_MIP_LINEAR; + + ProgramStore mProgramStore_BLEND_NONE_DEPTH_TEST; + ProgramStore mProgramStore_BLEND_NONE_DEPTH_NO_DEPTH; + ProgramStore mProgramStore_BLEND_NONE_DEPTH_NO_TEST; + ProgramStore mProgramStore_BLEND_NONE_DEPTH_NO_WRITE; + ProgramStore mProgramStore_BLEND_ALPHA_DEPTH_TEST; + ProgramStore mProgramStore_BLEND_ALPHA_DEPTH_NO_DEPTH; + ProgramStore mProgramStore_BLEND_ALPHA_DEPTH_NO_TEST; + ProgramStore mProgramStore_BLEND_ALPHA_DEPTH_NO_WRITE; + ProgramStore mProgramStore_BLEND_ADD_DEPTH_TEST; + ProgramStore mProgramStore_BLEND_ADD_DEPTH_NO_DEPTH; + ProgramStore mProgramStore_BLEND_ADD_DEPTH_NO_TEST; + ProgramStore mProgramStore_BLEND_ADD_DEPTH_NO_WRITE; + + ProgramRaster mProgramRaster_CULL_BACK; + ProgramRaster mProgramRaster_CULL_FRONT; + ProgramRaster mProgramRaster_CULL_NONE; /////////////////////////////////////////////////////////////////////////////////// // @@ -264,17 +608,24 @@ public class RenderScript { // This function is a temporary solution. The final solution will // used typed allocations where the message id is the type indicator. int[] rbuf = new int[16]; - mRS.nContextInitToClient(); + mRS.nContextInitToClient(mRS.mContext); while(mRun) { - int msg = mRS.nContextGetMessage(rbuf, true); - if (msg == 0) { - // Should only happen during teardown. - // But we want to avoid starving other threads during - // teardown by yielding until the next line in the destructor - // can execute to set mRun = false - try { - sleep(1, 0); - } catch(InterruptedException e) { + rbuf[0] = 0; + int msg = mRS.nContextGetMessage(mRS.mContext, rbuf, true); + if ((msg == 0) && mRun) { + // Can happen for two reasons + if (rbuf[0] > 0) { + // 1: Buffer needs to be enlarged. + rbuf = new int[rbuf[0] + 2]; + } else { + // 2: teardown. + // But we want to avoid starving other threads during + // teardown by yielding until the next line in the destructor + // can execute to set mRun = false + try { + sleep(1, 0); + } catch(InterruptedException e) { + } } } if(mRS.mMessageCallback != null) { @@ -282,7 +633,6 @@ public class RenderScript { mRS.mMessageCallback.mID = msg; mRS.mMessageCallback.run(); } - //Log.d(LOG_TAG, "MessageThread msg " + msg + " v1 " + rbuf[0] + " v2 " + rbuf[1] + " v3 " +rbuf[2]); } Log.d(LOG_TAG, "MessageThread exiting."); } @@ -307,12 +657,20 @@ public class RenderScript { nContextDump(bits); } + public void finish() { + nContextFinish(); + } + public void destroy() { validate(); - nContextDeinitToClient(); + nContextDeinitToClient(mContext); mMessageThread.mRun = false; + try { + mMessageThread.join(); + } catch(InterruptedException e) { + } - nContextDestroy(mContext); + nContextDestroy(); mContext = 0; nDeviceDestroy(mDev); @@ -335,3 +693,4 @@ public class RenderScript { } + diff --git a/graphics/java/android/renderscript/RenderScriptGL.java b/graphics/java/android/renderscript/RenderScriptGL.java index d1df23d9d0a121f713d42f9e004b9153dacf6c2d..61ecc8df26108fc6d1adec007e9bfae59c1bc298 100644 --- a/graphics/java/android/renderscript/RenderScriptGL.java +++ b/graphics/java/android/renderscript/RenderScriptGL.java @@ -74,9 +74,9 @@ public class RenderScriptGL extends RenderScript { nContextBindRootScript(safeID(s)); } - public void contextBindProgramFragmentStore(ProgramStore p) { + public void contextBindProgramStore(ProgramStore p) { validate(); - nContextBindProgramFragmentStore(safeID(p)); + nContextBindProgramStore(safeID(p)); } public void contextBindProgramFragment(ProgramFragment p) { @@ -102,8 +102,7 @@ public class RenderScriptGL extends RenderScript { public class File extends BaseObj { File(int id) { - super(RenderScriptGL.this); - mID = id; + super(id, RenderScriptGL.this); } } diff --git a/graphics/java/android/renderscript/Sampler.java b/graphics/java/android/renderscript/Sampler.java index 40ba7225f95ee0756300856d90bd766a5e60bfdc..343fcdb55501645554af477592b5e02c35b6baff 100644 --- a/graphics/java/android/renderscript/Sampler.java +++ b/graphics/java/android/renderscript/Sampler.java @@ -47,10 +47,82 @@ public class Sampler extends BaseObj { } Sampler(int id, RenderScript rs) { - super(rs); - mID = id; + super(id, rs); } + public static Sampler CLAMP_NEAREST(RenderScript rs) { + if(rs.mSampler_CLAMP_NEAREST == null) { + Builder b = new Builder(rs); + b.setMin(Value.NEAREST); + b.setMag(Value.NEAREST); + b.setWrapS(Value.CLAMP); + b.setWrapT(Value.CLAMP); + rs.mSampler_CLAMP_NEAREST = b.create(); + } + return rs.mSampler_CLAMP_NEAREST; + } + + public static Sampler CLAMP_LINEAR(RenderScript rs) { + if(rs.mSampler_CLAMP_LINEAR == null) { + Builder b = new Builder(rs); + b.setMin(Value.LINEAR); + b.setMag(Value.LINEAR); + b.setWrapS(Value.CLAMP); + b.setWrapT(Value.CLAMP); + rs.mSampler_CLAMP_LINEAR = b.create(); + } + return rs.mSampler_CLAMP_LINEAR; + } + + public static Sampler CLAMP_LINEAR_MIP_LINEAR(RenderScript rs) { + if(rs.mSampler_CLAMP_LINEAR_MIP_LINEAR == null) { + Builder b = new Builder(rs); + b.setMin(Value.LINEAR_MIP_LINEAR); + b.setMag(Value.LINEAR); + b.setWrapS(Value.CLAMP); + b.setWrapT(Value.CLAMP); + rs.mSampler_CLAMP_LINEAR_MIP_LINEAR = b.create(); + } + return rs.mSampler_CLAMP_LINEAR_MIP_LINEAR; + } + + public static Sampler WRAP_NEAREST(RenderScript rs) { + if(rs.mSampler_WRAP_NEAREST == null) { + Builder b = new Builder(rs); + b.setMin(Value.NEAREST); + b.setMag(Value.NEAREST); + b.setWrapS(Value.WRAP); + b.setWrapT(Value.WRAP); + rs.mSampler_WRAP_NEAREST = b.create(); + } + return rs.mSampler_WRAP_NEAREST; + } + + public static Sampler WRAP_LINEAR(RenderScript rs) { + if(rs.mSampler_WRAP_LINEAR == null) { + Builder b = new Builder(rs); + b.setMin(Value.LINEAR); + b.setMag(Value.LINEAR); + b.setWrapS(Value.WRAP); + b.setWrapT(Value.WRAP); + rs.mSampler_WRAP_LINEAR = b.create(); + } + return rs.mSampler_WRAP_LINEAR; + } + + public static Sampler WRAP_LINEAR_MIP_LINEAR(RenderScript rs) { + if(rs.mSampler_WRAP_LINEAR_MIP_LINEAR == null) { + Builder b = new Builder(rs); + b.setMin(Value.LINEAR_MIP_LINEAR); + b.setMag(Value.LINEAR); + b.setWrapS(Value.WRAP); + b.setWrapT(Value.WRAP); + rs.mSampler_WRAP_LINEAR_MIP_LINEAR = b.create(); + } + return rs.mSampler_WRAP_LINEAR_MIP_LINEAR; + } + + public static class Builder { RenderScript mRS; Value mMin; diff --git a/graphics/java/android/renderscript/Script.java b/graphics/java/android/renderscript/Script.java index 57ccfa36325b04c70f75abb3471107862eac1487..8772c4ce40717db44af426e7854c9d2bf982952e 100644 --- a/graphics/java/android/renderscript/Script.java +++ b/graphics/java/android/renderscript/Script.java @@ -42,29 +42,50 @@ public class Script extends BaseObj { } } + protected void invoke(int slot) { + mRS.nScriptInvoke(mID, slot); + } + + protected void invoke(int slot, FieldPacker v) { + if (v != null) { + mRS.nScriptInvokeV(mID, slot, v.getData()); + } else { + mRS.nScriptInvoke(mID, slot); + } + } + + Script(int id, RenderScript rs) { - super(rs); - mID = id; + super(id, rs); } public void bindAllocation(Allocation va, int slot) { mRS.validate(); - mRS.nScriptBindAllocation(mID, va.mID, slot); + if (va != null) { + mRS.nScriptBindAllocation(mID, va.mID, slot); + } else { + mRS.nScriptBindAllocation(mID, 0, slot); + } } - public void setClearColor(float r, float g, float b, float a) { - mRS.validate(); - mRS.nScriptSetClearColor(mID, r, g, b, a); + public void setVar(int index, float v) { + mRS.nScriptSetVarF(mID, index, v); } - public void setClearDepth(float d) { - mRS.validate(); - mRS.nScriptSetClearDepth(mID, d); + public void setVar(int index, double v) { + mRS.nScriptSetVarD(mID, index, v); } - public void setClearStencil(int stencil) { - mRS.validate(); - mRS.nScriptSetClearStencil(mID, stencil); + public void setVar(int index, int v) { + mRS.nScriptSetVarI(mID, index, v); + } + + public void setVar(int index, boolean v) { + mRS.nScriptSetVarI(mID, index, v ? 1 : 0); + } + + public void setVar(int index, FieldPacker v) { + mRS.nScriptSetVarV(mID, index, v.getData()); } public void setTimeZone(String timeZone) { @@ -78,72 +99,54 @@ public class Script extends BaseObj { public static class Builder { RenderScript mRS; - boolean mIsRoot = false; - Type[] mTypes; - String[] mNames; - boolean[] mWritable; - int mInvokableCount = 0; - Invokable[] mInvokables; Builder(RenderScript rs) { mRS = rs; - mTypes = new Type[MAX_SLOT]; - mNames = new String[MAX_SLOT]; - mWritable = new boolean[MAX_SLOT]; - mInvokables = new Invokable[MAX_SLOT]; } + } + + + public static class FieldBase { + protected Element mElement; + protected Type mType; + protected Allocation mAllocation; - public void setType(Type t, int slot) { - mTypes[slot] = t; - mNames[slot] = null; + protected void init(RenderScript rs, int dimx) { + mAllocation = Allocation.createSized(rs, mElement, dimx); + mType = mAllocation.getType(); } - public void setType(Type t, String name, int slot) { - mTypes[slot] = t; - mNames[slot] = name; + protected FieldBase() { } - public Invokable addInvokable(String func) { - Invokable i = new Invokable(); - i.mName = func; - i.mRS = mRS; - i.mSlot = mInvokableCount; - mInvokables[mInvokableCount++] = i; - return i; + public Element getElement() { + return mElement; } - public void setType(boolean writable, int slot) { - mWritable[slot] = writable; + public Type getType() { + return mType; } - void transferCreate() { - mRS.nScriptSetRoot(mIsRoot); - for(int ct=0; ct < mTypes.length; ct++) { - if(mTypes[ct] != null) { - mRS.nScriptSetType(mTypes[ct].mID, mWritable[ct], mNames[ct], ct); - } - } - for(int ct=0; ct < mInvokableCount; ct++) { - mRS.nScriptSetInvokable(mInvokables[ct].mName, ct); - } + public Allocation getAllocation() { + return mAllocation; } - void transferObject(Script s) { - s.mIsRoot = mIsRoot; - s.mTypes = mTypes; - s.mInvokables = new Invokable[mInvokableCount]; - for(int ct=0; ct < mInvokableCount; ct++) { - s.mInvokables[ct] = mInvokables[ct]; - s.mInvokables[ct].mScript = s; - } - s.mInvokables = null; + //@Override + public void updateAllocation() { } - public void setRoot(boolean r) { - mIsRoot = r; + + // + /* + public class ScriptField_UserField + extends android.renderscript.Script.FieldBase { + + protected + } - } + */ + } } diff --git a/graphics/java/android/renderscript/ScriptC.java b/graphics/java/android/renderscript/ScriptC.java index bb99e23e8fa65b9c6f90106e05c5c025c0448dac..5959be4412f1e1b8d6e3234f8387b6271d43bc44 100644 --- a/graphics/java/android/renderscript/ScriptC.java +++ b/graphics/java/android/renderscript/ScriptC.java @@ -37,11 +37,49 @@ public class ScriptC extends Script { super(id, rs); } + protected ScriptC(RenderScript rs, Resources resources, int resourceID, boolean isRoot) { + super(0, rs); + mID = internalCreate(rs, resources, resourceID); + } + + + private static synchronized int internalCreate(RenderScript rs, Resources resources, int resourceID) { + byte[] pgm; + int pgmLength; + InputStream is = resources.openRawResource(resourceID); + try { + try { + pgm = new byte[1024]; + pgmLength = 0; + while(true) { + int bytesLeft = pgm.length - pgmLength; + if (bytesLeft == 0) { + byte[] buf2 = new byte[pgm.length * 2]; + System.arraycopy(pgm, 0, buf2, 0, pgm.length); + pgm = buf2; + bytesLeft = pgm.length - pgmLength; + } + int bytesRead = is.read(pgm, pgmLength, bytesLeft); + if (bytesRead <= 0) { + break; + } + pgmLength += bytesRead; + } + } finally { + is.close(); + } + } catch(IOException e) { + throw new Resources.NotFoundException(); + } + + rs.nScriptCBegin(); + rs.nScriptCSetScript(pgm, 0, pgmLength); + return rs.nScriptCCreate(); + } + public static class Builder extends Script.Builder { byte[] mProgram; int mProgramLength; - HashMap mIntDefines = new HashMap(); - HashMap mFloatDefines = new HashMap(); public Builder(RenderScript rs) { super(rs); @@ -92,66 +130,20 @@ public class ScriptC extends Script { static synchronized ScriptC internalCreate(Builder b) { b.mRS.nScriptCBegin(); - b.transferCreate(); - - for (Entry e: b.mIntDefines.entrySet()) { - b.mRS.nScriptCAddDefineI32(e.getKey(), e.getValue().intValue()); - } - for (Entry e: b.mFloatDefines.entrySet()) { - b.mRS.nScriptCAddDefineF(e.getKey(), e.getValue().floatValue()); - } + android.util.Log.e("rs", "len = " + b.mProgramLength); b.mRS.nScriptCSetScript(b.mProgram, 0, b.mProgramLength); int id = b.mRS.nScriptCCreate(); ScriptC obj = new ScriptC(id, b.mRS); - b.transferObject(obj); - return obj; } - public void addDefine(String name, int value) { - mIntDefines.put(name, value); - } - - public void addDefine(String name, float value) { - mFloatDefines.put(name, value); - } - - /** - * Takes the all public static final fields for a class, and adds defines - * for them, using the name of the field as the name of the define. - */ - public void addDefines(Class cl) { - addDefines(cl.getFields(), (Modifier.STATIC | Modifier.FINAL | Modifier.PUBLIC), null); - } - - /** - * Takes the all public fields for an object, and adds defines - * for them, using the name of the field as the name of the define. - */ - public void addDefines(Object o) { - addDefines(o.getClass().getFields(), Modifier.PUBLIC, o); - } - - void addDefines(Field[] fields, int mask, Object o) { - for (Field f: fields) { - try { - if ((f.getModifiers() & mask) == mask) { - Class t = f.getType(); - if (t == int.class) { - mIntDefines.put(f.getName(), f.getInt(o)); - } - else if (t == float.class) { - mFloatDefines.put(f.getName(), f.getFloat(o)); - } - } - } catch (IllegalAccessException ex) { - // TODO: Do we want this log? - Log.d(TAG, "addDefines skipping field " + f.getName()); - } - } - } + public void addDefine(String name, int value) {} + public void addDefine(String name, float value) {} + public void addDefines(Class cl) {} + public void addDefines(Object o) {} + void addDefines(Field[] fields, int mask, Object o) {} public ScriptC create() { return internalCreate(this); diff --git a/graphics/java/android/renderscript/Short2.java b/graphics/java/android/renderscript/Short2.java new file mode 100644 index 0000000000000000000000000000000000000000..426801f1d93dc35547690930269bf99db3ce8969 --- /dev/null +++ b/graphics/java/android/renderscript/Short2.java @@ -0,0 +1,37 @@ +/* + * 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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Short2 { + public Short2() { + } + + public short x; + public short y; +} + + + + diff --git a/graphics/java/android/renderscript/Short3.java b/graphics/java/android/renderscript/Short3.java new file mode 100644 index 0000000000000000000000000000000000000000..7b9c305d8c4d88dffbced321e784f6b998ac429c --- /dev/null +++ b/graphics/java/android/renderscript/Short3.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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Short3 { + public Short3() { + } + + public short x; + public short y; + public short z; +} + + + + diff --git a/graphics/java/android/renderscript/Short4.java b/graphics/java/android/renderscript/Short4.java new file mode 100644 index 0000000000000000000000000000000000000000..9a474e27d0b3564ed3dc319858add3ac0775ff53 --- /dev/null +++ b/graphics/java/android/renderscript/Short4.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.renderscript; + +import java.lang.Math; +import android.util.Log; + + +/** + * @hide + * + **/ +public class Short4 { + public Short4() { + } + + public short x; + public short y; + public short z; + public short w; +} + + + diff --git a/graphics/java/android/renderscript/SimpleMesh.java b/graphics/java/android/renderscript/SimpleMesh.java deleted file mode 100644 index 4a217a9e4d6825d6939479b04adb82f0979b13e2..0000000000000000000000000000000000000000 --- a/graphics/java/android/renderscript/SimpleMesh.java +++ /dev/null @@ -1,364 +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.renderscript; - -import android.util.Config; -import android.util.Log; - -/** - * @hide - * - **/ -public class SimpleMesh extends BaseObj { - Type[] mVertexTypes; - Type mIndexType; - //Type mBatcheType; - Primitive mPrimitive; - - SimpleMesh(int id, RenderScript rs) { - super(rs); - mID = id; - } - - public void bindVertexAllocation(Allocation a, int slot) { - mRS.validate(); - mRS.nSimpleMeshBindVertex(mID, a.mID, slot); - } - - public void bindIndexAllocation(Allocation a) { - mRS.validate(); - mRS.nSimpleMeshBindIndex(mID, a.mID); - } - - public Allocation createVertexAllocation(int slot) { - mRS.validate(); - return Allocation.createTyped(mRS, mVertexTypes[slot]); - } - - public Allocation createIndexAllocation() { - mRS.validate(); - return Allocation.createTyped(mRS, mIndexType); - } - - public Type getVertexType(int slot) { - return mVertexTypes[slot]; - } - - public Type getIndexType() { - return mIndexType; - } - - public static class Builder { - RenderScript mRS; - - class Entry { - Type t; - Element e; - int size; - } - - int mVertexTypeCount; - Entry[] mVertexTypes; - Entry mIndexType; - //Entry mBatchType; - Primitive mPrimitive; - - - public Builder(RenderScript rs) { - mRS = rs; - mVertexTypeCount = 0; - mVertexTypes = new Entry[16]; - mIndexType = new Entry(); - } - - public int addVertexType(Type t) throws IllegalStateException { - if (mVertexTypeCount >= mVertexTypes.length) { - throw new IllegalStateException("Max vertex types exceeded."); - } - - int addedIndex = mVertexTypeCount; - mVertexTypes[mVertexTypeCount] = new Entry(); - mVertexTypes[mVertexTypeCount].t = t; - mVertexTypeCount++; - return addedIndex; - } - - public int addVertexType(Element e, int size) throws IllegalStateException { - if (mVertexTypeCount >= mVertexTypes.length) { - throw new IllegalStateException("Max vertex types exceeded."); - } - - int addedIndex = mVertexTypeCount; - mVertexTypes[mVertexTypeCount] = new Entry(); - mVertexTypes[mVertexTypeCount].e = e; - mVertexTypes[mVertexTypeCount].size = size; - mVertexTypeCount++; - return addedIndex; - } - - public void setIndexType(Type t) { - mIndexType.t = t; - mIndexType.e = null; - mIndexType.size = 0; - } - - public void setIndexType(Element e, int size) { - mIndexType.t = null; - mIndexType.e = e; - mIndexType.size = size; - } - - public void setPrimitive(Primitive p) { - mPrimitive = p; - } - - - Type newType(Element e, int size) { - Type.Builder tb = new Type.Builder(mRS, e); - tb.add(Dimension.X, size); - return tb.create(); - } - - static synchronized SimpleMesh internalCreate(RenderScript rs, Builder b) { - Type[] toDestroy = new Type[18]; - int toDestroyCount = 0; - - int indexID = 0; - if (b.mIndexType.t != null) { - indexID = b.mIndexType.t.mID; - } else if (b.mIndexType.size != 0) { - b.mIndexType.t = b.newType(b.mIndexType.e, b.mIndexType.size); - indexID = b.mIndexType.t.mID; - toDestroy[toDestroyCount++] = b.mIndexType.t; - } - - int[] IDs = new int[b.mVertexTypeCount]; - for(int ct=0; ct < b.mVertexTypeCount; ct++) { - if (b.mVertexTypes[ct].t != null) { - IDs[ct] = b.mVertexTypes[ct].t.mID; - } else { - b.mVertexTypes[ct].t = b.newType(b.mVertexTypes[ct].e, b.mVertexTypes[ct].size); - IDs[ct] = b.mVertexTypes[ct].t.mID; - toDestroy[toDestroyCount++] = b.mVertexTypes[ct].t; - } - } - - int id = rs.nSimpleMeshCreate(0, indexID, IDs, b.mPrimitive.mID); - for(int ct=0; ct < toDestroyCount; ct++) { - toDestroy[ct].destroy(); - } - - return new SimpleMesh(id, rs); - } - - public SimpleMesh create() { - mRS.validate(); - SimpleMesh sm = internalCreate(mRS, this); - sm.mVertexTypes = new Type[mVertexTypeCount]; - for(int ct=0; ct < mVertexTypeCount; ct++) { - sm.mVertexTypes[ct] = mVertexTypes[ct].t; - } - sm.mIndexType = mIndexType.t; - sm.mPrimitive = mPrimitive; - return sm; - } - } - - public static class TriangleMeshBuilder { - float mVtxData[]; - int mVtxCount; - short mIndexData[]; - int mIndexCount; - RenderScript mRS; - Element mElement; - - float mNX = 0; - float mNY = 0; - float mNZ = -1; - float mS0 = 0; - float mT0 = 0; - float mR = 1; - float mG = 1; - float mB = 1; - float mA = 1; - - int mVtxSize; - int mFlags; - - public static final int COLOR = 0x0001; - public static final int NORMAL = 0x0002; - public static final int TEXTURE_0 = 0x0100; - - public TriangleMeshBuilder(RenderScript rs, int vtxSize, int flags) { - mRS = rs; - mVtxCount = 0; - mIndexCount = 0; - mVtxData = new float[128]; - mIndexData = new short[128]; - mVtxSize = vtxSize; - mFlags = flags; - - if (vtxSize < 2 || vtxSize > 3) { - throw new IllegalArgumentException("Vertex size out of range."); - } - } - - private void makeSpace(int count) { - if ((mVtxCount + count) >= mVtxData.length) { - float t[] = new float[mVtxData.length * 2]; - System.arraycopy(mVtxData, 0, t, 0, mVtxData.length); - mVtxData = t; - } - } - - private void latch() { - if ((mFlags & COLOR) != 0) { - makeSpace(4); - mVtxData[mVtxCount++] = mR; - mVtxData[mVtxCount++] = mG; - mVtxData[mVtxCount++] = mB; - mVtxData[mVtxCount++] = mA; - } - if ((mFlags & TEXTURE_0) != 0) { - makeSpace(2); - mVtxData[mVtxCount++] = mS0; - mVtxData[mVtxCount++] = mT0; - } - if ((mFlags & NORMAL) != 0) { - makeSpace(3); - mVtxData[mVtxCount++] = mNX; - mVtxData[mVtxCount++] = mNY; - mVtxData[mVtxCount++] = mNZ; - } - } - - public void addVertex(float x, float y) { - if (mVtxSize != 2) { - throw new IllegalStateException("add mistmatch with declared components."); - } - makeSpace(2); - mVtxData[mVtxCount++] = x; - mVtxData[mVtxCount++] = y; - latch(); - } - - public void addVertex(float x, float y, float z) { - if (mVtxSize != 3) { - throw new IllegalStateException("add mistmatch with declared components."); - } - makeSpace(3); - mVtxData[mVtxCount++] = x; - mVtxData[mVtxCount++] = y; - mVtxData[mVtxCount++] = z; - latch(); - } - - public void setTexture(float s, float t) { - if ((mFlags & TEXTURE_0) == 0) { - throw new IllegalStateException("add mistmatch with declared components."); - } - mS0 = s; - mT0 = t; - } - - public void setNormal(float x, float y, float z) { - if ((mFlags & NORMAL) == 0) { - throw new IllegalStateException("add mistmatch with declared components."); - } - mNX = x; - mNY = y; - mNZ = z; - } - - public void setColor(float r, float g, float b, float a) { - if ((mFlags & COLOR) == 0) { - throw new IllegalStateException("add mistmatch with declared components."); - } - mR = r; - mG = g; - mB = b; - mA = a; - } - - public void addTriangle(int idx1, int idx2, int idx3) { - if((idx1 >= mVtxCount) || (idx1 < 0) || - (idx2 >= mVtxCount) || (idx2 < 0) || - (idx3 >= mVtxCount) || (idx3 < 0)) { - throw new IllegalStateException("Index provided greater than vertex count."); - } - if ((mIndexCount + 3) >= mIndexData.length) { - short t[] = new short[mIndexData.length * 2]; - System.arraycopy(mIndexData, 0, t, 0, mIndexData.length); - mIndexData = t; - } - mIndexData[mIndexCount++] = (short)idx1; - mIndexData[mIndexCount++] = (short)idx2; - mIndexData[mIndexCount++] = (short)idx3; - } - - public SimpleMesh create() { - Element.Builder b = new Element.Builder(mRS); - int floatCount = mVtxSize; - b.add(Element.createAttrib(mRS, - Element.DataType.FLOAT_32, - Element.DataKind.POSITION, - mVtxSize), "position"); - if ((mFlags & COLOR) != 0) { - floatCount += 4; - b.add(Element.createAttrib(mRS, - Element.DataType.FLOAT_32, - Element.DataKind.COLOR, - 4), "color"); - } - if ((mFlags & TEXTURE_0) != 0) { - floatCount += 2; - b.add(Element.createAttrib(mRS, - Element.DataType.FLOAT_32, - Element.DataKind.TEXTURE, - 2), "texture"); - } - if ((mFlags & NORMAL) != 0) { - floatCount += 3; - b.add(Element.createAttrib(mRS, - Element.DataType.FLOAT_32, - Element.DataKind.NORMAL, - 3), "normal"); - } - mElement = b.create(); - - Builder smb = new Builder(mRS); - smb.addVertexType(mElement, mVtxCount / floatCount); - smb.setIndexType(Element.createIndex(mRS), mIndexCount); - smb.setPrimitive(Primitive.TRIANGLE); - SimpleMesh sm = smb.create(); - - Allocation vertexAlloc = sm.createVertexAllocation(0); - Allocation indexAlloc = sm.createIndexAllocation(); - sm.bindVertexAllocation(vertexAlloc, 0); - sm.bindIndexAllocation(indexAlloc); - - vertexAlloc.data(mVtxData); - vertexAlloc.uploadToBufferObject(); - - indexAlloc.data(mIndexData); - indexAlloc.uploadToBufferObject(); - - return sm; - } - } -} - diff --git a/graphics/java/android/renderscript/Type.java b/graphics/java/android/renderscript/Type.java index 62d3867ceec9e099e2c6ca5336434998e42ce7af..0b3db69464acc41f8118e1e36212054443591038 100644 --- a/graphics/java/android/renderscript/Type.java +++ b/graphics/java/android/renderscript/Type.java @@ -16,7 +16,9 @@ package android.renderscript; + import java.lang.reflect.Field; +import android.util.Log; /** * @hide @@ -31,7 +33,6 @@ public class Type extends BaseObj { int mElementCount; Element mElement; - private int mNativeCache; Class mJavaClass; public Element getElement() { @@ -95,61 +96,37 @@ public class Type extends BaseObj { Type(int id, RenderScript rs) { - super(rs); - mID = id; - mNativeCache = 0; + super(id, rs); } protected void finalize() throws Throwable { - if(mNativeCache != 0) { - mRS.nTypeFinalDestroy(this); - mNativeCache = 0; - } super.finalize(); } - public static Type createFromClass(RenderScript rs, Class c, int size) { - Element e = Element.createFromClass(rs, c); - Builder b = new Builder(rs, e); - b.add(Dimension.X, size); - Type t = b.create(); - e.destroy(); - - // native fields - { - Field[] fields = c.getFields(); - int[] arTypes = new int[fields.length]; - int[] arBits = new int[fields.length]; - - for(int ct=0; ct < fields.length; ct++) { - Field f = fields[ct]; - Class fc = f.getType(); - if(fc == int.class) { - arTypes[ct] = Element.DataType.SIGNED_32.mID; - arBits[ct] = 32; - } else if(fc == short.class) { - arTypes[ct] = Element.DataType.SIGNED_16.mID; - arBits[ct] = 16; - } else if(fc == byte.class) { - arTypes[ct] = Element.DataType.SIGNED_8.mID; - arBits[ct] = 8; - } else if(fc == float.class) { - arTypes[ct] = Element.DataType.FLOAT_32.mID; - arBits[ct] = 32; - } else { - throw new IllegalArgumentException("Unkown field type"); - } - } - rs.nTypeSetupFields(t, arTypes, arBits, fields); + @Override + void updateFromNative() { + // We have 6 integer to obtain mDimX; mDimY; mDimZ; + // mDimLOD; mDimFaces; mElement; + int[] dataBuffer = new int[6]; + mRS.nTypeGetNativeData(mID, dataBuffer); + + mDimX = dataBuffer[0]; + mDimY = dataBuffer[1]; + mDimZ = dataBuffer[2]; + mDimLOD = dataBuffer[3] == 1 ? true : false; + mDimFaces = dataBuffer[4] == 1 ? true : false; + + int elementID = dataBuffer[5]; + if(elementID != 0) { + mElement = new Element(elementID, mRS); + mElement.updateFromNative(); } - t.mJavaClass = c; - return t; + calcElementCount(); } public static Type createFromClass(RenderScript rs, Class c, int size, String scriptName) { - Type t = createFromClass(rs, c, size); - t.setName(scriptName); - return t; + android.util.Log.e("RenderScript", "Calling depricated createFromClass"); + return null; } diff --git a/graphics/jni/Android.mk b/graphics/jni/Android.mk index 8476be155f6fdbb0bfbc875e0077cd59a24f7b34..4c4a12831291f33e20da1f13056dc8f27700af5f 100644 --- a/graphics/jni/Android.mk +++ b/graphics/jni/Android.mk @@ -13,14 +13,13 @@ LOCAL_SRC_FILES:= \ LOCAL_SHARED_LIBRARIES := \ libandroid_runtime \ - libacc \ libnativehelper \ libRS \ libcutils \ libskia \ libutils \ libui \ - libsurfaceflinger_client + libsurfaceflinger_client LOCAL_STATIC_LIBRARIES := diff --git a/graphics/jni/android_renderscript_RenderScript.cpp b/graphics/jni/android_renderscript_RenderScript.cpp index 45cc72e7e41bc764693dcfb4cdadb143324324e7..ee2080ef73192243c20f9a1d0dc2e192fb96d5be 100644 --- a/graphics/jni/android_renderscript_RenderScript.cpp +++ b/graphics/jni/android_renderscript_RenderScript.cpp @@ -37,6 +37,7 @@ #include "jni.h" #include "JNIHelp.h" #include "android_runtime/AndroidRuntime.h" +#include "android_runtime/android_view_Surface.h" #include #include @@ -69,9 +70,6 @@ static void _nInit(JNIEnv *_env, jclass _this) jclass bitmapClass = _env->FindClass("android/graphics/Bitmap"); gNativeBitmapID = _env->GetFieldID(bitmapClass, "mNativeBitmap", "I"); - - jclass typeClass = _env->FindClass("android/renderscript/Type"); - gTypeNativeCache = _env->GetFieldID(typeClass, "mNativeCache", "I"); } static void nInitElements(JNIEnv *_env, jobject _this, jint a8, jint rgba4444, jint rgba8888, jint rgb565) @@ -85,41 +83,43 @@ static void nInitElements(JNIEnv *_env, jobject _this, jint a8, jint rgba4444, j // --------------------------------------------------------------------------- static void -nAssignName(JNIEnv *_env, jobject _this, jint obj, jbyteArray str) +nContextFinish(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nAssignName, con(%p), obj(%p)", con, (void *)obj); + LOG_API("nContextFinish, con(%p)", con); + rsContextFinish(con); +} +static void +nAssignName(JNIEnv *_env, jobject _this, RsContext con, jint obj, jbyteArray str) +{ + LOG_API("nAssignName, con(%p), obj(%p)", con, (void *)obj); jint len = _env->GetArrayLength(str); jbyte * cptr = (jbyte *) _env->GetPrimitiveArrayCritical(str, 0); rsAssignName(con, (void *)obj, (const char *)cptr, len); _env->ReleasePrimitiveArrayCritical(str, cptr, JNI_ABORT); } -static void -nObjDestroy(JNIEnv *_env, jobject _this, jint obj) +static jstring +nGetName(JNIEnv *_env, jobject _this, RsContext con, jint obj) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nObjDestroy, con(%p) obj(%p)", con, (void *)obj); - rsObjDestroy(con, (void *)obj); + LOG_API("nGetName, con(%p), obj(%p)", con, (void *)obj); + const char *name = NULL; + rsGetName(con, (void *)obj, &name); + return _env->NewStringUTF(name); } static void -nObjDestroyOOB(JNIEnv *_env, jobject _this, jint obj) +nObjDestroy(JNIEnv *_env, jobject _this, RsContext con, jint obj) { - // This function only differs from nObjDestroy in that it calls the - // special Out Of Band version of ObjDestroy which is thread safe. - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nObjDestroyOOB, con(%p) obj(%p)", con, (void *)obj); - rsObjDestroyOOB(con, (void *)obj); + LOG_API("nObjDestroy, con(%p) obj(%p)", con, (void *)obj); + rsObjDestroy(con, (void *)obj); } + static jint -nFileOpen(JNIEnv *_env, jobject _this, jbyteArray str) +nFileOpen(JNIEnv *_env, jobject _this, RsContext con, jbyteArray str) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nFileOpen, con(%p)", con); - jint len = _env->GetArrayLength(str); jbyte * cptr = (jbyte *) _env->GetPrimitiveArrayCritical(str, 0); jint ret = (jint)rsFileOpen(con, (const char *)cptr, len); @@ -165,9 +165,8 @@ nContextCreateGL(JNIEnv *_env, jobject _this, jint dev, jint ver, jboolean useDe } static void -nContextSetPriority(JNIEnv *_env, jobject _this, jint p) +nContextSetPriority(JNIEnv *_env, jobject _this, RsContext con, jint p) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("ContextSetPriority, con(%p), priority(%i)", con, p); rsContextSetPriority(con, p); } @@ -175,101 +174,92 @@ nContextSetPriority(JNIEnv *_env, jobject _this, jint p) static void -nContextSetSurface(JNIEnv *_env, jobject _this, jint width, jint height, jobject wnd) +nContextSetSurface(JNIEnv *_env, jobject _this, RsContext con, jint width, jint height, jobject wnd) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextSetSurface, con(%p), width(%i), height(%i), surface(%p)", con, width, height, (Surface *)wnd); Surface * window = NULL; if (wnd == NULL) { } else { - jclass surface_class = _env->FindClass("android/view/Surface"); - jfieldID surfaceFieldID = _env->GetFieldID(surface_class, ANDROID_VIEW_SURFACE_JNI_ID, "I"); - window = (Surface*)_env->GetIntField(wnd, surfaceFieldID); + window = (Surface*) android_Surface_getNativeWindow(_env, wnd).get(); } rsContextSetSurface(con, width, height, window); } static void -nContextDestroy(JNIEnv *_env, jobject _this, jint con) +nContextDestroy(JNIEnv *_env, jobject _this, RsContext con) { - LOG_API("nContextDestroy, con(%p)", (RsContext)con); - rsContextDestroy((RsContext)con); + LOG_API("nContextDestroy, con(%p)", con); + rsContextDestroy(con); } static void -nContextDump(JNIEnv *_env, jobject _this, jint bits) +nContextDump(JNIEnv *_env, jobject _this, RsContext con, jint bits) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextDump, con(%p) bits(%i)", (RsContext)con, bits); rsContextDump((RsContext)con, bits); } static void -nContextPause(JNIEnv *_env, jobject _this) +nContextPause(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextPause, con(%p)", con); rsContextPause(con); } static void -nContextResume(JNIEnv *_env, jobject _this) +nContextResume(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextResume, con(%p)", con); rsContextResume(con); } static jint -nContextGetMessage(JNIEnv *_env, jobject _this, jintArray data, jboolean wait) +nContextGetMessage(JNIEnv *_env, jobject _this, RsContext con, jintArray data, jboolean wait) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nContextGetMessage, con(%p), len(%i)", con, len); jint *ptr = _env->GetIntArrayElements(data, NULL); size_t receiveLen; int id = rsContextGetMessage(con, ptr, &receiveLen, len * 4, wait); if (!id && receiveLen) { - LOGE("message receive buffer too small. %i", receiveLen); + LOGV("message receive buffer too small. %i", receiveLen); + *ptr = (jint)receiveLen; } _env->ReleaseIntArrayElements(data, ptr, 0); return id; } -static void nContextInitToClient(JNIEnv *_env, jobject _this) +static void nContextInitToClient(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextInitToClient, con(%p)", con); rsContextInitToClient(con); } -static void nContextDeinitToClient(JNIEnv *_env, jobject _this) +static void nContextDeinitToClient(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextDeinitToClient, con(%p)", con); rsContextDeinitToClient(con); } static jint -nElementCreate(JNIEnv *_env, jobject _this, jint type, jint kind, jboolean norm, jint size) +nElementCreate(JNIEnv *_env, jobject _this, RsContext con, jint type, jint kind, jboolean norm, jint size) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nElementCreate, con(%p), type(%i), kind(%i), norm(%i), size(%i)", con, type, kind, norm, size); return (jint)rsElementCreate(con, (RsDataType)type, (RsDataKind)kind, norm, size); } static jint -nElementCreate2(JNIEnv *_env, jobject _this, jintArray _ids, jobjectArray _names) +nElementCreate2(JNIEnv *_env, jobject _this, RsContext con, jintArray _ids, jobjectArray _names, jintArray _arraySizes) { int fieldCount = _env->GetArrayLength(_ids); - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nElementCreate2, con(%p)", con); jint *ids = _env->GetIntArrayElements(_ids, NULL); + jint *arraySizes = _env->GetIntArrayElements(_arraySizes, NULL); const char ** nameArray = (const char **)calloc(fieldCount, sizeof(char *)); size_t* sizeArray = (size_t*)calloc(fieldCount, sizeof(size_t)); @@ -278,183 +268,116 @@ nElementCreate2(JNIEnv *_env, jobject _this, jintArray _ids, jobjectArray _names nameArray[ct] = _env->GetStringUTFChars(s, NULL); sizeArray[ct] = _env->GetStringUTFLength(s); } - jint id = (jint)rsElementCreate2(con, fieldCount, (RsElement *)ids, nameArray, sizeArray); + jint id = (jint)rsElementCreate2(con, fieldCount, (RsElement *)ids, nameArray, sizeArray, (const uint32_t *)arraySizes); for (int ct=0; ct < fieldCount; ct++) { jstring s = (jstring)_env->GetObjectArrayElement(_names, ct); _env->ReleaseStringUTFChars(s, nameArray[ct]); } _env->ReleaseIntArrayElements(_ids, ids, JNI_ABORT); + _env->ReleaseIntArrayElements(_arraySizes, arraySizes, JNI_ABORT); free(nameArray); free(sizeArray); return (jint)id; } -// ----------------------------------- - static void -nTypeBegin(JNIEnv *_env, jobject _this, jint eID) +nElementGetNativeData(JNIEnv *_env, jobject _this, RsContext con, jint id, jintArray _elementData) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nTypeBegin, con(%p) e(%p)", con, (RsElement)eID); - rsTypeBegin(con, (RsElement)eID); -} + int dataSize = _env->GetArrayLength(_elementData); + LOG_API("nElementGetNativeData, con(%p)", con); -static void -nTypeAdd(JNIEnv *_env, jobject _this, jint dim, jint val) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nTypeAdd, con(%p) dim(%i), val(%i)", con, dim, val); - rsTypeAdd(con, (RsDimension)dim, val); -} + // we will pack mType; mKind; mNormalized; mVectorSize; NumSubElements + assert(dataSize == 5); -static jint -nTypeCreate(JNIEnv *_env, jobject _this) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nTypeCreate, con(%p)", con); - return (jint)rsTypeCreate(con); -} + uint32_t elementData[5]; + rsElementGetNativeData(con, (RsElement)id, elementData, dataSize); -static void * SF_LoadInt(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) -{ - ((int32_t *)buffer)[0] = _env->GetIntField(_obj, _field); - return ((uint8_t *)buffer) + 4; + for(jint i = 0; i < dataSize; i ++) { + _env->SetIntArrayRegion(_elementData, i, 1, (const jint*)&elementData[i]); + } } -static void * SF_LoadShort(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) -{ - ((int16_t *)buffer)[0] = _env->GetShortField(_obj, _field); - return ((uint8_t *)buffer) + 2; -} -static void * SF_LoadByte(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) +static void +nElementGetSubElements(JNIEnv *_env, jobject _this, RsContext con, jint id, jintArray _IDs, jobjectArray _names) { - ((int8_t *)buffer)[0] = _env->GetByteField(_obj, _field); - return ((uint8_t *)buffer) + 1; -} + int dataSize = _env->GetArrayLength(_IDs); + LOG_API("nElementGetSubElements, con(%p)", con); -static void * SF_LoadFloat(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) -{ - ((float *)buffer)[0] = _env->GetFloatField(_obj, _field); - return ((uint8_t *)buffer) + 4; -} + uint32_t *ids = (uint32_t *)malloc((uint32_t)dataSize * sizeof(uint32_t)); + const char **names = (const char **)malloc((uint32_t)dataSize * sizeof(const char *)); -static void * SF_SaveInt(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) -{ - _env->SetIntField(_obj, _field, ((int32_t *)buffer)[0]); - return ((uint8_t *)buffer) + 4; -} + rsElementGetSubElements(con, (RsElement)id, ids, names, (uint32_t)dataSize); -static void * SF_SaveShort(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) -{ - _env->SetShortField(_obj, _field, ((int16_t *)buffer)[0]); - return ((uint8_t *)buffer) + 2; + for(jint i = 0; i < dataSize; i ++) { + _env->SetObjectArrayElement(_names, i, _env->NewStringUTF(names[i])); + _env->SetIntArrayRegion(_IDs, i, 1, (const jint*)&ids[i]); + } + + free(ids); + free(names); } -static void * SF_SaveByte(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) +// ----------------------------------- + +static void +nTypeBegin(JNIEnv *_env, jobject _this, RsContext con, jint eID) { - _env->SetByteField(_obj, _field, ((int8_t *)buffer)[0]); - return ((uint8_t *)buffer) + 1; + LOG_API("nTypeBegin, con(%p) e(%p)", con, (RsElement)eID); + rsTypeBegin(con, (RsElement)eID); } -static void * SF_SaveFloat(JNIEnv *_env, jobject _obj, jfieldID _field, void *buffer) +static void +nTypeAdd(JNIEnv *_env, jobject _this, RsContext con, jint dim, jint val) { - _env->SetFloatField(_obj, _field, ((float *)buffer)[0]); - return ((uint8_t *)buffer) + 4; + LOG_API("nTypeAdd, con(%p) dim(%i), val(%i)", con, dim, val); + rsTypeAdd(con, (RsDimension)dim, val); } -struct TypeFieldCache { - jfieldID field; - int bits; - void * (*ptr)(JNIEnv *, jobject, jfieldID, void *buffer); - void * (*readPtr)(JNIEnv *, jobject, jfieldID, void *buffer); -}; - -struct TypeCache { - int fieldCount; - int size; - TypeFieldCache fields[1]; -}; - -//{"nTypeFinalDestroy", "(Landroid/renderscript/Type;)V", (void*)nTypeFinalDestroy }, -static void -nTypeFinalDestroy(JNIEnv *_env, jobject _this, jobject _type) +static jint +nTypeCreate(JNIEnv *_env, jobject _this, RsContext con) { - TypeCache *tc = (TypeCache *)_env->GetIntField(_type, gTypeNativeCache); - free(tc); + LOG_API("nTypeCreate, con(%p)", con); + return (jint)rsTypeCreate(con); } -// native void nTypeSetupFields(Type t, int[] types, int[] bits, Field[] IDs); static void -nTypeSetupFields(JNIEnv *_env, jobject _this, jobject _type, jintArray _types, jintArray _bits, jobjectArray _IDs) +nTypeGetNativeData(JNIEnv *_env, jobject _this, RsContext con, jint id, jintArray _typeData) { - int fieldCount = _env->GetArrayLength(_types); - size_t structSize = sizeof(TypeCache) + (sizeof(TypeFieldCache) * (fieldCount-1)); - TypeCache *tc = (TypeCache *)malloc(structSize); - memset(tc, 0, structSize); + // We are packing 6 items: mDimX; mDimY; mDimZ; + // mDimLOD; mDimFaces; mElement; into typeData + int elementCount = _env->GetArrayLength(_typeData); - TypeFieldCache *tfc = &tc->fields[0]; - tc->fieldCount = fieldCount; - _env->SetIntField(_type, gTypeNativeCache, (jint)tc); + assert(elementCount == 6); + LOG_API("nTypeCreate, con(%p)", con); - jint *fType = _env->GetIntArrayElements(_types, NULL); - jint *fBits = _env->GetIntArrayElements(_bits, NULL); - for (int ct=0; ct < fieldCount; ct++) { - jobject field = _env->GetObjectArrayElement(_IDs, ct); - tfc[ct].field = _env->FromReflectedField(field); - tfc[ct].bits = fBits[ct]; - - switch(fType[ct]) { - case RS_TYPE_FLOAT_32: - tfc[ct].ptr = SF_LoadFloat; - tfc[ct].readPtr = SF_SaveFloat; - break; - case RS_TYPE_UNSIGNED_32: - case RS_TYPE_SIGNED_32: - tfc[ct].ptr = SF_LoadInt; - tfc[ct].readPtr = SF_SaveInt; - break; - case RS_TYPE_UNSIGNED_16: - case RS_TYPE_SIGNED_16: - tfc[ct].ptr = SF_LoadShort; - tfc[ct].readPtr = SF_SaveShort; - break; - case RS_TYPE_UNSIGNED_8: - case RS_TYPE_SIGNED_8: - tfc[ct].ptr = SF_LoadByte; - tfc[ct].readPtr = SF_SaveByte; - break; - } - tc->size += 4; - } + uint32_t typeData[6]; + rsTypeGetNativeData(con, (RsType)id, typeData, 6); - _env->ReleaseIntArrayElements(_types, fType, JNI_ABORT); - _env->ReleaseIntArrayElements(_bits, fBits, JNI_ABORT); + for(jint i = 0; i < elementCount; i ++) { + _env->SetIntArrayRegion(_typeData, i, 1, (const jint*)&typeData[i]); + } } - // ----------------------------------- static jint -nAllocationCreateTyped(JNIEnv *_env, jobject _this, jint e) +nAllocationCreateTyped(JNIEnv *_env, jobject _this, RsContext con, jint e) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAllocationCreateTyped, con(%p), e(%p)", con, (RsElement)e); return (jint) rsAllocationCreateTyped(con, (RsElement)e); } static void -nAllocationUploadToTexture(JNIEnv *_env, jobject _this, jint a, jboolean genMip, jint mip) +nAllocationUploadToTexture(JNIEnv *_env, jobject _this, RsContext con, jint a, jboolean genMip, jint mip) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAllocationUploadToTexture, con(%p), a(%p), genMip(%i), mip(%i)", con, (RsAllocation)a, genMip, mip); rsAllocationUploadToTexture(con, (RsAllocation)a, genMip, mip); } static void -nAllocationUploadToBufferObject(JNIEnv *_env, jobject _this, jint a) +nAllocationUploadToBufferObject(JNIEnv *_env, jobject _this, RsContext con, jint a) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAllocationUploadToBufferObject, con(%p), a(%p)", con, (RsAllocation)a); rsAllocationUploadToBufferObject(con, (RsAllocation)a); } @@ -480,9 +403,8 @@ static RsElement SkBitmapToPredefined(SkBitmap::Config cfg) } static int -nAllocationCreateFromBitmap(JNIEnv *_env, jobject _this, jint dstFmt, jboolean genMips, jobject jbitmap) +nAllocationCreateFromBitmap(JNIEnv *_env, jobject _this, RsContext con, jint dstFmt, jboolean genMips, jobject jbitmap) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); SkBitmap const * nativeBitmap = (SkBitmap const *)_env->GetIntField(jbitmap, gNativeBitmapID); const SkBitmap& bitmap(*nativeBitmap); @@ -508,9 +430,8 @@ static void ReleaseBitmapCallback(void *bmp) } static int -nAllocationCreateBitmapRef(JNIEnv *_env, jobject _this, jint type, jobject jbitmap) +nAllocationCreateBitmapRef(JNIEnv *_env, jobject _this, RsContext con, jint type, jobject jbitmap) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); SkBitmap * nativeBitmap = (SkBitmap *)_env->GetIntField(jbitmap, gNativeBitmapID); @@ -522,10 +443,8 @@ nAllocationCreateBitmapRef(JNIEnv *_env, jobject _this, jint type, jobject jbitm } static int -nAllocationCreateFromAssetStream(JNIEnv *_env, jobject _this, jint dstFmt, jboolean genMips, jint native_asset) +nAllocationCreateFromAssetStream(JNIEnv *_env, jobject _this, RsContext con, jint dstFmt, jboolean genMips, jint native_asset) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - Asset* asset = reinterpret_cast(native_asset); SkBitmap bitmap; SkImageDecoder::DecodeMemory(asset->getBuffer(false), asset->getLength(), @@ -548,9 +467,8 @@ nAllocationCreateFromAssetStream(JNIEnv *_env, jobject _this, jint dstFmt, jbool } static int -nAllocationCreateFromBitmapBoxed(JNIEnv *_env, jobject _this, jint dstFmt, jboolean genMips, jobject jbitmap) +nAllocationCreateFromBitmapBoxed(JNIEnv *_env, jobject _this, RsContext con, jint dstFmt, jboolean genMips, jobject jbitmap) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); SkBitmap const * nativeBitmap = (SkBitmap const *)_env->GetIntField(jbitmap, gNativeBitmapID); const SkBitmap& bitmap(*nativeBitmap); @@ -572,9 +490,8 @@ nAllocationCreateFromBitmapBoxed(JNIEnv *_env, jobject _this, jint dstFmt, jbool static void -nAllocationSubData1D_i(JNIEnv *_env, jobject _this, jint alloc, jint offset, jint count, jintArray data, int sizeBytes) +nAllocationSubData1D_i(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jint offset, jint count, jintArray data, int sizeBytes) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocation1DSubData_i, con(%p), adapter(%p), offset(%i), count(%i), len(%i), sizeBytes(%i)", con, (RsAllocation)alloc, offset, count, len, sizeBytes); jint *ptr = _env->GetIntArrayElements(data, NULL); @@ -583,9 +500,8 @@ nAllocationSubData1D_i(JNIEnv *_env, jobject _this, jint alloc, jint offset, jin } static void -nAllocationSubData1D_s(JNIEnv *_env, jobject _this, jint alloc, jint offset, jint count, jshortArray data, int sizeBytes) +nAllocationSubData1D_s(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jint offset, jint count, jshortArray data, int sizeBytes) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocation1DSubData_s, con(%p), adapter(%p), offset(%i), count(%i), len(%i), sizeBytes(%i)", con, (RsAllocation)alloc, offset, count, len, sizeBytes); jshort *ptr = _env->GetShortArrayElements(data, NULL); @@ -594,9 +510,8 @@ nAllocationSubData1D_s(JNIEnv *_env, jobject _this, jint alloc, jint offset, jin } static void -nAllocationSubData1D_b(JNIEnv *_env, jobject _this, jint alloc, jint offset, jint count, jbyteArray data, int sizeBytes) +nAllocationSubData1D_b(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jint offset, jint count, jbyteArray data, int sizeBytes) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocation1DSubData_b, con(%p), adapter(%p), offset(%i), count(%i), len(%i), sizeBytes(%i)", con, (RsAllocation)alloc, offset, count, len, sizeBytes); jbyte *ptr = _env->GetByteArrayElements(data, NULL); @@ -605,9 +520,8 @@ nAllocationSubData1D_b(JNIEnv *_env, jobject _this, jint alloc, jint offset, jin } static void -nAllocationSubData1D_f(JNIEnv *_env, jobject _this, jint alloc, jint offset, jint count, jfloatArray data, int sizeBytes) +nAllocationSubData1D_f(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jint offset, jint count, jfloatArray data, int sizeBytes) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocation1DSubData_f, con(%p), adapter(%p), offset(%i), count(%i), len(%i), sizeBytes(%i)", con, (RsAllocation)alloc, offset, count, len, sizeBytes); jfloat *ptr = _env->GetFloatArrayElements(data, NULL); @@ -616,9 +530,19 @@ nAllocationSubData1D_f(JNIEnv *_env, jobject _this, jint alloc, jint offset, jin } static void -nAllocationSubData2D_i(JNIEnv *_env, jobject _this, jint alloc, jint xoff, jint yoff, jint w, jint h, jintArray data, int sizeBytes) +// native void rsnAllocationSubElementData1D(int con, int id, int xoff, int compIdx, byte[] d, int sizeBytes); +nAllocationSubElementData1D(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jint offset, jint compIdx, jbyteArray data, int sizeBytes) +{ + jint len = _env->GetArrayLength(data); + LOG_API("nAllocationSubElementData1D, con(%p), alloc(%p), offset(%i), comp(%i), len(%i), sizeBytes(%i)", con, (RsAllocation)alloc, offset, compIdx, len, sizeBytes); + jbyte *ptr = _env->GetByteArrayElements(data, NULL); + rsAllocation1DSubElementData(con, (RsAllocation)alloc, offset, ptr, compIdx, sizeBytes); + _env->ReleaseByteArrayElements(data, ptr, JNI_ABORT); +} + +static void +nAllocationSubData2D_i(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jint xoff, jint yoff, jint w, jint h, jintArray data, int sizeBytes) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocation2DSubData_i, con(%p), adapter(%p), xoff(%i), yoff(%i), w(%i), h(%i), len(%i)", con, (RsAllocation)alloc, xoff, yoff, w, h, len); jint *ptr = _env->GetIntArrayElements(data, NULL); @@ -627,9 +551,8 @@ nAllocationSubData2D_i(JNIEnv *_env, jobject _this, jint alloc, jint xoff, jint } static void -nAllocationSubData2D_f(JNIEnv *_env, jobject _this, jint alloc, jint xoff, jint yoff, jint w, jint h, jfloatArray data, int sizeBytes) +nAllocationSubData2D_f(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jint xoff, jint yoff, jint w, jint h, jfloatArray data, int sizeBytes) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocation2DSubData_i, con(%p), adapter(%p), xoff(%i), yoff(%i), w(%i), h(%i), len(%i)", con, (RsAllocation)alloc, xoff, yoff, w, h, len); jfloat *ptr = _env->GetFloatArrayElements(data, NULL); @@ -638,9 +561,8 @@ nAllocationSubData2D_f(JNIEnv *_env, jobject _this, jint alloc, jint xoff, jint } static void -nAllocationRead_i(JNIEnv *_env, jobject _this, jint alloc, jintArray data) +nAllocationRead_i(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jintArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocationRead_i, con(%p), alloc(%p), len(%i)", con, (RsAllocation)alloc, len); jint *ptr = _env->GetIntArrayElements(data, NULL); @@ -649,9 +571,8 @@ nAllocationRead_i(JNIEnv *_env, jobject _this, jint alloc, jintArray data) } static void -nAllocationRead_f(JNIEnv *_env, jobject _this, jint alloc, jfloatArray data) +nAllocationRead_f(JNIEnv *_env, jobject _this, RsContext con, jint alloc, jfloatArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAllocationRead_f, con(%p), alloc(%p), len(%i)", con, (RsAllocation)alloc, len); jfloat *ptr = _env->GetFloatArrayElements(data, NULL); @@ -659,70 +580,89 @@ nAllocationRead_f(JNIEnv *_env, jobject _this, jint alloc, jfloatArray data) _env->ReleaseFloatArrayElements(data, ptr, 0); } +static jint +nAllocationGetType(JNIEnv *_env, jobject _this, RsContext con, jint a) +{ + LOG_API("nAllocationGetType, con(%p), a(%p)", con, (RsAllocation)a); + return (jint) rsAllocationGetType(con, (RsAllocation)a); +} -//{"nAllocationDataFromObject", "(ILandroid/renderscript/Type;Ljava/lang/Object;)V", (void*)nAllocationDataFromObject }, -static void -nAllocationSubDataFromObject(JNIEnv *_env, jobject _this, jint alloc, jobject _type, jint offset, jobject _o) +// ----------------------------------- + +static int +nFileA3DCreateFromAssetStream(JNIEnv *_env, jobject _this, RsContext con, jint native_asset) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nAllocationDataFromObject con(%p), alloc(%p)", con, (RsAllocation)alloc); + LOGV("______nFileA3D %u", (uint32_t) native_asset); - const TypeCache *tc = (TypeCache *)_env->GetIntField(_type, gTypeNativeCache); + Asset* asset = reinterpret_cast(native_asset); - void * bufAlloc = malloc(tc->size); - void * buf = bufAlloc; - for (int ct=0; ct < tc->fieldCount; ct++) { - const TypeFieldCache *tfc = &tc->fields[ct]; - buf = tfc->ptr(_env, _o, tfc->field, buf); - } - rsAllocation1DSubData(con, (RsAllocation)alloc, offset, 1, bufAlloc, tc->size); - free(bufAlloc); + jint id = (jint)rsFileA3DCreateFromAssetStream(con, asset->getBuffer(false), asset->getLength()); + return id; +} + +static int +nFileA3DGetNumIndexEntries(JNIEnv *_env, jobject _this, RsContext con, jint fileA3D) +{ + int32_t numEntries = 0; + rsFileA3DGetNumIndexEntries(con, &numEntries, (RsFile)fileA3D); + return numEntries; } static void -nAllocationSubReadFromObject(JNIEnv *_env, jobject _this, jint alloc, jobject _type, jint offset, jobject _o) +nFileA3DGetIndexEntries(JNIEnv *_env, jobject _this, RsContext con, jint fileA3D, jint numEntries, jintArray _ids, jobjectArray _entries) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nAllocationReadFromObject con(%p), alloc(%p)", con, (RsAllocation)alloc); + LOGV("______nFileA3D %u", (uint32_t) fileA3D); + RsFileIndexEntry *fileEntries = (RsFileIndexEntry*)malloc((uint32_t)numEntries * sizeof(RsFileIndexEntry)); - assert(offset == 0); + rsFileA3DGetIndexEntries(con, fileEntries, (uint32_t)numEntries, (RsFile)fileA3D); - const TypeCache *tc = (TypeCache *)_env->GetIntField(_type, gTypeNativeCache); + for(jint i = 0; i < numEntries; i ++) { + _env->SetObjectArrayElement(_entries, i, _env->NewStringUTF(fileEntries[i].objectName)); + _env->SetIntArrayRegion(_ids, i, 1, (const jint*)&fileEntries[i].classID); + } - void * bufAlloc = malloc(tc->size); - void * buf = bufAlloc; - rsAllocationRead(con, (RsAllocation)alloc, bufAlloc); + free(fileEntries); +} - for (int ct=0; ct < tc->fieldCount; ct++) { - const TypeFieldCache *tfc = &tc->fields[ct]; - buf = tfc->readPtr(_env, _o, tfc->field, buf); - } - free(bufAlloc); +static int +nFileA3DGetEntryByIndex(JNIEnv *_env, jobject _this, RsContext con, jint fileA3D, jint index) +{ + LOGV("______nFileA3D %u", (uint32_t) fileA3D); + jint id = (jint)rsFileA3DGetEntryByIndex(con, (uint32_t)index, (RsFile)fileA3D); + return id; +} + +// ----------------------------------- + +static int +nFontCreateFromFile(JNIEnv *_env, jobject _this, RsContext con, jstring fileName, jint fontSize, jint dpi) +{ + const char* fileNameUTF = _env->GetStringUTFChars(fileName, NULL); + + jint id = (jint)rsFontCreateFromFile(con, fileNameUTF, fontSize, dpi); + return id; } // ----------------------------------- static void -nAdapter1DBindAllocation(JNIEnv *_env, jobject _this, jint adapter, jint alloc) +nAdapter1DBindAllocation(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint alloc) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAdapter1DBindAllocation, con(%p), adapter(%p), alloc(%p)", con, (RsAdapter1D)adapter, (RsAllocation)alloc); rsAdapter1DBindAllocation(con, (RsAdapter1D)adapter, (RsAllocation)alloc); } static void -nAdapter1DSetConstraint(JNIEnv *_env, jobject _this, jint adapter, jint dim, jint value) +nAdapter1DSetConstraint(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint dim, jint value) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAdapter1DSetConstraint, con(%p), adapter(%p), dim(%i), value(%i)", con, (RsAdapter1D)adapter, dim, value); rsAdapter1DSetConstraint(con, (RsAdapter1D)adapter, (RsDimension)dim, value); } static void -nAdapter1DData_i(JNIEnv *_env, jobject _this, jint adapter, jintArray data) +nAdapter1DData_i(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jintArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter1DData_i, con(%p), adapter(%p), len(%i)", con, (RsAdapter1D)adapter, len); jint *ptr = _env->GetIntArrayElements(data, NULL); @@ -731,9 +671,8 @@ nAdapter1DData_i(JNIEnv *_env, jobject _this, jint adapter, jintArray data) } static void -nAdapter1DSubData_i(JNIEnv *_env, jobject _this, jint adapter, jint offset, jint count, jintArray data) +nAdapter1DSubData_i(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint offset, jint count, jintArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter1DSubData_i, con(%p), adapter(%p), offset(%i), count(%i), len(%i)", con, (RsAdapter1D)adapter, offset, count, len); jint *ptr = _env->GetIntArrayElements(data, NULL); @@ -742,9 +681,8 @@ nAdapter1DSubData_i(JNIEnv *_env, jobject _this, jint adapter, jint offset, jint } static void -nAdapter1DData_f(JNIEnv *_env, jobject _this, jint adapter, jfloatArray data) +nAdapter1DData_f(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jfloatArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter1DData_f, con(%p), adapter(%p), len(%i)", con, (RsAdapter1D)adapter, len); jfloat *ptr = _env->GetFloatArrayElements(data, NULL); @@ -753,9 +691,8 @@ nAdapter1DData_f(JNIEnv *_env, jobject _this, jint adapter, jfloatArray data) } static void -nAdapter1DSubData_f(JNIEnv *_env, jobject _this, jint adapter, jint offset, jint count, jfloatArray data) +nAdapter1DSubData_f(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint offset, jint count, jfloatArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter1DSubData_f, con(%p), adapter(%p), offset(%i), count(%i), len(%i)", con, (RsAdapter1D)adapter, offset, count, len); jfloat *ptr = _env->GetFloatArrayElements(data, NULL); @@ -764,9 +701,8 @@ nAdapter1DSubData_f(JNIEnv *_env, jobject _this, jint adapter, jint offset, jint } static jint -nAdapter1DCreate(JNIEnv *_env, jobject _this) +nAdapter1DCreate(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAdapter1DCreate, con(%p)", con); return (jint)rsAdapter1DCreate(con); } @@ -774,25 +710,22 @@ nAdapter1DCreate(JNIEnv *_env, jobject _this) // ----------------------------------- static void -nAdapter2DBindAllocation(JNIEnv *_env, jobject _this, jint adapter, jint alloc) +nAdapter2DBindAllocation(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint alloc) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAdapter2DBindAllocation, con(%p), adapter(%p), alloc(%p)", con, (RsAdapter2D)adapter, (RsAllocation)alloc); rsAdapter2DBindAllocation(con, (RsAdapter2D)adapter, (RsAllocation)alloc); } static void -nAdapter2DSetConstraint(JNIEnv *_env, jobject _this, jint adapter, jint dim, jint value) +nAdapter2DSetConstraint(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint dim, jint value) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAdapter2DSetConstraint, con(%p), adapter(%p), dim(%i), value(%i)", con, (RsAdapter2D)adapter, dim, value); rsAdapter2DSetConstraint(con, (RsAdapter2D)adapter, (RsDimension)dim, value); } static void -nAdapter2DData_i(JNIEnv *_env, jobject _this, jint adapter, jintArray data) +nAdapter2DData_i(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jintArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter2DData_i, con(%p), adapter(%p), len(%i)", con, (RsAdapter2D)adapter, len); jint *ptr = _env->GetIntArrayElements(data, NULL); @@ -801,9 +734,8 @@ nAdapter2DData_i(JNIEnv *_env, jobject _this, jint adapter, jintArray data) } static void -nAdapter2DData_f(JNIEnv *_env, jobject _this, jint adapter, jfloatArray data) +nAdapter2DData_f(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jfloatArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter2DData_f, con(%p), adapter(%p), len(%i)", con, (RsAdapter2D)adapter, len); jfloat *ptr = _env->GetFloatArrayElements(data, NULL); @@ -812,9 +744,8 @@ nAdapter2DData_f(JNIEnv *_env, jobject _this, jint adapter, jfloatArray data) } static void -nAdapter2DSubData_i(JNIEnv *_env, jobject _this, jint adapter, jint xoff, jint yoff, jint w, jint h, jintArray data) +nAdapter2DSubData_i(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint xoff, jint yoff, jint w, jint h, jintArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter2DSubData_i, con(%p), adapter(%p), xoff(%i), yoff(%i), w(%i), h(%i), len(%i)", con, (RsAdapter2D)adapter, xoff, yoff, w, h, len); @@ -824,9 +755,8 @@ nAdapter2DSubData_i(JNIEnv *_env, jobject _this, jint adapter, jint xoff, jint y } static void -nAdapter2DSubData_f(JNIEnv *_env, jobject _this, jint adapter, jint xoff, jint yoff, jint w, jint h, jfloatArray data) +nAdapter2DSubData_f(JNIEnv *_env, jobject _this, RsContext con, jint adapter, jint xoff, jint yoff, jint w, jint h, jfloatArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint len = _env->GetArrayLength(data); LOG_API("nAdapter2DSubData_f, con(%p), adapter(%p), xoff(%i), yoff(%i), w(%i), h(%i), len(%i)", con, (RsAdapter2D)adapter, xoff, yoff, w, h, len); @@ -836,9 +766,8 @@ nAdapter2DSubData_f(JNIEnv *_env, jobject _this, jint adapter, jint xoff, jint y } static jint -nAdapter2DCreate(JNIEnv *_env, jobject _this) +nAdapter2DCreate(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nAdapter2DCreate, con(%p)", con); return (jint)rsAdapter2DCreate(con); } @@ -846,41 +775,47 @@ nAdapter2DCreate(JNIEnv *_env, jobject _this) // ----------------------------------- static void -nScriptBindAllocation(JNIEnv *_env, jobject _this, jint script, jint alloc, jint slot) +nScriptBindAllocation(JNIEnv *_env, jobject _this, RsContext con, jint script, jint alloc, jint slot) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nScriptBindAllocation, con(%p), script(%p), alloc(%p), slot(%i)", con, (RsScript)script, (RsAllocation)alloc, slot); rsScriptBindAllocation(con, (RsScript)script, (RsAllocation)alloc, slot); } static void -nScriptSetClearColor(JNIEnv *_env, jobject _this, jint script, jfloat r, jfloat g, jfloat b, jfloat a) +nScriptSetVarI(JNIEnv *_env, jobject _this, RsContext con, jint script, jint slot, jint val) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nScriptSetClearColor, con(%p), s(%p), r(%f), g(%f), b(%f), a(%f)", con, (void *)script, r, g, b, a); - rsScriptSetClearColor(con, (RsScript)script, r, g, b, a); + LOG_API("nScriptSetVarI, con(%p), s(%p), slot(%i), val(%i)", con, (void *)script, slot, val); + rsScriptSetVarI(con, (RsScript)script, slot, val); } static void -nScriptSetClearDepth(JNIEnv *_env, jobject _this, jint script, jfloat d) +nScriptSetVarF(JNIEnv *_env, jobject _this, RsContext con, jint script, jint slot, float val) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nScriptCSetClearDepth, con(%p), s(%p), depth(%f)", con, (void *)script, d); - rsScriptSetClearDepth(con, (RsScript)script, d); + LOG_API("nScriptSetVarF, con(%p), s(%p), slot(%i), val(%f)", con, (void *)script, slot, val); + rsScriptSetVarF(con, (RsScript)script, slot, val); } static void -nScriptSetClearStencil(JNIEnv *_env, jobject _this, jint script, jint stencil) +nScriptSetVarD(JNIEnv *_env, jobject _this, RsContext con, jint script, jint slot, double val) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nScriptCSetClearStencil, con(%p), s(%p), stencil(%i)", con, (void *)script, stencil); - rsScriptSetClearStencil(con, (RsScript)script, stencil); + LOG_API("nScriptSetVarD, con(%p), s(%p), slot(%i), val(%lf)", con, (void *)script, slot, val); + rsScriptSetVarD(con, (RsScript)script, slot, val); } static void -nScriptSetTimeZone(JNIEnv *_env, jobject _this, jint script, jbyteArray timeZone) +nScriptSetVarV(JNIEnv *_env, jobject _this, RsContext con, jint script, jint slot, jbyteArray data) +{ + LOG_API("nScriptSetVarV, con(%p), s(%p), slot(%i)", con, (void *)script, slot); + jint len = _env->GetArrayLength(data); + jbyte *ptr = _env->GetByteArrayElements(data, NULL); + rsScriptSetVarV(con, (RsScript)script, slot, ptr, len); + _env->ReleaseByteArrayElements(data, ptr, JNI_ABORT); +} + + +static void +nScriptSetTimeZone(JNIEnv *_env, jobject _this, RsContext con, jint script, jbyteArray timeZone) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nScriptCSetTimeZone, con(%p), s(%p), timeZone(%s)", con, (void *)script, (const char *)timeZone); jint length = _env->GetArrayLength(timeZone); @@ -895,66 +830,36 @@ nScriptSetTimeZone(JNIEnv *_env, jobject _this, jint script, jbyteArray timeZone } static void -nScriptSetType(JNIEnv *_env, jobject _this, jint type, jboolean writable, jstring _str, jint slot) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nScriptCAddType, con(%p), type(%p), writable(%i), slot(%i)", con, (RsType)type, writable, slot); - const char* n = NULL; - if (_str) { - n = _env->GetStringUTFChars(_str, NULL); - } - rsScriptSetType(con, (RsType)type, slot, writable, n); - if (n) { - _env->ReleaseStringUTFChars(_str, n); - } -} - -static void -nScriptSetInvoke(JNIEnv *_env, jobject _this, jstring _str, jint slot) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nScriptSetInvoke, con(%p)", con); - const char* n = NULL; - if (_str) { - n = _env->GetStringUTFChars(_str, NULL); - } - rsScriptSetInvoke(con, n, slot); - if (n) { - _env->ReleaseStringUTFChars(_str, n); - } -} - -static void -nScriptInvoke(JNIEnv *_env, jobject _this, jint obj, jint slot) +nScriptInvoke(JNIEnv *_env, jobject _this, RsContext con, jint obj, jint slot) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nScriptInvoke, con(%p), script(%p)", con, (void *)obj); rsScriptInvoke(con, (RsScript)obj, slot); } static void -nScriptSetRoot(JNIEnv *_env, jobject _this, jboolean isRoot) +nScriptInvokeV(JNIEnv *_env, jobject _this, RsContext con, jint script, jint slot, jbyteArray data) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nScriptCSetRoot, con(%p), isRoot(%i)", con, isRoot); - rsScriptSetRoot(con, isRoot); + LOG_API("nScriptInvokeV, con(%p), s(%p), slot(%i)", con, (void *)script, slot); + jint len = _env->GetArrayLength(data); + jbyte *ptr = _env->GetByteArrayElements(data, NULL); + rsScriptInvokeV(con, (RsScript)script, slot, ptr, len); + _env->ReleaseByteArrayElements(data, ptr, JNI_ABORT); } + // ----------------------------------- static void -nScriptCBegin(JNIEnv *_env, jobject _this) +nScriptCBegin(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nScriptCBegin, con(%p)", con); rsScriptCBegin(con); } static void -nScriptCSetScript(JNIEnv *_env, jobject _this, jbyteArray scriptRef, +nScriptCSetScript(JNIEnv *_env, jobject _this, RsContext con, jbyteArray scriptRef, jint offset, jint length) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("!!! nScriptCSetScript, con(%p)", con); jint _exception = 0; jint remaining; @@ -995,114 +900,82 @@ exit: } static jint -nScriptCCreate(JNIEnv *_env, jobject _this) +nScriptCCreate(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nScriptCCreate, con(%p)", con); return (jint)rsScriptCCreate(con); } -static void -nScriptCAddDefineI32(JNIEnv *_env, jobject _this, jstring name, jint value) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - const char* n = _env->GetStringUTFChars(name, NULL); - LOG_API("nScriptCAddDefineI32, con(%p) name(%s) value(%d)", con, n, value); - rsScriptCSetDefineI32(con, n, value); - _env->ReleaseStringUTFChars(name, n); -} - -static void -nScriptCAddDefineF(JNIEnv *_env, jobject _this, jstring name, jfloat value) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - const char* n = _env->GetStringUTFChars(name, NULL); - LOG_API("nScriptCAddDefineF, con(%p) name(%s) value(%f)", con, n, value); - rsScriptCSetDefineF(con, n, value); - _env->ReleaseStringUTFChars(name, n); -} - // --------------------------------------------------------------------------- static void -nProgramFragmentStoreBegin(JNIEnv *_env, jobject _this, jint in, jint out) +nProgramStoreBegin(JNIEnv *_env, jobject _this, RsContext con, jint in, jint out) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramFragmentStoreBegin, con(%p), in(%p), out(%p)", con, (RsElement)in, (RsElement)out); - rsProgramFragmentStoreBegin(con, (RsElement)in, (RsElement)out); + LOG_API("nProgramStoreBegin, con(%p), in(%p), out(%p)", con, (RsElement)in, (RsElement)out); + rsProgramStoreBegin(con, (RsElement)in, (RsElement)out); } static void -nProgramFragmentStoreDepthFunc(JNIEnv *_env, jobject _this, jint func) +nProgramStoreDepthFunc(JNIEnv *_env, jobject _this, RsContext con, jint func) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramFragmentStoreDepthFunc, con(%p), func(%i)", con, func); - rsProgramFragmentStoreDepthFunc(con, (RsDepthFunc)func); + LOG_API("nProgramStoreDepthFunc, con(%p), func(%i)", con, func); + rsProgramStoreDepthFunc(con, (RsDepthFunc)func); } static void -nProgramFragmentStoreDepthMask(JNIEnv *_env, jobject _this, jboolean enable) +nProgramStoreDepthMask(JNIEnv *_env, jobject _this, RsContext con, jboolean enable) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramFragmentStoreDepthMask, con(%p), enable(%i)", con, enable); - rsProgramFragmentStoreDepthMask(con, enable); + LOG_API("nProgramStoreDepthMask, con(%p), enable(%i)", con, enable); + rsProgramStoreDepthMask(con, enable); } static void -nProgramFragmentStoreColorMask(JNIEnv *_env, jobject _this, jboolean r, jboolean g, jboolean b, jboolean a) +nProgramStoreColorMask(JNIEnv *_env, jobject _this, RsContext con, jboolean r, jboolean g, jboolean b, jboolean a) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramFragmentStoreColorMask, con(%p), r(%i), g(%i), b(%i), a(%i)", con, r, g, b, a); - rsProgramFragmentStoreColorMask(con, r, g, b, a); + LOG_API("nProgramStoreColorMask, con(%p), r(%i), g(%i), b(%i), a(%i)", con, r, g, b, a); + rsProgramStoreColorMask(con, r, g, b, a); } static void -nProgramFragmentStoreBlendFunc(JNIEnv *_env, jobject _this, int src, int dst) +nProgramStoreBlendFunc(JNIEnv *_env, jobject _this, RsContext con, int src, int dst) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramFragmentStoreBlendFunc, con(%p), src(%i), dst(%i)", con, src, dst); - rsProgramFragmentStoreBlendFunc(con, (RsBlendSrcFunc)src, (RsBlendDstFunc)dst); + LOG_API("nProgramStoreBlendFunc, con(%p), src(%i), dst(%i)", con, src, dst); + rsProgramStoreBlendFunc(con, (RsBlendSrcFunc)src, (RsBlendDstFunc)dst); } static void -nProgramFragmentStoreDither(JNIEnv *_env, jobject _this, jboolean enable) +nProgramStoreDither(JNIEnv *_env, jobject _this, RsContext con, jboolean enable) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramFragmentStoreDither, con(%p), enable(%i)", con, enable); - rsProgramFragmentStoreDither(con, enable); + LOG_API("nProgramStoreDither, con(%p), enable(%i)", con, enable); + rsProgramStoreDither(con, enable); } static jint -nProgramFragmentStoreCreate(JNIEnv *_env, jobject _this) +nProgramStoreCreate(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramFragmentStoreCreate, con(%p)", con); - - return (jint)rsProgramFragmentStoreCreate(con); + LOG_API("nProgramStoreCreate, con(%p)", con); + return (jint)rsProgramStoreCreate(con); } // --------------------------------------------------------------------------- static void -nProgramBindConstants(JNIEnv *_env, jobject _this, jint vpv, jint slot, jint a) +nProgramBindConstants(JNIEnv *_env, jobject _this, RsContext con, jint vpv, jint slot, jint a) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nProgramBindConstants, con(%p), vpf(%p), sloat(%i), a(%p)", con, (RsProgramVertex)vpv, slot, (RsAllocation)a); rsProgramBindConstants(con, (RsProgram)vpv, slot, (RsAllocation)a); } static void -nProgramBindTexture(JNIEnv *_env, jobject _this, jint vpf, jint slot, jint a) +nProgramBindTexture(JNIEnv *_env, jobject _this, RsContext con, jint vpf, jint slot, jint a) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nProgramBindTexture, con(%p), vpf(%p), slot(%i), a(%p)", con, (RsProgramFragment)vpf, slot, (RsAllocation)a); rsProgramBindTexture(con, (RsProgramFragment)vpf, slot, (RsAllocation)a); } static void -nProgramBindSampler(JNIEnv *_env, jobject _this, jint vpf, jint slot, jint a) +nProgramBindSampler(JNIEnv *_env, jobject _this, RsContext con, jint vpf, jint slot, jint a) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nProgramBindSampler, con(%p), vpf(%p), slot(%i), a(%p)", con, (RsProgramFragment)vpf, slot, (RsSampler)a); rsProgramBindSampler(con, (RsProgramFragment)vpf, slot, (RsSampler)a); } @@ -1110,9 +983,8 @@ nProgramBindSampler(JNIEnv *_env, jobject _this, jint vpf, jint slot, jint a) // --------------------------------------------------------------------------- static jint -nProgramFragmentCreate(JNIEnv *_env, jobject _this, jintArray params) +nProgramFragmentCreate(JNIEnv *_env, jobject _this, RsContext con, jintArray params) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); jint *paramPtr = _env->GetIntArrayElements(params, NULL); jint paramLen = _env->GetArrayLength(params); @@ -1124,9 +996,8 @@ nProgramFragmentCreate(JNIEnv *_env, jobject _this, jintArray params) } static jint -nProgramFragmentCreate2(JNIEnv *_env, jobject _this, jstring shader, jintArray params) +nProgramFragmentCreate2(JNIEnv *_env, jobject _this, RsContext con, jstring shader, jintArray params) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); const char* shaderUTF = _env->GetStringUTFChars(shader, NULL); jint shaderLen = _env->GetStringUTFLength(shader); jint *paramPtr = _env->GetIntArrayElements(params, NULL); @@ -1144,17 +1015,15 @@ nProgramFragmentCreate2(JNIEnv *_env, jobject _this, jstring shader, jintArray p // --------------------------------------------------------------------------- static jint -nProgramVertexCreate(JNIEnv *_env, jobject _this, jboolean texMat) +nProgramVertexCreate(JNIEnv *_env, jobject _this, RsContext con, jboolean texMat) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nProgramVertexCreate, con(%p), texMat(%i)", con, texMat); return (jint)rsProgramVertexCreate(con, texMat); } static jint -nProgramVertexCreate2(JNIEnv *_env, jobject _this, jstring shader, jintArray params) +nProgramVertexCreate2(JNIEnv *_env, jobject _this, RsContext con, jstring shader, jintArray params) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); const char* shaderUTF = _env->GetStringUTFChars(shader, NULL); jint shaderLen = _env->GetStringUTFLength(shader); jint *paramPtr = _env->GetIntArrayElements(params, NULL); @@ -1171,70 +1040,61 @@ nProgramVertexCreate2(JNIEnv *_env, jobject _this, jstring shader, jintArray par // --------------------------------------------------------------------------- static jint -nProgramRasterCreate(JNIEnv *_env, jobject _this, jint in, jint out, - jboolean pointSmooth, jboolean lineSmooth, jboolean pointSprite) +nProgramRasterCreate(JNIEnv *_env, jobject _this, RsContext con, jboolean pointSmooth, jboolean lineSmooth, jboolean pointSprite) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramRasterCreate, con(%p), in(%p), out(%p), pointSmooth(%i), lineSmooth(%i), pointSprite(%i)", - con, (RsElement)in, (RsElement)out, pointSmooth, lineSmooth, pointSprite); - return (jint)rsProgramRasterCreate(con, (RsElement)in, (RsElement)out, pointSmooth, lineSmooth, pointSprite); + LOG_API("nProgramRasterCreate, con(%p), pointSmooth(%i), lineSmooth(%i), pointSprite(%i)", + con, pointSmooth, lineSmooth, pointSprite); + return (jint)rsProgramRasterCreate(con, pointSmooth, lineSmooth, pointSprite); } static void -nProgramRasterSetPointSize(JNIEnv *_env, jobject _this, jint vpr, jfloat v) +nProgramRasterSetLineWidth(JNIEnv *_env, jobject _this, RsContext con, jint vpr, jfloat v) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramRasterSetPointSize, con(%p), vpf(%p), value(%f)", con, (RsProgramRaster)vpr, v); - rsProgramRasterSetPointSize(con, (RsProgramFragment)vpr, v); + LOG_API("nProgramRasterSetLineWidth, con(%p), vpf(%p), value(%f)", con, (RsProgramRaster)vpr, v); + rsProgramRasterSetLineWidth(con, (RsProgramRaster)vpr, v); } static void -nProgramRasterSetLineWidth(JNIEnv *_env, jobject _this, jint vpr, jfloat v) +nProgramRasterSetCullMode(JNIEnv *_env, jobject _this, RsContext con, jint vpr, jint v) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nProgramRasterSetLineWidth, con(%p), vpf(%p), value(%f)", con, (RsProgramRaster)vpr, v); - rsProgramRasterSetLineWidth(con, (RsProgramFragment)vpr, v); + LOG_API("nProgramRasterSetCullMode, con(%p), vpf(%p), value(%i)", con, (RsProgramRaster)vpr, v); + rsProgramRasterSetCullMode(con, (RsProgramRaster)vpr, (RsCullMode)v); } // --------------------------------------------------------------------------- static void -nContextBindRootScript(JNIEnv *_env, jobject _this, jint script) +nContextBindRootScript(JNIEnv *_env, jobject _this, RsContext con, jint script) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextBindRootScript, con(%p), script(%p)", con, (RsScript)script); rsContextBindRootScript(con, (RsScript)script); } static void -nContextBindProgramFragmentStore(JNIEnv *_env, jobject _this, jint pfs) +nContextBindProgramStore(JNIEnv *_env, jobject _this, RsContext con, jint pfs) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nContextBindProgramFragmentStore, con(%p), pfs(%p)", con, (RsProgramFragmentStore)pfs); - rsContextBindProgramFragmentStore(con, (RsProgramFragmentStore)pfs); + LOG_API("nContextBindProgramStore, con(%p), pfs(%p)", con, (RsProgramStore)pfs); + rsContextBindProgramStore(con, (RsProgramStore)pfs); } static void -nContextBindProgramFragment(JNIEnv *_env, jobject _this, jint pf) +nContextBindProgramFragment(JNIEnv *_env, jobject _this, RsContext con, jint pf) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextBindProgramFragment, con(%p), pf(%p)", con, (RsProgramFragment)pf); rsContextBindProgramFragment(con, (RsProgramFragment)pf); } static void -nContextBindProgramVertex(JNIEnv *_env, jobject _this, jint pf) +nContextBindProgramVertex(JNIEnv *_env, jobject _this, RsContext con, jint pf) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextBindProgramVertex, con(%p), pf(%p)", con, (RsProgramVertex)pf); rsContextBindProgramVertex(con, (RsProgramVertex)pf); } static void -nContextBindProgramRaster(JNIEnv *_env, jobject _this, jint pf) +nContextBindProgramRaster(JNIEnv *_env, jobject _this, RsContext con, jint pf) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nContextBindProgramRaster, con(%p), pf(%p)", con, (RsProgramRaster)pf); rsContextBindProgramRaster(con, (RsProgramRaster)pf); } @@ -1243,108 +1103,100 @@ nContextBindProgramRaster(JNIEnv *_env, jobject _this, jint pf) // --------------------------------------------------------------------------- static void -nSamplerBegin(JNIEnv *_env, jobject _this) +nSamplerBegin(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nSamplerBegin, con(%p)", con); rsSamplerBegin(con); } static void -nSamplerSet(JNIEnv *_env, jobject _this, jint p, jint v) +nSamplerSet(JNIEnv *_env, jobject _this, RsContext con, jint p, jint v) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nSamplerSet, con(%p), param(%i), value(%i)", con, p, v); rsSamplerSet(con, (RsSamplerParam)p, (RsSamplerValue)v); } static jint -nSamplerCreate(JNIEnv *_env, jobject _this) +nSamplerCreate(JNIEnv *_env, jobject _this, RsContext con) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); LOG_API("nSamplerCreate, con(%p)", con); return (jint)rsSamplerCreate(con); } // --------------------------------------------------------------------------- -static void -nLightBegin(JNIEnv *_env, jobject _this) +static jint +nMeshCreate(JNIEnv *_env, jobject _this, RsContext con, jint vtxCount, jint idxCount) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nLightBegin, con(%p)", con); - rsLightBegin(con); + LOG_API("nMeshCreate, con(%p), vtxCount(%i), idxCount(%i)", con, vtxCount, idxCount); + int id = (int)rsMeshCreate(con, vtxCount, idxCount); + return id; } static void -nLightSetIsMono(JNIEnv *_env, jobject _this, jboolean isMono) +nMeshBindVertex(JNIEnv *_env, jobject _this, RsContext con, jint mesh, jint alloc, jint slot) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nLightSetIsMono, con(%p), isMono(%i)", con, isMono); - rsLightSetMonochromatic(con, isMono); + LOG_API("nMeshBindVertex, con(%p), Mesh(%p), Alloc(%p), slot(%i)", con, (RsMesh)mesh, (RsAllocation)alloc, slot); + rsMeshBindVertex(con, (RsMesh)mesh, (RsAllocation)alloc, slot); } static void -nLightSetIsLocal(JNIEnv *_env, jobject _this, jboolean isLocal) +nMeshBindIndex(JNIEnv *_env, jobject _this, RsContext con, jint mesh, jint alloc, jint primID, jint slot) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nLightSetIsLocal, con(%p), isLocal(%i)", con, isLocal); - rsLightSetLocal(con, isLocal); + LOG_API("nMeshBindIndex, con(%p), Mesh(%p), Alloc(%p)", con, (RsMesh)mesh, (RsAllocation)alloc); + rsMeshBindIndex(con, (RsMesh)mesh, (RsAllocation)alloc, primID, slot); } static jint -nLightCreate(JNIEnv *_env, jobject _this) +nMeshGetVertexBufferCount(JNIEnv *_env, jobject _this, RsContext con, jint mesh) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nLightCreate, con(%p)", con); - return (jint)rsLightCreate(con); + LOG_API("nMeshGetVertexBufferCount, con(%p), Mesh(%p)", con, (RsMesh)mesh); + jint vtxCount = 0; + rsMeshGetVertexBufferCount(con, (RsMesh)mesh, &vtxCount); + return vtxCount; } -static void -nLightSetColor(JNIEnv *_env, jobject _this, jint light, float r, float g, float b) +static jint +nMeshGetIndexCount(JNIEnv *_env, jobject _this, RsContext con, jint mesh) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nLightSetColor, con(%p), light(%p), r(%f), g(%f), b(%f)", con, (RsLight)light, r, g, b); - rsLightSetColor(con, (RsLight)light, r, g, b); + LOG_API("nMeshGetIndexCount, con(%p), Mesh(%p)", con, (RsMesh)mesh); + jint idxCount = 0; + rsMeshGetIndexCount(con, (RsMesh)mesh, &idxCount); + return idxCount; } static void -nLightSetPosition(JNIEnv *_env, jobject _this, jint light, float x, float y, float z) +nMeshGetVertices(JNIEnv *_env, jobject _this, RsContext con, jint mesh, jintArray _ids, int numVtxIDs) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nLightSetPosition, con(%p), light(%p), x(%f), y(%f), z(%f)", con, (RsLight)light, x, y, z); - rsLightSetPosition(con, (RsLight)light, x, y, z); -} + LOG_API("nMeshGetVertices, con(%p), Mesh(%p)", con, (RsMesh)mesh); -// --------------------------------------------------------------------------- + RsAllocation *allocs = (RsAllocation*)malloc((uint32_t)numVtxIDs * sizeof(RsAllocation)); + rsMeshGetVertices(con, (RsMesh)mesh, allocs, (uint32_t)numVtxIDs); -static jint -nSimpleMeshCreate(JNIEnv *_env, jobject _this, jint batchID, jint indexID, jintArray vtxIDs, jint primID) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - jint len = _env->GetArrayLength(vtxIDs); - LOG_API("nSimpleMeshCreate, con(%p), batchID(%i), indexID(%i), vtxIDs.len(%i), primID(%i)", - con, batchID, indexID, len, primID); - jint *ptr = _env->GetIntArrayElements(vtxIDs, NULL); - int id = (int)rsSimpleMeshCreate(con, (void *)batchID, (void *)indexID, (void **)ptr, len, primID); - _env->ReleaseIntArrayElements(vtxIDs, ptr, 0/*JNI_ABORT*/); - return id; -} + for(jint i = 0; i < numVtxIDs; i ++) { + _env->SetIntArrayRegion(_ids, i, 1, (const jint*)&allocs[i]); + } -static void -nSimpleMeshBindVertex(JNIEnv *_env, jobject _this, jint s, jint alloc, jint slot) -{ - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nSimpleMeshBindVertex, con(%p), SimpleMesh(%p), Alloc(%p), slot(%i)", con, (RsSimpleMesh)s, (RsAllocation)alloc, slot); - rsSimpleMeshBindVertex(con, (RsSimpleMesh)s, (RsAllocation)alloc, slot); + free(allocs); } static void -nSimpleMeshBindIndex(JNIEnv *_env, jobject _this, jint s, jint alloc) +nMeshGetIndices(JNIEnv *_env, jobject _this, RsContext con, jint mesh, jintArray _idxIds, jintArray _primitives, int numIndices) { - RsContext con = (RsContext)(_env->GetIntField(_this, gContextId)); - LOG_API("nSimpleMeshBindIndex, con(%p), SimpleMesh(%p), Alloc(%p)", con, (RsSimpleMesh)s, (RsAllocation)alloc); - rsSimpleMeshBindIndex(con, (RsSimpleMesh)s, (RsAllocation)alloc); + LOG_API("nMeshGetVertices, con(%p), Mesh(%p)", con, (RsMesh)mesh); + + RsAllocation *allocs = (RsAllocation*)malloc((uint32_t)numIndices * sizeof(RsAllocation)); + uint32_t *prims= (uint32_t*)malloc((uint32_t)numIndices * sizeof(uint32_t)); + + rsMeshGetIndices(con, (RsMesh)mesh, allocs, prims, (uint32_t)numIndices); + + for(jint i = 0; i < numIndices; i ++) { + _env->SetIntArrayRegion(_idxIds, i, 1, (const jint*)&allocs[i]); + _env->SetIntArrayRegion(_primitives, i, 1, (const jint*)&prims[i]); + } + + free(allocs); + free(prims); } // --------------------------------------------------------------------------- @@ -1359,124 +1211,130 @@ static JNINativeMethod methods[] = { {"nDeviceCreate", "()I", (void*)nDeviceCreate }, {"nDeviceDestroy", "(I)V", (void*)nDeviceDestroy }, {"nDeviceSetConfig", "(III)V", (void*)nDeviceSetConfig }, -{"nContextCreate", "(II)I", (void*)nContextCreate }, -{"nContextCreateGL", "(IIZ)I", (void*)nContextCreateGL }, -{"nContextSetPriority", "(I)V", (void*)nContextSetPriority }, -{"nContextSetSurface", "(IILandroid/view/Surface;)V", (void*)nContextSetSurface }, -{"nContextDestroy", "(I)V", (void*)nContextDestroy }, -{"nContextDump", "(I)V", (void*)nContextDump }, -{"nContextPause", "()V", (void*)nContextPause }, -{"nContextResume", "()V", (void*)nContextResume }, -{"nAssignName", "(I[B)V", (void*)nAssignName }, -{"nObjDestroy", "(I)V", (void*)nObjDestroy }, -{"nObjDestroyOOB", "(I)V", (void*)nObjDestroyOOB }, -{"nContextGetMessage", "([IZ)I", (void*)nContextGetMessage }, -{"nContextInitToClient", "()V", (void*)nContextInitToClient }, -{"nContextDeinitToClient", "()V", (void*)nContextDeinitToClient }, - -{"nFileOpen", "([B)I", (void*)nFileOpen }, - -{"nElementCreate", "(IIZI)I", (void*)nElementCreate }, -{"nElementCreate2", "([I[Ljava/lang/String;)I", (void*)nElementCreate2 }, - -{"nTypeBegin", "(I)V", (void*)nTypeBegin }, -{"nTypeAdd", "(II)V", (void*)nTypeAdd }, -{"nTypeCreate", "()I", (void*)nTypeCreate }, -{"nTypeFinalDestroy", "(Landroid/renderscript/Type;)V", (void*)nTypeFinalDestroy }, -{"nTypeSetupFields", "(Landroid/renderscript/Type;[I[I[Ljava/lang/reflect/Field;)V", (void*)nTypeSetupFields }, - -{"nAllocationCreateTyped", "(I)I", (void*)nAllocationCreateTyped }, -{"nAllocationCreateFromBitmap", "(IZLandroid/graphics/Bitmap;)I", (void*)nAllocationCreateFromBitmap }, -{"nAllocationCreateBitmapRef", "(ILandroid/graphics/Bitmap;)I", (void*)nAllocationCreateBitmapRef }, -{"nAllocationCreateFromBitmapBoxed","(IZLandroid/graphics/Bitmap;)I", (void*)nAllocationCreateFromBitmapBoxed }, -{"nAllocationCreateFromAssetStream","(IZI)I", (void*)nAllocationCreateFromAssetStream }, -{"nAllocationUploadToTexture", "(IZI)V", (void*)nAllocationUploadToTexture }, -{"nAllocationUploadToBufferObject","(I)V", (void*)nAllocationUploadToBufferObject }, -{"nAllocationSubData1D", "(III[II)V", (void*)nAllocationSubData1D_i }, -{"nAllocationSubData1D", "(III[SI)V", (void*)nAllocationSubData1D_s }, -{"nAllocationSubData1D", "(III[BI)V", (void*)nAllocationSubData1D_b }, -{"nAllocationSubData1D", "(III[FI)V", (void*)nAllocationSubData1D_f }, -{"nAllocationSubData2D", "(IIIII[II)V", (void*)nAllocationSubData2D_i }, -{"nAllocationSubData2D", "(IIIII[FI)V", (void*)nAllocationSubData2D_f }, -{"nAllocationRead", "(I[I)V", (void*)nAllocationRead_i }, -{"nAllocationRead", "(I[F)V", (void*)nAllocationRead_f }, -{"nAllocationSubDataFromObject", "(ILandroid/renderscript/Type;ILjava/lang/Object;)V", (void*)nAllocationSubDataFromObject }, -{"nAllocationSubReadFromObject", "(ILandroid/renderscript/Type;ILjava/lang/Object;)V", (void*)nAllocationSubReadFromObject }, - -{"nAdapter1DBindAllocation", "(II)V", (void*)nAdapter1DBindAllocation }, -{"nAdapter1DSetConstraint", "(III)V", (void*)nAdapter1DSetConstraint }, -{"nAdapter1DData", "(I[I)V", (void*)nAdapter1DData_i }, -{"nAdapter1DData", "(I[F)V", (void*)nAdapter1DData_f }, -{"nAdapter1DSubData", "(III[I)V", (void*)nAdapter1DSubData_i }, -{"nAdapter1DSubData", "(III[F)V", (void*)nAdapter1DSubData_f }, -{"nAdapter1DCreate", "()I", (void*)nAdapter1DCreate }, - -{"nAdapter2DBindAllocation", "(II)V", (void*)nAdapter2DBindAllocation }, -{"nAdapter2DSetConstraint", "(III)V", (void*)nAdapter2DSetConstraint }, -{"nAdapter2DData", "(I[I)V", (void*)nAdapter2DData_i }, -{"nAdapter2DData", "(I[F)V", (void*)nAdapter2DData_f }, -{"nAdapter2DSubData", "(IIIII[I)V", (void*)nAdapter2DSubData_i }, -{"nAdapter2DSubData", "(IIIII[F)V", (void*)nAdapter2DSubData_f }, -{"nAdapter2DCreate", "()I", (void*)nAdapter2DCreate }, - -{"nScriptBindAllocation", "(III)V", (void*)nScriptBindAllocation }, -{"nScriptSetClearColor", "(IFFFF)V", (void*)nScriptSetClearColor }, -{"nScriptSetClearDepth", "(IF)V", (void*)nScriptSetClearDepth }, -{"nScriptSetClearStencil", "(II)V", (void*)nScriptSetClearStencil }, -{"nScriptSetTimeZone", "(I[B)V", (void*)nScriptSetTimeZone }, -{"nScriptSetType", "(IZLjava/lang/String;I)V", (void*)nScriptSetType }, -{"nScriptSetRoot", "(Z)V", (void*)nScriptSetRoot }, -{"nScriptSetInvokable", "(Ljava/lang/String;I)V", (void*)nScriptSetInvoke }, -{"nScriptInvoke", "(II)V", (void*)nScriptInvoke }, - -{"nScriptCBegin", "()V", (void*)nScriptCBegin }, -{"nScriptCSetScript", "([BII)V", (void*)nScriptCSetScript }, -{"nScriptCCreate", "()I", (void*)nScriptCCreate }, -{"nScriptCAddDefineI32", "(Ljava/lang/String;I)V", (void*)nScriptCAddDefineI32 }, -{"nScriptCAddDefineF", "(Ljava/lang/String;F)V", (void*)nScriptCAddDefineF }, - -{"nProgramFragmentStoreBegin", "(II)V", (void*)nProgramFragmentStoreBegin }, -{"nProgramFragmentStoreDepthFunc", "(I)V", (void*)nProgramFragmentStoreDepthFunc }, -{"nProgramFragmentStoreDepthMask", "(Z)V", (void*)nProgramFragmentStoreDepthMask }, -{"nProgramFragmentStoreColorMask", "(ZZZZ)V", (void*)nProgramFragmentStoreColorMask }, -{"nProgramFragmentStoreBlendFunc", "(II)V", (void*)nProgramFragmentStoreBlendFunc }, -{"nProgramFragmentStoreDither", "(Z)V", (void*)nProgramFragmentStoreDither }, -{"nProgramFragmentStoreCreate", "()I", (void*)nProgramFragmentStoreCreate }, - -{"nProgramBindConstants", "(III)V", (void*)nProgramBindConstants }, -{"nProgramBindTexture", "(III)V", (void*)nProgramBindTexture }, -{"nProgramBindSampler", "(III)V", (void*)nProgramBindSampler }, - -{"nProgramFragmentCreate", "([I)I", (void*)nProgramFragmentCreate }, -{"nProgramFragmentCreate2", "(Ljava/lang/String;[I)I", (void*)nProgramFragmentCreate2 }, - -{"nProgramRasterCreate", "(IIZZZ)I", (void*)nProgramRasterCreate }, -{"nProgramRasterSetPointSize", "(IF)V", (void*)nProgramRasterSetPointSize }, -{"nProgramRasterSetLineWidth", "(IF)V", (void*)nProgramRasterSetLineWidth }, - -{"nProgramVertexCreate", "(Z)I", (void*)nProgramVertexCreate }, -{"nProgramVertexCreate2", "(Ljava/lang/String;[I)I", (void*)nProgramVertexCreate2 }, - -{"nLightBegin", "()V", (void*)nLightBegin }, -{"nLightSetIsMono", "(Z)V", (void*)nLightSetIsMono }, -{"nLightSetIsLocal", "(Z)V", (void*)nLightSetIsLocal }, -{"nLightCreate", "()I", (void*)nLightCreate }, -{"nLightSetColor", "(IFFF)V", (void*)nLightSetColor }, -{"nLightSetPosition", "(IFFF)V", (void*)nLightSetPosition }, - -{"nContextBindRootScript", "(I)V", (void*)nContextBindRootScript }, -{"nContextBindProgramFragmentStore","(I)V", (void*)nContextBindProgramFragmentStore }, -{"nContextBindProgramFragment", "(I)V", (void*)nContextBindProgramFragment }, -{"nContextBindProgramVertex", "(I)V", (void*)nContextBindProgramVertex }, -{"nContextBindProgramRaster", "(I)V", (void*)nContextBindProgramRaster }, - -{"nSamplerBegin", "()V", (void*)nSamplerBegin }, -{"nSamplerSet", "(II)V", (void*)nSamplerSet }, -{"nSamplerCreate", "()I", (void*)nSamplerCreate }, - -{"nSimpleMeshCreate", "(II[II)I", (void*)nSimpleMeshCreate }, -{"nSimpleMeshBindVertex", "(III)V", (void*)nSimpleMeshBindVertex }, -{"nSimpleMeshBindIndex", "(II)V", (void*)nSimpleMeshBindIndex }, +{"nContextGetMessage", "(I[IZ)I", (void*)nContextGetMessage }, +{"nContextInitToClient", "(I)V", (void*)nContextInitToClient }, +{"nContextDeinitToClient", "(I)V", (void*)nContextDeinitToClient }, + + +// All methods below are thread protected in java. +{"rsnContextCreate", "(II)I", (void*)nContextCreate }, +{"rsnContextCreateGL", "(IIZ)I", (void*)nContextCreateGL }, +{"rsnContextFinish", "(I)V", (void*)nContextFinish }, +{"rsnContextSetPriority", "(II)V", (void*)nContextSetPriority }, +{"rsnContextSetSurface", "(IIILandroid/view/Surface;)V", (void*)nContextSetSurface }, +{"rsnContextDestroy", "(I)V", (void*)nContextDestroy }, +{"rsnContextDump", "(II)V", (void*)nContextDump }, +{"rsnContextPause", "(I)V", (void*)nContextPause }, +{"rsnContextResume", "(I)V", (void*)nContextResume }, +{"rsnAssignName", "(II[B)V", (void*)nAssignName }, +{"rsnGetName", "(II)Ljava/lang/String;", (void*)nGetName }, +{"rsnObjDestroy", "(II)V", (void*)nObjDestroy }, + +{"rsnFileOpen", "(I[B)I", (void*)nFileOpen }, +{"rsnFileA3DCreateFromAssetStream", "(II)I", (void*)nFileA3DCreateFromAssetStream }, +{"rsnFileA3DGetNumIndexEntries", "(II)I", (void*)nFileA3DGetNumIndexEntries }, +{"rsnFileA3DGetIndexEntries", "(III[I[Ljava/lang/String;)V", (void*)nFileA3DGetIndexEntries }, +{"rsnFileA3DGetEntryByIndex", "(III)I", (void*)nFileA3DGetEntryByIndex }, + +{"rsnFontCreateFromFile", "(ILjava/lang/String;II)I", (void*)nFontCreateFromFile }, + +{"rsnElementCreate", "(IIIZI)I", (void*)nElementCreate }, +{"rsnElementCreate2", "(I[I[Ljava/lang/String;[I)I", (void*)nElementCreate2 }, +{"rsnElementGetNativeData", "(II[I)V", (void*)nElementGetNativeData }, +{"rsnElementGetSubElements", "(II[I[Ljava/lang/String;)V", (void*)nElementGetSubElements }, + +{"rsnTypeBegin", "(II)V", (void*)nTypeBegin }, +{"rsnTypeAdd", "(III)V", (void*)nTypeAdd }, +{"rsnTypeCreate", "(I)I", (void*)nTypeCreate }, +{"rsnTypeGetNativeData", "(II[I)V", (void*)nTypeGetNativeData }, + +{"rsnAllocationCreateTyped", "(II)I", (void*)nAllocationCreateTyped }, +{"rsnAllocationCreateFromBitmap", "(IIZLandroid/graphics/Bitmap;)I", (void*)nAllocationCreateFromBitmap }, +{"rsnAllocationCreateBitmapRef", "(IILandroid/graphics/Bitmap;)I", (void*)nAllocationCreateBitmapRef }, +{"rsnAllocationCreateFromBitmapBoxed","(IIZLandroid/graphics/Bitmap;)I", (void*)nAllocationCreateFromBitmapBoxed }, +{"rsnAllocationCreateFromAssetStream","(IIZI)I", (void*)nAllocationCreateFromAssetStream }, +{"rsnAllocationUploadToTexture", "(IIZI)V", (void*)nAllocationUploadToTexture }, +{"rsnAllocationUploadToBufferObject","(II)V", (void*)nAllocationUploadToBufferObject }, +{"rsnAllocationSubData1D", "(IIII[II)V", (void*)nAllocationSubData1D_i }, +{"rsnAllocationSubData1D", "(IIII[SI)V", (void*)nAllocationSubData1D_s }, +{"rsnAllocationSubData1D", "(IIII[BI)V", (void*)nAllocationSubData1D_b }, +{"rsnAllocationSubData1D", "(IIII[FI)V", (void*)nAllocationSubData1D_f }, +{"rsnAllocationSubElementData1D", "(IIII[BI)V", (void*)nAllocationSubElementData1D }, +{"rsnAllocationSubData2D", "(IIIIII[II)V", (void*)nAllocationSubData2D_i }, +{"rsnAllocationSubData2D", "(IIIIII[FI)V", (void*)nAllocationSubData2D_f }, +{"rsnAllocationRead", "(II[I)V", (void*)nAllocationRead_i }, +{"rsnAllocationRead", "(II[F)V", (void*)nAllocationRead_f }, +{"rsnAllocationGetType", "(II)I", (void*)nAllocationGetType}, + +{"rsnAdapter1DBindAllocation", "(III)V", (void*)nAdapter1DBindAllocation }, +{"rsnAdapter1DSetConstraint", "(IIII)V", (void*)nAdapter1DSetConstraint }, +{"rsnAdapter1DData", "(II[I)V", (void*)nAdapter1DData_i }, +{"rsnAdapter1DData", "(II[F)V", (void*)nAdapter1DData_f }, +{"rsnAdapter1DSubData", "(IIII[I)V", (void*)nAdapter1DSubData_i }, +{"rsnAdapter1DSubData", "(IIII[F)V", (void*)nAdapter1DSubData_f }, +{"rsnAdapter1DCreate", "(I)I", (void*)nAdapter1DCreate }, + +{"rsnAdapter2DBindAllocation", "(III)V", (void*)nAdapter2DBindAllocation }, +{"rsnAdapter2DSetConstraint", "(IIII)V", (void*)nAdapter2DSetConstraint }, +{"rsnAdapter2DData", "(II[I)V", (void*)nAdapter2DData_i }, +{"rsnAdapter2DData", "(II[F)V", (void*)nAdapter2DData_f }, +{"rsnAdapter2DSubData", "(IIIIII[I)V", (void*)nAdapter2DSubData_i }, +{"rsnAdapter2DSubData", "(IIIIII[F)V", (void*)nAdapter2DSubData_f }, +{"rsnAdapter2DCreate", "(I)I", (void*)nAdapter2DCreate }, + +{"rsnScriptBindAllocation", "(IIII)V", (void*)nScriptBindAllocation }, +{"rsnScriptSetTimeZone", "(II[B)V", (void*)nScriptSetTimeZone }, +{"rsnScriptInvoke", "(III)V", (void*)nScriptInvoke }, +{"rsnScriptInvokeV", "(III[B)V", (void*)nScriptInvokeV }, +{"rsnScriptSetVarI", "(IIII)V", (void*)nScriptSetVarI }, +{"rsnScriptSetVarF", "(IIIF)V", (void*)nScriptSetVarF }, +{"rsnScriptSetVarD", "(IIID)V", (void*)nScriptSetVarD }, +{"rsnScriptSetVarV", "(III[B)V", (void*)nScriptSetVarV }, + +{"rsnScriptCBegin", "(I)V", (void*)nScriptCBegin }, +{"rsnScriptCSetScript", "(I[BII)V", (void*)nScriptCSetScript }, +{"rsnScriptCCreate", "(I)I", (void*)nScriptCCreate }, + +{"rsnProgramStoreBegin", "(III)V", (void*)nProgramStoreBegin }, +{"rsnProgramStoreDepthFunc", "(II)V", (void*)nProgramStoreDepthFunc }, +{"rsnProgramStoreDepthMask", "(IZ)V", (void*)nProgramStoreDepthMask }, +{"rsnProgramStoreColorMask", "(IZZZZ)V", (void*)nProgramStoreColorMask }, +{"rsnProgramStoreBlendFunc", "(III)V", (void*)nProgramStoreBlendFunc }, +{"rsnProgramStoreDither", "(IZ)V", (void*)nProgramStoreDither }, +{"rsnProgramStoreCreate", "(I)I", (void*)nProgramStoreCreate }, + +{"rsnProgramBindConstants", "(IIII)V", (void*)nProgramBindConstants }, +{"rsnProgramBindTexture", "(IIII)V", (void*)nProgramBindTexture }, +{"rsnProgramBindSampler", "(IIII)V", (void*)nProgramBindSampler }, + +{"rsnProgramFragmentCreate", "(I[I)I", (void*)nProgramFragmentCreate }, +{"rsnProgramFragmentCreate2", "(ILjava/lang/String;[I)I", (void*)nProgramFragmentCreate2 }, + +{"rsnProgramRasterCreate", "(IZZZ)I", (void*)nProgramRasterCreate }, +{"rsnProgramRasterSetLineWidth", "(IIF)V", (void*)nProgramRasterSetLineWidth }, +{"rsnProgramRasterSetCullMode", "(III)V", (void*)nProgramRasterSetCullMode }, + +{"rsnProgramVertexCreate", "(IZ)I", (void*)nProgramVertexCreate }, +{"rsnProgramVertexCreate2", "(ILjava/lang/String;[I)I", (void*)nProgramVertexCreate2 }, + +{"rsnContextBindRootScript", "(II)V", (void*)nContextBindRootScript }, +{"rsnContextBindProgramStore", "(II)V", (void*)nContextBindProgramStore }, +{"rsnContextBindProgramFragment", "(II)V", (void*)nContextBindProgramFragment }, +{"rsnContextBindProgramVertex", "(II)V", (void*)nContextBindProgramVertex }, +{"rsnContextBindProgramRaster", "(II)V", (void*)nContextBindProgramRaster }, + +{"rsnSamplerBegin", "(I)V", (void*)nSamplerBegin }, +{"rsnSamplerSet", "(III)V", (void*)nSamplerSet }, +{"rsnSamplerCreate", "(I)I", (void*)nSamplerCreate }, + +{"rsnMeshCreate", "(III)I", (void*)nMeshCreate }, +{"rsnMeshBindVertex", "(IIII)V", (void*)nMeshBindVertex }, +{"rsnMeshBindIndex", "(IIIII)V", (void*)nMeshBindIndex }, + +{"rsnMeshGetVertexBufferCount", "(II)I", (void*)nMeshGetVertexBufferCount }, +{"rsnMeshGetIndexCount", "(II)I", (void*)nMeshGetIndexCount }, +{"rsnMeshGetVertices", "(II[II)V", (void*)nMeshGetVertices }, +{"rsnMeshGetIndices", "(II[I[II)V", (void*)nMeshGetIndices }, }; @@ -1510,3 +1368,4 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) bail: return result; } + diff --git a/graphics/tests/graphicstests/src/android/graphics/BitmapFactoryTest.java b/graphics/tests/graphicstests/src/android/graphics/BitmapFactoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..09820efefedaa3af6b2a0fd991d53b2a9618b6d0 --- /dev/null +++ b/graphics/tests/graphicstests/src/android/graphics/BitmapFactoryTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import android.os.ParcelFileDescriptor; +import android.test.suitebuilder.annotation.SmallTest; + +import java.io.ByteArrayOutputStream; +import java.io.FileDescriptor; + +import junit.framework.TestCase; + + +public class BitmapFactoryTest extends TestCase { + + // tests that we can decode bitmaps from MemoryFiles + @SmallTest + public void testBitmapParcelFileDescriptor() throws Exception { + Bitmap bitmap1 = Bitmap.createBitmap( + new int[] { Color.BLUE }, 1, 1, Bitmap.Config.RGB_565); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + bitmap1.compress(Bitmap.CompressFormat.PNG, 100, out); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromData(out.toByteArray(), null); + FileDescriptor fd = pfd.getFileDescriptor(); + assertNotNull("Got null FileDescriptor", fd); + assertTrue("Got invalid FileDescriptor", fd.valid()); + Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd); + assertNotNull("BitmapFactory returned null", bitmap); + assertEquals("Bitmap width", 1, bitmap.getWidth()); + assertEquals("Bitmap height", 1, bitmap.getHeight()); + } + +} diff --git a/icu4j/java/android/icu/text/ArabicShaping.java b/icu4j/java/android/icu/text/ArabicShaping.java new file mode 100644 index 0000000000000000000000000000000000000000..13e2175340a3a4333033520a63ee94abee9171fd --- /dev/null +++ b/icu4j/java/android/icu/text/ArabicShaping.java @@ -0,0 +1,1947 @@ +/* +******************************************************************************* +* Copyright (C) 2001-2009, International Business Machines +* Corporation and others. All Rights Reserved. +******************************************************************************* +*/ + +/* + * Ported with minor modifications from ICU4J 4.2's + * com.ibm.icu.text.ArabicShaping class. + */ + +package android.icu.text; + + +/** + * Shape Arabic text on a character basis. + * + *

    ArabicShaping performs basic operations for "shaping" Arabic text. It is most + * useful for use with legacy data formats and legacy display technology + * (simple terminals). All operations are performed on Unicode characters.

    + * + *

    Text-based shaping means that some character code points in the text are + * replaced by others depending on the context. It transforms one kind of text + * into another. In comparison, modern displays for Arabic text select + * appropriate, context-dependent font glyphs for each text element, which means + * that they transform text into a glyph vector.

    + * + *

    Text transformations are necessary when modern display technology is not + * available or when text needs to be transformed to or from legacy formats that + * use "shaped" characters. Since the Arabic script is cursive, connecting + * adjacent letters to each other, computers select images for each letter based + * on the surrounding letters. This usually results in four images per Arabic + * letter: initial, middle, final, and isolated forms. In Unicode, on the other + * hand, letters are normally stored abstract, and a display system is expected + * to select the necessary glyphs. (This makes searching and other text + * processing easier because the same letter has only one code.) It is possible + * to mimic this with text transformations because there are characters in + * Unicode that are rendered as letters with a specific shape + * (or cursive connectivity). They were included for interoperability with + * legacy systems and codepages, and for unsophisticated display systems.

    + * + *

    A second kind of text transformations is supported for Arabic digits: + * For compatibility with legacy codepages that only include European digits, + * it is possible to replace one set of digits by another, changing the + * character code points. These operations can be performed for either + * Arabic-Indic Digits (U+0660...U+0669) or Eastern (Extended) Arabic-Indic + * digits (U+06f0...U+06f9).

    + * + *

    Some replacements may result in more or fewer characters (code points). + * By default, this means that the destination buffer may receive text with a + * length different from the source length. Some legacy systems rely on the + * length of the text to be constant. They expect extra spaces to be added + * or consumed either next to the affected character or at the end of the + * text.

    + * @stable ICU 2.0 + * + * @hide + */ +public class ArabicShaping { + private final int options; + private boolean isLogical; // convenience + private boolean spacesRelativeToTextBeginEnd; + private char tailChar; + + public static final ArabicShaping SHAPER = new ArabicShaping( + ArabicShaping.TEXT_DIRECTION_LOGICAL | + ArabicShaping.LENGTH_FIXED_SPACES_NEAR | + ArabicShaping.LETTERS_SHAPE | + ArabicShaping.DIGITS_NOOP); + + /** + * Convert a range of text in the source array, putting the result + * into a range of text in the destination array, and return the number + * of characters written. + * + * @param source An array containing the input text + * @param sourceStart The start of the range of text to convert + * @param sourceLength The length of the range of text to convert + * @param dest The destination array that will receive the result. + * It may be NULL only if destSize is 0. + * @param destStart The start of the range of the destination buffer to use. + * @param destSize The size (capacity) of the destination buffer. + * If destSize is 0, then no output is produced, + * but the necessary buffer size is returned ("preflighting"). This + * does not validate the text against the options, for example, + * if letters are being unshaped, and spaces are being consumed + * following lamalef, this will not detect a lamalef without a + * corresponding space. An error will be thrown when the actual + * conversion is attempted. + * @return The number of chars written to the destination buffer. + * If an error occurs, then no output was written, or it may be + * incomplete. + * @throws ArabicShapingException if the text cannot be converted according to the options. + * @stable ICU 2.0 + */ + public int shape(char[] source, int sourceStart, int sourceLength, + char[] dest, int destStart, int destSize) throws ArabicShapingException { + if (source == null) { + throw new IllegalArgumentException("source can not be null"); + } + if (sourceStart < 0 || sourceLength < 0 || sourceStart + sourceLength > source.length) { + throw new IllegalArgumentException("bad source start (" + sourceStart + + ") or length (" + sourceLength + + ") for buffer of length " + source.length); + } + if (dest == null && destSize != 0) { + throw new IllegalArgumentException("null dest requires destSize == 0"); + } + if ((destSize != 0) && + (destStart < 0 || destSize < 0 || destStart + destSize > dest.length)) { + throw new IllegalArgumentException("bad dest start (" + destStart + + ") or size (" + destSize + + ") for buffer of length " + dest.length); + } + /* Validate input options */ + if ( ((options&TASHKEEL_MASK) > 0) && + !(((options & TASHKEEL_MASK)==TASHKEEL_BEGIN) || + ((options & TASHKEEL_MASK)==TASHKEEL_END ) || + ((options & TASHKEEL_MASK)==TASHKEEL_RESIZE )|| + ((options & TASHKEEL_MASK)==TASHKEEL_REPLACE_BY_TATWEEL)) ){ + throw new IllegalArgumentException("Wrong Tashkeel argument"); + } + + ///CLOVER:OFF + //According to Steven Loomis, the code is unreachable when you OR all the constants within the if statements + if(((options&LAMALEF_MASK) > 0)&& + !(((options & LAMALEF_MASK)==LAMALEF_BEGIN) || + ((options & LAMALEF_MASK)==LAMALEF_END ) || + ((options & LAMALEF_MASK)==LAMALEF_RESIZE )|| + ((options & LAMALEF_MASK)==LAMALEF_AUTO) || + ((options & LAMALEF_MASK)==LAMALEF_NEAR))){ + throw new IllegalArgumentException("Wrong Lam Alef argument"); + } + ///CLOVER:ON + + /* Validate Tashkeel (Tashkeel replacement options should be enabled in shaping mode only)*/ + if(((options&TASHKEEL_MASK) > 0) && (options&LETTERS_MASK) == LETTERS_UNSHAPE) { + throw new IllegalArgumentException("Tashkeel replacement should not be enabled in deshaping mode "); + } + return internalShape(source, sourceStart, sourceLength, dest, destStart, destSize); + } + + /** + * Convert a range of text in place. This may only be used if the Length option + * does not grow or shrink the text. + * + * @param source An array containing the input text + * @param start The start of the range of text to convert + * @param length The length of the range of text to convert + * @throws ArabicShapingException if the text cannot be converted according to the options. + * @stable ICU 2.0 + */ + public void shape(char[] source, int start, int length) throws ArabicShapingException { + if ((options & LAMALEF_MASK) == LAMALEF_RESIZE) { + throw new ArabicShapingException("Cannot shape in place with length option resize."); + } + shape(source, start, length, source, start, length); + } + + /** + * Convert a string, returning the new string. + * + * @param text the string to convert + * @return the converted string + * @throws ArabicShapingException if the string cannot be converted according to the options. + * @stable ICU 2.0 + */ + public String shape(String text) throws ArabicShapingException { + char[] src = text.toCharArray(); + char[] dest = src; + if (((options & LAMALEF_MASK) == LAMALEF_RESIZE) && + ((options & LETTERS_MASK) == LETTERS_UNSHAPE)) { + + dest = new char[src.length * 2]; // max + } + int len = shape(src, 0, src.length, dest, 0, dest.length); + + return new String(dest, 0, len); + } + + /** + * Construct ArabicShaping using the options flags. + * The flags are as follows:
    + * 'LENGTH' flags control whether the text can change size, and if not, + * how to maintain the size of the text when LamAlef ligatures are + * formed or broken.
    + * 'TEXT_DIRECTION' flags control whether the text is read and written + * in visual order or in logical order.
    + * 'LETTERS_SHAPE' flags control whether conversion is to or from + * presentation forms.
    + * 'DIGITS' flags control whether digits are shaped, and whether from + * European to Arabic-Indic or vice-versa.
    + * 'DIGIT_TYPE' flags control whether standard or extended Arabic-Indic + * digits are used when performing digit conversion. + * @stable ICU 2.0 + */ + public ArabicShaping(int options) { + this.options = options; + if ((options & DIGITS_MASK) > 0x80) { + throw new IllegalArgumentException("bad DIGITS options"); + } + + isLogical = ( (options & TEXT_DIRECTION_MASK) == TEXT_DIRECTION_LOGICAL ); + /* Validate options */ + spacesRelativeToTextBeginEnd = ( (options & SPACES_RELATIVE_TO_TEXT_MASK) == SPACES_RELATIVE_TO_TEXT_BEGIN_END ); + if ( (options&SHAPE_TAIL_TYPE_MASK) == SHAPE_TAIL_NEW_UNICODE){ + tailChar = NEW_TAIL_CHAR; + } else { + tailChar = OLD_TAIL_CHAR; + } + } + + /* Seen Tail options */ + /** + * Memory option: the result must have the same length as the source. + * Shaping mode: The SEEN family character will expand into two characters using space near + * the SEEN family character(i.e. the space after the character). + * if there are no spaces found, ArabicShapingException will be thrown + * + * De-shaping mode: Any Seen character followed by Tail character will be + * replaced by one cell Seen and a space will replace the Tail. + * Affects: Seen options + */ + public static final int SEEN_TWOCELL_NEAR = 0x200000; + + /** Bit mask for Seen memory options. */ + public static final int SEEN_MASK = 0x700000; + + /* YehHamza options */ + /** + * Memory option: the result must have the same length as the source. + * Shaping mode: The YEHHAMZA character will expand into two characters using space near it + * (i.e. the space after the character) + * if there are no spaces found, ArabicShapingException will be thrown + * + * De-shaping mode: Any Yeh (final or isolated) character followed by Hamza character will be + * replaced by one cell YehHamza and space will replace the Hamza. + * Affects: YehHamza options + */ + public static final int YEHHAMZA_TWOCELL_NEAR = 0x1000000; + + + /** Bit mask for YehHamza memory options. */ + public static final int YEHHAMZA_MASK = 0x3800000; + + /* New Tashkeel options */ + /** + * Memory option: the result must have the same length as the source. + * Shaping mode: Tashkeel characters will be replaced by spaces. + * Spaces will be placed at beginning of the buffer + * + * De-shaping mode: N/A + * Affects: Tashkeel options + */ + public static final int TASHKEEL_BEGIN = 0x40000; + + /** + * Memory option: the result must have the same length as the source. + * Shaping mode: Tashkeel characters will be replaced by spaces. + * Spaces will be placed at end of the buffer + * + * De-shaping mode: N/A + * Affects: Tashkeel options + */ + public static final int TASHKEEL_END = 0x60000; + + /** + * Memory option: allow the result to have a different length than the source. + * Shaping mode: Tashkeel characters will be removed, buffer length will shrink. + * De-shaping mode: N/A + * + * Affects: Tashkeel options + */ + public static final int TASHKEEL_RESIZE = 0x80000; + + /** + * Memory option: the result must have the same length as the source. + * Shaping mode: Tashkeel characters will be replaced by Tatweel if it is connected to adjacent + * characters (i.e. shaped on Tatweel) or replaced by space if it is not connected. + * + * De-shaping mode: N/A + * Affects: YehHamza options + */ + public static final int TASHKEEL_REPLACE_BY_TATWEEL = 0xC0000; + + /** Bit mask for Tashkeel replacement with Space or Tatweel memory options. */ + public static final int TASHKEEL_MASK = 0xE0000; + + /* Space location Control options */ + /** + * This option effects the meaning of BEGIN and END options. if this option is not used the default + * for BEGIN and END will be as following: + * The Default (for both Visual LTR, Visual RTL and Logical Text) + * 1. BEGIN always refers to the start address of physical memory. + * 2. END always refers to the end address of physical memory. + * + * If this option is used it will swap the meaning of BEGIN and END only for Visual LTR text. + * + * The affect on BEGIN and END Memory Options will be as following: + * A. BEGIN For Visual LTR text: This will be the beginning (right side) of the visual text + * (corresponding to the physical memory address end, same as END in default behavior) + * B. BEGIN For Logical text: Same as BEGIN in default behavior. + * C. END For Visual LTR text: This will be the end (left side) of the visual text. (corresponding to + * the physical memory address beginning, same as BEGIN in default behavior) + * D. END For Logical text: Same as END in default behavior. + * Affects: All LamAlef BEGIN, END and AUTO options. + */ + public static final int SPACES_RELATIVE_TO_TEXT_BEGIN_END = 0x4000000; + + /** Bit mask for swapping BEGIN and END for Visual LTR text */ + public static final int SPACES_RELATIVE_TO_TEXT_MASK = 0x4000000; + + /** + * If this option is used, shaping will use the new Unicode code point for TAIL (i.e. 0xFE73). + * If this option is not specified (Default), old unofficial Unicode TAIL code point is used (i.e. 0x200B) + * De-shaping will not use this option as it will always search for both the new Unicode code point for the + * TAIL (i.e. 0xFE73) or the old unofficial Unicode TAIL code point (i.e. 0x200B) and de-shape the + * Seen-Family letter accordingly. + * + * Shaping Mode: Only shaping. + * De-shaping Mode: N/A. + * Affects: All Seen options + */ + public static final int SHAPE_TAIL_NEW_UNICODE = 0x8000000; + + /** Bit mask for new Unicode Tail option */ + public static final int SHAPE_TAIL_TYPE_MASK = 0x8000000; + + /** + * Memory option: allow the result to have a different length than the source. + * @stable ICU 2.0 + */ + public static final int LENGTH_GROW_SHRINK = 0; + + /** + * Memory option: allow the result to have a different length than the source. + * Affects: LamAlef options + * This option is an alias to LENGTH_GROW_SHRINK + */ + public static final int LAMALEF_RESIZE = 0; + + /** + * Memory option: the result must have the same length as the source. + * If more room is necessary, then try to consume spaces next to modified characters. + * @stable ICU 2.0 + */ + public static final int LENGTH_FIXED_SPACES_NEAR = 1; + + /** + * Memory option: the result must have the same length as the source. + * If more room is necessary, then try to consume spaces next to modified characters. + * Affects: LamAlef options + * This option is an alias to LENGTH_FIXED_SPACES_NEAR + */ + public static final int LAMALEF_NEAR = 1 ; + + /** + * Memory option: the result must have the same length as the source. + * If more room is necessary, then try to consume spaces at the end of the text. + * @stable ICU 2.0 + */ + public static final int LENGTH_FIXED_SPACES_AT_END = 2; + + + /** + * Memory option: the result must have the same length as the source. + * If more room is necessary, then try to consume spaces at the end of the text. + * Affects: LamAlef options + * This option is an alias to LENGTH_FIXED_SPACES_AT_END + */ + public static final int LAMALEF_END = 2; + + /** + * Memory option: the result must have the same length as the source. + * If more room is necessary, then try to consume spaces at the beginning of the text. + * @stable ICU 2.0 + */ + public static final int LENGTH_FIXED_SPACES_AT_BEGINNING = 3; + + /** + * Memory option: the result must have the same length as the source. + * If more room is necessary, then try to consume spaces at the beginning of the text. + * Affects: LamAlef options + * This option is an alias to LENGTH_FIXED_SPACES_AT_BEGINNING + */ + public static final int LAMALEF_BEGIN = 3; + + /** + * Memory option: the result must have the same length as the source. + * Shaping Mode: For each LAMALEF character found, expand LAMALEF using space at end. + * If there is no space at end, use spaces at beginning of the buffer. If there + * is no space at beginning of the buffer, use spaces at the near (i.e. the space + * after the LAMALEF character). + * + * Deshaping Mode: Perform the same function as the flag equals LAMALEF_END. + * Affects: LamAlef options + */ + public static final int LAMALEF_AUTO = 0x10000; + + /** + * Bit mask for memory options. + * @stable ICU 2.0 + */ + public static final int LENGTH_MASK = 0x10003; + + /** Bit mask for LamAlef memory options. */ + + public static final int LAMALEF_MASK = 0x10003; + + /** + * Direction indicator: the source is in logical (keyboard) order. + * @stable ICU 2.0 + */ + public static final int TEXT_DIRECTION_LOGICAL = 0; + + /** + * Direction indicator:the source is in visual RTL order, + * the rightmost displayed character stored first. + * This option is an alias to U_SHAPE_TEXT_DIRECTION_LOGICAL + */ + public static final int TEXT_DIRECTION_VISUAL_RTL = 0; + + /** + * Direction indicator: the source is in visual (display) order, that is, + * the leftmost displayed character is stored first. + * @stable ICU 2.0 + */ + public static final int TEXT_DIRECTION_VISUAL_LTR = 4; + + /** + * Bit mask for direction indicators. + * @stable ICU 2.0 + */ + public static final int TEXT_DIRECTION_MASK = 4; + + + /** + * Letter shaping option: do not perform letter shaping. + * @stable ICU 2.0 + */ + public static final int LETTERS_NOOP = 0; + + /** + * Letter shaping option: replace normative letter characters in the U+0600 (Arabic) block, + * by shaped ones in the U+FE70 (Presentation Forms B) block. Performs Lam-Alef ligature + * substitution. + * @stable ICU 2.0 + */ + public static final int LETTERS_SHAPE = 8; + + /** + * Letter shaping option: replace shaped letter characters in the U+FE70 (Presentation Forms B) block + * by normative ones in the U+0600 (Arabic) block. Converts Lam-Alef ligatures to pairs of Lam and + * Alef characters, consuming spaces if required. + * @stable ICU 2.0 + */ + public static final int LETTERS_UNSHAPE = 0x10; + + /** + * Letter shaping option: replace normative letter characters in the U+0600 (Arabic) block, + * except for the TASHKEEL characters at U+064B...U+0652, by shaped ones in the U+Fe70 + * (Presentation Forms B) block. The TASHKEEL characters will always be converted to + * the isolated forms rather than to their correct shape. + * @stable ICU 2.0 + */ + public static final int LETTERS_SHAPE_TASHKEEL_ISOLATED = 0x18; + + /** + * Bit mask for letter shaping options. + * @stable ICU 2.0 + */ + public static final int LETTERS_MASK = 0x18; + + + /** + * Digit shaping option: do not perform digit shaping. + * @stable ICU 2.0 + */ + public static final int DIGITS_NOOP = 0; + + /** + * Digit shaping option: Replace European digits (U+0030...U+0039) by Arabic-Indic digits. + * @stable ICU 2.0 + */ + public static final int DIGITS_EN2AN = 0x20; + + /** + * Digit shaping option: Replace Arabic-Indic digits by European digits (U+0030...U+0039). + * @stable ICU 2.0 + */ + public static final int DIGITS_AN2EN = 0x40; + + /** + * Digit shaping option: + * Replace European digits (U+0030...U+0039) by Arabic-Indic digits + * if the most recent strongly directional character + * is an Arabic letter (its Bidi direction value is RIGHT_TO_LEFT_ARABIC). + * The initial state at the start of the text is assumed to be not an Arabic, + * letter, so European digits at the start of the text will not change. + * Compare to DIGITS_ALEN2AN_INIT_AL. + * @stable ICU 2.0 + */ + public static final int DIGITS_EN2AN_INIT_LR = 0x60; + + /** + * Digit shaping option: + * Replace European digits (U+0030...U+0039) by Arabic-Indic digits + * if the most recent strongly directional character + * is an Arabic letter (its Bidi direction value is RIGHT_TO_LEFT_ARABIC). + * The initial state at the start of the text is assumed to be an Arabic, + * letter, so European digits at the start of the text will change. + * Compare to DIGITS_ALEN2AN_INT_LR. + * @stable ICU 2.0 + */ + public static final int DIGITS_EN2AN_INIT_AL = 0x80; + + /** Not a valid option value. */ + //private static final int DIGITS_RESERVED = 0xa0; + + /** + * Bit mask for digit shaping options. + * @stable ICU 2.0 + */ + public static final int DIGITS_MASK = 0xe0; + + /** + * Digit type option: Use Arabic-Indic digits (U+0660...U+0669). + * @stable ICU 2.0 + */ + public static final int DIGIT_TYPE_AN = 0; + + /** + * Digit type option: Use Eastern (Extended) Arabic-Indic digits (U+06f0...U+06f9). + * @stable ICU 2.0 + */ + public static final int DIGIT_TYPE_AN_EXTENDED = 0x100; + + /** + * Bit mask for digit type options. + * @stable ICU 2.0 + */ + public static final int DIGIT_TYPE_MASK = 0x0100; // 0x3f00? + + /** + * some constants + */ + private static final char HAMZAFE_CHAR = '\ufe80'; + private static final char HAMZA06_CHAR = '\u0621'; + private static final char YEH_HAMZA_CHAR = '\u0626'; + private static final char YEH_HAMZAFE_CHAR = '\uFE89'; + private static final char LAMALEF_SPACE_SUB = '\uffff'; + private static final char TASHKEEL_SPACE_SUB = '\ufffe'; + private static final char LAM_CHAR = '\u0644'; + private static final char SPACE_CHAR = '\u0020'; + private static final char SPACE_CHAR_FOR_LAMALEF = '\ufeff'; // XXX: tweak for TextLine use + private static final char SHADDA_CHAR = '\uFE7C'; + private static final char TATWEEL_CHAR = '\u0640'; + private static final char SHADDA_TATWEEL_CHAR = '\uFE7D'; + private static final char NEW_TAIL_CHAR = '\uFE73'; + private static final char OLD_TAIL_CHAR = '\u200B'; + private static final int SHAPE_MODE = 0; + private static final int DESHAPE_MODE = 1; + + /** + * @stable ICU 2.0 + */ + public boolean equals(Object rhs) { + return rhs != null && + rhs.getClass() == ArabicShaping.class && + options == ((ArabicShaping)rhs).options; + } + + /** + * @stable ICU 2.0 + */ + ///CLOVER:OFF + public int hashCode() { + return options; + } + + /** + * @stable ICU 2.0 + */ + public String toString() { + StringBuffer buf = new StringBuffer(super.toString()); + buf.append('['); + + switch (options & LAMALEF_MASK) { + case LAMALEF_RESIZE: buf.append("LamAlef resize"); break; + case LAMALEF_NEAR: buf.append("LamAlef spaces at near"); break; + case LAMALEF_BEGIN: buf.append("LamAlef spaces at begin"); break; + case LAMALEF_END: buf.append("LamAlef spaces at end"); break; + case LAMALEF_AUTO: buf.append("lamAlef auto"); break; + } + switch (options & TEXT_DIRECTION_MASK) { + case TEXT_DIRECTION_LOGICAL: buf.append(", logical"); break; + case TEXT_DIRECTION_VISUAL_LTR: buf.append(", visual"); break; + } + switch (options & LETTERS_MASK) { + case LETTERS_NOOP: buf.append(", no letter shaping"); break; + case LETTERS_SHAPE: buf.append(", shape letters"); break; + case LETTERS_SHAPE_TASHKEEL_ISOLATED: buf.append(", shape letters tashkeel isolated"); break; + case LETTERS_UNSHAPE: buf.append(", unshape letters"); break; + } + switch (options & SEEN_MASK) { + case SEEN_TWOCELL_NEAR: buf.append(", Seen at near"); break; + } + switch (options & YEHHAMZA_MASK) { + case YEHHAMZA_TWOCELL_NEAR: buf.append(", Yeh Hamza at near"); break; + } + switch (options & TASHKEEL_MASK) { + case TASHKEEL_BEGIN: buf.append(", Tashkeel at begin"); break; + case TASHKEEL_END: buf.append(", Tashkeel at end"); break; + case TASHKEEL_REPLACE_BY_TATWEEL: buf.append(", Tashkeel replace with tatweel"); break; + case TASHKEEL_RESIZE: buf.append(", Tashkeel resize"); break; + } + + switch (options & DIGITS_MASK) { + case DIGITS_NOOP: buf.append(", no digit shaping"); break; + case DIGITS_EN2AN: buf.append(", shape digits to AN"); break; + case DIGITS_AN2EN: buf.append(", shape digits to EN"); break; + case DIGITS_EN2AN_INIT_LR: buf.append(", shape digits to AN contextually: default EN"); break; + case DIGITS_EN2AN_INIT_AL: buf.append(", shape digits to AN contextually: default AL"); break; + } + switch (options & DIGIT_TYPE_MASK) { + case DIGIT_TYPE_AN: buf.append(", standard Arabic-Indic digits"); break; + case DIGIT_TYPE_AN_EXTENDED: buf.append(", extended Arabic-Indic digits"); break; + } + buf.append("]"); + + return buf.toString(); + } + ///CLOVER:ON + + // + // ported api + // + + private static final int IRRELEVANT = 4; + private static final int LAMTYPE = 16; + private static final int ALEFTYPE = 32; + + private static final int LINKR = 1; + private static final int LINKL = 2; + private static final int LINK_MASK = 3; + + private static final int irrelevantPos[] = { + 0x0, 0x2, 0x4, 0x6, 0x8, 0xA, 0xC, 0xE + }; + +/* + private static final char convertLamAlef[] = { + '\u0622', // FEF5 + '\u0622', // FEF6 + '\u0623', // FEF7 + '\u0623', // FEF8 + '\u0625', // FEF9 + '\u0625', // FEFA + '\u0627', // FEFB + '\u0627' // FEFC + }; +*/ + + private static final int tailFamilyIsolatedFinal[] = { + /* FEB1 */ 1, + /* FEB2 */ 1, + /* FEB3 */ 0, + /* FEB4 */ 0, + /* FEB5 */ 1, + /* FEB6 */ 1, + /* FEB7 */ 0, + /* FEB8 */ 0, + /* FEB9 */ 1, + /* FEBA */ 1, + /* FEBB */ 0, + /* FEBC */ 0, + /* FEBD */ 1, + /* FEBE */ 1 + }; + + private static final int tashkeelMedial[] = { + /* FE70 */ 0, + /* FE71 */ 1, + /* FE72 */ 0, + /* FE73 */ 0, + /* FE74 */ 0, + /* FE75 */ 0, + /* FE76 */ 0, + /* FE77 */ 1, + /* FE78 */ 0, + /* FE79 */ 1, + /* FE7A */ 0, + /* FE7B */ 1, + /* FE7C */ 0, + /* FE7D */ 1, + /* FE7E */ 0, + /* FE7F */ 1 + }; + + private static final char yehHamzaToYeh[] = + { + /* isolated*/ 0xFEEF, + /* final */ 0xFEF0 + }; + + private static final char convertNormalizedLamAlef[] = { + '\u0622', // 065C + '\u0623', // 065D + '\u0625', // 065E + '\u0627', // 065F + }; + + private static final int[] araLink = { + 1 + 32 + 256 * 0x11, /*0x0622*/ + 1 + 32 + 256 * 0x13, /*0x0623*/ + 1 + 256 * 0x15, /*0x0624*/ + 1 + 32 + 256 * 0x17, /*0x0625*/ + 1 + 2 + 256 * 0x19, /*0x0626*/ + 1 + 32 + 256 * 0x1D, /*0x0627*/ + 1 + 2 + 256 * 0x1F, /*0x0628*/ + 1 + 256 * 0x23, /*0x0629*/ + 1 + 2 + 256 * 0x25, /*0x062A*/ + 1 + 2 + 256 * 0x29, /*0x062B*/ + 1 + 2 + 256 * 0x2D, /*0x062C*/ + 1 + 2 + 256 * 0x31, /*0x062D*/ + 1 + 2 + 256 * 0x35, /*0x062E*/ + 1 + 256 * 0x39, /*0x062F*/ + 1 + 256 * 0x3B, /*0x0630*/ + 1 + 256 * 0x3D, /*0x0631*/ + 1 + 256 * 0x3F, /*0x0632*/ + 1 + 2 + 256 * 0x41, /*0x0633*/ + 1 + 2 + 256 * 0x45, /*0x0634*/ + 1 + 2 + 256 * 0x49, /*0x0635*/ + 1 + 2 + 256 * 0x4D, /*0x0636*/ + 1 + 2 + 256 * 0x51, /*0x0637*/ + 1 + 2 + 256 * 0x55, /*0x0638*/ + 1 + 2 + 256 * 0x59, /*0x0639*/ + 1 + 2 + 256 * 0x5D, /*0x063A*/ + 0, 0, 0, 0, 0, /*0x063B-0x063F*/ + 1 + 2, /*0x0640*/ + 1 + 2 + 256 * 0x61, /*0x0641*/ + 1 + 2 + 256 * 0x65, /*0x0642*/ + 1 + 2 + 256 * 0x69, /*0x0643*/ + 1 + 2 + 16 + 256 * 0x6D, /*0x0644*/ + 1 + 2 + 256 * 0x71, /*0x0645*/ + 1 + 2 + 256 * 0x75, /*0x0646*/ + 1 + 2 + 256 * 0x79, /*0x0647*/ + 1 + 256 * 0x7D, /*0x0648*/ + 1 + 256 * 0x7F, /*0x0649*/ + 1 + 2 + 256 * 0x81, /*0x064A*/ + 4, 4, 4, 4, /*0x064B-0x064E*/ + 4, 4, 4, 4, /*0x064F-0x0652*/ + 4, 4, 4, 0, 0, /*0x0653-0x0657*/ + 0, 0, 0, 0, /*0x0658-0x065B*/ + 1 + 256 * 0x85, /*0x065C*/ + 1 + 256 * 0x87, /*0x065D*/ + 1 + 256 * 0x89, /*0x065E*/ + 1 + 256 * 0x8B, /*0x065F*/ + 0, 0, 0, 0, 0, /*0x0660-0x0664*/ + 0, 0, 0, 0, 0, /*0x0665-0x0669*/ + 0, 0, 0, 0, 0, 0, /*0x066A-0x066F*/ + 4, /*0x0670*/ + 0, /*0x0671*/ + 1 + 32, /*0x0672*/ + 1 + 32, /*0x0673*/ + 0, /*0x0674*/ + 1 + 32, /*0x0675*/ + 1, 1, /*0x0676-0x0677*/ + 1+2, 1+2, 1+2, 1+2, 1+2, 1+2, /*0x0678-0x067D*/ + 1+2, 1+2, 1+2, 1+2, 1+2, 1+2, /*0x067E-0x0683*/ + 1+2, 1+2, 1+2, 1+2, /*0x0684-0x0687*/ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /*0x0688-0x0691*/ + 1, 1, 1, 1, 1, 1, 1, 1, /*0x0692-0x0699*/ + 1+2, 1+2, 1+2, 1+2, 1+2, 1+2, /*0x069A-0x06A3*/ + 1+2, 1+2, 1+2, 1+2, /*0x069A-0x06A3*/ + 1+2, 1+2, 1+2, 1+2, 1+2, 1+2, /*0x06A4-0x06AD*/ + 1+2, 1+2, 1+2, 1+2, /*0x06A4-0x06AD*/ + 1+2, 1+2, 1+2, 1+2, 1+2, 1+2, /*0x06AE-0x06B7*/ + 1+2, 1+2, 1+2, 1+2, /*0x06AE-0x06B7*/ + 1+2, 1+2, 1+2, 1+2, 1+2, 1+2, /*0x06B8-0x06BF*/ + 1+2, 1+2, /*0x06B8-0x06BF*/ + 1, /*0x06C0*/ + 1+2, /*0x06C1*/ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /*0x06C2-0x06CB*/ + 1+2, /*0x06CC*/ + 1, /*0x06CD*/ + 1+2, 1+2, 1+2, 1+2, /*0x06CE-0x06D1*/ + 1, 1 /*0x06D2-0x06D3*/ + }; + + private static final int[] presLink = { + 1 + 2, /*0xFE70*/ + 1 + 2, /*0xFE71*/ + 1 + 2, 0, 1+ 2, 0, 1+ 2, /*0xFE72-0xFE76*/ + 1 + 2, /*0xFE77*/ + 1+ 2, 1 + 2, 1+2, 1 + 2, /*0xFE78-0xFE81*/ + 1+ 2, 1 + 2, 1+2, 1 + 2, /*0xFE82-0xFE85*/ + 0, 0 + 32, 1 + 32, 0 + 32, /*0xFE86-0xFE89*/ + 1 + 32, 0, 1, 0 + 32, /*0xFE8A-0xFE8D*/ + 1 + 32, 0, 2, 1 + 2, /*0xFE8E-0xFE91*/ + 1, 0 + 32, 1 + 32, 0, /*0xFE92-0xFE95*/ + 2, 1 + 2, 1, 0, /*0xFE96-0xFE99*/ + 1, 0, 2, 1 + 2, /*0xFE9A-0xFE9D*/ + 1, 0, 2, 1 + 2, /*0xFE9E-0xFEA1*/ + 1, 0, 2, 1 + 2, /*0xFEA2-0xFEA5*/ + 1, 0, 2, 1 + 2, /*0xFEA6-0xFEA9*/ + 1, 0, 2, 1 + 2, /*0xFEAA-0xFEAD*/ + 1, 0, 1, 0, /*0xFEAE-0xFEB1*/ + 1, 0, 1, 0, /*0xFEB2-0xFEB5*/ + 1, 0, 2, 1+2, /*0xFEB6-0xFEB9*/ + 1, 0, 2, 1+2, /*0xFEBA-0xFEBD*/ + 1, 0, 2, 1+2, /*0xFEBE-0xFEC1*/ + 1, 0, 2, 1+2, /*0xFEC2-0xFEC5*/ + 1, 0, 2, 1+2, /*0xFEC6-0xFEC9*/ + 1, 0, 2, 1+2, /*0xFECA-0xFECD*/ + 1, 0, 2, 1+2, /*0xFECE-0xFED1*/ + 1, 0, 2, 1+2, /*0xFED2-0xFED5*/ + 1, 0, 2, 1+2, /*0xFED6-0xFED9*/ + 1, 0, 2, 1+2, /*0xFEDA-0xFEDD*/ + 1, 0, 2, 1+2, /*0xFEDE-0xFEE1*/ + 1, 0 + 16, 2 + 16, 1 + 2 +16, /*0xFEE2-0xFEE5*/ + 1 + 16, 0, 2, 1+2, /*0xFEE6-0xFEE9*/ + 1, 0, 2, 1+2, /*0xFEEA-0xFEED*/ + 1, 0, 2, 1+2, /*0xFEEE-0xFEF1*/ + 1, 0, 1, 0, /*0xFEF2-0xFEF5*/ + 1, 0, 2, 1+2, /*0xFEF6-0xFEF9*/ + 1, 0, 1, 0, /*0xFEFA-0xFEFD*/ + 1, 0, 1, 0, + 1 + }; + + private static int[] convertFEto06 = { + /***********0******1******2******3******4******5******6******7******8******9******A******B******C******D******E******F***/ + /*FE7*/ 0x64B, 0x64B, 0x64C, 0x64C, 0x64D, 0x64D, 0x64E, 0x64E, 0x64F, 0x64F, 0x650, 0x650, 0x651, 0x651, 0x652, 0x652, + /*FE8*/ 0x621, 0x622, 0x622, 0x623, 0x623, 0x624, 0x624, 0x625, 0x625, 0x626, 0x626, 0x626, 0x626, 0x627, 0x627, 0x628, + /*FE9*/ 0x628, 0x628, 0x628, 0x629, 0x629, 0x62A, 0x62A, 0x62A, 0x62A, 0x62B, 0x62B, 0x62B, 0x62B, 0x62C, 0x62C, 0x62C, + /*FEA*/ 0x62C, 0x62D, 0x62D, 0x62D, 0x62D, 0x62E, 0x62E, 0x62E, 0x62E, 0x62F, 0x62F, 0x630, 0x630, 0x631, 0x631, 0x632, + /*FEB*/ 0x632, 0x633, 0x633, 0x633, 0x633, 0x634, 0x634, 0x634, 0x634, 0x635, 0x635, 0x635, 0x635, 0x636, 0x636, 0x636, + /*FEC*/ 0x636, 0x637, 0x637, 0x637, 0x637, 0x638, 0x638, 0x638, 0x638, 0x639, 0x639, 0x639, 0x639, 0x63A, 0x63A, 0x63A, + /*FED*/ 0x63A, 0x641, 0x641, 0x641, 0x641, 0x642, 0x642, 0x642, 0x642, 0x643, 0x643, 0x643, 0x643, 0x644, 0x644, 0x644, + /*FEE*/ 0x644, 0x645, 0x645, 0x645, 0x645, 0x646, 0x646, 0x646, 0x646, 0x647, 0x647, 0x647, 0x647, 0x648, 0x648, 0x649, + /*FEF*/ 0x649, 0x64A, 0x64A, 0x64A, 0x64A, 0x65C, 0x65C, 0x65D, 0x65D, 0x65E, 0x65E, 0x65F, 0x65F + }; + + private static final int shapeTable[][][] = { + { {0,0,0,0}, {0,0,0,0}, {0,1,0,3}, {0,1,0,1} }, + { {0,0,2,2}, {0,0,1,2}, {0,1,1,2}, {0,1,1,3} }, + { {0,0,0,0}, {0,0,0,0}, {0,1,0,3}, {0,1,0,3} }, + { {0,0,1,2}, {0,0,1,2}, {0,1,1,2}, {0,1,1,3} } + }; + + /* + * This function shapes European digits to Arabic-Indic digits + * in-place, writing over the input characters. Data is in visual + * order. + */ + private void shapeToArabicDigitsWithContext(char[] dest, + int start, + int length, + char digitBase, + boolean lastStrongWasAL) { + digitBase -= '0'; // move common adjustment out of loop + + for(int i = start + length; --i >= start;) { + char ch = dest[i]; + switch (Character.getDirectionality(ch)) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + lastStrongWasAL = false; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + lastStrongWasAL = true; + break; + case Character.DIRECTIONALITY_EUROPEAN_NUMBER: + if (lastStrongWasAL && ch <= '\u0039') { + dest[i] = (char)(ch + digitBase); + } + break; + default: + break; + } + } + } + + /* + * Name : invertBuffer + * Function: This function inverts the buffer, it's used + * in case the user specifies the buffer to be + * TEXT_DIRECTION_LOGICAL + */ + private static void invertBuffer(char[] buffer, + int start, + int length) { + + for(int i = start, j = start + length - 1; i < j; i++, --j) { + char temp = buffer[i]; + buffer[i] = buffer[j]; + buffer[j] = temp; + } + } + + /* + * Name : changeLamAlef + * Function: Converts the Alef characters into an equivalent + * LamAlef location in the 0x06xx Range, this is an + * intermediate stage in the operation of the program + * later it'll be converted into the 0xFExx LamAlefs + * in the shaping function. + */ + private static char changeLamAlef(char ch) { + switch(ch) { + case '\u0622': return '\u065C'; + case '\u0623': return '\u065D'; + case '\u0625': return '\u065E'; + case '\u0627': return '\u065F'; + default: return '\u0000'; // not a lamalef + } + } + + /* + * Name : specialChar + * Function: Special Arabic characters need special handling in the shapeUnicode + * function, this function returns 1 or 2 for these special characters + */ + private static int specialChar(char ch) { + if ((ch > '\u0621' && ch < '\u0626') || + (ch == '\u0627') || + (ch > '\u062E' && ch < '\u0633') || + (ch > '\u0647' && ch < '\u064A') || + (ch == '\u0629')) { + return 1; + } else if (ch >= '\u064B' && ch<= '\u0652') { + return 2; + } else if (ch >= 0x0653 && ch <= 0x0655 || + ch == 0x0670 || + ch >= 0xFE70 && ch <= 0xFE7F) { + return 3; + } else { + return 0; + } + } + + /* + * Name : getLink + * Function: Resolves the link between the characters as + * Arabic characters have four forms : + * Isolated, Initial, Middle and Final Form + */ + private static int getLink(char ch) { + if (ch >= '\u0622' && ch <= '\u06D3') { + return araLink[ch - '\u0622']; + } else if (ch == '\u200D') { + return 3; + } else if (ch >= '\u206D' && ch <= '\u206F') { + return 4; + } else if (ch >= '\uFE70' && ch <= '\uFEFC') { + return presLink[ch - '\uFE70']; + } else { + return 0; + } + } + + /* + * Name : countSpaces + * Function: Counts the number of spaces + * at each end of the logical buffer + */ + private static int countSpacesLeft(char[] dest, + int start, + int count) { + for (int i = start, e = start + count; i < e; ++i) { + if (dest[i] != SPACE_CHAR) { + return i - start; + } + } + return count; + } + + private static int countSpacesRight(char[] dest, + int start, + int count) { + + for (int i = start + count; --i >= start;) { + if (dest[i] != SPACE_CHAR) { + return start + count - 1 - i; + } + } + return count; + } + + /* + * Name : isTashkeelChar + * Function: Returns true for Tashkeel characters else return false + */ + private static boolean isTashkeelChar(char ch) { + return ( ch >='\u064B' && ch <= '\u0652' ); + } + + /* + *Name : isSeenTailFamilyChar + *Function : returns 1 if the character is a seen family isolated character + * in the FE range otherwise returns 0 + */ + + private static int isSeenTailFamilyChar(char ch) { + if (ch >= 0xfeb1 && ch < 0xfebf){ + return tailFamilyIsolatedFinal [ch - 0xFEB1]; + } else { + return 0; + } + } + + /* Name : isSeenFamilyChar + * Function : returns 1 if the character is a seen family character in the Unicode + * 06 range otherwise returns 0 + */ + + private static int isSeenFamilyChar(char ch){ + if (ch >= 0x633 && ch <= 0x636){ + return 1; + }else { + return 0; + } + } + + /* + *Name : isTailChar + *Function : returns true if the character matches one of the tail characters + * (0xfe73 or 0x200b) otherwise returns false + */ + + private static boolean isTailChar(char ch) { + if(ch == OLD_TAIL_CHAR || ch == NEW_TAIL_CHAR){ + return true; + }else{ + return false; + } + } + + /* + *Name : isAlefMaksouraChar + *Function : returns true if the character is a Alef Maksoura Final or isolated + * otherwise returns false + */ + private static boolean isAlefMaksouraChar(char ch) { + return ( (ch == 0xFEEF) || ( ch == 0xFEF0) || (ch == 0x0649)); + } + + /* + * Name : isYehHamzaChar + * Function : returns true if the character is a yehHamza isolated or yehhamza + * final is found otherwise returns false + */ + private static boolean isYehHamzaChar(char ch) { + if((ch==0xFE89)||(ch==0xFE8A)){ + return true; + }else{ + return false; + } + } + + /* + *Name : isTashkeelCharFE + *Function : Returns true for Tashkeel characters in FE range else return false + */ + + private static boolean isTashkeelCharFE(char ch) { + return ( ch!=0xFE75 &&(ch>=0xFE70 && ch<= 0xFE7F) ); + } + + /* + * Name: isTashkeelOnTatweelChar + * Function: Checks if the Tashkeel Character is on Tatweel or not,if the + * Tashkeel on tatweel (FE range), it returns 1 else if the + * Tashkeel with shadda on tatweel (FC range)return 2 otherwise + * returns 0 + */ + private static int isTashkeelOnTatweelChar(char ch){ + if (ch >= 0xfe70 && ch <= 0xfe7f && ch != NEW_TAIL_CHAR && ch != 0xFE75 && ch != SHADDA_TATWEEL_CHAR) + { + return tashkeelMedial [ch - 0xFE70]; + } else if( (ch >= 0xfcf2 && ch <= 0xfcf4) || (ch == SHADDA_TATWEEL_CHAR)) { + return 2; + } else { + return 0; + } + } + + /* + * Name: isIsolatedTashkeelChar + * Function: Checks if the Tashkeel Character is in the isolated form + * (i.e. Unicode FE range) returns 1 else if the Tashkeel + * with shadda is in the isolated form (i.e. Unicode FC range) + * returns 1 otherwise returns 0 + */ + private static int isIsolatedTashkeelChar(char ch){ + if (ch >= 0xfe70 && ch <= 0xfe7f && ch != NEW_TAIL_CHAR && ch != 0xFE75){ + return (1 - tashkeelMedial [ch - 0xFE70]); + } else if(ch >= 0xfc5e && ch <= 0xfc63){ + return 1; + } else{ + return 0; + } + } + + /* + * Name : isAlefChar + * Function: Returns 1 for Alef characters else return 0 + */ + private static boolean isAlefChar(char ch) { + return ch == '\u0622' || ch == '\u0623' || ch == '\u0625' || ch == '\u0627'; + } + + /* + * Name : isLamAlefChar + * Function: Returns true for LamAlef characters else return false + */ + private static boolean isLamAlefChar(char ch) { + return ch >= '\uFEF5' && ch <= '\uFEFC'; + } + + private static boolean isNormalizedLamAlefChar(char ch) { + return ch >= '\u065C' && ch <= '\u065F'; + } + + /* + * Name : calculateSize + * Function: This function calculates the destSize to be used in preflighting + * when the destSize is equal to 0 + */ + private int calculateSize(char[] source, + int sourceStart, + int sourceLength) { + + int destSize = sourceLength; + + switch (options & LETTERS_MASK) { + case LETTERS_SHAPE: + case LETTERS_SHAPE_TASHKEEL_ISOLATED: + if (isLogical) { + for (int i = sourceStart, e = sourceStart + sourceLength - 1; i < e; ++i) { + if ((source[i] == LAM_CHAR && isAlefChar(source[i+1])) || isTashkeelCharFE(source[i])){ + --destSize; + } + } + } else { // visual + for(int i = sourceStart + 1, e = sourceStart + sourceLength; i < e; ++i) { + if ((source[i] == LAM_CHAR && isAlefChar(source[i-1])) || isTashkeelCharFE(source[i])) { + --destSize; + } + } + } + break; + + case LETTERS_UNSHAPE: + for(int i = sourceStart, e = sourceStart + sourceLength; i < e; ++i) { + if (isLamAlefChar(source[i])) { + destSize++; + } + } + break; + + default: + break; + } + + return destSize; + } + + + /* + * Name : countSpaceSub + * Function: Counts number of times the subChar appears in the array + */ + public static int countSpaceSub(char [] dest,int length, char subChar){ + int i = 0; + int count = 0; + while (i < length) { + if (dest[i] == subChar) { + count++; + } + i++; + } + return count; + } + + /* + * Name : shiftArray + * Function: Shifts characters to replace space sub characters + */ + public static void shiftArray(char [] dest,int start, int e, char subChar){ + int w = e; + int r = e; + while (--r >= start) { + char ch = dest[r]; + if (ch != subChar) { + --w; + if (w != r) { + dest[w] = ch; + } + } + } + } + + /* + * Name : flipArray + * Function: inverts array, so that start becomes end and vice versa + */ + public static int flipArray(char [] dest, int start, int e, int w){ + int r; + if (w > start) { + // shift, assume small buffer size so don't use arraycopy + r = w; + w = start; + while (r < e) { + dest[w++] = dest[r++]; + } + } else { + w = e; + } + return w; + } + + /* + * Name : handleTashkeelWithTatweel + * Function : Replaces Tashkeel as following: + * Case 1 :if the Tashkeel on tatweel, replace it with Tatweel. + * Case 2 :if the Tashkeel aggregated with Shadda on Tatweel, replace + * it with Shadda on Tatweel. + * Case 3: if the Tashkeel is isolated replace it with Space. + * + */ + private static int handleTashkeelWithTatweel(char[] dest, int sourceLength) { + int i; + for(i = 0; i < sourceLength; i++){ + if((isTashkeelOnTatweelChar(dest[i]) == 1)){ + dest[i] = TATWEEL_CHAR; + }else if((isTashkeelOnTatweelChar(dest[i]) == 2)){ + dest[i] = SHADDA_TATWEEL_CHAR; + }else if((isIsolatedTashkeelChar(dest[i])==1) && dest[i] != SHADDA_CHAR){ + dest[i] = SPACE_CHAR; + } + } + return sourceLength; + } + + /* + *Name : handleGeneratedSpaces + *Function : The shapeUnicode function converts Lam + Alef into LamAlef + space, + * and Tashkeel to space. + * handleGeneratedSpaces function puts these generated spaces + * according to the options the user specifies. LamAlef and Tashkeel + * spaces can be replaced at begin, at end, at near or decrease the + * buffer size. + * + * There is also Auto option for LamAlef and tashkeel, which will put + * the spaces at end of the buffer (or end of text if the user used + * the option SPACES_RELATIVE_TO_TEXT_BEGIN_END). + * + * If the text type was visual_LTR and the option + * SPACES_RELATIVE_TO_TEXT_BEGIN_END was selected the END + * option will place the space at the beginning of the buffer and + * BEGIN will place the space at the end of the buffer. + */ + private int handleGeneratedSpaces(char[] dest, + int start, + int length) { + + int lenOptionsLamAlef = options & LAMALEF_MASK; + int lenOptionsTashkeel = options & TASHKEEL_MASK; + boolean lamAlefOn = false; + boolean tashkeelOn = false; + + if (!isLogical & !spacesRelativeToTextBeginEnd) { + switch (lenOptionsLamAlef) { + case LAMALEF_BEGIN: lenOptionsLamAlef = LAMALEF_END; break; + case LAMALEF_END: lenOptionsLamAlef = LAMALEF_BEGIN; break; + default: break; + } + switch (lenOptionsTashkeel){ + case TASHKEEL_BEGIN: lenOptionsTashkeel = TASHKEEL_END; break; + case TASHKEEL_END: lenOptionsTashkeel = TASHKEEL_BEGIN; break; + default: break; + } + } + + + if (lenOptionsLamAlef == LAMALEF_NEAR) { + for (int i = start, e = i + length; i < e; ++i) { + if (dest[i] == LAMALEF_SPACE_SUB) { + dest[i] = SPACE_CHAR_FOR_LAMALEF; + } + } + + } else { + + final int e = start + length; + int wL = countSpaceSub(dest, length, LAMALEF_SPACE_SUB); + int wT = countSpaceSub(dest, length, TASHKEEL_SPACE_SUB); + + if (lenOptionsLamAlef == LAMALEF_END){ + lamAlefOn = true; + } + if (lenOptionsTashkeel == TASHKEEL_END){ + tashkeelOn = true; + } + + + if (lamAlefOn && (lenOptionsLamAlef == LAMALEF_END)) { + shiftArray(dest, start, e, LAMALEF_SPACE_SUB); + while (wL > start) { + dest[--wL] = SPACE_CHAR; + } + } + + if (tashkeelOn && (lenOptionsTashkeel == TASHKEEL_END)){ + shiftArray(dest, start, e, TASHKEEL_SPACE_SUB); + while (wT > start) { + dest[--wT] = SPACE_CHAR; + } + } + + lamAlefOn = false; + tashkeelOn = false; + + if (lenOptionsLamAlef == LAMALEF_RESIZE){ + lamAlefOn = true; + } + if (lenOptionsTashkeel == TASHKEEL_RESIZE){ + tashkeelOn = true; + } + + if (lamAlefOn && (lenOptionsLamAlef == LAMALEF_RESIZE)){ + shiftArray(dest, start, e, LAMALEF_SPACE_SUB); + wL = flipArray(dest,start,e, wL); + length = wL - start; + } + if (tashkeelOn && (lenOptionsTashkeel == TASHKEEL_RESIZE)) { + shiftArray(dest, start, e, TASHKEEL_SPACE_SUB); + wT = flipArray(dest,start,e, wT); + length = wT - start; + } + + lamAlefOn = false; + tashkeelOn = false; + + if ((lenOptionsLamAlef == LAMALEF_BEGIN) || + (lenOptionsLamAlef == LAMALEF_AUTO)){ + lamAlefOn = true; + } + if (lenOptionsTashkeel == TASHKEEL_BEGIN){ + tashkeelOn = true; + } + + if (lamAlefOn && ((lenOptionsLamAlef == LAMALEF_BEGIN)|| + (lenOptionsLamAlef == LAMALEF_AUTO))) { // spaces at beginning + shiftArray(dest, start, e, LAMALEF_SPACE_SUB); + wL = flipArray(dest,start,e, wL); + while (wL < e) { + dest[wL++] = SPACE_CHAR; + } + } + if(tashkeelOn && (lenOptionsTashkeel == TASHKEEL_BEGIN)){ + shiftArray(dest, start, e, TASHKEEL_SPACE_SUB); + wT = flipArray(dest,start,e, wT); + while (wT < e) { + dest[wT++] = SPACE_CHAR; + } + } + } + + return length; + } + + + /* + *Name :expandCompositCharAtBegin + *Function :Expands the LamAlef character to Lam and Alef consuming the required + * space from beginning of the buffer. If the text type was visual_LTR + * and the option SPACES_RELATIVE_TO_TEXT_BEGIN_END was selected + * the spaces will be located at end of buffer. + * If there are no spaces to expand the LamAlef, an exception is thrown. +*/ + private boolean expandCompositCharAtBegin(char[] dest,int start, int length, + int lacount) { + boolean spaceNotFound = false; + + if (lacount > countSpacesRight(dest, start, length)) { + spaceNotFound = true; + return spaceNotFound; + } + for (int r = start + length - lacount, w = start + length; --r >= start;) { + char ch = dest[r]; + if (isNormalizedLamAlefChar(ch)) { + dest[--w] = LAM_CHAR; + dest[--w] = convertNormalizedLamAlef[ch - '\u065C']; + } else { + dest[--w] = ch; + } + } + return spaceNotFound; + + } + + /* + *Name : expandCompositCharAtEnd + *Function : Expands the LamAlef character to Lam and Alef consuming the + * required space from end of the buffer. If the text type was + * Visual LTR and the option SPACES_RELATIVE_TO_TEXT_BEGIN_END + * was used, the spaces will be consumed from begin of buffer. If + * there are no spaces to expand the LamAlef, an exception is thrown. + */ + + private boolean expandCompositCharAtEnd(char[] dest,int start, int length, + int lacount){ + boolean spaceNotFound = false; + + if (lacount > countSpacesLeft(dest, start, length)) { + spaceNotFound = true; + return spaceNotFound; + } + for (int r = start + lacount, w = start, e = start + length; r < e; ++r) { + char ch = dest[r]; + if (isNormalizedLamAlefChar(ch)) { + dest[w++] = convertNormalizedLamAlef[ch - '\u065C']; + dest[w++] = LAM_CHAR; + } else { + dest[w++] = ch; + } + } + return spaceNotFound; + } + + /* + *Name : expandCompositCharAtNear + *Function : Expands the LamAlef character into Lam + Alef, YehHamza character + * into Yeh + Hamza, SeenFamily character into SeenFamily character + * + Tail, while consuming the space next to the character. + */ + + private boolean expandCompositCharAtNear(char[] dest,int start, int length, + int yehHamzaOption, int seenTailOption, int lamAlefOption){ + + boolean spaceNotFound = false; + + + + if (isNormalizedLamAlefChar(dest[start])) { + spaceNotFound = true; + return spaceNotFound; + } + for (int i = start + length; --i >=start;) { + char ch = dest[i]; + if (lamAlefOption == 1 && isNormalizedLamAlefChar(ch)) { + if (i>start &&dest[i-1] == SPACE_CHAR) { + dest[i] = LAM_CHAR; + dest[--i] = convertNormalizedLamAlef[ch - '\u065C']; + } else { + spaceNotFound = true; + return spaceNotFound; + } + }else if(seenTailOption == 1 && isSeenTailFamilyChar(ch) == 1){ + if(i>start &&dest[i-1] == SPACE_CHAR){ + dest[i-1] = tailChar; + } else{ + spaceNotFound = true; + return spaceNotFound; + } + }else if(yehHamzaOption == 1 && isYehHamzaChar(ch)){ + + if(i>start &&dest[i-1] == SPACE_CHAR){ + dest[i] = yehHamzaToYeh[ch - YEH_HAMZAFE_CHAR]; + dest[i-1] = HAMZAFE_CHAR; + }else{ + spaceNotFound = true; + return spaceNotFound; + } + + + } + } + return false; + + } + + /* + * Name : expandCompositChar + * Function: LamAlef needs special handling as the LamAlef is + * one character while expanding it will give two + * characters Lam + Alef, so we need to expand the LamAlef + * in near or far spaces according to the options the user + * specifies or increase the buffer size. + * Dest has enough room for the expansion if we are growing. + * lamalef are normalized to the 'special characters' + */ + private int expandCompositChar(char[] dest, + int start, + int length, + int lacount, + int shapingMode) throws ArabicShapingException { + + int lenOptionsLamAlef = options & LAMALEF_MASK; + int lenOptionsSeen = options & SEEN_MASK; + int lenOptionsYehHamza = options & YEHHAMZA_MASK; + boolean spaceNotFound = false; + + if (!isLogical && !spacesRelativeToTextBeginEnd) { + switch (lenOptionsLamAlef) { + case LAMALEF_BEGIN: lenOptionsLamAlef = LAMALEF_END; break; + case LAMALEF_END: lenOptionsLamAlef = LAMALEF_BEGIN; break; + default: break; + } + } + + if(shapingMode == 1){ + if(lenOptionsLamAlef == LAMALEF_AUTO){ + if(isLogical){ + spaceNotFound = expandCompositCharAtEnd(dest, start, length, lacount); + if(spaceNotFound){ + spaceNotFound = expandCompositCharAtBegin(dest, start, length, lacount); + } + if(spaceNotFound){ + spaceNotFound = expandCompositCharAtNear(dest, start, length,0,0,1); + } + if(spaceNotFound){ + throw new ArabicShapingException("No spacefor lamalef"); + } + }else{ + spaceNotFound = expandCompositCharAtBegin(dest, start, length, lacount); + if(spaceNotFound){ + spaceNotFound = expandCompositCharAtEnd(dest, start, length, lacount); + } + if(spaceNotFound){ + spaceNotFound = expandCompositCharAtNear(dest, start, length,0,0,1); + } + if(spaceNotFound){ + throw new ArabicShapingException("No spacefor lamalef"); + } + } + }else if(lenOptionsLamAlef == LAMALEF_END){ + spaceNotFound = expandCompositCharAtEnd(dest, start, length, lacount); + if(spaceNotFound){ + throw new ArabicShapingException("No spacefor lamalef"); + } + }else if(lenOptionsLamAlef == LAMALEF_BEGIN){ + spaceNotFound = expandCompositCharAtBegin(dest, start, length, lacount); + if(spaceNotFound){ + throw new ArabicShapingException("No spacefor lamalef"); + } + }else if(lenOptionsLamAlef == LAMALEF_NEAR){ + spaceNotFound = expandCompositCharAtNear(dest, start, length,0,0,1); + if(spaceNotFound){ + throw new ArabicShapingException("No spacefor lamalef"); + } + }else if(lenOptionsLamAlef == LAMALEF_RESIZE){ + for (int r = start + length, w = r + lacount; --r >= start;) { + char ch = dest[r]; + if (isNormalizedLamAlefChar(ch)) { + dest[--w] = '\u0644'; + dest[--w] = convertNormalizedLamAlef[ch - '\u065C']; + } else { + dest[--w] = ch; + } + } + length += lacount; + } + }else{ + if(lenOptionsSeen == SEEN_TWOCELL_NEAR){ + spaceNotFound = expandCompositCharAtNear(dest, start, length,0,1,0); + if(spaceNotFound){ + throw new ArabicShapingException("No space for Seen tail expansion"); + } + } + if(lenOptionsYehHamza == YEHHAMZA_TWOCELL_NEAR){ + spaceNotFound = expandCompositCharAtNear(dest, start, length,1,0,0); + if(spaceNotFound){ + throw new ArabicShapingException("No space for YehHamza expansion"); + } + } + } + return length; + } + + + /* Convert the input buffer from FExx Range into 06xx Range + * to put all characters into the 06xx range + * even the lamalef is converted to the special region in + * the 06xx range. Return the number of lamalef chars found. + */ + private int normalize(char[] dest, int start, int length) { + int lacount = 0; + for (int i = start, e = i + length; i < e; ++i) { + char ch = dest[i]; + if (ch >= '\uFE70' && ch <= '\uFEFC') { + if (isLamAlefChar(ch)) { + ++lacount; + } + dest[i] = (char)convertFEto06[ch - '\uFE70']; + } + } + return lacount; + } + + /* + * Name : deshapeNormalize + * Function: Convert the input buffer from FExx Range into 06xx Range + * even the lamalef is converted to the special region in the 06xx range. + * According to the options the user enters, all seen family characters + * followed by a tail character are merged to seen tail family character and + * any yeh followed by a hamza character are merged to yehhamza character. + * Method returns the number of lamalef chars found. + */ + private int deshapeNormalize(char[] dest, int start, int length) { + int lacount = 0; + int yehHamzaComposeEnabled = 0; + int seenComposeEnabled = 0; + + yehHamzaComposeEnabled = ((options&YEHHAMZA_MASK) == YEHHAMZA_TWOCELL_NEAR) ? 1 : 0; + seenComposeEnabled = ((options&SEEN_MASK) == SEEN_TWOCELL_NEAR)? 1 : 0; + + for (int i = start, e = i + length; i < e; ++i) { + char ch = dest[i]; + + if( (yehHamzaComposeEnabled == 1) && ((ch == HAMZA06_CHAR) || (ch == HAMZAFE_CHAR)) + && (i < (length - 1)) && isAlefMaksouraChar(dest[i+1] )) { + dest[i] = SPACE_CHAR; + dest[i+1] = YEH_HAMZA_CHAR; + } else if ( (seenComposeEnabled == 1) && (isTailChar(ch)) && (i< (length - 1)) + && (isSeenTailFamilyChar(dest[i+1])==1) ) { + dest[i] = SPACE_CHAR; + } + else if (ch >= '\uFE70' && ch <= '\uFEFC') { + if (isLamAlefChar(ch)) { + ++lacount; + } + dest[i] = (char)convertFEto06[ch - '\uFE70']; + } + } + return lacount; + } + + /* + * Name : shapeUnicode + * Function: Converts an Arabic Unicode buffer in 06xx Range into a shaped + * arabic Unicode buffer in FExx Range + */ + private int shapeUnicode(char[] dest, + int start, + int length, + int destSize, + int tashkeelFlag)throws ArabicShapingException { + + int lamalef_count = normalize(dest, start, length); + + // resolve the link between the characters. + // Arabic characters have four forms: Isolated, Initial, Medial and Final. + // Tashkeel characters have two, isolated or medial, and sometimes only isolated. + // tashkeelFlag == 0: shape normally, 1: shape isolated, 2: don't shape + + boolean lamalef_found = false, seenfam_found = false; + boolean yehhamza_found = false, tashkeel_found = false; + int i = start + length - 1; + int currLink = getLink(dest[i]); + int nextLink = 0; + int prevLink = 0; + int lastLink = 0; + //int prevPos = i; + int lastPos = i; + int nx = -2; + int nw = 0; + + while (i >= 0) { + // If high byte of currLink > 0 then there might be more than one shape + if ((currLink & '\uFF00') > 0 || isTashkeelChar(dest[i])) { + nw = i - 1; + nx = -2; + while (nx < 0) { // we need to know about next char + if (nw == -1) { + nextLink = 0; + nx = Integer.MAX_VALUE; + } else { + nextLink = getLink(dest[nw]); + if ((nextLink & IRRELEVANT) == 0) { + nx = nw; + } else { + --nw; + } + } + } + + if (((currLink & ALEFTYPE) > 0) && ((lastLink & LAMTYPE) > 0)) { + lamalef_found = true; + char wLamalef = changeLamAlef(dest[i]); // get from 0x065C-0x065f + if (wLamalef != '\u0000') { + // replace alef by marker, it will be removed later + dest[i] = '\uffff'; + dest[lastPos] = wLamalef; + i = lastPos; + } + + lastLink = prevLink; + currLink = getLink(wLamalef); // requires '\u0000', unfortunately + } + if ((i > 0) && (dest[i-1] == SPACE_CHAR)) + { + if ( isSeenFamilyChar(dest[i]) == 1){ + seenfam_found = true; + } else if (dest[i] == YEH_HAMZA_CHAR) { + yehhamza_found = true; + } + } + else if(i==0){ + if ( isSeenFamilyChar(dest[i]) == 1){ + seenfam_found = true; + } else if (dest[i] == YEH_HAMZA_CHAR) { + yehhamza_found = true; + } + } + + + // get the proper shape according to link ability of neighbors + // and of character; depends on the order of the shapes + // (isolated, initial, middle, final) in the compatibility area + + int flag = specialChar(dest[i]); + + int shape = shapeTable[nextLink & LINK_MASK] + [lastLink & LINK_MASK] + [currLink & LINK_MASK]; + + if (flag == 1) { + shape &= 0x1; + } else if (flag == 2) { + if (tashkeelFlag == 0 && + ((lastLink & LINKL) != 0) && + ((nextLink & LINKR) != 0) && + dest[i] != '\u064C' && + dest[i] != '\u064D' && + !((nextLink & ALEFTYPE) == ALEFTYPE && + (lastLink & LAMTYPE) == LAMTYPE)) { + + shape = 1; + } else { + shape = 0; + } + } + if (flag == 2) { + if (tashkeelFlag == 2) { + dest[i] = TASHKEEL_SPACE_SUB; + tashkeel_found = true; + } + else{ + dest[i] = (char)('\uFE70' + irrelevantPos[dest[i] - '\u064B'] + shape); + } + // else leave tashkeel alone + } else { + dest[i] = (char)('\uFE70' + (currLink >> 8) + shape); + } + } + + // move one notch forward + if ((currLink & IRRELEVANT) == 0) { + prevLink = lastLink; + lastLink = currLink; + //prevPos = lastPos; + lastPos = i; + } + + --i; + if (i == nx) { + currLink = nextLink; + nx = -2; + } else if (i != -1) { + currLink = getLink(dest[i]); + } + } + + // If we found a lam/alef pair in the buffer + // call handleGeneratedSpaces to remove the spaces that were added + + destSize = length; + if (lamalef_found || tashkeel_found) { + destSize = handleGeneratedSpaces(dest, start, length); + } + if (seenfam_found || yehhamza_found){ + destSize = expandCompositChar(dest, start, destSize, lamalef_count, SHAPE_MODE); + } + return destSize; + } + + /* + * Name : deShapeUnicode + * Function: Converts an Arabic Unicode buffer in FExx Range into unshaped + * arabic Unicode buffer in 06xx Range + */ + private int deShapeUnicode(char[] dest, + int start, + int length, + int destSize) throws ArabicShapingException { + + int lamalef_count = deshapeNormalize(dest, start, length); + + // If there was a lamalef in the buffer call expandLamAlef + if (lamalef_count != 0) { + // need to adjust dest to fit expanded buffer... !!! + destSize = expandCompositChar(dest, start, length, lamalef_count,DESHAPE_MODE); + } else { + destSize = length; + } + + return destSize; + } + + private int internalShape(char[] source, + int sourceStart, + int sourceLength, + char[] dest, + int destStart, + int destSize) throws ArabicShapingException { + + if (sourceLength == 0) { + return 0; + } + + if (destSize == 0) { + if (((options & LETTERS_MASK) != LETTERS_NOOP) && + ((options & LAMALEF_MASK) == LAMALEF_RESIZE)) { + + return calculateSize(source, sourceStart, sourceLength); + } else { + return sourceLength; // by definition + } + } + + // always use temp buffer + char[] temp = new char[sourceLength * 2]; // all lamalefs requiring expansion + System.arraycopy(source, sourceStart, temp, 0, sourceLength); + + if (isLogical) { + invertBuffer(temp, 0, sourceLength); + } + + int outputSize = sourceLength; + + switch (options & LETTERS_MASK) { + case LETTERS_SHAPE_TASHKEEL_ISOLATED: + outputSize = shapeUnicode(temp, 0, sourceLength, destSize, 1); + break; + + case LETTERS_SHAPE: + if( ((options&TASHKEEL_MASK)> 0) && + ((options&TASHKEEL_MASK) !=TASHKEEL_REPLACE_BY_TATWEEL)) { + /* Call the shaping function with tashkeel flag == 2 for removal of tashkeel */ + outputSize = shapeUnicode(temp, 0, sourceLength, destSize, 2); + }else { + //default Call the shaping function with tashkeel flag == 1 */ + outputSize = shapeUnicode(temp, 0, sourceLength, destSize, 0); + + /*After shaping text check if user wants to remove tashkeel and replace it with tatweel*/ + if( (options&TASHKEEL_MASK) == TASHKEEL_REPLACE_BY_TATWEEL){ + outputSize = handleTashkeelWithTatweel(temp,sourceLength); + } + } + break; + + case LETTERS_UNSHAPE: + outputSize = deShapeUnicode(temp, 0, sourceLength, destSize); + break; + + default: + break; + } + + if (outputSize > destSize) { + throw new ArabicShapingException("not enough room for result data"); + } + + if ((options & DIGITS_MASK) != DIGITS_NOOP) { + char digitBase = '\u0030'; // European digits + switch (options & DIGIT_TYPE_MASK) { + case DIGIT_TYPE_AN: + digitBase = '\u0660'; // Arabic-Indic digits + break; + + case DIGIT_TYPE_AN_EXTENDED: + digitBase = '\u06f0'; // Eastern Arabic-Indic digits (Persian and Urdu) + break; + + default: + break; + } + + switch (options & DIGITS_MASK) { + case DIGITS_EN2AN: + { + int digitDelta = digitBase - '\u0030'; + for (int i = 0; i < outputSize; ++i) { + char ch = temp[i]; + if (ch <= '\u0039' && ch >= '\u0030') { + temp[i] += digitDelta; + } + } + } + break; + + case DIGITS_AN2EN: + { + char digitTop = (char)(digitBase + 9); + int digitDelta = '\u0030' - digitBase; + for (int i = 0; i < outputSize; ++i) { + char ch = temp[i]; + if (ch <= digitTop && ch >= digitBase) { + temp[i] += digitDelta; + } + } + } + break; + + case DIGITS_EN2AN_INIT_LR: + shapeToArabicDigitsWithContext(temp, 0, outputSize, digitBase, false); + break; + + case DIGITS_EN2AN_INIT_AL: + shapeToArabicDigitsWithContext(temp, 0, outputSize, digitBase, true); + break; + + default: + break; + } + } + + if (isLogical) { + invertBuffer(temp, 0, outputSize); + } + + System.arraycopy(temp, 0, dest, destStart, outputSize); + + return outputSize; + } + + private static class ArabicShapingException extends RuntimeException { + ArabicShapingException(String msg) { + super(msg); + } + } +} diff --git a/icu4j/license.html b/icu4j/license.html new file mode 100644 index 0000000000000000000000000000000000000000..b905ddf41e8eaba5b0b4973ab8d74aa9e26b77cb --- /dev/null +++ b/icu4j/license.html @@ -0,0 +1,51 @@ + + + + +ICU License - ICU 1.8.1 and later + + + +

    ICU License - ICU 1.8.1 and later

    + +

    COPYRIGHT AND PERMISSION NOTICE

    + +

    +Copyright (c) 1995-2006 International Business Machines Corporation and others +

    +

    +All rights reserved. +

    +

    +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies +of the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. +

    +

    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, +OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. +

    +

    +Except as contained in this notice, the name of a copyright holder shall not be +used in advertising or otherwise to promote the sale, use or other dealings in +this Software without prior written authorization of the copyright holder. +

    + +
    +

    +All trademarks and registered trademarks mentioned herein are the property of their respective owners. +

    + + diff --git a/include/android_runtime/AndroidRuntime.h b/include/android_runtime/AndroidRuntime.h index 09f0de1e64d33dfee579f8ee7e0059260b5c1a89..22c9b72b62761e8f20dee97e97eb1b9ddd9f9329 100644 --- a/include/android_runtime/AndroidRuntime.h +++ b/include/android_runtime/AndroidRuntime.h @@ -30,7 +30,9 @@ namespace android { - + +class CursorWindow; + class AndroidRuntime { public: @@ -122,6 +124,8 @@ private: // Returns the Unix file descriptor for a ParcelFileDescriptor object extern int getParcelFileDescriptorFD(JNIEnv* env, jobject object); +extern CursorWindow * get_window_from_object(JNIEnv * env, jobject javaWindow); + } #endif diff --git a/core/jni/CursorWindow.h b/include/binder/CursorWindow.h similarity index 97% rename from core/jni/CursorWindow.h rename to include/binder/CursorWindow.h index 3fcb560227207d91456e8af2e21f6a1c6dc37156..4fbff2ac9476ebef4da6fe9877adacd40c1ee8c7 100644 --- a/core/jni/CursorWindow.h +++ b/include/binder/CursorWindow.h @@ -1,16 +1,16 @@ /* * Copyright (C) 2006 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this 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. */ @@ -24,8 +24,6 @@ #include #include -#include - #define DEFAULT_WINDOW_SIZE 4096 #define MAX_WINDOW_SIZE (1024 * 1024) #define WINDOW_ALLOCATION_SIZE 4096 @@ -82,11 +80,11 @@ typedef struct } data; } __attribute__((packed)) field_slot_t; +#define FIELD_TYPE_NULL 0 #define FIELD_TYPE_INTEGER 1 #define FIELD_TYPE_FLOAT 2 #define FIELD_TYPE_STRING 3 #define FIELD_TYPE_BLOB 4 -#define FIELD_TYPE_NULL 5 /** * This class stores a set of rows from a database in a buffer. The begining of the @@ -172,7 +170,7 @@ public: row_slot_t * allocRowSlot(); row_slot_t * getRowSlot(int row); - + /** * return NULL if Failed to find rowSlot or * Invalid rowSlot diff --git a/include/camera/Camera.h b/include/camera/Camera.h index 964700b9fd8363d6d71ddc6d0bc5f147115dc299..f7b3b4283909ea85a6d3fd53b381b0d23f7c4bab 100644 --- a/include/camera/Camera.h +++ b/include/camera/Camera.h @@ -22,8 +22,6 @@ namespace android { -class ISurface; - /* * A set of bit masks for specifying how the received preview frames are * handled before the previewCallback() call. @@ -84,6 +82,14 @@ enum { CAMERA_CMD_START_SMOOTH_ZOOM = 1, CAMERA_CMD_STOP_SMOOTH_ZOOM = 2, CAMERA_CMD_SET_DISPLAY_ORIENTATION = 3, + + // cmdType to disable/enable shutter sound. + // In sendCommand passing arg1 = 0 will disable, + // while passing arg1 = 1 will enable the shutter sound. + CAMERA_CMD_ENABLE_SHUTTER_SOUND = 4, + + // cmdType to play recording sound. + CAMERA_CMD_PLAY_RECORDING_SOUND = 5, }; // camera fatal errors @@ -152,9 +158,8 @@ public: status_t getStatus() { return mStatus; } - // pass the buffered ISurface to the camera service + // pass the buffered Surface to the camera service status_t setPreviewDisplay(const sp& surface); - status_t setPreviewDisplay(const sp& surface); // start preview mode, must call setPreviewDisplay first status_t startPreview(); diff --git a/include/camera/CameraHardwareInterface.h b/include/camera/CameraHardwareInterface.h index 6a66e3cfb5b0b4a8ec4331e6de88a9406e4b3ce7..515d87991c72342f55078d19e2db331182f872d9 100644 --- a/include/camera/CameraHardwareInterface.h +++ b/include/camera/CameraHardwareInterface.h @@ -18,6 +18,7 @@ #define ANDROID_HARDWARE_CAMERA_HARDWARE_INTERFACE_H #include +#include #include #include #include @@ -86,8 +87,8 @@ class CameraHardwareInterface : public virtual RefBase { public: virtual ~CameraHardwareInterface() { } - /** Return the IMemoryHeap for the preview image heap */ - virtual sp getPreviewHeap() const = 0; + /** Set the ISurface from which the preview buffers should be dequeued */ + virtual status_t setPreviewWindow(const sp& buf) = 0; /** Return the IMemoryHeap for the raw image heap */ virtual sp getRawHeap() const = 0; diff --git a/include/camera/CameraParameters.h b/include/camera/CameraParameters.h index 53039a09153fd5822a734228a420b0f21f0fb165..705b10182d44e1b17622c721e4c2f00935a474fe 100644 --- a/include/camera/CameraParameters.h +++ b/include/camera/CameraParameters.h @@ -354,7 +354,10 @@ public: // for barcode reading. static const char SCENE_MODE_BARCODE[]; - // Formats for setPreviewFormat and setPictureFormat. + // Pixel color formats for KEY_PREVIEW_FORMAT, KEY_PICTURE_FORMAT, + // and KEY_VIDEO_FRAME_FORMAT + // Planar variant of the YUV420 color format + static const char PIXEL_FORMAT_YUV420P[]; static const char PIXEL_FORMAT_YUV422SP[]; static const char PIXEL_FORMAT_YUV420SP[]; // NV21 static const char PIXEL_FORMAT_YUV422I[]; // YUY2 diff --git a/include/camera/ICamera.h b/include/camera/ICamera.h index 6fcf9e5aa7d7c441026f2944d39c48c8aaa119d4..8bceea5dce6cc781aff7dc66b82c30cba89288de 100644 --- a/include/camera/ICamera.h +++ b/include/camera/ICamera.h @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include #include #include @@ -45,8 +45,8 @@ public: // allow other processes to use this ICamera interface virtual status_t unlock() = 0; - // pass the buffered ISurface to the camera service - virtual status_t setPreviewDisplay(const sp& surface) = 0; + // pass the buffered Surface to the camera service + virtual status_t setPreviewDisplay(const sp& surface) = 0; // set the preview callback flag to affect how the received frames from // preview are handled. diff --git a/include/drm/DrmConstraints.h b/include/drm/DrmConstraints.h new file mode 100644 index 0000000000000000000000000000000000000000..a9ec94252f20805ef67a344835331b36347656c4 --- /dev/null +++ b/include/drm/DrmConstraints.h @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_CONSTRAINTS_H__ +#define __DRM_CONSTRAINTS_H__ + +#include "drm_framework_common.h" + +namespace android { + +/** + * This is an utility class which contains the constraints information. + * + * As a result of DrmManagerClient::getConstraints(const String8*, const int) + * an instance of DrmConstraints would be returned. + * + */ +class DrmConstraints { +public: + /** + * The following variables are replica of android.drm.DrmStore.ConstraintsColumns + * Any changes should also be incorporated with Java Layer as well + */ + /** + * The max repeat count + */ + static const String8 MAX_REPEAT_COUNT; + /** + * The remaining repeat count + */ + static const String8 REMAINING_REPEAT_COUNT; + + /** + * The time before which the protected file can not be played/viewed + */ + static const String8 LICENSE_START_TIME; + + /** + * The time after which the protected file can not be played/viewed + */ + static const String8 LICENSE_EXPIRY_TIME; + + /** + * The available time for license + */ + static const String8 LICENSE_AVAILABLE_TIME; + + /** + * The data stream for extended metadata + */ + static const String8 EXTENDED_METADATA; + +public: + /** + * Iterator for key + */ + class KeyIterator { + friend class DrmConstraints; + private: + KeyIterator(DrmConstraints* drmConstraints) + : mDrmConstraints(drmConstraints), mIndex(0) {} + + public: + KeyIterator(const KeyIterator& keyIterator); + KeyIterator& operator=(const KeyIterator& keyIterator); + virtual ~KeyIterator() {} + + public: + bool hasNext(); + const String8& next(); + + private: + DrmConstraints* mDrmConstraints; + unsigned int mIndex; + }; + + /** + * Iterator for constraints + */ + class Iterator { + friend class DrmConstraints; + private: + Iterator(DrmConstraints* drmConstraints) + : mDrmConstraints(drmConstraints), mIndex(0) {} + + public: + Iterator(const Iterator& iterator); + Iterator& operator=(const Iterator& iterator); + virtual ~Iterator() {} + + public: + bool hasNext(); + String8 next(); + + private: + DrmConstraints* mDrmConstraints; + unsigned int mIndex; + }; + +public: + DrmConstraints() {} + virtual ~DrmConstraints() { + DrmConstraints::KeyIterator keyIt = this->keyIterator(); + + while (keyIt.hasNext()) { + String8 key = keyIt.next(); + const char* value = this->getAsByteArray(&key); + if (NULL != value) { + delete[] value; + value = NULL; + } + } + mConstraintMap.clear(); + } +public: + /** + * Returns the number of constraints contained in this instance + * + * @return Number of constraints + */ + int getCount(void) const; + + /** + * Adds constraint information as pair to this instance + * + * @param[in] key Key to add + * @param[in] value Value to add + * @return Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t put(const String8* key, const char* value); + + /** + * Retrieves the value of given key + * + * @param key Key whose value to be retrieved + * @return The value + */ + String8 get(const String8& key) const; + + /** + * Retrieves the value as byte array of given key + * @param key Key whose value to be retrieved as byte array + * @return The byte array value + */ + const char* getAsByteArray(const String8* key) const; + + /** + * Returns KeyIterator object to walk through the keys associated with this instance + * + * @return KeyIterator object + */ + KeyIterator keyIterator(); + + /** + * Returns Iterator object to walk through the values associated with this instance + * + * @return Iterator object + */ + Iterator iterator(); +private: + const char* getValue(const String8* key) const; +private: + typedef KeyedVector DrmConstraintsMap; + DrmConstraintsMap mConstraintMap; +}; + +}; + +#endif /* __DRM_CONSTRAINTS_H__ */ + diff --git a/include/drm/DrmConvertedStatus.h b/include/drm/DrmConvertedStatus.h new file mode 100644 index 0000000000000000000000000000000000000000..679e48df99905568ffc0c2a65472e2eb45a68c81 --- /dev/null +++ b/include/drm/DrmConvertedStatus.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_CONVERTED_STATUS_H__ +#define __DRM_CONVERTED_STATUS_H__ + +#include "drm_framework_common.h" + +namespace android { + +/** + * This is an utility class which wraps the status of the conversion, the converted + * data/checksum data and the offset. Offset is going to be used in the case of close + * session where the agent will inform where the header and body signature should be added + * + * As a result of DrmManagerClient::convertData(int, const DrmBuffer*) and + * DrmManagerClient::closeConvertSession(int) an instance of DrmConvertedStatus + * would be returned. + * + */ +class DrmConvertedStatus { +public: + // Should be in sync with DrmConvertedStatus.java + static const int STATUS_OK = 1; + static const int STATUS_INPUTDATA_ERROR = 2; + static const int STATUS_ERROR = 3; + +public: + /** + * Constructor for DrmConvertedStatus + * + * @param[in] _statusCode Status of the conversion + * @param[in] _convertedData Converted data/checksum data + * @param[in] _offset Offset value + */ + DrmConvertedStatus(int _statusCode, const DrmBuffer* _convertedData, int _offset); + + /** + * Destructor for DrmConvertedStatus + */ + virtual ~DrmConvertedStatus() { + + } + +public: + int statusCode; + const DrmBuffer* convertedData; + int offset; +}; + +}; + +#endif /* __DRM_CONVERTED_STATUS_H__ */ + diff --git a/include/drm/DrmInfo.h b/include/drm/DrmInfo.h new file mode 100644 index 0000000000000000000000000000000000000000..7b4854187a5755dedc67f2b6fd7b03e1fdef6c31 --- /dev/null +++ b/include/drm/DrmInfo.h @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_INFO_H__ +#define __DRM_INFO_H__ + +#include "drm_framework_common.h" + +namespace android { + +/** + * This is an utility class in which necessary information required to transact + * between device and online DRM server is described. DRM Framework achieves + * server registration, license acquisition and any other server related transaction + * by passing an instance of this class to DrmManagerClient::processDrmInfo(const DrmInfo*). + * + * The Caller can retrieve the DrmInfo instance by using + * DrmManagerClient::acquireDrmInfo(const DrmInfoRequest*) by passing DrmInfoRequest instance. + * + */ +class DrmInfo { +public: + /** + * Constructor for DrmInfo + * + * @param[in] infoType Type of information + * @param[in] drmBuffer Trigger data + * @param[in] mimeType MIME type + */ + DrmInfo(int infoType, const DrmBuffer& drmBuffer, const String8& mimeType); + + /** + * Destructor for DrmInfo + */ + virtual ~DrmInfo() {} + +public: + /** + * Iterator for key + */ + class KeyIterator { + friend class DrmInfo; + + private: + KeyIterator(const DrmInfo* drmInfo) + : mDrmInfo(const_cast (drmInfo)), mIndex(0) {} + + public: + KeyIterator(const KeyIterator& keyIterator); + KeyIterator& operator=(const KeyIterator& keyIterator); + virtual ~KeyIterator() {} + + public: + bool hasNext(); + const String8& next(); + + private: + DrmInfo* mDrmInfo; + unsigned int mIndex; + }; + + /** + * Iterator + */ + class Iterator { + friend class DrmInfo; + + private: + Iterator(const DrmInfo* drmInfo) + : mDrmInfo(const_cast (drmInfo)), mIndex(0) {} + + public: + Iterator(const Iterator& iterator); + Iterator& operator=(const Iterator& iterator); + virtual ~Iterator() {} + + public: + bool hasNext(); + String8& next(); + + private: + DrmInfo* mDrmInfo; + unsigned int mIndex; + }; + +public: + /** + * Returns information type associated with this instance + * + * @return Information type + */ + int getInfoType(void) const; + + /** + * Returns MIME type associated with this instance + * + * @return MIME type + */ + String8 getMimeType(void) const; + + /** + * Returns the trigger data associated with this instance + * + * @return Trigger data + */ + const DrmBuffer& getData(void) const; + + /** + * Returns the number of attributes contained in this instance + * + * @return Number of attributes + */ + int getCount(void) const; + + /** + * Adds optional information as pair to this instance + * + * @param[in] key Key to add + * @param[in] value Value to add + * @return Returns the error code + */ + status_t put(const String8& key, const String8& value); + + /** + * Retrieves the value of given key + * + * @param key Key whose value to be retrieved + * @return The value + */ + String8 get(const String8& key) const; + + /** + * Returns KeyIterator object to walk through the keys associated with this instance + * + * @return KeyIterator object + */ + KeyIterator keyIterator() const; + + /** + * Returns Iterator object to walk through the values associated with this instance + * + * @return Iterator object + */ + Iterator iterator() const; + + /** + * Returns index of the given key + * + * @return index + */ + int indexOfKey(const String8& key) const; + +protected: + int mInfoType; + DrmBuffer mData; + String8 mMimeType; + KeyedVector mAttributes; +}; + +}; + +#endif /* __DRM_INFO_H__ */ + diff --git a/include/drm/DrmInfoEvent.h b/include/drm/DrmInfoEvent.h new file mode 100644 index 0000000000000000000000000000000000000000..5e8817c75542f9fcf4c86a169e4363066819c651 --- /dev/null +++ b/include/drm/DrmInfoEvent.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_INFO_EVENT_H__ +#define __DRM_INFO_EVENT_H__ + +namespace android { + +class String8; + +/** + * This is an entity class which would be passed to caller in + * DrmManagerClient::OnInfoListener::onInfo(const DrmInfoEvent&). + */ +class DrmInfoEvent { +public: + //! TYPE_ALREADY_REGISTERED_BY_ANOTHER_ACCOUNT, when registration has been + //! already done by another account ID. + static const int TYPE_ALREADY_REGISTERED_BY_ANOTHER_ACCOUNT = 0x0000001; + //! TYPE_REMOVE_RIGHTS, when the rights needs to be removed completely. + static const int TYPE_REMOVE_RIGHTS = 0x0000002; + //! TYPE_RIGHTS_INSTALLED, when the rights are downloaded and installed ok. + static const int TYPE_RIGHTS_INSTALLED = 0x0000003; + //! TYPE_RIGHTS_NOT_INSTALLED, when something went wrong installing the rights + static const int TYPE_RIGHTS_NOT_INSTALLED = 0x0000004; + //! TYPE_RIGHTS_RENEWAL_NOT_ALLOWED, when the server rejects renewal of rights + static const int TYPE_RIGHTS_RENEWAL_NOT_ALLOWED = 0x0000005; + //! TYPE_NOT_SUPPORTED, when answer from server can not be handled by the native agent + static const int TYPE_NOT_SUPPORTED = 0x0000006; + //! TYPE_WAIT_FOR_RIGHTS, rights object is on it's way to phone, + //! wait before calling checkRights again + static const int TYPE_WAIT_FOR_RIGHTS = 0x0000007; + //! TYPE_OUT_OF_MEMORY, when memory allocation fail during renewal. + //! Can in the future perhaps be used to trigger garbage collector + static const int TYPE_OUT_OF_MEMORY = 0x0000008; + //! TYPE_NO_INTERNET_CONNECTION, when the Internet connection is missing and no attempt + //! can be made to renew rights + static const int TYPE_NO_INTERNET_CONNECTION = 0x0000009; + +public: + /** + * Constructor for DrmInfoEvent + * + * @param[in] uniqueId Unique session identifier + * @param[in] infoType Type of information + * @param[in] message Message description + */ + DrmInfoEvent(int uniqueId, int infoType, const String8& message); + + /** + * Destructor for DrmInfoEvent + */ + virtual ~DrmInfoEvent() {} + +public: + /** + * Returns the Unique Id associated with this instance + * + * @return Unique Id + */ + int getUniqueId() const; + + /** + * Returns the Type of information associated with this object + * + * @return Type of information + */ + int getType() const; + + /** + * Returns the message description associated with this object + * + * @return Message description + */ + const String8& getMessage() const; + +private: + int mUniqueId; + int mInfoType; + const String8& mMessage; +}; + +}; + +#endif /* __DRM_INFO_EVENT_H__ */ + diff --git a/include/drm/DrmInfoRequest.h b/include/drm/DrmInfoRequest.h new file mode 100644 index 0000000000000000000000000000000000000000..3e48ecc8599f9ab8fdd6b05f75c18052f66ac4c4 --- /dev/null +++ b/include/drm/DrmInfoRequest.h @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_INFO_REQUEST_H__ +#define __DRM_INFO_REQUEST_H__ + +#include "drm_framework_common.h" + +namespace android { + +/** + * This is an utility class used to pass required parameters to get + * the necessary information to communicate with online DRM server + * + * An instance of this class is passed to + * DrmManagerClient::acquireDrmInfo(const DrmInfoRequest*) to get the + * instance of DrmInfo. + * + */ +class DrmInfoRequest { +public: + // Changes in following constants should be in sync with DrmInfoRequest.java + static const int TYPE_REGISTRATION_INFO = 1; + static const int TYPE_UNREGISTRATION_INFO = 2; + static const int TYPE_RIGHTS_ACQUISITION_INFO = 3; + static const int TYPE_RIGHTS_ACQUISITION_PROGRESS_INFO = 4; + + /** + * Key to pass the unique id for the account or the user + */ + static const String8 ACCOUNT_ID; + /** + * Key to pass the subscription id + */ + static const String8 SUBSCRIPTION_ID; + +public: + /** + * Constructor for DrmInfoRequest + * + * @param[in] infoType Type of information + * @param[in] mimeType MIME type + */ + DrmInfoRequest(int infoType, const String8& mimeType); + + /** + * Destructor for DrmInfoRequest + */ + virtual ~DrmInfoRequest() {} + +public: + /** + * Iterator for key + */ + class KeyIterator { + friend class DrmInfoRequest; + + private: + KeyIterator(const DrmInfoRequest* drmInfoRequest) + : mDrmInfoRequest(const_cast (drmInfoRequest)), mIndex(0) {} + + public: + KeyIterator(const KeyIterator& keyIterator); + KeyIterator& operator=(const KeyIterator& keyIterator); + virtual ~KeyIterator() {} + + public: + bool hasNext(); + const String8& next(); + + private: + DrmInfoRequest* mDrmInfoRequest; + unsigned int mIndex; + }; + + /** + * Iterator + */ + class Iterator { + friend class DrmInfoRequest; + + private: + Iterator(const DrmInfoRequest* drmInfoRequest) + : mDrmInfoRequest(const_cast (drmInfoRequest)), mIndex(0) {} + + public: + Iterator(const Iterator& iterator); + Iterator& operator=(const Iterator& iterator); + virtual ~Iterator() {} + + public: + bool hasNext(); + String8& next(); + + private: + DrmInfoRequest* mDrmInfoRequest; + unsigned int mIndex; + }; + +public: + /** + * Returns information type associated with this instance + * + * @return Information type + */ + int getInfoType(void) const; + + /** + * Returns MIME type associated with this instance + * + * @return MIME type + */ + String8 getMimeType(void) const; + + /** + * Returns the number of entries in DrmRequestInfoMap + * + * @return Number of entries + */ + int getCount(void) const; + + /** + * Adds optional information as pair to this instance + * + * @param[in] key Key to add + * @param[in] value Value to add + * @return Returns the error code + */ + status_t put(const String8& key, const String8& value); + + /** + * Retrieves the value of given key + * + * @param key Key whose value to be retrieved + * @return The value + */ + String8 get(const String8& key) const; + + /** + * Returns KeyIterator object to walk through the keys associated with this instance + * + * @return KeyIterator object + */ + KeyIterator keyIterator() const; + + /** + * Returns Iterator object to walk through the values associated with this instance + * + * @return Iterator object + */ + Iterator iterator() const; + +private: + int mInfoType; + String8 mMimeType; + + typedef KeyedVector DrmRequestInfoMap; + DrmRequestInfoMap mRequestInformationMap; +}; + +}; + +#endif /* __DRM_INFO_REQUEST_H__ */ + diff --git a/include/drm/DrmInfoStatus.h b/include/drm/DrmInfoStatus.h new file mode 100644 index 0000000000000000000000000000000000000000..806aea1dd6f118e99783880fd281b143e42d8303 --- /dev/null +++ b/include/drm/DrmInfoStatus.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_INFO_STATUS_H__ +#define __DRM_INFO_STATUS_H__ + +#include "drm_framework_common.h" + +namespace android { + +/** + * This is an utility class which wraps the result of communication between device + * and online DRM server. + * + * As a result of DrmManagerClient::processDrmInfo(const DrmInfo*) an instance of + * DrmInfoStatus would be returned. This class holds DrmBuffer which could be + * used to instantiate DrmRights in license acquisition. + * + */ +class DrmInfoStatus { +public: + // Should be in sync with DrmInfoStatus.java + static const int STATUS_OK = 1; + static const int STATUS_ERROR = 2; + +public: + /** + * Constructor for DrmInfoStatus + * + * @param[in] _statusCode Status of the communication + * @param[in] _drmBuffer Rights information + * @param[in] _mimeType MIME type + */ + DrmInfoStatus(int _statusCode, const DrmBuffer* _drmBuffer, const String8& _mimeType); + + /** + * Destructor for DrmInfoStatus + */ + virtual ~DrmInfoStatus() { + + } + +public: + int statusCode; + const DrmBuffer* drmBuffer; + String8 mimeType; +}; + +}; + +#endif /* __DRM_INFO_STATUS_H__ */ + diff --git a/include/drm/DrmManagerClient.h b/include/drm/DrmManagerClient.h new file mode 100644 index 0000000000000000000000000000000000000000..7d14c44ca15d5da38d01d9ac0ccf3c4e0fd14dff --- /dev/null +++ b/include/drm/DrmManagerClient.h @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_MANAGER_CLIENT_H__ +#define __DRM_MANAGER_CLIENT_H__ + +#include +#include "drm_framework_common.h" + +namespace android { + +class DrmInfo; +class DrmRights; +class DrmInfoEvent; +class DrmInfoStatus; +class DrmInfoRequest; +class DrmSupportInfo; +class DrmConstraints; +class DrmConvertedStatus; +class DrmManagerClientImpl; + +/** + * The Native application will instantiate this class and access DRM Framework + * services through this class. + * + */ +class DrmManagerClient { +public: + DrmManagerClient(); + + virtual ~DrmManagerClient(); + +public: + class OnInfoListener: virtual public RefBase { + + public: + virtual void onInfo(const DrmInfoEvent& event) = 0; + }; + +/** + * APIs which will be used by native modules (e.g. StageFright) + * + */ +public: + /** + * Open the decrypt session to decrypt the given protected content + * + * @param[in] fd File descriptor of the protected content to be decrypted + * @param[in] offset Start position of the content + * @param[in] length The length of the protected content + * @return + * Handle for the decryption session + */ + DecryptHandle* openDecryptSession(int fd, int offset, int length); + + /** + * Close the decrypt session for the given handle + * + * @param[in] decryptHandle Handle for the decryption session + */ + void closeDecryptSession(DecryptHandle* decryptHandle); + + /** + * Consumes the rights for a content. + * If the reserve parameter is true the rights is reserved until the same + * application calls this api again with the reserve parameter set to false. + * + * @param[in] decryptHandle Handle for the decryption session + * @param[in] action Action to perform. (Action::DEFAULT, Action::PLAY, etc) + * @param[in] reserve True if the rights should be reserved. + */ + void consumeRights(DecryptHandle* decryptHandle, int action, bool reserve); + + /** + * Informs the DRM engine about the playback actions performed on the DRM files. + * + * @param[in] decryptHandle Handle for the decryption session + * @param[in] playbackStatus Playback action (Playback::START, Playback::STOP, Playback::PAUSE) + * @param[in] position Position in the file (in milliseconds) where the start occurs. + * Only valid together with Playback::START. + */ + void setPlaybackStatus(DecryptHandle* decryptHandle, int playbackStatus, int position); + + /** + * Initialize decryption for the given unit of the protected content + * + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + * @param[in] headerInfo Information for initializing decryption of this decrypUnit + */ + void initializeDecryptUnit( + DecryptHandle* decryptHandle, int decryptUnitId, const DrmBuffer* headerInfo); + + /** + * Decrypt the protected content buffers for the given unit + * This method will be called any number of times, based on number of + * encrypted streams received from application. + * + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + * @param[in] encBuffer Encrypted data block + * @param[out] decBuffer Decrypted data block + * @return status_t + * Returns the error code for this API + * DRM_NO_ERROR for success, and one of DRM_ERROR_UNKNOWN, DRM_ERROR_LICENSE_EXPIRED + * DRM_ERROR_SESSION_NOT_OPENED, DRM_ERROR_DECRYPT_UNIT_NOT_INITIALIZED, + * DRM_ERROR_DECRYPT for failure. + */ + status_t decrypt( + DecryptHandle* decryptHandle, int decryptUnitId, + const DrmBuffer* encBuffer, DrmBuffer** decBuffer); + + /** + * Finalize decryption for the given unit of the protected content + * + * @param[in] decryptHandle Handle for the decryption session + * @param[in] decryptUnitId ID which specifies decryption unit, such as track ID + */ + void finalizeDecryptUnit(DecryptHandle* decryptHandle, int decryptUnitId); + + /** + * Reads the specified number of bytes from an open DRM file. + * + * @param[in] decryptHandle Handle for the decryption session + * @param[out] buffer Reference to the buffer that should receive the read data. + * @param[in] numBytes Number of bytes to read. + * @param[in] offset Offset with which to update the file position. + * + * @return Number of bytes read. Returns -1 for Failure. + */ + ssize_t pread(DecryptHandle* decryptHandle, void* buffer, ssize_t numBytes, off_t offset); + + /** + * Validates whether an action on the DRM content is allowed or not. + * + * @param[in] path Path of the protected content + * @param[in] action Action to validate. (Action::DEFAULT, Action::PLAY, etc) + * @param[in] description Detailed description of the action + * @return true if the action is allowed. + */ + bool validateAction(const String8& path, int action, const ActionDescription& description); + +/** + * APIs which are just the underlying implementation for the Java API + * + */ +public: + /** + * Register a callback to be invoked when the caller required to + * receive necessary information + * + * @param[in] infoListener Listener + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t setOnInfoListener(const sp& infoListener); + + /** + * Get constraint information associated with input content + * + * @param[in] path Path of the protected content + * @param[in] action Actions defined such as, + * Action::DEFAULT, Action::PLAY, etc + * @return DrmConstraints + * key-value pairs of constraint are embedded in it + * @note + * In case of error, return NULL + */ + DrmConstraints* getConstraints(const String8* path, const int action); + + /** + * Check whether the given mimetype or path can be handled + * + * @param[in] path Path of the content needs to be handled + * @param[in] mimetype Mimetype of the content needs to be handled + * @return + * True if DrmManager can handle given path or mime type. + */ + bool canHandle(const String8& path, const String8& mimeType); + + /** + * Executes given drm information based on its type + * + * @param[in] drmInfo Information needs to be processed + * @return DrmInfoStatus + * instance as a result of processing given input + */ + DrmInfoStatus* processDrmInfo(const DrmInfo* drmInfo); + + /** + * Retrieves necessary information for registration, unregistration or rights + * acquisition information. + * + * @param[in] drmInfoRequest Request information to retrieve drmInfo + * @return DrmInfo + * instance as a result of processing given input + */ + DrmInfo* acquireDrmInfo(const DrmInfoRequest* drmInfoRequest); + + /** + * Save DRM rights to specified rights path + * and make association with content path + * + * @param[in] drmRights DrmRights to be saved + * @param[in] rightsPath File path where rights to be saved + * @param[in] contentPath File path where content was saved + */ + void saveRights( + const DrmRights& drmRights, const String8& rightsPath, const String8& contentPath); + + /** + * Retrieves the mime type embedded inside the original content + * + * @param[in] path the path of the protected content + * @return String8 + * Returns mime-type of the original content, such as "video/mpeg" + */ + String8 getOriginalMimeType(const String8& path); + + /** + * Retrieves the type of the protected object (content, rights, etc..) + * by using specified path or mimetype. At least one parameter should be non null + * to retrieve DRM object type + * + * @param[in] path Path of the content or null. + * @param[in] mimeType Mime type of the content or null. + * @return type of the DRM content, + * such as DrmObjectType::CONTENT, DrmObjectType::RIGHTS_OBJECT + */ + int getDrmObjectType(const String8& path, const String8& mimeType); + + /** + * Check whether the given content has valid rights or not + * + * @param[in] path Path of the protected content + * @param[in] action Action to perform + * @return the status of the rights for the protected content, + * such as RightsStatus::RIGHTS_VALID, RightsStatus::RIGHTS_EXPIRED, etc. + */ + int checkRightsStatus(const String8& path, int action); + + /** + * Removes the rights associated with the given protected content + * + * @param[in] path Path of the protected content + */ + void removeRights(const String8& path); + + /** + * Removes all the rights information of each plug-in associated with + * DRM framework. Will be used in master reset + * + */ + void removeAllRights(); + + /** + * This API is for Forward Lock DRM. + * Each time the application tries to download a new DRM file + * which needs to be converted, then the application has to + * begin with calling this API. + * + * @param[in] convertId Handle for the convert session + * @param[in] mimeType Description/MIME type of the input data packet + * @return Return handle for the convert session + */ + int openConvertSession(const String8& mimeType); + + /** + * Passes the input data which need to be converted. The resultant + * converted data and the status is returned in the DrmConvertedInfo + * object. This method will be called each time there are new block + * of data received by the application. + * + * @param[in] convertId Handle for the convert session + * @param[in] inputData Input Data which need to be converted + * @return Return object contains the status of the data conversion, + * the output converted data and offset. In this case the + * application will ignore the offset information. + */ + DrmConvertedStatus* convertData(int convertId, const DrmBuffer* inputData); + + /** + * When there is no more data which need to be converted or when an + * error occurs that time the application has to inform the Drm agent + * via this API. Upon successful conversion of the complete data, + * the agent will inform that where the header and body signature + * should be added. This signature appending is needed to integrity + * protect the converted file. + * + * @param[in] convertId Handle for the convert session + * @return Return object contains the status of the data conversion, + * the header and body signature data. It also informs + * the application on which offset these signature data + * should be appended. + */ + DrmConvertedStatus* closeConvertSession(int convertId); + + /** + * Retrieves all DrmSupportInfo instance that native DRM framework can handle. + * This interface is meant to be used by JNI layer + * + * @param[out] length Number of elements in drmSupportInfoArray + * @param[out] drmSupportInfoArray Array contains all DrmSupportInfo + * that native DRM framework can handle + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t getAllSupportInfo(int* length, DrmSupportInfo** drmSupportInfoArray); + +private: + /** + * Initialize DRM Manager + * load available plug-ins from default plugInDirPath + * + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t loadPlugIns(); + + /** + * Finalize DRM Manager + * release resources associated with each plug-in + * unload all plug-ins and etc. + * + * @return status_t + * Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t unloadPlugIns(); + +private: + int mUniqueId; + DrmManagerClientImpl* mDrmManagerClientImpl; +}; + +}; + +#endif /* __DRM_MANAGER_CLIENT_H__ */ + diff --git a/include/drm/DrmRights.h b/include/drm/DrmRights.h new file mode 100644 index 0000000000000000000000000000000000000000..e04a066d78581aa8fc908a109b3d9899562fa703 --- /dev/null +++ b/include/drm/DrmRights.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_RIGHTS_H__ +#define __DRM_RIGHTS_H__ + +#include "drm_framework_common.h" + +namespace android { + +/** + * This is an utility class which wraps the license information which was + * retrieved from the online DRM server. + * + * Caller can instantiate DrmRights by invoking DrmRights(const DrmBuffer&, String) + * constructor by using the result of DrmManagerClient::ProcessDrmInfo(const DrmInfo*) API. + * Caller can also instantiate DrmRights using the file path which contains rights information. + * + */ +class DrmRights { +public: + /** + * Constructor for DrmRights + * + * @param[in] rightsFilePath Path of the file containing rights data + * @param[in] mimeType MIME type + * @param[in] accountId Account Id of the user + * @param[in] subscriptionId Subscription Id of the user + */ + DrmRights( + const String8& rightsFilePath, const String8& mimeType, + const String8& accountId = String8("_NO_USER"), + const String8& subscriptionId = String8("")); + + /** + * Constructor for DrmRights + * + * @param[in] rightsData Rights data + * @param[in] mimeType MIME type + * @param[in] accountId Account Id of the user + * @param[in] subscriptionId Subscription Id of the user + */ + DrmRights( + const DrmBuffer& rightsData, const String8& mimeType, + const String8& accountId = String8("_NO_USER"), + const String8& subscriptionId = String8("")); + + /** + * Destructor for DrmRights + */ + virtual ~DrmRights() {} + +public: + /** + * Returns the rights data associated with this instance + * + * @return Rights data + */ + const DrmBuffer& getData(void) const; + + /** + * Returns MIME type associated with this instance + * + * @return MIME type + */ + String8 getMimeType(void) const; + + /** + * Returns the account-id associated with this instance + * + * @return Account Id + */ + String8 getAccountId(void) const; + + /** + * Returns the subscription-id associated with this object + * + * @return Subscription Id + */ + String8 getSubscriptionId(void) const; + +private: + DrmBuffer mData; + String8 mMimeType; + String8 mAccountId; + String8 mSubscriptionId; +}; + +}; + +#endif /* __DRM_RIGHTS_H__ */ + diff --git a/include/drm/DrmSupportInfo.h b/include/drm/DrmSupportInfo.h new file mode 100644 index 0000000000000000000000000000000000000000..bf12b0b37e9dc3bce73bd896e93e1535a1bce8ee --- /dev/null +++ b/include/drm/DrmSupportInfo.h @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_SUPPORT_INFO_H__ +#define __DRM_SUPPORT_INFO_H__ + +#include "drm_framework_common.h" + +namespace android { + +/** + * This is an utility class which wraps the capability of each plug-in, + * such as mimetype's and file suffixes it could handle. + * + * Plug-in developer could return the capability of the plugin by passing + * DrmSupportInfo instance. + * + */ +class DrmSupportInfo { +public: + /** + * Iterator for mMimeTypeVector + */ + class MimeTypeIterator { + friend class DrmSupportInfo; + private: + MimeTypeIterator(DrmSupportInfo* drmSupportInfo) + : mDrmSupportInfo(drmSupportInfo), mIndex(0) {} + public: + MimeTypeIterator(const MimeTypeIterator& iterator); + MimeTypeIterator& operator=(const MimeTypeIterator& iterator); + virtual ~MimeTypeIterator() {} + + public: + bool hasNext(); + String8& next(); + + private: + DrmSupportInfo* mDrmSupportInfo; + unsigned int mIndex; + }; + + /** + * Iterator for mFileSuffixVector + */ + class FileSuffixIterator { + friend class DrmSupportInfo; + + private: + FileSuffixIterator(DrmSupportInfo* drmSupportInfo) + : mDrmSupportInfo(drmSupportInfo), mIndex(0) {} + public: + FileSuffixIterator(const FileSuffixIterator& iterator); + FileSuffixIterator& operator=(const FileSuffixIterator& iterator); + virtual ~FileSuffixIterator() {} + + public: + bool hasNext(); + String8& next(); + + private: + DrmSupportInfo* mDrmSupportInfo; + unsigned int mIndex; + }; + +public: + /** + * Constructor for DrmSupportInfo + */ + DrmSupportInfo(); + + /** + * Copy constructor for DrmSupportInfo + */ + DrmSupportInfo(const DrmSupportInfo& drmSupportInfo); + + /** + * Destructor for DrmSupportInfo + */ + virtual ~DrmSupportInfo() {} + + DrmSupportInfo& operator=(const DrmSupportInfo& drmSupportInfo); + bool operator<(const DrmSupportInfo& drmSupportInfo) const; + bool operator==(const DrmSupportInfo& drmSupportInfo) const; + + /** + * Returns FileSuffixIterator object to walk through file suffix values + * associated with this instance + * + * @return FileSuffixIterator object + */ + FileSuffixIterator getFileSuffixIterator(); + + /** + * Returns MimeTypeIterator object to walk through mimetype values + * associated with this instance + * + * @return MimeTypeIterator object + */ + MimeTypeIterator getMimeTypeIterator(); + +public: + /** + * Returns the number of mimetypes supported. + * + * @return Number of mimetypes supported + */ + int getMimeTypeCount(void) const; + + /** + * Returns the number of file types supported. + * + * @return Number of file types supported + */ + int getFileSuffixCount(void) const; + + /** + * Adds the mimetype to the list of supported mimetypes + * + * @param[in] mimeType mimetype to be added + * @return Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t addMimeType(const String8& mimeType); + + /** + * Adds the filesuffix to the list of supported file types + * + * @param[in] filesuffix file suffix to be added + * @return Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t addFileSuffix(const String8& fileSuffix); + + /** + * Set the unique description about the plugin + * + * @param[in] description Unique description + * @return Returns DRM_NO_ERROR for success, DRM_ERROR_UNKNOWN for failure + */ + status_t setDescription(const String8& description); + + /** + * Returns the unique description associated with the plugin + * + * @return Unique description + */ + String8 getDescription() const; + + /** + * Returns whether given mimetype is supported or not + * + * @param[in] mimeType MIME type + * @return + * true - if mime-type is supported + * false - if mime-type is not supported + */ + bool isSupportedMimeType(const String8& mimeType) const; + + /** + * Returns whether given file type is supported or not + * + * @param[in] fileType File type + * @return + * true if file type is supported + * false if file type is not supported + */ + bool isSupportedFileSuffix(const String8& fileType) const; + +private: + Vector mMimeTypeVector; + Vector mFileSuffixVector; + + String8 mDescription; +}; + +}; + +#endif /* __DRM_SUPPORT_INFO_H__ */ + diff --git a/include/drm/drm_framework_common.h b/include/drm/drm_framework_common.h new file mode 100644 index 0000000000000000000000000000000000000000..8b8a9f56d267c06ee9ef395c7a595fb77a31457b --- /dev/null +++ b/include/drm/drm_framework_common.h @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DRM_FRAMEWORK_COMMON_H__ +#define __DRM_FRAMEWORK_COMMON_H__ + +#include +#include +#include +#include + +#define INVALID_VALUE -1 + +namespace android { + +/** + * Error code for DRM Frameowrk + */ +enum { + DRM_ERROR_BASE = -2000, + + DRM_ERROR_UNKNOWN = DRM_ERROR_BASE, + DRM_ERROR_LICENSE_EXPIRED = DRM_ERROR_BASE - 1, + DRM_ERROR_SESSION_NOT_OPENED = DRM_ERROR_BASE - 2, + DRM_ERROR_DECRYPT_UNIT_NOT_INITIALIZED = DRM_ERROR_BASE - 3, + DRM_ERROR_DECRYPT = DRM_ERROR_BASE - 4, + DRM_ERROR_CANNOT_HANDLE = DRM_ERROR_BASE - 5, + + DRM_NO_ERROR = NO_ERROR +}; + +/** + * Defines DRM Buffer + */ +class DrmBuffer { +public: + char* data; + int length; + + DrmBuffer() : + data(NULL), + length(0) { + } + + DrmBuffer(char* dataBytes, int dataLength) : + data(dataBytes), + length(dataLength) { + } + +}; + +/** + * Defines detailed description of the action + */ +class ActionDescription { +public: + ActionDescription(int _outputType, int _configuration) : + outputType(_outputType), + configuration(_configuration) { + } + +public: + int outputType; /* BLUETOOTH , HDMI*/ + int configuration; /* RESOLUTION_720_480 , RECORDABLE etc.*/ +}; + +/** + * Defines constants related to DRM types + */ +class DrmObjectType { +private: + DrmObjectType(); + +public: + /** + * Field specifies the unknown type + */ + static const int UNKNOWN = 0x00; + /** + * Field specifies the protected content type + */ + static const int CONTENT = 0x01; + /** + * Field specifies the rights information + */ + static const int RIGHTS_OBJECT = 0x02; + /** + * Field specifies the trigger information + */ + static const int TRIGGER_OBJECT = 0x03; +}; + +/** + * Defines constants related to play back + */ +class Playback { +private: + Playback(); + +public: + /** + * Constant field signifies playback start + */ + static const int START = 0x00; + /** + * Constant field signifies playback stop + */ + static const int STOP = 0x01; + /** + * Constant field signifies playback paused + */ + static const int PAUSE = 0x02; + /** + * Constant field signifies playback resumed + */ + static const int RESUME = 0x03; +}; + +/** + * Defines actions that can be performed on protected content + */ +class Action { +private: + Action(); + +public: + /** + * Constant field signifies that the default action + */ + static const int DEFAULT = 0x00; + /** + * Constant field signifies that the content can be played + */ + static const int PLAY = 0x01; + /** + * Constant field signifies that the content can be set as ring tone + */ + static const int RINGTONE = 0x02; + /** + * Constant field signifies that the content can be transfered + */ + static const int TRANSFER = 0x03; + /** + * Constant field signifies that the content can be set as output + */ + static const int OUTPUT = 0x04; + /** + * Constant field signifies that preview is allowed + */ + static const int PREVIEW = 0x05; + /** + * Constant field signifies that the content can be executed + */ + static const int EXECUTE = 0x06; + /** + * Constant field signifies that the content can displayed + */ + static const int DISPLAY = 0x07; +}; + +/** + * Defines constants related to status of the rights + */ +class RightsStatus { +private: + RightsStatus(); + +public: + /** + * Constant field signifies that the rights are valid + */ + static const int RIGHTS_VALID = 0x00; + /** + * Constant field signifies that the rights are invalid + */ + static const int RIGHTS_INVALID = 0x01; + /** + * Constant field signifies that the rights are expired for the content + */ + static const int RIGHTS_EXPIRED = 0x02; + /** + * Constant field signifies that the rights are not acquired for the content + */ + static const int RIGHTS_NOT_ACQUIRED = 0x03; +}; + +/** + * Defines API set for decryption + */ +class DecryptApiType { +private: + DecryptApiType(); + +public: + /** + * Decrypt API set for non encrypted content + */ + static const int NON_ENCRYPTED = 0x00; + /** + * Decrypt API set for ES based DRM + */ + static const int ELEMENTARY_STREAM_BASED = 0x01; + /** + * POSIX based Decrypt API set for container based DRM + */ + static const int CONTAINER_BASED = 0x02; +}; + +/** + * Defines decryption information + */ +class DecryptInfo { +public: + /** + * size of memory to be allocated to get the decrypted content. + */ + int decryptBufferLength; + /** + * reserved for future purpose + */ +}; + +/** + * Defines decryption handle + */ +class DecryptHandle { +public: + /** + * Decryption session Handle + */ + int decryptId; + /** + * Mimetype of the content to be used to select the media extractor + * For e.g., "video/mpeg" or "audio/mp3" + */ + String8 mimeType; + /** + * Defines which decryption pattern should be used to decrypt the given content + * DrmFramework provides two different set of decryption APIs. + * 1. Decrypt APIs for elementary stream based DRM + * (file format is not encrypted but ES is encrypted) + * e.g., Marlin DRM (MP4 file format), WM-DRM (asf file format) + * + * DecryptAPI::TYPE_ELEMENTARY_STREAM_BASED + * Decryption API set for ES based DRM + * initializeDecryptUnit(), decrypt(), and finalizeDecryptUnit() + * 2. Decrypt APIs for container based DRM (file format itself is encrypted) + * e.g., OMA DRM (dcf file format) + * + * DecryptAPI::TYPE_CONTAINER_BASED + * POSIX based Decryption API set for container based DRM + * pread() + */ + int decryptApiType; + /** + * Defines the status of the rights like + * RIGHTS_VALID, RIGHTS_INVALID, RIGHTS_EXPIRED or RIGHTS_NOT_ACQUIRED + */ + int status; + /** + * Information required to decrypt content + * e.g. size of memory to be allocated to get the decrypted content. + */ + DecryptInfo* decryptInfo; + +public: + DecryptHandle(): + decryptId(INVALID_VALUE), + mimeType(""), + decryptApiType(INVALID_VALUE), + status(INVALID_VALUE) { + + } + + bool operator<(const DecryptHandle& handle) const { + return (decryptId < handle.decryptId); + } + + bool operator==(const DecryptHandle& handle) const { + return (decryptId == handle.decryptId); + } +}; + +}; + +#endif /* __DRM_FRAMEWORK_COMMON_H__ */ + diff --git a/include/media/IMediaPlayer.h b/include/media/IMediaPlayer.h index af9a7edf9966d263ec93e8632db9f3357f010258..a1ce113d80f3cc23656ef8b666ba24bec2cfba49 100644 --- a/include/media/IMediaPlayer.h +++ b/include/media/IMediaPlayer.h @@ -25,6 +25,7 @@ namespace android { class Parcel; class ISurface; +class Surface; class IMediaPlayer: public IInterface { @@ -33,7 +34,8 @@ public: virtual void disconnect() = 0; - virtual status_t setVideoSurface(const sp& surface) = 0; + virtual status_t setVideoISurface(const sp& surface) = 0; + virtual status_t setVideoSurface(const sp& surface) = 0; virtual status_t prepareAsync() = 0; virtual status_t start() = 0; virtual status_t stop() = 0; diff --git a/include/media/IMediaRecorder.h b/include/media/IMediaRecorder.h index 54adca8d09f718df87ef3456a02905574ef957e3..28be7c100c368636406d8ca2b25a0eb9bfd61588 100644 --- a/include/media/IMediaRecorder.h +++ b/include/media/IMediaRecorder.h @@ -22,7 +22,7 @@ namespace android { -class ISurface; +class Surface; class ICamera; class IMediaRecorderClient; @@ -32,7 +32,7 @@ public: DECLARE_META_INTERFACE(MediaRecorder); virtual status_t setCamera(const sp& camera) = 0; - virtual status_t setPreviewSurface(const sp& surface) = 0; + virtual status_t setPreviewSurface(const sp& surface) = 0; virtual status_t setVideoSource(int vs) = 0; virtual status_t setAudioSource(int as) = 0; virtual status_t setOutputFormat(int of) = 0; @@ -40,6 +40,7 @@ public: virtual status_t setAudioEncoder(int ae) = 0; virtual status_t setOutputFile(const char* path) = 0; virtual status_t setOutputFile(int fd, int64_t offset, int64_t length) = 0; + virtual status_t setOutputFileAuxiliary(int fd) = 0; virtual status_t setVideoSize(int width, int height) = 0; virtual status_t setVideoFrameRate(int frames_per_second) = 0; virtual status_t setParameters(const String8& params) = 0; @@ -68,4 +69,3 @@ public: }; // namespace android #endif // ANDROID_IMEDIARECORDER_H - diff --git a/include/media/IOMX.h b/include/media/IOMX.h index 2f61cbe1cabda611118921c939617176e38b3447..1f8ce71a595b88a4df8af52df13c8f560d53a83a 100644 --- a/include/media/IOMX.h +++ b/include/media/IOMX.h @@ -19,6 +19,7 @@ #define ANDROID_IOMX_H_ #include +#include #include #include @@ -78,10 +79,17 @@ public: node_id node, OMX_INDEXTYPE index, const void *params, size_t size) = 0; + virtual status_t enableGraphicBuffers( + node_id node, OMX_U32 port_index, OMX_BOOL enable) = 0; + virtual status_t useBuffer( node_id node, OMX_U32 port_index, const sp ¶ms, buffer_id *buffer) = 0; + virtual status_t useGraphicBuffer( + node_id node, OMX_U32 port_index, + const sp &graphicBuffer, buffer_id *buffer) = 0; + // This API clearly only makes sense if the caller lives in the // same process as the callee, i.e. is the media_server, as the // returned "buffer_data" pointer is just that, a pointer into local diff --git a/include/media/MediaPlayerInterface.h b/include/media/MediaPlayerInterface.h index 36629836b52ab56ccca01a3e36c3620fda8d4610..13c73accc7723e46a5f2656f6d13864f5a421a12 100644 --- a/include/media/MediaPlayerInterface.h +++ b/include/media/MediaPlayerInterface.h @@ -33,6 +33,7 @@ namespace android { class Parcel; class ISurface; +class Surface; template class SortedVector; @@ -104,7 +105,8 @@ public: const KeyedVector *headers = NULL) = 0; virtual status_t setDataSource(int fd, int64_t offset, int64_t length) = 0; - virtual status_t setVideoSurface(const sp& surface) = 0; + virtual status_t setVideoISurface(const sp& surface) = 0; + virtual status_t setVideoSurface(const sp& surface) = 0; virtual status_t prepare() = 0; virtual status_t prepareAsync() = 0; virtual status_t start() = 0; diff --git a/include/media/MediaProfiles.h b/include/media/MediaProfiles.h index c3cd361b25f109863cdf3686fb06c2e854a03aff..aa97874026414e79191cafd0a62ea2204b51ba88 100644 --- a/include/media/MediaProfiles.h +++ b/include/media/MediaProfiles.h @@ -25,7 +25,20 @@ namespace android { enum camcorder_quality { CAMCORDER_QUALITY_LOW = 0, - CAMCORDER_QUALITY_HIGH = 1 + CAMCORDER_QUALITY_HIGH = 1, + CAMCORDER_QUALITY_QCIF = 2, + CAMCORDER_QUALITY_CIF = 3, + CAMCORDER_QUALITY_480P = 4, + CAMCORDER_QUALITY_720P = 5, + CAMCORDER_QUALITY_1080P = 6, + + CAMCORDER_QUALITY_TIME_LAPSE_LOW = 1000, + CAMCORDER_QUALITY_TIME_LAPSE_HIGH = 1001, + CAMCORDER_QUALITY_TIME_LAPSE_QCIF = 1002, + CAMCORDER_QUALITY_TIME_LAPSE_CIF = 1003, + CAMCORDER_QUALITY_TIME_LAPSE_480P = 1004, + CAMCORDER_QUALITY_TIME_LAPSE_720P = 1005, + CAMCORDER_QUALITY_TIME_LAPSE_1080P = 1006 }; enum video_decoder { @@ -67,6 +80,12 @@ public: int getCamcorderProfileParamByName(const char *name, int cameraId, camcorder_quality quality) const; + /** + * Returns true if a profile for the given camera at the given quality exists, + * or false if not. + */ + bool hasCamcorderProfile(int cameraId, camcorder_quality quality) const; + /** * Returns the output file formats supported. */ @@ -252,6 +271,8 @@ private: Vector mLevels; }; + int getCamcorderProfileIndex(int cameraId, camcorder_quality quality) const; + // Debug static void logVideoCodec(const VideoCodec& codec); static void logAudioCodec(const AudioCodec& codec); @@ -281,8 +302,25 @@ private: // If the xml configuration file does not exist, use hard-coded values static MediaProfiles* createDefaultInstance(); - static CamcorderProfile *createDefaultCamcorderLowProfile(); - static CamcorderProfile *createDefaultCamcorderHighProfile(); + + static CamcorderProfile *createDefaultCamcorderQcifProfile(camcorder_quality quality); + static CamcorderProfile *createDefaultCamcorderCifProfile(camcorder_quality quality); + static void createDefaultCamcorderLowProfiles( + MediaProfiles::CamcorderProfile **lowProfile, + MediaProfiles::CamcorderProfile **lowSpecificProfile); + static void createDefaultCamcorderHighProfiles( + MediaProfiles::CamcorderProfile **highProfile, + MediaProfiles::CamcorderProfile **highSpecificProfile); + + static CamcorderProfile *createDefaultCamcorderTimeLapseQcifProfile(camcorder_quality quality); + static CamcorderProfile *createDefaultCamcorderTimeLapse480pProfile(camcorder_quality quality); + static void createDefaultCamcorderTimeLapseLowProfiles( + MediaProfiles::CamcorderProfile **lowTimeLapseProfile, + MediaProfiles::CamcorderProfile **lowSpecificTimeLapseProfile); + static void createDefaultCamcorderTimeLapseHighProfiles( + MediaProfiles::CamcorderProfile **highTimeLapseProfile, + MediaProfiles::CamcorderProfile **highSpecificTimeLapseProfile); + static void createDefaultCamcorderProfiles(MediaProfiles *profiles); static void createDefaultVideoEncoders(MediaProfiles *profiles); static void createDefaultAudioEncoders(MediaProfiles *profiles); diff --git a/include/media/MediaRecorderBase.h b/include/media/MediaRecorderBase.h index 5e9e3681a6728657e8c9db7ca7cc0c7500d7d520..c42346e1f6189e7767446489fd81b439923ba08c 100644 --- a/include/media/MediaRecorderBase.h +++ b/include/media/MediaRecorderBase.h @@ -22,7 +22,7 @@ namespace android { -class ISurface; +class Surface; struct MediaRecorderBase { MediaRecorderBase() {} @@ -37,9 +37,10 @@ struct MediaRecorderBase { virtual status_t setVideoSize(int width, int height) = 0; virtual status_t setVideoFrameRate(int frames_per_second) = 0; virtual status_t setCamera(const sp& camera) = 0; - virtual status_t setPreviewSurface(const sp& surface) = 0; + virtual status_t setPreviewSurface(const sp& surface) = 0; virtual status_t setOutputFile(const char *path) = 0; virtual status_t setOutputFile(int fd, int64_t offset, int64_t length) = 0; + virtual status_t setOutputFileAuxiliary(int fd) {return INVALID_OPERATION;} virtual status_t setParameters(const String8& params) = 0; virtual status_t setListener(const sp& listener) = 0; virtual status_t prepare() = 0; diff --git a/include/media/PVMediaRecorder.h b/include/media/PVMediaRecorder.h index c091c39b3f6a06bc60dccc4b14a4c34e99d48af4..4b44ccc551ec7a1fbfd2bb520c92741784cc06b8 100644 --- a/include/media/PVMediaRecorder.h +++ b/include/media/PVMediaRecorder.h @@ -23,7 +23,7 @@ namespace android { -class ISurface; +class Surface; class ICamera; class AuthorDriverWrapper; @@ -41,7 +41,7 @@ public: virtual status_t setVideoSize(int width, int height); virtual status_t setVideoFrameRate(int frames_per_second); virtual status_t setCamera(const sp& camera); - virtual status_t setPreviewSurface(const sp& surface); + virtual status_t setPreviewSurface(const sp& surface); virtual status_t setOutputFile(const char *path); virtual status_t setOutputFile(int fd, int64_t offset, int64_t length); virtual status_t setParameters(const String8& params); @@ -66,4 +66,3 @@ private: }; // namespace android #endif // ANDROID_PVMEDIARECORDER_H - diff --git a/include/media/PVPlayer.h b/include/media/PVPlayer.h index df5098121d705fd69ec78de29ec986f58898206f..657e7a6734653c9a07f9415ab61bcdf6d65db545 100644 --- a/include/media/PVPlayer.h +++ b/include/media/PVPlayer.h @@ -43,7 +43,8 @@ public: const char *url, const KeyedVector *headers); virtual status_t setDataSource(int fd, int64_t offset, int64_t length); - virtual status_t setVideoSurface(const sp& surface); + virtual status_t setVideoISurface(const sp& surface); + virtual status_t setVideoSurface(const sp& surface); virtual status_t prepare(); virtual status_t prepareAsync(); virtual status_t start(); diff --git a/include/media/mediarecorder.h b/include/media/mediarecorder.h index 291b18aab8a8dcd8dd300d253416af318a9dea77..a600f6b52b1e4757da3506e2a2923e0276f46581 100644 --- a/include/media/mediarecorder.h +++ b/include/media/mediarecorder.h @@ -170,6 +170,7 @@ public: status_t setAudioEncoder(int ae); status_t setOutputFile(const char* path); status_t setOutputFile(int fd, int64_t offset, int64_t length); + status_t setOutputFileAuxiliary(int fd); status_t setVideoSize(int width, int height); status_t setVideoFrameRate(int frames_per_second); status_t setParameters(const String8& params); @@ -196,6 +197,7 @@ private: bool mIsAudioEncoderSet; bool mIsVideoEncoderSet; bool mIsOutputFileSet; + bool mIsAuxiliaryOutputFileSet; Mutex mLock; Mutex mNotifyLock; }; diff --git a/include/media/mediascanner.h b/include/media/mediascanner.h index 0d397acc91d8a3f170342b1cd60425f5682e8a45..74c9d5dd476217a28b87015a879f62f32d5d796a 100644 --- a/include/media/mediascanner.h +++ b/include/media/mediascanner.h @@ -38,8 +38,7 @@ struct MediaScanner { typedef bool (*ExceptionCheck)(void* env); virtual status_t processDirectory( - const char *path, const char *extensions, - MediaScannerClient &client, + const char *path, MediaScannerClient &client, ExceptionCheck exceptionCheck, void *exceptionEnv); void setLocale(const char *locale); @@ -55,9 +54,8 @@ private: char *mLocale; status_t doProcessDirectory( - char *path, int pathRemaining, const char *extensions, - MediaScannerClient &client, ExceptionCheck exceptionCheck, - void *exceptionEnv); + char *path, int pathRemaining, MediaScannerClient &client, + ExceptionCheck exceptionCheck, void *exceptionEnv); MediaScanner(const MediaScanner &); MediaScanner &operator=(const MediaScanner &); diff --git a/include/media/stagefright/CameraSource.h b/include/media/stagefright/CameraSource.h index 3192d03a0643ba835f71c33cffd22350057fb30f..ed5f09f8eda8b8aa5466d78a39fedae0043aaa1d 100644 --- a/include/media/stagefright/CameraSource.h +++ b/include/media/stagefright/CameraSource.h @@ -22,7 +22,6 @@ #include #include #include -#include namespace android { @@ -47,12 +46,34 @@ public: virtual void signalBufferReturned(MediaBuffer* buffer); -private: - friend class CameraSourceListener; - +protected: sp mCamera; sp mMeta; + int64_t mStartTimeUs; + int32_t mNumFramesReceived; + int64_t mLastFrameTimestampUs; + bool mStarted; + + CameraSource(const sp &camera); + + virtual void startCameraRecording(); + virtual void stopCameraRecording(); + virtual void releaseRecordingFrame(const sp& frame); + + // Returns true if need to skip the current frame. + // Called from dataCallbackTimestamp. + virtual bool skipCurrentFrame(int64_t timestampUs) {return false;} + + // Callback called when still camera raw data is available. + virtual void dataCallback(int32_t msgType, const sp &data) {} + + virtual void dataCallbackTimestamp(int64_t timestampUs, int32_t msgType, + const sp &data); + +private: + friend class CameraSourceListener; + Mutex mLock; Condition mFrameAvailableCondition; Condition mFrameCompleteCondition; @@ -60,21 +81,12 @@ private: List > mFramesBeingEncoded; List mFrameTimes; - int64_t mStartTimeUs; int64_t mFirstFrameTimeUs; - int64_t mLastFrameTimestampUs; - int32_t mNumFramesReceived; int32_t mNumFramesEncoded; int32_t mNumFramesDropped; int32_t mNumGlitches; int64_t mGlitchDurationThresholdUs; bool mCollectStats; - bool mStarted; - - CameraSource(const sp &camera); - - void dataCallbackTimestamp( - int64_t timestampUs, int32_t msgType, const sp &data); void releaseQueuedFrames(); void releaseOneRecordingFrame(const sp& frame); diff --git a/include/media/stagefright/CameraSourceTimeLapse.h b/include/media/stagefright/CameraSourceTimeLapse.h new file mode 100644 index 0000000000000000000000000000000000000000..3b303f84771edb0dc4a9f5d0298b5d8c02bc6aaf --- /dev/null +++ b/include/media/stagefright/CameraSourceTimeLapse.h @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CAMERA_SOURCE_TIME_LAPSE_H_ + +#define CAMERA_SOURCE_TIME_LAPSE_H_ + +#include + +#include +#include + +namespace android { + +class ICamera; +class IMemory; +class Camera; + +class CameraSourceTimeLapse : public CameraSource { +public: + static CameraSourceTimeLapse *Create( + int64_t timeBetweenTimeLapseFrameCaptureUs, + int32_t width, int32_t height, + int32_t videoFrameRate); + + static CameraSourceTimeLapse *CreateFromCamera(const sp &camera, + int64_t timeBetweenTimeLapseFrameCaptureUs, + int32_t width, int32_t height, + int32_t videoFrameRate); + + virtual ~CameraSourceTimeLapse(); + + // If the frame capture interval is large, read will block for a long time. + // Due to the way the mediaRecorder framework works, a stop() call from + // mediaRecorder waits until the read returns, causing a long wait for + // stop() to return. To avoid this, we can make read() return a copy of the + // last read frame with the same time stamp frequently. This keeps the + // read() call from blocking too long. Calling this function quickly + // captures another frame, keeps its copy, and enables this mode of read() + // returning quickly. + void startQuickReadReturns(); + +private: + // If true, will use still camera takePicture() for time lapse frames + // If false, will use the videocamera frames instead. + bool mUseStillCameraForTimeLapse; + + // Size of picture taken from still camera. This may be larger than the size + // of the video, as still camera may not support the exact video resolution + // demanded. See setPictureSizeToClosestSupported(). + int32_t mPictureWidth; + int32_t mPictureHeight; + + // size of the encoded video. + int32_t mVideoWidth; + int32_t mVideoHeight; + + // True if we need to crop the still camera image to get the video frame. + bool mNeedCropping; + + // Start location of the cropping rectangle. + int32_t mCropRectStartX; + int32_t mCropRectStartY; + + // Time between capture of two frames during time lapse recording + // Negative value indicates that timelapse is disabled. + int64_t mTimeBetweenTimeLapseFrameCaptureUs; + + // Time between two frames in final video (1/frameRate) + int64_t mTimeBetweenTimeLapseVideoFramesUs; + + // Real timestamp of the last encoded time lapse frame + int64_t mLastTimeLapseFrameRealTimestampUs; + + // Thread id of thread which takes still picture and sleeps in a loop. + pthread_t mThreadTimeLapse; + + // Variable set in dataCallbackTimestamp() to help skipCurrentFrame() + // to know if current frame needs to be skipped. + bool mSkipCurrentFrame; + + // Lock for accessing mCameraIdle + Mutex mCameraIdleLock; + + // Condition variable to wait on if camera is is not yet idle. Once the + // camera gets idle, this variable will be signalled. + Condition mCameraIdleCondition; + + // True if camera is in preview mode and ready for takePicture(). + // False after a call to takePicture() but before the final compressed + // data callback has been called and preview has been restarted. + volatile bool mCameraIdle; + + // True if stop() is waiting for camera to get idle, i.e. for the last + // takePicture() to complete. This is needed so that dataCallbackTimestamp() + // can return immediately. + volatile bool mStopWaitingForIdleCamera; + + // Lock for accessing quick stop variables. + Mutex mQuickStopLock; + + // Condition variable to wake up still picture thread. + Condition mTakePictureCondition; + + // mQuickStop is set to true if we use quick read() returns, otherwise it is set + // to false. Once in this mode read() return a copy of the last read frame + // with the same time stamp. See startQuickReadReturns(). + volatile bool mQuickStop; + + // Forces the next frame passed to dataCallbackTimestamp() to be read + // as a time lapse frame. Used by startQuickReadReturns() so that the next + // frame wakes up any blocking read. + volatile bool mForceRead; + + // Stores a copy of the MediaBuffer read in the last read() call after + // mQuickStop was true. + MediaBuffer* mLastReadBufferCopy; + + // Status code for last read. + status_t mLastReadStatus; + + CameraSourceTimeLapse(const sp &camera, + int64_t timeBetweenTimeLapseFrameCaptureUs, + int32_t width, int32_t height, + int32_t videoFrameRate); + + // Wrapper over CameraSource::signalBufferReturned() to implement quick stop. + // It only handles the case when mLastReadBufferCopy is signalled. Otherwise + // it calls the base class' function. + virtual void signalBufferReturned(MediaBuffer* buffer); + + // Wrapper over CameraSource::read() to implement quick stop. + virtual status_t read(MediaBuffer **buffer, const ReadOptions *options = NULL); + + // For still camera case starts a thread which calls camera's takePicture() + // in a loop. For video camera case, just starts the camera's video recording. + virtual void startCameraRecording(); + + // For still camera case joins the thread created in startCameraRecording(). + // For video camera case, just stops the camera's video recording. + virtual void stopCameraRecording(); + + // For still camera case don't need to do anything as memory is locally + // allocated with refcounting. + // For video camera case just tell the camera to release the frame. + virtual void releaseRecordingFrame(const sp& frame); + + // mSkipCurrentFrame is set to true in dataCallbackTimestamp() if the current + // frame needs to be skipped and this function just returns the value of mSkipCurrentFrame. + virtual bool skipCurrentFrame(int64_t timestampUs); + + // Handles the callback to handle raw frame data from the still camera. + // Creates a copy of the frame data as the camera can reuse the frame memory + // once this callback returns. The function also sets a new timstamp corresponding + // to one frame time ahead of the last encoded frame's time stamp. It then + // calls dataCallbackTimestamp() of the base class with the copied data and the + // modified timestamp, which will think that it recieved the frame from a video + // camera and proceed as usual. + virtual void dataCallback(int32_t msgType, const sp &data); + + // In the video camera case calls skipFrameAndModifyTimeStamp() to modify + // timestamp and set mSkipCurrentFrame. + // Then it calls the base CameraSource::dataCallbackTimestamp() + virtual void dataCallbackTimestamp(int64_t timestampUs, int32_t msgType, + const sp &data); + + // Convenience function to fill mLastReadBufferCopy from the just read + // buffer. + void fillLastReadBufferCopy(MediaBuffer& sourceBuffer); + + // If the passed in size (width x height) is a supported preview size, + // the function sets the camera's preview size to it and returns true. + // Otherwise returns false. + bool trySettingPreviewSize(int32_t width, int32_t height); + + // The still camera may not support the demanded video width and height. + // We look for the supported picture sizes from the still camera and + // choose the smallest one with either dimensions higher than the corresponding + // video dimensions. The still picture will be cropped to get the video frame. + // The function returns true if the camera supports picture sizes greater than + // or equal to the passed in width and height, and false otherwise. + bool setPictureSizeToClosestSupported(int32_t width, int32_t height); + + // Computes the offset of the rectangle from where to start cropping the + // still image into the video frame. We choose the center of the image to be + // cropped. The offset is stored in (mCropRectStartX, mCropRectStartY). + bool computeCropRectangleOffset(); + + // Crops the source data into a smaller image starting at + // (mCropRectStartX, mCropRectStartY) and of the size of the video frame. + // The data is returned into a newly allocated IMemory. + sp cropYUVImage(const sp &source_data); + + // When video camera is used for time lapse capture, returns true + // until enough time has passed for the next time lapse frame. When + // the frame needs to be encoded, it returns false and also modifies + // the time stamp to be one frame time ahead of the last encoded + // frame's time stamp. + bool skipFrameAndModifyTimeStamp(int64_t *timestampUs); + + // Wrapper to enter threadTimeLapseEntry() + static void *ThreadTimeLapseWrapper(void *me); + + // Runs a loop which sleeps until a still picture is required + // and then calls mCamera->takePicture() to take the still picture. + // Used only in the case mUseStillCameraForTimeLapse = true. + void threadTimeLapseEntry(); + + // Wrapper to enter threadStartPreview() + static void *ThreadStartPreviewWrapper(void *me); + + // Starts the camera's preview. + void threadStartPreview(); + + // Starts thread ThreadStartPreviewWrapper() for restarting preview. + // Needs to be done in a thread so that dataCallback() which calls this function + // can return, and the camera can know that takePicture() is done. + void restartPreview(); + + // Creates a copy of source_data into a new memory of final type MemoryBase. + sp createIMemoryCopy(const sp &source_data); + + CameraSourceTimeLapse(const CameraSourceTimeLapse &); + CameraSourceTimeLapse &operator=(const CameraSourceTimeLapse &); +}; + +} // namespace android + +#endif // CAMERA_SOURCE_TIME_LAPSE_H_ diff --git a/include/media/stagefright/HardwareAPI.h b/include/media/stagefright/HardwareAPI.h index 221c6796306f8b9ebd9882e9405d8f7be5c64665..4ded5e833829e26802d6e1b7a9cefdfa6e2eeeca 100644 --- a/include/media/stagefright/HardwareAPI.h +++ b/include/media/stagefright/HardwareAPI.h @@ -21,10 +21,60 @@ #include #include #include +#include #include #include +namespace android { + +// A pointer to this struct is passed to the OMX_SetParameter when the extension +// index for the 'OMX.google.android.index.enableAndroidNativeBuffers' extension +// is given. +// +// When Android native buffer use is disabled for a port (the default state), +// the OMX node should operate as normal, and expect UseBuffer calls to set its +// buffers. This is the mode that will be used when CPU access to the buffer is +// required. +// +// When Android native buffer use has been enabled, the OMX node must support +// only color formats in the range [OMX_COLOR_FormatAndroidPrivateStart, +// OMX_COLOR_FormatAndroidPrivateEnd). The node should then expect to receive +// UseAndroidNativeBuffer calls (via OMX_SetParameter) rather than UseBuffer +// calls. +struct EnableAndroidNativeBuffersParams { + OMX_U32 nSize; + OMX_VERSIONTYPE nVersion; + OMX_U32 nPortIndex; + OMX_BOOL enable; +}; + +// Color formats in the range [OMX_COLOR_FormatAndroidPrivateStart, +// OMX_COLOR_FormatAndroidPrivateEnd) will be converted to a gralloc pixel +// format when used to allocate Android native buffers via gralloc. The +// conversion is done by subtracting OMX_COLOR_FormatAndroidPrivateStart from +// the color format reported by the codec. +enum { + OMX_COLOR_FormatAndroidPrivateStart = 0xA0000000, + OMX_COLOR_FormatAndroidPrivateEnd = 0xB0000000, +}; + +// A pointer to this struct is passed to OMX_SetParameter when the extension +// index for the 'OMX.google.android.index.useAndroidNativeBuffer' extension is +// given. This call will only be performed if a prior call was made with the +// 'OMX.google.android.index.enableAndroidNativeBuffers' extension index, +// enabling use of Android native buffers. +struct UseAndroidNativeBufferParams { + OMX_U32 nSize; + OMX_VERSIONTYPE nVersion; + OMX_U32 nPortIndex; + OMX_PTR pAppPrivate; + OMX_BUFFERHEADERTYPE **bufferHeader; + const sp& nativeBuffer; +}; + +} // namespace android + extern android::VideoRenderer *createRenderer( const android::sp &surface, const char *componentName, @@ -35,4 +85,3 @@ extern android::VideoRenderer *createRenderer( extern android::OMXPluginBase *createOMXPlugin(); #endif // HARDWARE_API_H_ - diff --git a/include/media/stagefright/MediaSourceSplitter.h b/include/media/stagefright/MediaSourceSplitter.h new file mode 100644 index 0000000000000000000000000000000000000000..568f4c296db2904fa3a8c948cdf985aa272ceba9 --- /dev/null +++ b/include/media/stagefright/MediaSourceSplitter.h @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This class provides a way to split a single media source into multiple sources. +// The constructor takes in the real mediaSource and createClient() can then be +// used to create multiple sources served from this real mediaSource. +// +// Usage: +// - Create MediaSourceSplitter by passing in a real mediaSource from which +// multiple duplicate channels are needed. +// - Create a client using createClient() and use it as any other mediaSource. +// +// Note that multiple clients can be created using createClient() and +// started/stopped in any order. MediaSourceSplitter stops the real source only +// when all clients have been stopped. +// +// If a new client is created/started after some existing clients have already +// started, the new client will start getting its read frames from the current +// time. + +#ifndef MEDIA_SOURCE_SPLITTER_H_ + +#define MEDIA_SOURCE_SPLITTER_H_ + +#include +#include +#include +#include + +namespace android { + +class MediaBuffer; +class MetaData; + +class MediaSourceSplitter : public RefBase { +public: + // Constructor + // mediaSource: The real mediaSource. The class keeps a reference to it to + // implement the various clients. + MediaSourceSplitter(sp mediaSource); + + ~MediaSourceSplitter(); + + // Creates a new client of base type MediaSource. Multiple clients can be + // created which get their data through the same real mediaSource. These + // clients can then be used like any other MediaSource, all of which provide + // data from the same real source. + sp createClient(); + +private: + // Total number of clients created through createClient(). + int32_t mNumberOfClients; + + // reference to the real MediaSource passed to the constructor. + sp mSource; + + // Stores pointer to the MediaBuffer read from the real MediaSource. + // All clients use this to implement the read() call. + MediaBuffer *mLastReadMediaBuffer; + + // Status code for read from the real MediaSource. All clients return + // this for their read(). + status_t mLastReadStatus; + + // Boolean telling whether the real MediaSource has started. + bool mSourceStarted; + + // List of booleans, one for each client, storing whether the corresponding + // client's start() has been called. + Vector mClientsStarted; + + // Stores the number of clients which are currently started. + int32_t mNumberOfClientsStarted; + + // Since different clients call read() asynchronously, we need to keep track + // of what data is currently read into the mLastReadMediaBuffer. + // mCurrentReadBit stores the bit for the current read buffer. This bit + // flips each time a new buffer is read from the source. + // mClientsDesiredReadBit stores the bit for the next desired read buffer + // for each client. This bit flips each time read() is completed for this + // client. + bool mCurrentReadBit; + Vector mClientsDesiredReadBit; + + // Number of clients whose current read has been completed. + int32_t mNumberOfCurrentReads; + + // Boolean telling whether the last read has been completed for all clients. + // The variable is reset to false each time buffer is read from the real + // source. + bool mLastReadCompleted; + + // A global mutex for access to critical sections. + Mutex mLock; + + // Condition variable for waiting on read from source to complete. + Condition mReadFromSourceCondition; + + // Condition variable for waiting on all client's last read to complete. + Condition mAllReadsCompleteCondition; + + // Functions used by Client to implement the MediaSource interface. + + // If the real source has not been started yet by any client, starts it. + status_t start(int clientId, MetaData *params); + + // Stops the real source after all clients have called stop(). + status_t stop(int clientId); + + // returns the real source's getFormat(). + sp getFormat(int clientId); + + // If the client's desired buffer has already been read into + // mLastReadMediaBuffer, points the buffer to that. Otherwise if it is the + // master client, reads the buffer from source or else waits for the master + // client to read the buffer and uses that. + status_t read(int clientId, + MediaBuffer **buffer, const MediaSource::ReadOptions *options = NULL); + + // Not implemented right now. + status_t pause(int clientId); + + // Function which reads a buffer from the real source into + // mLastReadMediaBuffer + void readFromSource_lock(const MediaSource::ReadOptions *options); + + // Waits until read from the real source has been completed. + // _lock means that the function should be called when the thread has already + // obtained the lock for the mutex mLock. + void waitForReadFromSource_lock(int32_t clientId); + + // Waits until all clients have read the current buffer in + // mLastReadCompleted. + void waitForAllClientsLastRead_lock(int32_t clientId); + + // Each client calls this after it completes its read(). Once all clients + // have called this for the current buffer, the function calls + // mAllReadsCompleteCondition.broadcast() to signal the waiting clients. + void signalReadComplete_lock(bool readAborted); + + // Make these constructors private. + MediaSourceSplitter(); + MediaSourceSplitter(const MediaSourceSplitter &); + MediaSourceSplitter &operator=(const MediaSourceSplitter &); + + // This class implements the MediaSource interface. Each client stores a + // reference to the parent MediaSourceSplitter and uses it to complete the + // various calls. + class Client : public MediaSource { + public: + // Constructor stores reference to the parent MediaSourceSplitter and it + // client id. + Client(sp splitter, int32_t clientId); + + // MediaSource interface + virtual status_t start(MetaData *params = NULL); + + virtual status_t stop(); + + virtual sp getFormat(); + + virtual status_t read( + MediaBuffer **buffer, const ReadOptions *options = NULL); + + virtual status_t pause(); + + private: + // Refernce to the parent MediaSourceSplitter + sp mSplitter; + + // Id of this client. + int32_t mClientId; + }; + + friend class Client; +}; + +} // namespace android + +#endif // MEDIA_SOURCE_SPLITTER_H_ diff --git a/include/media/stagefright/VideoSourceDownSampler.h b/include/media/stagefright/VideoSourceDownSampler.h new file mode 100644 index 0000000000000000000000000000000000000000..439918cd5971dab0b496224a2b767738edae8cd5 --- /dev/null +++ b/include/media/stagefright/VideoSourceDownSampler.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// VideoSourceDownSampler implements the MediaSource interface, +// downsampling frames provided from a real video source. + +#ifndef VIDEO_SOURCE_DOWN_SAMPLER_H_ + +#define VIDEO_SOURCE_DOWN_SAMPLER_H_ + +#include +#include + +namespace android { + +class IMemory; +class MediaBuffer; +class MetaData; + +class VideoSourceDownSampler : public MediaSource { +public: + virtual ~VideoSourceDownSampler(); + + // Constructor: + // videoSource: The real video source which provides the original frames. + // width, height: The desired width, height. These should be less than or equal + // to those of the real video source. We then downsample the original frames to + // this size. + VideoSourceDownSampler(const sp &videoSource, + int32_t width, int32_t height); + + // MediaSource interface + virtual status_t start(MetaData *params = NULL); + + virtual status_t stop(); + + virtual sp getFormat(); + + virtual status_t read( + MediaBuffer **buffer, const ReadOptions *options = NULL); + + virtual status_t pause(); + +private: + // Reference to the real video source. + sp mRealVideoSource; + + // Size of frames to be provided by this source. + int32_t mWidth; + int32_t mHeight; + + // Size of frames provided by the real source. + int32_t mRealSourceWidth; + int32_t mRealSourceHeight; + + // Down sampling paramters. + int32_t mDownSampleOffsetX; + int32_t mDownSampleOffsetY; + int32_t mDownSampleSkipX; + int32_t mDownSampleSkipY; + + // True if we need to crop the still video image to get the video frame. + bool mNeedDownSampling; + + // Meta data. This is a copy of the real source except for the width and + // height parameters. + sp mMeta; + + // Computes the offset, skip parameters for downsampling the original frame + // to the desired size. + void computeDownSamplingParameters(); + + // Downsamples the frame in sourceBuffer to size (mWidth x mHeight). A new + // buffer is created which stores the downsampled image. + void downSampleYUVImage(const MediaBuffer &sourceBuffer, MediaBuffer **buffer) const; + + // Disallow these. + VideoSourceDownSampler(const VideoSourceDownSampler &); + VideoSourceDownSampler &operator=(const VideoSourceDownSampler &); +}; + +} // namespace android + +#endif // VIDEO_SOURCE_DOWN_SAMPLER_H_ diff --git a/include/media/stagefright/YUVCanvas.h b/include/media/stagefright/YUVCanvas.h new file mode 100644 index 0000000000000000000000000000000000000000..ff70923f040a70ba502060a99f233de56afdd038 --- /dev/null +++ b/include/media/stagefright/YUVCanvas.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// YUVCanvas holds a reference to a YUVImage on which it can do various +// drawing operations. It provides various utility functions for filling, +// cropping, etc. + + +#ifndef YUV_CANVAS_H_ + +#define YUV_CANVAS_H_ + +#include + +namespace android { + +class YUVImage; +class Rect; + +class YUVCanvas { +public: + + // Constructor takes in reference to a yuvImage on which it can do + // various drawing opreations. + YUVCanvas(YUVImage &yuvImage); + ~YUVCanvas(); + + // Fills the entire image with the given YUV values. + void FillYUV(uint8_t yValue, uint8_t uValue, uint8_t vValue); + + // Fills the rectangular region [startX,endX]x[startY,endY] with the given YUV values. + void FillYUVRectangle(const Rect& rect, + uint8_t yValue, uint8_t uValue, uint8_t vValue); + + // Copies the region [startX,endX]x[startY,endY] from srcImage into the + // canvas' target image (mYUVImage) starting at + // (destinationStartX,destinationStartY). + // Note that undefined behavior may occur if srcImage is same as the canvas' + // target image. + void CopyImageRect( + const Rect& srcRect, + int32_t destStartX, int32_t destStartY, + const YUVImage &srcImage); + + // Downsamples the srcImage into the canvas' target image (mYUVImage) + // The downsampling copies pixels from the source image starting at + // (srcOffsetX, srcOffsetY) to the target image, starting at (0, 0). + // For each X increment in the target image, skipX pixels are skipped + // in the source image. + // Similarly for each Y increment in the target image, skipY pixels + // are skipped in the source image. + void downsample( + int32_t srcOffsetX, int32_t srcOffsetY, + int32_t skipX, int32_t skipY, + const YUVImage &srcImage); + +private: + YUVImage& mYUVImage; + + YUVCanvas(const YUVCanvas &); + YUVCanvas &operator=(const YUVCanvas &); +}; + +} // namespace android + +#endif // YUV_CANVAS_H_ diff --git a/include/media/stagefright/YUVImage.h b/include/media/stagefright/YUVImage.h new file mode 100644 index 0000000000000000000000000000000000000000..4e98618948a68b3e02e4397c9bff03f49bfb8263 --- /dev/null +++ b/include/media/stagefright/YUVImage.h @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// A container class to hold YUV data and provide various utilities, +// e.g. to set/get pixel values. +// Supported formats: +// - YUV420 Planar +// - YUV420 Semi Planar +// +// Currently does not support variable strides. +// +// Implementation: Two simple abstractions are done to simplify access +// to YUV channels for different formats: +// - initializeYUVPointers() sets up pointers (mYdata, mUdata, mVdata) to +// point to the right start locations of the different channel data depending +// on the format. +// - getOffsets() returns the correct offset for the different channels +// depending on the format. +// Location of any pixel's YUV channels can then be easily computed using these. +// + +#ifndef YUV_IMAGE_H_ + +#define YUV_IMAGE_H_ + +#include +#include + +namespace android { + +class Rect; + +class YUVImage { +public: + // Supported YUV formats + enum YUVFormat { + YUV420Planar, + YUV420SemiPlanar + }; + + // Constructs an image with the given size, format. Also allocates and owns + // the required memory. + YUVImage(YUVFormat yuvFormat, int32_t width, int32_t height); + + // Constructs an image with the given size, format. The memory is provided + // by the caller and we don't own it. + YUVImage(YUVFormat yuvFormat, int32_t width, int32_t height, uint8_t *buffer); + + // Destructor to delete the memory if it owns it. + ~YUVImage(); + + // Returns the size of the buffer required to store the YUV data for the given + // format and geometry. Useful when the caller wants to allocate the requisite + // memory. + static size_t bufferSize(YUVFormat yuvFormat, int32_t width, int32_t height); + + int32_t width() const {return mWidth;} + int32_t height() const {return mHeight;} + + // Returns true if pixel is the range [0, width-1] x [0, height-1] + // and false otherwise. + bool validPixel(int32_t x, int32_t y) const; + + // Get the pixel YUV value at pixel (x,y). + // Note that the range of x is [0, width-1] and the range of y is [0, height-1]. + // Returns true if get was successful and false otherwise. + bool getPixelValue(int32_t x, int32_t y, + uint8_t *yPtr, uint8_t *uPtr, uint8_t *vPtr) const; + + // Set the pixel YUV value at pixel (x,y). + // Note that the range of x is [0, width-1] and the range of y is [0, height-1]. + // Returns true if set was successful and false otherwise. + bool setPixelValue(int32_t x, int32_t y, + uint8_t yValue, uint8_t uValue, uint8_t vValue); + + // Uses memcpy to copy an entire row of data + static void fastCopyRectangle420Planar( + const Rect& srcRect, + int32_t destStartX, int32_t destStartY, + const YUVImage &srcImage, YUVImage &destImage); + + // Uses memcpy to copy an entire row of data + static void fastCopyRectangle420SemiPlanar( + const Rect& srcRect, + int32_t destStartX, int32_t destStartY, + const YUVImage &srcImage, YUVImage &destImage); + + // Tries to use memcopy to copy entire rows of data. + // Returns false if fast copy is not possible for the passed image formats. + static bool fastCopyRectangle( + const Rect& srcRect, + int32_t destStartX, int32_t destStartY, + const YUVImage &srcImage, YUVImage &destImage); + + // Convert the given YUV value to RGB. + void yuv2rgb(uint8_t yValue, uint8_t uValue, uint8_t vValue, + uint8_t *r, uint8_t *g, uint8_t *b) const; + + // Write the image to a human readable PPM file. + // Returns true if write was succesful and false otherwise. + bool writeToPPM(const char *filename) const; + +private: + // YUV Format of the image. + YUVFormat mYUVFormat; + + int32_t mWidth; + int32_t mHeight; + + // Pointer to the memory buffer. + uint8_t *mBuffer; + + // Boolean telling whether we own the memory buffer. + bool mOwnBuffer; + + // Pointer to start of the Y data plane. + uint8_t *mYdata; + + // Pointer to start of the U data plane. Note that in case of interleaved formats like + // YUV420 semiplanar, mUdata points to the start of the U data in the UV plane. + uint8_t *mUdata; + + // Pointer to start of the V data plane. Note that in case of interleaved formats like + // YUV420 semiplanar, mVdata points to the start of the V data in the UV plane. + uint8_t *mVdata; + + // Initialize the pointers mYdata, mUdata, mVdata to point to the right locations for + // the given format and geometry. + // Returns true if initialize was succesful and false otherwise. + bool initializeYUVPointers(); + + // For the given pixel location, this returns the offset of the location of y, u and v + // data from the corresponding base pointers -- mYdata, mUdata, mVdata. + // Note that the range of x is [0, width-1] and the range of y is [0, height-1]. + // Returns true if getting offsets was succesful and false otherwise. + bool getOffsets(int32_t x, int32_t y, + int32_t *yOffset, int32_t *uOffset, int32_t *vOffset) const; + + // Returns the offset increments incurred in going from one data row to the next data row + // for the YUV channels. Note that this corresponds to data rows and not pixel rows. + // E.g. depending on formats, U/V channels may have only one data row corresponding + // to two pixel rows. + bool getOffsetIncrementsPerDataRow( + int32_t *yDataOffsetIncrement, + int32_t *uDataOffsetIncrement, + int32_t *vDataOffsetIncrement) const; + + // Given the offset return the address of the corresponding channel's data. + uint8_t* getYAddress(int32_t offset) const; + uint8_t* getUAddress(int32_t offset) const; + uint8_t* getVAddress(int32_t offset) const; + + // Given the pixel location, returns the address of the corresponding channel's data. + // Note that the range of x is [0, width-1] and the range of y is [0, height-1]. + bool getYUVAddresses(int32_t x, int32_t y, + uint8_t **yAddr, uint8_t **uAddr, uint8_t **vAddr) const; + + // Disallow implicit casting and copying. + YUVImage(const YUVImage &); + YUVImage &operator=(const YUVImage &); +}; + +} // namespace android + +#endif // YUV_IMAGE_H_ diff --git a/include/surfaceflinger/Surface.h b/include/surfaceflinger/Surface.h index 7c5a39b6803937b973d1930d43f17ecec8883b2c..a2108804af5e8e11caaaf5593aaa601c0468ae86 100644 --- a/include/surfaceflinger/Surface.h +++ b/include/surfaceflinger/Surface.h @@ -94,7 +94,7 @@ private: friend class SurfaceComposerClient; // camera and camcorder need access to the ISurface binder interface for preview - friend class Camera; + friend class CameraService; friend class MediaRecorder; // mediaplayer needs access to ISurface for display friend class MediaPlayer; @@ -173,11 +173,12 @@ private: * (eventually this should go away and be replaced by proper APIs) */ // camera and camcorder need access to the ISurface binder interface for preview - friend class Camera; + friend class CameraService; friend class MediaRecorder; // MediaPlayer needs access to ISurface for display friend class MediaPlayer; friend class IOMX; + friend class SoftwareRenderer; // this is just to be able to write some unit tests friend class Test; @@ -312,4 +313,3 @@ private: }; // namespace android #endif // ANDROID_SF_SURFACE_H - diff --git a/include/utils/Singleton.h b/include/utils/Singleton.h index 3b975b4c4448318e65d892ab607a60ca2c7ffe25..e1ee8eb068fbcd1be236ef70b1cfa4f0f0ab90f5 100644 --- a/include/utils/Singleton.h +++ b/include/utils/Singleton.h @@ -37,6 +37,11 @@ public: } return *instance; } + + static bool hasInstance() { + Mutex::Autolock _l(sLock); + return sInstance != 0; + } protected: ~Singleton() { }; diff --git a/libs/binder/Android.mk b/libs/binder/Android.mk index 13dc500d9445af7b3098ce6d04b7b31bc37afc6e..f9d9f2592291847073efb183df49bf3a205b6d39 100644 --- a/libs/binder/Android.mk +++ b/libs/binder/Android.mk @@ -16,6 +16,7 @@ sources := \ Binder.cpp \ BpBinder.cpp \ + CursorWindow.cpp \ IInterface.cpp \ IMemory.cpp \ IPCThreadState.cpp \ diff --git a/core/jni/CursorWindow.cpp b/libs/binder/CursorWindow.cpp similarity index 99% rename from core/jni/CursorWindow.cpp rename to libs/binder/CursorWindow.cpp index 78779214b6ed110156f8157dbcb3e6a58b6061ca..20b27c9eb94804632b3336d6f6127ebc1aada0c6 100644 --- a/core/jni/CursorWindow.cpp +++ b/libs/binder/CursorWindow.cpp @@ -18,6 +18,7 @@ #define LOG_TAG "CursorWindow" #include +#include #include #include @@ -25,12 +26,6 @@ #include #include -#include -#include - -#include "CursorWindow.h" - - namespace android { CursorWindow::CursorWindow(size_t maxSize) : diff --git a/libs/camera/Camera.cpp b/libs/camera/Camera.cpp index 7efc6d781448d4a692f08127d63ba72fe6733e91..b5f78e8a0d10f3c4e93e90276a68ee488f34ff73 100644 --- a/libs/camera/Camera.cpp +++ b/libs/camera/Camera.cpp @@ -167,32 +167,20 @@ status_t Camera::unlock() return c->unlock(); } -// pass the buffered ISurface to the camera service +// pass the buffered Surface to the camera service status_t Camera::setPreviewDisplay(const sp& surface) { - LOGV("setPreviewDisplay"); + LOGV("setPreviewDisplay(%p)", surface.get()); sp c = mCamera; if (c == 0) return NO_INIT; if (surface != 0) { - return c->setPreviewDisplay(surface->getISurface()); + return c->setPreviewDisplay(surface); } else { LOGD("app passed NULL surface"); return c->setPreviewDisplay(0); } } -status_t Camera::setPreviewDisplay(const sp& surface) -{ - LOGV("setPreviewDisplay"); - if (surface == 0) { - LOGD("app passed NULL surface"); - } - sp c = mCamera; - if (c == 0) return NO_INIT; - return c->setPreviewDisplay(surface); -} - - // start preview mode status_t Camera::startPreview() { @@ -375,4 +363,3 @@ void Camera::DeathNotifier::binderDied(const wp& who) { } }; // namespace android - diff --git a/libs/camera/CameraParameters.cpp b/libs/camera/CameraParameters.cpp index 83e5e57f1f73487e8a37b31a2c25f3f4f2d96d65..af58f776b21a6a6538df25a69b50bf9b18ce6c3c 100644 --- a/libs/camera/CameraParameters.cpp +++ b/libs/camera/CameraParameters.cpp @@ -129,7 +129,7 @@ const char CameraParameters::SCENE_MODE_PARTY[] = "party"; const char CameraParameters::SCENE_MODE_CANDLELIGHT[] = "candlelight"; const char CameraParameters::SCENE_MODE_BARCODE[] = "barcode"; -// Formats for setPreviewFormat and setPictureFormat. +const char CameraParameters::PIXEL_FORMAT_YUV420P[] = "yuv420p"; const char CameraParameters::PIXEL_FORMAT_YUV422SP[] = "yuv422sp"; const char CameraParameters::PIXEL_FORMAT_YUV420SP[] = "yuv420sp"; const char CameraParameters::PIXEL_FORMAT_YUV422I[] = "yuv422i-yuyv"; diff --git a/libs/camera/ICamera.cpp b/libs/camera/ICamera.cpp index 13673b53ea4cd5cefb6d20a47b01eb10901c850a..94dc5c184f0d19b565131d6a5b0c8af9e272c076 100644 --- a/libs/camera/ICamera.cpp +++ b/libs/camera/ICamera.cpp @@ -64,13 +64,13 @@ public: remote()->transact(DISCONNECT, data, &reply); } - // pass the buffered ISurface to the camera service - status_t setPreviewDisplay(const sp& surface) + // pass the buffered Surface to the camera service + status_t setPreviewDisplay(const sp& surface) { LOGV("setPreviewDisplay"); Parcel data, reply; data.writeInterfaceToken(ICamera::getInterfaceDescriptor()); - data.writeStrongBinder(surface->asBinder()); + Surface::writeToParcel(surface, &data); remote()->transact(SET_PREVIEW_DISPLAY, data, &reply); return reply.readInt32(); } @@ -258,7 +258,7 @@ status_t BnCamera::onTransact( case SET_PREVIEW_DISPLAY: { LOGV("SET_PREVIEW_DISPLAY"); CHECK_INTERFACE(ICamera, data, reply); - sp surface = interface_cast(data.readStrongBinder()); + sp surface = Surface::readFromParcel(data); reply->writeInt32(setPreviewDisplay(surface)); return NO_ERROR; } break; @@ -376,4 +376,3 @@ status_t BnCamera::onTransact( // ---------------------------------------------------------------------------- }; // namespace android - diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..0469508b8ac57e09a7b4ce91b55074b57a901553 --- /dev/null +++ b/libs/hwui/Android.mk @@ -0,0 +1,43 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +# Only build libhwui when USE_OPENGL_RENDERER is +# defined in the current device/board configuration +ifeq ($(USE_OPENGL_RENDERER),true) + LOCAL_SRC_FILES:= \ + FontRenderer.cpp \ + GammaFontRenderer.cpp \ + GradientCache.cpp \ + LayerCache.cpp \ + Matrix.cpp \ + OpenGLRenderer.cpp \ + Patch.cpp \ + PatchCache.cpp \ + PathCache.cpp \ + Program.cpp \ + ProgramCache.cpp \ + SkiaColorFilter.cpp \ + SkiaShader.cpp \ + TextureCache.cpp \ + TextDropShadowCache.cpp + + LOCAL_C_INCLUDES += \ + $(JNI_H_INCLUDE) \ + $(LOCAL_PATH)/../../include/utils \ + external/skia/include/core \ + external/skia/include/effects \ + external/skia/include/images \ + external/skia/src/ports \ + external/skia/include/utils + + LOCAL_CFLAGS += -DUSE_OPENGL_RENDERER + LOCAL_MODULE_CLASS := SHARED_LIBRARIES + LOCAL_SHARED_LIBRARIES := libcutils libutils libGLESv2 libskia + LOCAL_MODULE := libhwui + LOCAL_MODULE_TAGS := optional + LOCAL_PRELINK_MODULE := false + + include $(BUILD_SHARED_LIBRARY) + + include $(call all-makefiles-under,$(LOCAL_PATH)) +endif diff --git a/libs/hwui/Caches.h b/libs/hwui/Caches.h new file mode 100644 index 0000000000000000000000000000000000000000..9c67885f96014c0006d3797165c78c5442617ba3 --- /dev/null +++ b/libs/hwui/Caches.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_CACHES_H +#define ANDROID_UI_CACHES_H + +#ifndef LOG_TAG + #define LOG_TAG "OpenGLRenderer" +#endif + +#include + +#include "TextureCache.h" +#include "LayerCache.h" +#include "GradientCache.h" +#include "PatchCache.h" +#include "GammaFontRenderer.h" +#include "ProgramCache.h" +#include "PathCache.h" +#include "TextDropShadowCache.h" + +namespace android { +namespace uirenderer { + +struct CacheLogger { + CacheLogger() { + LOGD("Creating caches"); + } +}; // struct CacheLogger + +class Caches: public Singleton { + Caches(): Singleton(), blend(false), lastSrcMode(GL_ZERO), + lastDstMode(GL_ZERO), currentProgram(NULL) { + } + + friend class Singleton; + + CacheLogger logger; + +public: + bool blend; + GLenum lastSrcMode; + GLenum lastDstMode; + Program* currentProgram; + + TextureCache textureCache; + LayerCache layerCache; + GradientCache gradientCache; + ProgramCache programCache; + PathCache pathCache; + PatchCache patchCache; + TextDropShadowCache dropShadowCache; + GammaFontRenderer fontRenderer; +}; // class Caches + +}; // namespace uirenderer + +#ifdef USE_OPENGL_RENDERER +using namespace uirenderer; +ANDROID_SINGLETON_STATIC_INSTANCE(Caches); +#endif + +}; // namespace android + +#endif // ANDROID_UI_CACHES_H diff --git a/libs/hwui/Extensions.h b/libs/hwui/Extensions.h new file mode 100644 index 0000000000000000000000000000000000000000..d50d36eabaa55773bd63906833ddb5a162ce9f46 --- /dev/null +++ b/libs/hwui/Extensions.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_EXTENSIONS_H +#define ANDROID_UI_EXTENSIONS_H + +#include +#include + +#include +#include + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Defines +/////////////////////////////////////////////////////////////////////////////// + +// Debug +#define DEBUG_EXTENSIONS 0 + +// Debug +#if DEBUG_EXTENSIONS + #define EXT_LOGD(...) LOGD(__VA_ARGS__) +#else + #define EXT_LOGD(...) +#endif + +class Extensions { +public: + Extensions() { + const char* buffer = (const char*) glGetString(GL_EXTENSIONS); + const char* current = buffer; + const char* head = current; + EXT_LOGD("Available GL extensions:"); + do { + head = strchr(current, ' '); + String8 s(current, head ? head - current : strlen(current)); + if (s.length()) { + mExtensionList.add(s); + EXT_LOGD(" %s", s.string()); + } + current = head + 1; + } while (head); + + mHasNPot = hasExtension("GL_OES_texture_npot"); + mHasDrawPath = hasExtension("GL_NV_draw_path"); + mHasCoverageSample = hasExtension("GL_NV_coverage_sample"); + mHasFramebufferFetch = hasExtension("GL_NV_shader_framebuffer_fetch"); + + mExtensions = buffer; + } + + inline bool hasNPot() const { return mHasNPot; } + inline bool hasDrawPath() const { return mHasDrawPath; } + inline bool hasCoverageSample() const { return mHasCoverageSample; } + inline bool hasFramebufferFetch() const { return mHasFramebufferFetch; } + + bool hasExtension(const char* extension) const { + const String8 s(extension); + return mExtensionList.indexOf(s) >= 0; + } + + void dump() { + LOGD("Supported extensions:\n%s", mExtensions); + } + +private: + SortedVector mExtensionList; + + const char* mExtensions; + + bool mHasNPot; + bool mHasDrawPath; + bool mHasCoverageSample; + bool mHasFramebufferFetch; +}; // class Extensions + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_EXTENSIONS_H diff --git a/libs/hwui/FontRenderer.cpp b/libs/hwui/FontRenderer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2959814984dfa71a261567d74788c1be132124e6 --- /dev/null +++ b/libs/hwui/FontRenderer.cpp @@ -0,0 +1,828 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include +#include + +#include "FontRenderer.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Defines +/////////////////////////////////////////////////////////////////////////////// + +#define DEFAULT_TEXT_CACHE_WIDTH 1024 +#define DEFAULT_TEXT_CACHE_HEIGHT 256 + +/////////////////////////////////////////////////////////////////////////////// +// Font +/////////////////////////////////////////////////////////////////////////////// + +Font::Font(FontRenderer* state, uint32_t fontId, float fontSize) : + mState(state), mFontId(fontId), mFontSize(fontSize) { +} + + +Font::~Font() { + for (uint32_t ct = 0; ct < mState->mActiveFonts.size(); ct++) { + if (mState->mActiveFonts[ct] == this) { + mState->mActiveFonts.removeAt(ct); + break; + } + } + + for (uint32_t i = 0; i < mCachedGlyphs.size(); i++) { + CachedGlyphInfo* glyph = mCachedGlyphs.valueAt(i); + delete glyph; + } +} + +void Font::invalidateTextureCache() { + for (uint32_t i = 0; i < mCachedGlyphs.size(); i++) { + mCachedGlyphs.valueAt(i)->mIsValid = false; + } +} + +void Font::measureCachedGlyph(CachedGlyphInfo *glyph, int x, int y, Rect *bounds) { + int nPenX = x + glyph->mBitmapLeft; + int nPenY = y + glyph->mBitmapTop; + + int width = (int) glyph->mBitmapWidth; + int height = (int) glyph->mBitmapHeight; + + if (bounds->bottom > nPenY) { + bounds->bottom = nPenY; + } + if (bounds->left > nPenX) { + bounds->left = nPenX; + } + if (bounds->right < nPenX + width) { + bounds->right = nPenX + width; + } + if (bounds->top < nPenY + height) { + bounds->top = nPenY + height; + } +} + +void Font::drawCachedGlyph(CachedGlyphInfo* glyph, int x, int y) { + int nPenX = x + glyph->mBitmapLeft; + int nPenY = y + glyph->mBitmapTop + glyph->mBitmapHeight; + + float u1 = glyph->mBitmapMinU; + float u2 = glyph->mBitmapMaxU; + float v1 = glyph->mBitmapMinV; + float v2 = glyph->mBitmapMaxV; + + int width = (int) glyph->mBitmapWidth; + int height = (int) glyph->mBitmapHeight; + + mState->appendMeshQuad(nPenX, nPenY, 0, u1, v2, + nPenX + width, nPenY, 0, u2, v2, + nPenX + width, nPenY - height, 0, u2, v1, + nPenX, nPenY - height, 0, u1, v1); +} + +void Font::drawCachedGlyph(CachedGlyphInfo* glyph, int x, int y, + uint8_t* bitmap, uint32_t bitmapW, uint32_t bitmapH) { + int nPenX = x + glyph->mBitmapLeft; + int nPenY = y + glyph->mBitmapTop; + + uint32_t endX = glyph->mStartX + glyph->mBitmapWidth; + uint32_t endY = glyph->mStartY + glyph->mBitmapHeight; + + uint32_t cacheWidth = mState->getCacheWidth(); + const uint8_t* cacheBuffer = mState->getTextTextureData(); + + uint32_t cacheX = 0, cacheY = 0; + int32_t bX = 0, bY = 0; + for (cacheX = glyph->mStartX, bX = nPenX; cacheX < endX; cacheX++, bX++) { + for (cacheY = glyph->mStartY, bY = nPenY; cacheY < endY; cacheY++, bY++) { + if (bX < 0 || bY < 0 || bX >= (int32_t) bitmapW || bY >= (int32_t) bitmapH) { + LOGE("Skipping invalid index"); + continue; + } + uint8_t tempCol = cacheBuffer[cacheY * cacheWidth + cacheX]; + bitmap[bY * bitmapW + bX] = tempCol; + } + } + +} + +Font::CachedGlyphInfo* Font::getCachedUTFChar(SkPaint* paint, int32_t utfChar) { + CachedGlyphInfo* cachedGlyph = NULL; + ssize_t index = mCachedGlyphs.indexOfKey(utfChar); + if (index >= 0) { + cachedGlyph = mCachedGlyphs.valueAt(index); + } else { + cachedGlyph = cacheGlyph(paint, utfChar); + } + + // Is the glyph still in texture cache? + if (!cachedGlyph->mIsValid) { + const SkGlyph& skiaGlyph = paint->getUnicharMetrics(utfChar); + updateGlyphCache(paint, skiaGlyph, cachedGlyph); + } + + return cachedGlyph; +} + +void Font::renderUTF(SkPaint* paint, const char* text, uint32_t start, uint32_t len, + int numGlyphs, int x, int y, uint8_t *bitmap, uint32_t bitmapW, uint32_t bitmapH) { + if (bitmap != NULL && bitmapW > 0 && bitmapH > 0) { + renderUTF(paint, text, start, len, numGlyphs, x, y, BITMAP, bitmap, + bitmapW, bitmapH, NULL); + } else { + renderUTF(paint, text, start, len, numGlyphs, x, y, FRAMEBUFFER, NULL, 0, 0, NULL); + } + +} + +void Font::measureUTF(SkPaint* paint, const char* text, uint32_t start, uint32_t len, + int numGlyphs, Rect *bounds) { + if (bounds == NULL) { + LOGE("No return rectangle provided to measure text"); + return; + } + bounds->set(1e6, -1e6, -1e6, 1e6); + renderUTF(paint, text, start, len, numGlyphs, 0, 0, MEASURE, NULL, 0, 0, bounds); +} + +#define SkAutoKern_AdjustF(prev, next) (((next) - (prev) + 32) >> 6 << 16) + +void Font::renderUTF(SkPaint* paint, const char* text, uint32_t start, uint32_t len, + int numGlyphs, int x, int y, RenderMode mode, uint8_t *bitmap, + uint32_t bitmapW, uint32_t bitmapH,Rect *bounds) { + if (numGlyphs == 0 || text == NULL || len == 0) { + return; + } + + SkFixed penX = SkIntToFixed(x); + int penY = y; + int glyphsLeft = 1; + if (numGlyphs > 0) { + glyphsLeft = numGlyphs; + } + + SkFixed prevRsbDelta = 0; + penX += SK_Fixed1 / 2; + + text += start; + + while (glyphsLeft > 0) { + int32_t utfChar = SkUTF16_NextUnichar((const uint16_t**) &text); + + // Reached the end of the string + if (utfChar < 0) { + break; + } + + CachedGlyphInfo* cachedGlyph = getCachedUTFChar(paint, utfChar); + penX += SkAutoKern_AdjustF(prevRsbDelta, cachedGlyph->mLsbDelta); + prevRsbDelta = cachedGlyph->mRsbDelta; + + // If it's still not valid, we couldn't cache it, so we shouldn't draw garbage + if (cachedGlyph->mIsValid) { + switch(mode) { + case FRAMEBUFFER: + drawCachedGlyph(cachedGlyph, SkFixedFloor(penX), penY); + break; + case BITMAP: + drawCachedGlyph(cachedGlyph, SkFixedFloor(penX), penY, bitmap, bitmapW, bitmapH); + break; + case MEASURE: + measureCachedGlyph(cachedGlyph, SkFixedFloor(penX), penY, bounds); + break; + } + } + + penX += cachedGlyph->mAdvanceX; + + // If we were given a specific number of glyphs, decrement + if (numGlyphs > 0) { + glyphsLeft--; + } + } +} + +void Font::updateGlyphCache(SkPaint* paint, const SkGlyph& skiaGlyph, CachedGlyphInfo* glyph) { + glyph->mAdvanceX = skiaGlyph.fAdvanceX; + glyph->mAdvanceY = skiaGlyph.fAdvanceY; + glyph->mBitmapLeft = skiaGlyph.fLeft; + glyph->mBitmapTop = skiaGlyph.fTop; + glyph->mLsbDelta = skiaGlyph.fLsbDelta; + glyph->mRsbDelta = skiaGlyph.fRsbDelta; + + uint32_t startX = 0; + uint32_t startY = 0; + + // Get the bitmap for the glyph + paint->findImage(skiaGlyph); + glyph->mIsValid = mState->cacheBitmap(skiaGlyph, &startX, &startY); + + if (!glyph->mIsValid) { + return; + } + + uint32_t endX = startX + skiaGlyph.fWidth; + uint32_t endY = startY + skiaGlyph.fHeight; + + glyph->mStartX = startX; + glyph->mStartY = startY; + glyph->mBitmapWidth = skiaGlyph.fWidth; + glyph->mBitmapHeight = skiaGlyph.fHeight; + + uint32_t cacheWidth = mState->getCacheWidth(); + uint32_t cacheHeight = mState->getCacheHeight(); + + glyph->mBitmapMinU = (float) startX / (float) cacheWidth; + glyph->mBitmapMinV = (float) startY / (float) cacheHeight; + glyph->mBitmapMaxU = (float) endX / (float) cacheWidth; + glyph->mBitmapMaxV = (float) endY / (float) cacheHeight; + + mState->mUploadTexture = true; +} + +Font::CachedGlyphInfo* Font::cacheGlyph(SkPaint* paint, int32_t glyph) { + CachedGlyphInfo* newGlyph = new CachedGlyphInfo(); + mCachedGlyphs.add(glyph, newGlyph); + + const SkGlyph& skiaGlyph = paint->getUnicharMetrics(glyph); + newGlyph->mGlyphIndex = skiaGlyph.fID; + newGlyph->mIsValid = false; + + updateGlyphCache(paint, skiaGlyph, newGlyph); + + return newGlyph; +} + +Font* Font::create(FontRenderer* state, uint32_t fontId, float fontSize) { + Vector &activeFonts = state->mActiveFonts; + + for (uint32_t i = 0; i < activeFonts.size(); i++) { + Font* font = activeFonts[i]; + if (font->mFontId == fontId && font->mFontSize == fontSize) { + return font; + } + } + + Font* newFont = new Font(state, fontId, fontSize); + activeFonts.push(newFont); + return newFont; +} + +/////////////////////////////////////////////////////////////////////////////// +// FontRenderer +/////////////////////////////////////////////////////////////////////////////// + +FontRenderer::FontRenderer() { + LOGD("Creating FontRenderer"); + + mGammaTable = NULL; + mInitialized = false; + mMaxNumberOfQuads = 1024; + mCurrentQuadIndex = 0; + mTextureId = 0; + + mTextMeshPtr = NULL; + mTextTexture = NULL; + + mIndexBufferID = 0; + + mCacheWidth = DEFAULT_TEXT_CACHE_WIDTH; + mCacheHeight = DEFAULT_TEXT_CACHE_HEIGHT; + + char property[PROPERTY_VALUE_MAX]; + if (property_get(PROPERTY_TEXT_CACHE_WIDTH, property, NULL) > 0) { + LOGD(" Setting text cache width to %s pixels", property); + mCacheWidth = atoi(property); + } else { + LOGD(" Using default text cache width of %i pixels", mCacheWidth); + } + + if (property_get(PROPERTY_TEXT_CACHE_HEIGHT, property, NULL) > 0) { + LOGD(" Setting text cache width to %s pixels", property); + mCacheHeight = atoi(property); + } else { + LOGD(" Using default text cache height of %i pixels", mCacheHeight); + } +} + +FontRenderer::~FontRenderer() { + for (uint32_t i = 0; i < mCacheLines.size(); i++) { + delete mCacheLines[i]; + } + mCacheLines.clear(); + + if (mInitialized) { + delete[] mTextMeshPtr; + delete[] mTextTexture; + } + + if (mTextureId) { + glDeleteTextures(1, &mTextureId); + } + + Vector fontsToDereference = mActiveFonts; + for (uint32_t i = 0; i < fontsToDereference.size(); i++) { + delete fontsToDereference[i]; + } +} + +void FontRenderer::flushAllAndInvalidate() { + if (mCurrentQuadIndex != 0) { + issueDrawCommand(); + mCurrentQuadIndex = 0; + } + for (uint32_t i = 0; i < mActiveFonts.size(); i++) { + mActiveFonts[i]->invalidateTextureCache(); + } + for (uint32_t i = 0; i < mCacheLines.size(); i++) { + mCacheLines[i]->mCurrentCol = 0; + } +} + +bool FontRenderer::cacheBitmap(const SkGlyph& glyph, uint32_t* retOriginX, uint32_t* retOriginY) { + // If the glyph is too tall, don't cache it + if (glyph.fHeight > mCacheLines[mCacheLines.size() - 1]->mMaxHeight) { + LOGE("Font size to large to fit in cache. width, height = %i, %i", + (int) glyph.fWidth, (int) glyph.fHeight); + return false; + } + + // Now copy the bitmap into the cache texture + uint32_t startX = 0; + uint32_t startY = 0; + + bool bitmapFit = false; + for (uint32_t i = 0; i < mCacheLines.size(); i++) { + bitmapFit = mCacheLines[i]->fitBitmap(glyph, &startX, &startY); + if (bitmapFit) { + break; + } + } + + // If the new glyph didn't fit, flush the state so far and invalidate everything + if (!bitmapFit) { + flushAllAndInvalidate(); + + // Try to fit it again + for (uint32_t i = 0; i < mCacheLines.size(); i++) { + bitmapFit = mCacheLines[i]->fitBitmap(glyph, &startX, &startY); + if (bitmapFit) { + break; + } + } + + // if we still don't fit, something is wrong and we shouldn't draw + if (!bitmapFit) { + LOGE("Bitmap doesn't fit in cache. width, height = %i, %i", + (int) glyph.fWidth, (int) glyph.fHeight); + return false; + } + } + + *retOriginX = startX; + *retOriginY = startY; + + uint32_t endX = startX + glyph.fWidth; + uint32_t endY = startY + glyph.fHeight; + + uint32_t cacheWidth = mCacheWidth; + + uint8_t* cacheBuffer = mTextTexture; + uint8_t* bitmapBuffer = (uint8_t*) glyph.fImage; + unsigned int stride = glyph.rowBytes(); + + uint32_t cacheX = 0, bX = 0, cacheY = 0, bY = 0; + for (cacheX = startX, bX = 0; cacheX < endX; cacheX++, bX++) { + for (cacheY = startY, bY = 0; cacheY < endY; cacheY++, bY++) { + uint8_t tempCol = bitmapBuffer[bY * stride + bX]; + cacheBuffer[cacheY * cacheWidth + cacheX] = mGammaTable[tempCol]; + } + } + + return true; +} + +void FontRenderer::initTextTexture() { + mTextTexture = new uint8_t[mCacheWidth * mCacheHeight]; + mUploadTexture = false; + + glGenTextures(1, &mTextureId); + glBindTexture(GL_TEXTURE_2D, mTextureId); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + // Initialize texture dimentions + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, mCacheWidth, mCacheHeight, 0, + GL_ALPHA, GL_UNSIGNED_BYTE, 0); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Split up our cache texture into lines of certain widths + int nextLine = 0; + mCacheLines.push(new CacheTextureLine(mCacheWidth, 16, nextLine, 0)); + nextLine += mCacheLines.top()->mMaxHeight; + mCacheLines.push(new CacheTextureLine(mCacheWidth, 24, nextLine, 0)); + nextLine += mCacheLines.top()->mMaxHeight; + mCacheLines.push(new CacheTextureLine(mCacheWidth, 24, nextLine, 0)); + nextLine += mCacheLines.top()->mMaxHeight; + mCacheLines.push(new CacheTextureLine(mCacheWidth, 32, nextLine, 0)); + nextLine += mCacheLines.top()->mMaxHeight; + mCacheLines.push(new CacheTextureLine(mCacheWidth, 32, nextLine, 0)); + nextLine += mCacheLines.top()->mMaxHeight; + mCacheLines.push(new CacheTextureLine(mCacheWidth, 40, nextLine, 0)); + nextLine += mCacheLines.top()->mMaxHeight; + mCacheLines.push(new CacheTextureLine(mCacheWidth, mCacheHeight - nextLine, nextLine, 0)); +} + +// Avoid having to reallocate memory and render quad by quad +void FontRenderer::initVertexArrayBuffers() { + uint32_t numIndicies = mMaxNumberOfQuads * 6; + uint32_t indexBufferSizeBytes = numIndicies * sizeof(uint16_t); + uint16_t* indexBufferData = (uint16_t*) malloc(indexBufferSizeBytes); + + // Four verts, two triangles , six indices per quad + for (uint32_t i = 0; i < mMaxNumberOfQuads; i++) { + int i6 = i * 6; + int i4 = i * 4; + + indexBufferData[i6 + 0] = i4 + 0; + indexBufferData[i6 + 1] = i4 + 1; + indexBufferData[i6 + 2] = i4 + 2; + + indexBufferData[i6 + 3] = i4 + 0; + indexBufferData[i6 + 4] = i4 + 2; + indexBufferData[i6 + 5] = i4 + 3; + } + + glGenBuffers(1, &mIndexBufferID); + glBindBuffer(GL_ARRAY_BUFFER, mIndexBufferID); + glBufferData(GL_ARRAY_BUFFER, indexBufferSizeBytes, indexBufferData, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + free(indexBufferData); + + uint32_t coordSize = 3; + uint32_t uvSize = 2; + uint32_t vertsPerQuad = 4; + uint32_t vertexBufferSize = mMaxNumberOfQuads * vertsPerQuad * coordSize * uvSize; + mTextMeshPtr = new float[vertexBufferSize]; +} + +// We don't want to allocate anything unless we actually draw text +void FontRenderer::checkInit() { + if (mInitialized) { + return; + } + + initTextTexture(); + initVertexArrayBuffers(); + + // We store a string with letters in a rough frequency of occurrence + mLatinPrecache = String16("eisarntolcdugpmhbyfvkwzxjq "); + mLatinPrecache += String16("EISARNTOLCDUGPMHBYFVKWZXJQ"); + mLatinPrecache += String16(",.?!()-+@;:`'"); + mLatinPrecache += String16("0123456789"); + + mInitialized = true; +} + +void FontRenderer::checkTextureUpdate() { + if (!mUploadTexture) { + return; + } + + glBindTexture(GL_TEXTURE_2D, mTextureId); + + // Iterate over all the cache lines and see which ones need to be updated + for (uint32_t i = 0; i < mCacheLines.size(); i++) { + CacheTextureLine* cl = mCacheLines[i]; + if(cl->mDirty) { + uint32_t xOffset = 0; + uint32_t yOffset = cl->mCurrentRow; + uint32_t width = mCacheWidth; + uint32_t height = cl->mMaxHeight; + void* textureData = mTextTexture + yOffset*width; + + glTexSubImage2D(GL_TEXTURE_2D, 0, xOffset, yOffset, width, height, + GL_ALPHA, GL_UNSIGNED_BYTE, textureData); + + cl->mDirty = false; + } + } + + mUploadTexture = false; +} + +void FontRenderer::issueDrawCommand() { + checkTextureUpdate(); + + float* vtx = mTextMeshPtr; + float* tex = vtx + 3; + + // position is slot 0 + uint32_t slot = 0; + glVertexAttribPointer(slot, 3, GL_FLOAT, false, 20, vtx); + + // texture0 is slot 1 + slot = 1; + glVertexAttribPointer(slot, 2, GL_FLOAT, false, 20, tex); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBufferID); + glDrawElements(GL_TRIANGLES, mCurrentQuadIndex * 6, GL_UNSIGNED_SHORT, NULL); +} + +void FontRenderer::appendMeshQuad(float x1, float y1, float z1, float u1, float v1, float x2, + float y2, float z2, float u2, float v2, float x3, float y3, float z3, float u3, float v3, + float x4, float y4, float z4, float u4, float v4) { + if (x1 > mClip->right || y1 < mClip->top || x2 < mClip->left || y4 > mClip->bottom) { + return; + } + + const uint32_t vertsPerQuad = 4; + const uint32_t floatsPerVert = 5; + float* currentPos = mTextMeshPtr + mCurrentQuadIndex * vertsPerQuad * floatsPerVert; + + (*currentPos++) = x1; + (*currentPos++) = y1; + (*currentPos++) = z1; + (*currentPos++) = u1; + (*currentPos++) = v1; + + (*currentPos++) = x2; + (*currentPos++) = y2; + (*currentPos++) = z2; + (*currentPos++) = u2; + (*currentPos++) = v2; + + (*currentPos++) = x3; + (*currentPos++) = y3; + (*currentPos++) = z3; + (*currentPos++) = u3; + (*currentPos++) = v3; + + (*currentPos++) = x4; + (*currentPos++) = y4; + (*currentPos++) = z4; + (*currentPos++) = u4; + (*currentPos++) = v4; + + mCurrentQuadIndex++; + + if (mCurrentQuadIndex == mMaxNumberOfQuads) { + issueDrawCommand(); + mCurrentQuadIndex = 0; + } +} + +uint32_t FontRenderer::getRemainingCacheCapacity() { + uint32_t remainingCapacity = 0; + float totalPixels = 0; + for(uint32_t i = 0; i < mCacheLines.size(); i ++) { + remainingCapacity += (mCacheLines[i]->mMaxWidth - mCacheLines[i]->mCurrentCol); + totalPixels += mCacheLines[i]->mMaxWidth; + } + remainingCapacity = (remainingCapacity * 100) / totalPixels; + return remainingCapacity; +} + +void FontRenderer::precacheLatin(SkPaint* paint) { + // Remaining capacity is measured in % + uint32_t remainingCapacity = getRemainingCacheCapacity(); + uint32_t precacheIdx = 0; + while(remainingCapacity > 25 && precacheIdx < mLatinPrecache.size()) { + mCurrentFont->getCachedUTFChar(paint, (int32_t)mLatinPrecache[precacheIdx]); + remainingCapacity = getRemainingCacheCapacity(); + precacheIdx ++; + } +} + +void FontRenderer::setFont(SkPaint* paint, uint32_t fontId, float fontSize) { + uint32_t currentNumFonts = mActiveFonts.size(); + mCurrentFont = Font::create(this, fontId, fontSize); + + const float maxPrecacheFontSize = 40.0f; + bool isNewFont = currentNumFonts != mActiveFonts.size(); + + if (isNewFont && fontSize <= maxPrecacheFontSize) { + precacheLatin(paint); + } +} +FontRenderer::DropShadow FontRenderer::renderDropShadow(SkPaint* paint, const char *text, + uint32_t startIndex, uint32_t len, int numGlyphs, uint32_t radius) { + checkInit(); + + if (!mCurrentFont) { + DropShadow image; + image.width = 0; + image.height = 0; + image.image = NULL; + image.penX = 0; + image.penY = 0; + return image; + } + + Rect bounds; + mCurrentFont->measureUTF(paint, text, startIndex, len, numGlyphs, &bounds); + uint32_t paddedWidth = (uint32_t) (bounds.right - bounds.left) + 2 * radius; + uint32_t paddedHeight = (uint32_t) (bounds.top - bounds.bottom) + 2 * radius; + uint8_t* dataBuffer = new uint8_t[paddedWidth * paddedHeight]; + for (uint32_t i = 0; i < paddedWidth * paddedHeight; i++) { + dataBuffer[i] = 0; + } + + int penX = radius - bounds.left; + int penY = radius - bounds.bottom; + + mCurrentFont->renderUTF(paint, text, startIndex, len, numGlyphs, penX, penY, + dataBuffer, paddedWidth, paddedHeight); + blurImage(dataBuffer, paddedWidth, paddedHeight, radius); + + DropShadow image; + image.width = paddedWidth; + image.height = paddedHeight; + image.image = dataBuffer; + image.penX = penX; + image.penY = penY; + return image; +} + +void FontRenderer::renderText(SkPaint* paint, const Rect* clip, const char *text, + uint32_t startIndex, uint32_t len, int numGlyphs, int x, int y) { + checkInit(); + + if (!mCurrentFont) { + LOGE("No font set"); + return; + } + + mClip = clip; + mCurrentFont->renderUTF(paint, text, startIndex, len, numGlyphs, x, y); + + if (mCurrentQuadIndex != 0) { + issueDrawCommand(); + mCurrentQuadIndex = 0; + } +} + +void FontRenderer::computeGaussianWeights(float* weights, int32_t radius) { + // Compute gaussian weights for the blur + // e is the euler's number + float e = 2.718281828459045f; + float pi = 3.1415926535897932f; + // g(x) = ( 1 / sqrt( 2 * pi ) * sigma) * e ^ ( -x^2 / 2 * sigma^2 ) + // x is of the form [-radius .. 0 .. radius] + // and sigma varies with radius. + // Based on some experimental radius values and sigma's + // we approximately fit sigma = f(radius) as + // sigma = radius * 0.3 + 0.6 + // The larger the radius gets, the more our gaussian blur + // will resemble a box blur since with large sigma + // the gaussian curve begins to lose its shape + float sigma = 0.3f * (float)radius + 0.6f; + + // Now compute the coefficints + // We will store some redundant values to save some math during + // the blur calculations + // precompute some values + float coeff1 = 1.0f / (sqrt( 2.0f * pi ) * sigma); + float coeff2 = - 1.0f / (2.0f * sigma * sigma); + + float normalizeFactor = 0.0f; + for(int32_t r = -radius; r <= radius; r ++) { + float floatR = (float)r; + weights[r + radius] = coeff1 * pow(e, floatR * floatR * coeff2); + normalizeFactor += weights[r + radius]; + } + + //Now we need to normalize the weights because all our coefficients need to add up to one + normalizeFactor = 1.0f / normalizeFactor; + for(int32_t r = -radius; r <= radius; r ++) { + weights[r + radius] *= normalizeFactor; + } +} + +void FontRenderer::horizontalBlur(float* weights, int32_t radius, + const uint8_t* source, uint8_t* dest, int32_t width, int32_t height) { + float blurredPixel = 0.0f; + float currentPixel = 0.0f; + + for(int32_t y = 0; y < height; y ++) { + + const uint8_t* input = source + y * width; + uint8_t* output = dest + y * width; + + for(int32_t x = 0; x < width; x ++) { + blurredPixel = 0.0f; + const float* gPtr = weights; + // Optimization for non-border pixels + if ((x > radius) && (x < (width - radius))) { + const uint8_t *i = input + (x - radius); + for(int r = -radius; r <= radius; r ++) { + currentPixel = (float)(*i); + blurredPixel += currentPixel * gPtr[0]; + gPtr++; + i++; + } + } else { + for(int32_t r = -radius; r <= radius; r ++) { + // Stepping left and right away from the pixel + int validW = x + r; + if(validW < 0) { + validW = 0; + } + if(validW > width - 1) { + validW = width - 1; + } + + currentPixel = (float)(input[validW]); + blurredPixel += currentPixel * gPtr[0]; + gPtr++; + } + } + *output = (uint8_t)blurredPixel; + output ++; + } + } +} + +void FontRenderer::verticalBlur(float* weights, int32_t radius, + const uint8_t* source, uint8_t* dest, int32_t width, int32_t height) { + float blurredPixel = 0.0f; + float currentPixel = 0.0f; + + for(int32_t y = 0; y < height; y ++) { + + uint8_t* output = dest + y * width; + + for(int32_t x = 0; x < width; x ++) { + blurredPixel = 0.0f; + const float* gPtr = weights; + const uint8_t* input = source + x; + // Optimization for non-border pixels + if ((y > radius) && (y < (height - radius))) { + const uint8_t *i = input + ((y - radius) * width); + for(int32_t r = -radius; r <= radius; r ++) { + currentPixel = (float)(*i); + blurredPixel += currentPixel * gPtr[0]; + gPtr++; + i += width; + } + } else { + for(int32_t r = -radius; r <= radius; r ++) { + int validH = y + r; + // Clamp to zero and width + if(validH < 0) { + validH = 0; + } + if(validH > height - 1) { + validH = height - 1; + } + + const uint8_t *i = input + validH * width; + currentPixel = (float)(*i); + blurredPixel += currentPixel * gPtr[0]; + gPtr++; + } + } + *output = (uint8_t)blurredPixel; + output ++; + } + } +} + + +void FontRenderer::blurImage(uint8_t *image, int32_t width, int32_t height, int32_t radius) { + float *gaussian = new float[2 * radius + 1]; + computeGaussianWeights(gaussian, radius); + uint8_t* scratch = new uint8_t[width * height]; + horizontalBlur(gaussian, radius, image, scratch, width, height); + verticalBlur(gaussian, radius, scratch, image, width, height); + delete[] gaussian; + delete[] scratch; +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/FontRenderer.h b/libs/hwui/FontRenderer.h new file mode 100644 index 0000000000000000000000000000000000000000..de5c019967db66d5b9382ed5870ac8e009dd3d99 --- /dev/null +++ b/libs/hwui/FontRenderer.h @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_FONT_RENDERER_H +#define ANDROID_UI_FONT_RENDERER_H + +#include +#include +#include +#include + +#include +#include + +#include + +#include "Rect.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +class FontRenderer; + +/** + * Represents a font, defined by a Skia font id and a font size. A font is used + * to generate glyphs and cache them in the FontState. + */ +class Font { +public: + ~Font(); + + /** + * Renders the specified string of text. + * If bitmap is specified, it will be used as the render target + */ + void renderUTF(SkPaint* paint, const char *text, uint32_t start, uint32_t len, + int numGlyphs, int x, int y, + uint8_t *bitmap = NULL, uint32_t bitmapW = 0, uint32_t bitmapH = 0); + /** + * Creates a new font associated with the specified font state. + */ + static Font* create(FontRenderer* state, uint32_t fontId, float fontSize); + +protected: + friend class FontRenderer; + + enum RenderMode { + FRAMEBUFFER, + BITMAP, + MEASURE, + }; + + void renderUTF(SkPaint* paint, const char *text, uint32_t start, uint32_t len, + int numGlyphs, int x, int y, RenderMode mode, + uint8_t *bitmap, uint32_t bitmapW, uint32_t bitmapH, + Rect *bounds); + + void measureUTF(SkPaint* paint, const char* text, uint32_t start, uint32_t len, + int numGlyphs, Rect *bounds); + + struct CachedGlyphInfo { + // Has the cache been invalidated? + bool mIsValid; + // Location of the cached glyph in the bitmap + // in case we need to resize the texture or + // render to bitmap + uint32_t mStartX; + uint32_t mStartY; + uint32_t mBitmapWidth; + uint32_t mBitmapHeight; + // Also cache texture coords for the quad + float mBitmapMinU; + float mBitmapMinV; + float mBitmapMaxU; + float mBitmapMaxV; + // Minimize how much we call freetype + uint32_t mGlyphIndex; + uint32_t mAdvanceX; + uint32_t mAdvanceY; + // Values below contain a glyph's origin in the bitmap + int32_t mBitmapLeft; + int32_t mBitmapTop; + // Auto-kerning + SkFixed mLsbDelta; + SkFixed mRsbDelta; + }; + + Font(FontRenderer* state, uint32_t fontId, float fontSize); + + DefaultKeyedVector mCachedGlyphs; + + void invalidateTextureCache(); + + CachedGlyphInfo* cacheGlyph(SkPaint* paint, int32_t glyph); + void updateGlyphCache(SkPaint* paint, const SkGlyph& skiaGlyph, CachedGlyphInfo *glyph); + void measureCachedGlyph(CachedGlyphInfo *glyph, int x, int y, Rect *bounds); + void drawCachedGlyph(CachedGlyphInfo *glyph, int x, int y); + void drawCachedGlyph(CachedGlyphInfo *glyph, int x, int y, + uint8_t *bitmap, uint32_t bitmapW, uint32_t bitmapH); + + CachedGlyphInfo* getCachedUTFChar(SkPaint* paint, int32_t utfChar); + + FontRenderer* mState; + uint32_t mFontId; + float mFontSize; +}; + +class FontRenderer { +public: + FontRenderer(); + ~FontRenderer(); + + void init(); + void deinit(); + + void setGammaTable(const uint8_t* gammaTable) { + mGammaTable = gammaTable; + } + + void setFont(SkPaint* paint, uint32_t fontId, float fontSize); + void renderText(SkPaint* paint, const Rect* clip, const char *text, uint32_t startIndex, + uint32_t len, int numGlyphs, int x, int y); + + struct DropShadow { + DropShadow() { }; + + DropShadow(const DropShadow& dropShadow): + width(dropShadow.width), height(dropShadow.height), + image(dropShadow.image), penX(dropShadow.penX), + penY(dropShadow.penY) { + } + + uint32_t width; + uint32_t height; + uint8_t* image; + int32_t penX; + int32_t penY; + }; + + // After renderDropShadow returns, the called owns the memory in DropShadow.image + // and is responsible for releasing it when it's done with it + DropShadow renderDropShadow(SkPaint* paint, const char *text, uint32_t startIndex, + uint32_t len, int numGlyphs, uint32_t radius); + + GLuint getTexture() { + checkInit(); + return mTextureId; + } + +protected: + friend class Font; + + const uint8_t* mGammaTable; + + struct CacheTextureLine { + uint16_t mMaxHeight; + uint16_t mMaxWidth; + uint32_t mCurrentRow; + uint32_t mCurrentCol; + bool mDirty; + + CacheTextureLine(uint16_t maxWidth, uint16_t maxHeight, uint32_t currentRow, + uint32_t currentCol): + mMaxHeight(maxHeight), + mMaxWidth(maxWidth), + mCurrentRow(currentRow), + mCurrentCol(currentCol), + mDirty(false) { + } + + bool fitBitmap(const SkGlyph& glyph, uint32_t *retOriginX, uint32_t *retOriginY) { + if (glyph.fHeight > mMaxHeight) { + return false; + } + + if (mCurrentCol + glyph.fWidth < mMaxWidth) { + *retOriginX = mCurrentCol; + *retOriginY = mCurrentRow; + mCurrentCol += glyph.fWidth; + mDirty = true; + return true; + } + + return false; + } + }; + + uint32_t getCacheWidth() const { + return mCacheWidth; + } + + uint32_t getCacheHeight() const { + return mCacheHeight; + } + + void initTextTexture(); + bool cacheBitmap(const SkGlyph& glyph, uint32_t *retOriginX, uint32_t *retOriginY); + + void flushAllAndInvalidate(); + void initVertexArrayBuffers(); + + void checkInit(); + + String16 mLatinPrecache; + void precacheLatin(SkPaint* paint); + + void issueDrawCommand(); + void appendMeshQuad(float x1, float y1, float z1, float u1, float v1, float x2, float y2, + float z2, float u2, float v2, float x3, float y3, float z3, float u3, float v3, + float x4, float y4, float z4, float u4, float v4); + + uint32_t mCacheWidth; + uint32_t mCacheHeight; + + Vector mCacheLines; + uint32_t getRemainingCacheCapacity(); + + Font* mCurrentFont; + Vector mActiveFonts; + + // Texture to cache glyph bitmaps + uint8_t* mTextTexture; + const uint8_t* getTextTextureData() const { + return mTextTexture; + } + GLuint mTextureId; + void checkTextureUpdate(); + bool mUploadTexture; + + // Pointer to vertex data to speed up frame to frame work + float *mTextMeshPtr; + uint32_t mCurrentQuadIndex; + uint32_t mMaxNumberOfQuads; + + uint32_t mIndexBufferID; + + const Rect* mClip; + + bool mInitialized; + + void computeGaussianWeights(float* weights, int32_t radius); + void horizontalBlur(float* weights, int32_t radius, const uint8_t *source, uint8_t *dest, + int32_t width, int32_t height); + void verticalBlur(float* weights, int32_t radius, const uint8_t *source, uint8_t *dest, + int32_t width, int32_t height); + void blurImage(uint8_t* image, int32_t width, int32_t height, int32_t radius); +}; + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_FONT_RENDERER_H diff --git a/libs/hwui/GammaFontRenderer.cpp b/libs/hwui/GammaFontRenderer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6d087e3baaae08d7a8fce0646930ebc604fc6dc9 --- /dev/null +++ b/libs/hwui/GammaFontRenderer.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include "GammaFontRenderer.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +GammaFontRenderer::GammaFontRenderer() { + LOGD("Creating gamma font renderer"); + + // Get the renderer properties + char property[PROPERTY_VALUE_MAX]; + + // Get the gamma + float gamma = DEFAULT_TEXT_GAMMA; + if (property_get(PROPERTY_TEXT_GAMMA, property, NULL) > 0) { + LOGD(" Setting text gamma to %s", property); + gamma = atof(property); + } else { + LOGD(" Using default text gamma of %.2f", DEFAULT_TEXT_GAMMA); + } + + // Get the black gamma threshold + mBlackThreshold = DEFAULT_TEXT_BLACK_GAMMA_THRESHOLD; + if (property_get(PROPERTY_TEXT_BLACK_GAMMA_THRESHOLD, property, NULL) > 0) { + LOGD(" Setting text black gamma threshold to %s", property); + mBlackThreshold = atoi(property); + } else { + LOGD(" Using default text black gamma threshold of %d", + DEFAULT_TEXT_BLACK_GAMMA_THRESHOLD); + } + + // Get the white gamma threshold + mWhiteThreshold = DEFAULT_TEXT_WHITE_GAMMA_THRESHOLD; + if (property_get(PROPERTY_TEXT_WHITE_GAMMA_THRESHOLD, property, NULL) > 0) { + LOGD(" Setting text white gamma threshold to %s", property); + mWhiteThreshold = atoi(property); + } else { + LOGD(" Using default white black gamma threshold of %d", + DEFAULT_TEXT_WHITE_GAMMA_THRESHOLD); + } + + // Compute the gamma tables + const float blackGamma = gamma; + const float whiteGamma = 1.0f / gamma; + + for (uint32_t i = 0; i <= 255; i++) { + mDefault[i] = i; + + const float v = i / 255.0f; + const float black = pow(v, blackGamma); + const float white = pow(v, whiteGamma); + + mBlackGamma[i] = uint8_t((float)::floor(black * 255.0f + 0.5f)); + mWhiteGamma[i] = uint8_t((float)::floor(white * 255.0f + 0.5f)); + } + + // Configure the font renderers + mDefaultRenderer.setGammaTable(&mDefault[0]); + mBlackGammaRenderer.setGammaTable(&mBlackGamma[0]); + mWhiteGammaRenderer.setGammaTable(&mWhiteGamma[0]); +} + +FontRenderer& GammaFontRenderer::getFontRenderer(const SkPaint* paint) { + if (paint->getShader() == NULL) { + uint32_t c = paint->getColor(); + const int r = (c >> 16) & 0xFF; + const int g = (c >> 8) & 0xFF; + const int b = (c ) & 0xFF; + const int luminance = (r * 2 + g * 5 + b) >> 3; + + if (luminance <= mBlackThreshold) { + return mBlackGammaRenderer; + } else if (luminance >= mWhiteThreshold) { + return mWhiteGammaRenderer; + } + } + return mDefaultRenderer; +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/GammaFontRenderer.h b/libs/hwui/GammaFontRenderer.h new file mode 100644 index 0000000000000000000000000000000000000000..5fa45cf67e9dc3ad71266bad1e74a499441b685d --- /dev/null +++ b/libs/hwui/GammaFontRenderer.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_GAMMA_FONT_RENDERER_H +#define ANDROID_UI_GAMMA_FONT_RENDERER_H + +#include + +#include "FontRenderer.h" + +namespace android { +namespace uirenderer { + +struct GammaFontRenderer { + GammaFontRenderer(); + + FontRenderer& getFontRenderer(const SkPaint* paint); + +private: + FontRenderer mDefaultRenderer; + FontRenderer mBlackGammaRenderer; + FontRenderer mWhiteGammaRenderer; + + int mBlackThreshold; + int mWhiteThreshold; + + uint8_t mDefault[256]; + uint8_t mBlackGamma[256]; + uint8_t mWhiteGamma[256]; +}; + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_GAMMA_FONT_RENDERER_H diff --git a/libs/hwui/GenerationCache.h b/libs/hwui/GenerationCache.h new file mode 100644 index 0000000000000000000000000000000000000000..070e33fb46214723d3779df4f36eaf7b5ec5392f --- /dev/null +++ b/libs/hwui/GenerationCache.h @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_GENERATION_CACHE_H +#define ANDROID_UI_GENERATION_CACHE_H + +#include +#include + +namespace android { +namespace uirenderer { + +template +class OnEntryRemoved { +public: + virtual ~OnEntryRemoved() { }; + virtual void operator()(EntryKey& key, EntryValue& value) = 0; +}; // class OnEntryRemoved + +template +struct Entry: public LightRefBase > { + Entry() { } + Entry(const Entry& e): + key(e.key), value(e.value), parent(e.parent), child(e.child) { } + Entry(sp > e): + key(e->key), value(e->value), parent(e->parent), child(e->child) { } + + EntryKey key; + EntryValue value; + + sp > parent; + sp > child; +}; // struct Entry + +template +class GenerationCache { +public: + GenerationCache(uint32_t maxCapacity); + virtual ~GenerationCache(); + + enum Capacity { + kUnlimitedCapacity, + }; + + void setOnEntryRemovedListener(OnEntryRemoved* listener); + + void clear(); + + bool contains(K key) const; + V get(K key); + K getKeyAt(uint32_t index) const; + void put(K key, V value); + V remove(K key); + V removeOldest(); + + uint32_t size() const; + + void addToCache(sp > entry, K key, V value); + void attachToCache(sp > entry); + void detachFromCache(sp > entry); + + V removeAt(ssize_t index); + + KeyedVector > > mCache; + uint32_t mMaxCapacity; + + OnEntryRemoved* mListener; + + sp > mOldest; + sp > mYoungest; +}; // class GenerationCache + +template +GenerationCache::GenerationCache(uint32_t maxCapacity): mMaxCapacity(maxCapacity), mListener(NULL) { +}; + +template +GenerationCache::~GenerationCache() { + clear(); +}; + +template +uint32_t GenerationCache::size() const { + return mCache.size(); +} + +template +void GenerationCache::setOnEntryRemovedListener(OnEntryRemoved* listener) { + mListener = listener; +} + +template +void GenerationCache::clear() { + if (mListener) { + for (uint32_t i = 0; i < mCache.size(); i++) { + sp > entry = mCache.valueAt(i); + if (mListener) { + (*mListener)(entry->key, entry->value); + } + } + } + mCache.clear(); + mYoungest.clear(); + mOldest.clear(); +} + +template +bool GenerationCache::contains(K key) const { + return mCache.indexOfKey(key) >= 0; +} + +template +K GenerationCache::getKeyAt(uint32_t index) const { + return mCache.keyAt(index); +} + +template +V GenerationCache::get(K key) { + ssize_t index = mCache.indexOfKey(key); + if (index >= 0) { + sp > entry = mCache.valueAt(index); + if (entry.get()) { + detachFromCache(entry); + attachToCache(entry); + return entry->value; + } + } + + return NULL; +} + +template +void GenerationCache::put(K key, V value) { + if (mMaxCapacity != kUnlimitedCapacity && mCache.size() >= mMaxCapacity) { + removeOldest(); + } + + ssize_t index = mCache.indexOfKey(key); + if (index < 0) { + sp > entry = new Entry; + addToCache(entry, key, value); + } +} + +template +void GenerationCache::addToCache(sp > entry, K key, V value) { + entry->key = key; + entry->value = value; + mCache.add(key, entry); + attachToCache(entry); +} + +template +V GenerationCache::remove(K key) { + ssize_t index = mCache.indexOfKey(key); + if (index >= 0) { + return removeAt(index); + } + + return NULL; +} + +template +V GenerationCache::removeAt(ssize_t index) { + sp > entry = mCache.valueAt(index); + if (mListener) { + (*mListener)(entry->key, entry->value); + } + mCache.removeItemsAt(index, 1); + detachFromCache(entry); + + return entry->value; +} + +template +V GenerationCache::removeOldest() { + if (mOldest.get()) { + ssize_t index = mCache.indexOfKey(mOldest->key); + if (index >= 0) { + return removeAt(index); + } + } + + return NULL; +} + +template +void GenerationCache::attachToCache(sp > entry) { + if (!mYoungest.get()) { + mYoungest = mOldest = entry; + } else { + entry->parent = mYoungest; + mYoungest->child = entry; + mYoungest = entry; + } +} + +template +void GenerationCache::detachFromCache(sp > entry) { + if (entry->parent.get()) { + entry->parent->child = entry->child; + } + + if (entry->child.get()) { + entry->child->parent = entry->parent; + } + + if (mOldest == entry) { + mOldest = entry->child; + } + + if (mYoungest == entry) { + mYoungest = entry->parent; + } + + entry->parent.clear(); + entry->child.clear(); +} + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_GENERATION_CACHE_H diff --git a/libs/hwui/GradientCache.cpp b/libs/hwui/GradientCache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..97f4cb468c535b3771f3e37003a4fd4a8f6be513 --- /dev/null +++ b/libs/hwui/GradientCache.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include +#include + +#include + +#include "GradientCache.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +GradientCache::GradientCache(): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(MB(DEFAULT_GRADIENT_CACHE_SIZE)) { + char property[PROPERTY_VALUE_MAX]; + if (property_get(PROPERTY_GRADIENT_CACHE_SIZE, property, NULL) > 0) { + LOGD(" Setting gradient cache size to %sMB", property); + setMaxSize(MB(atof(property))); + } else { + LOGD(" Using default gradient cache size of %.2fMB", DEFAULT_GRADIENT_CACHE_SIZE); + } + + mCache.setOnEntryRemovedListener(this); +} + +GradientCache::GradientCache(uint32_t maxByteSize): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(maxByteSize) { + mCache.setOnEntryRemovedListener(this); +} + +GradientCache::~GradientCache() { + Mutex::Autolock _l(mLock); + mCache.clear(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Size management +/////////////////////////////////////////////////////////////////////////////// + +uint32_t GradientCache::getSize() { + Mutex::Autolock _l(mLock); + return mSize; +} + +uint32_t GradientCache::getMaxSize() { + Mutex::Autolock _l(mLock); + return mMaxSize; +} + +void GradientCache::setMaxSize(uint32_t maxSize) { + Mutex::Autolock _l(mLock); + mMaxSize = maxSize; + while (mSize > mMaxSize) { + mCache.removeOldest(); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Callbacks +/////////////////////////////////////////////////////////////////////////////// + +void GradientCache::operator()(SkShader*& shader, Texture*& texture) { + // Already locked here + if (shader) { + const uint32_t size = texture->width * texture->height * 4; + mSize -= size; + } + + if (texture) { + glDeleteTextures(1, &texture->id); + delete texture; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Caching +/////////////////////////////////////////////////////////////////////////////// + +Texture* GradientCache::get(SkShader* shader) { + Mutex::Autolock _l(mLock); + return mCache.get(shader); +} + +void GradientCache::remove(SkShader* shader) { + Mutex::Autolock _l(mLock); + mCache.remove(shader); +} + +void GradientCache::clear() { + Mutex::Autolock _l(mLock); + mCache.clear(); +} + +Texture* GradientCache::addLinearGradient(SkShader* shader, uint32_t* colors, + float* positions, int count, SkShader::TileMode tileMode) { + SkBitmap bitmap; + bitmap.setConfig(SkBitmap::kARGB_8888_Config, 1024, 1); + bitmap.allocPixels(); + bitmap.eraseColor(0); + + SkCanvas canvas(bitmap); + + SkPoint points[2]; + points[0].set(0.0f, 0.0f); + points[1].set(bitmap.width(), 0.0f); + + SkShader* localShader = SkGradientShader::CreateLinear(points, + reinterpret_cast(colors), positions, count, tileMode); + + SkPaint p; + p.setStyle(SkPaint::kStrokeAndFill_Style); + p.setShader(localShader)->unref(); + + canvas.drawRectCoords(0.0f, 0.0f, bitmap.width(), 1.0f, p); + + mLock.lock(); + // Asume the cache is always big enough + const uint32_t size = bitmap.rowBytes() * bitmap.height(); + while (mSize + size > mMaxSize) { + mCache.removeOldest(); + } + mLock.unlock(); + + Texture* texture = new Texture; + generateTexture(&bitmap, texture); + + mLock.lock(); + mSize += size; + mCache.put(shader, texture); + mLock.unlock(); + + return texture; +} + +void GradientCache::generateTexture(SkBitmap* bitmap, Texture* texture) { + SkAutoLockPixels autoLock(*bitmap); + if (!bitmap->readyToDraw()) { + LOGE("Cannot generate texture from shader"); + return; + } + + texture->generation = bitmap->getGenerationID(); + texture->width = bitmap->width(); + texture->height = bitmap->height(); + + glGenTextures(1, &texture->id); + + glBindTexture(GL_TEXTURE_2D, texture->id); + glPixelStorei(GL_UNPACK_ALIGNMENT, bitmap->bytesPerPixel()); + + texture->blend = !bitmap->isOpaque(); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bitmap->rowBytesAsPixels(), texture->height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, bitmap->getPixels()); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/GradientCache.h b/libs/hwui/GradientCache.h new file mode 100644 index 0000000000000000000000000000000000000000..c829fd468007ff8796c0a50bd04d2d0d9283d02b --- /dev/null +++ b/libs/hwui/GradientCache.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_GRADIENT_CACHE_H +#define ANDROID_UI_GRADIENT_CACHE_H + +#include + +#include "Texture.h" +#include "GenerationCache.h" + +namespace android { +namespace uirenderer { + +/** + * A simple LRU gradient cache. The cache has a maximum size expressed in bytes. + * Any texture added to the cache causing the cache to grow beyond the maximum + * allowed size will also cause the oldest texture to be kicked out. + */ +class GradientCache: public OnEntryRemoved { +public: + GradientCache(); + GradientCache(uint32_t maxByteSize); + ~GradientCache(); + + /** + * Used as a callback when an entry is removed from the cache. + * Do not invoke directly. + */ + void operator()(SkShader*& shader, Texture*& texture); + + /** + * Adds a new linear gradient to the cache. The generated texture is + * returned. + */ + Texture* addLinearGradient(SkShader* shader, uint32_t* colors, float* positions, + int count, SkShader::TileMode tileMode = SkShader::kClamp_TileMode); + /** + * Returns the texture associated with the specified shader. + */ + Texture* get(SkShader* shader); + /** + * Removes the texture associated with the specified shader. Returns NULL + * if the texture cannot be found. Upon remove the texture is freed. + */ + void remove(SkShader* shader); + /** + * Clears the cache. This causes all textures to be deleted. + */ + void clear(); + + /** + * Sets the maximum size of the cache in bytes. + */ + void setMaxSize(uint32_t maxSize); + /** + * Returns the maximum size of the cache in bytes. + */ + uint32_t getMaxSize(); + /** + * Returns the current size of the cache in bytes. + */ + uint32_t getSize(); + +private: + void generateTexture(SkBitmap* bitmap, Texture* texture); + + GenerationCache mCache; + + uint32_t mSize; + uint32_t mMaxSize; + + /** + * Used to access mCache and mSize. All methods are accessed from a single + * thread except for remove(). + */ + mutable Mutex mLock; +}; // class GradientCache + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_GRADIENT_CACHE_H diff --git a/libs/hwui/Layer.h b/libs/hwui/Layer.h new file mode 100644 index 0000000000000000000000000000000000000000..c527038a41d3df9a4e39fd73d1fd803b05ef1c62 --- /dev/null +++ b/libs/hwui/Layer.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_LAYER_H +#define ANDROID_UI_LAYER_H + +#include + +#include + +#include + +#include "Rect.h" + +namespace android { +namespace uirenderer { + +/** + * Dimensions of a layer. + */ +struct LayerSize { + LayerSize(): width(0), height(0), id(0) { } + LayerSize(const uint32_t width, const uint32_t height): width(width), height(height), id(0) { } + LayerSize(const LayerSize& size): width(size.width), height(size.height), id(size.id) { } + + uint32_t width; + uint32_t height; + + // Incremental id used by the layer cache to store multiple + // LayerSize with the same dimensions + uint32_t id; + + bool operator<(const LayerSize& rhs) const { + if (id != 0 && rhs.id != 0) { + return id < rhs.id; + } + if (width == rhs.width) { + return height < rhs.height; + } + return width < rhs.width; + } + + bool operator==(const LayerSize& rhs) const { + return width == rhs.width && height == rhs.height; + } +}; // struct LayerSize + +/** + * A layer has dimensions and is backed by an OpenGL texture. + */ +struct Layer { + /** + * Coordinates of the layer. + */ + Rect layer; + /** + * Name of the texture used to render the layer. + */ + GLuint texture; + /** + * Opacity of the layer. + */ + int alpha; + /** + * Blending mode of the layer. + */ + SkXfermode::Mode mode; + /** + * Indicates whether this layer should be blended. + */ + bool blend; +}; // struct Layer + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_LAYER_H diff --git a/libs/hwui/LayerCache.cpp b/libs/hwui/LayerCache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..39c311115167b61c595d0d84b35e22a44e78e711 --- /dev/null +++ b/libs/hwui/LayerCache.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include + +#include "LayerCache.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +LayerCache::LayerCache(): + mCache(GenerationCache::kUnlimitedCapacity), + mIdGenerator(1), mSize(0), mMaxSize(MB(DEFAULT_LAYER_CACHE_SIZE)) { + char property[PROPERTY_VALUE_MAX]; + if (property_get(PROPERTY_LAYER_CACHE_SIZE, property, NULL) > 0) { + LOGD(" Setting layer cache size to %sMB", property); + setMaxSize(MB(atof(property))); + } else { + LOGD(" Using default layer cache size of %.2fMB", DEFAULT_LAYER_CACHE_SIZE); + } +} + +LayerCache::LayerCache(uint32_t maxByteSize): + mCache(GenerationCache::kUnlimitedCapacity), + mIdGenerator(1), mSize(0), mMaxSize(maxByteSize) { +} + +LayerCache::~LayerCache() { + clear(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Size management +/////////////////////////////////////////////////////////////////////////////// + +uint32_t LayerCache::getSize() { + return mSize; +} + +uint32_t LayerCache::getMaxSize() { + return mMaxSize; +} + +void LayerCache::setMaxSize(uint32_t maxSize) { + mMaxSize = maxSize; + while (mSize > mMaxSize) { + Layer* oldest = mCache.removeOldest(); + deleteLayer(oldest); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Callbacks +/////////////////////////////////////////////////////////////////////////////// + +void LayerCache::operator()(LayerSize& size, Layer*& layer) { + deleteLayer(layer); +} + +/////////////////////////////////////////////////////////////////////////////// +// Caching +/////////////////////////////////////////////////////////////////////////////// + +void LayerCache::deleteLayer(Layer* layer) { + if (layer) { + mSize -= layer->layer.getWidth() * layer->layer.getHeight() * 4; + + glDeleteTextures(1, &layer->texture); + delete layer; + } +} + +void LayerCache::clear() { + mCache.setOnEntryRemovedListener(this); + mCache.clear(); + mCache.setOnEntryRemovedListener(NULL); +} + +Layer* LayerCache::get(LayerSize& size) { + Layer* layer = mCache.remove(size); + if (layer) { + LAYER_LOGD("Reusing layer"); + + mSize -= layer->layer.getWidth() * layer->layer.getHeight() * 4; + } else { + LAYER_LOGD("Creating new layer"); + + layer = new Layer; + layer->blend = true; + + glGenTextures(1, &layer->texture); + glBindTexture(GL_TEXTURE_2D, layer->texture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.width, size.height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, NULL); + } + + return layer; +} + +bool LayerCache::put(LayerSize& layerSize, Layer* layer) { + const uint32_t size = layerSize.width * layerSize.height * 4; + // Don't even try to cache a layer that's bigger than the cache + if (size < mMaxSize) { + while (mSize + size > mMaxSize) { + Layer* oldest = mCache.removeOldest(); + deleteLayer(oldest); + } + + layerSize.id = mIdGenerator++; + mCache.put(layerSize, layer); + mSize += size; + + return true; + } + return false; +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/LayerCache.h b/libs/hwui/LayerCache.h new file mode 100644 index 0000000000000000000000000000000000000000..c0c7542ae001e9b9ff569499d4891d9620b4f040 --- /dev/null +++ b/libs/hwui/LayerCache.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_LAYER_CACHE_H +#define ANDROID_UI_LAYER_CACHE_H + +#include "Layer.h" +#include "GenerationCache.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Defines +/////////////////////////////////////////////////////////////////////////////// + +// Debug +#define DEBUG_LAYERS 0 + +// Debug +#if DEBUG_LAYERS + #define LAYER_LOGD(...) LOGD(__VA_ARGS__) +#else + #define LAYER_LOGD(...) +#endif + +/////////////////////////////////////////////////////////////////////////////// +// Cache +/////////////////////////////////////////////////////////////////////////////// + +class LayerCache: public OnEntryRemoved { +public: + LayerCache(); + LayerCache(uint32_t maxByteSize); + ~LayerCache(); + + /** + * Used as a callback when an entry is removed from the cache. + * Do not invoke directly. + */ + void operator()(LayerSize& size, Layer*& layer); + + /** + * Returns the layer of specified dimensions. If not suitable layer + * can be found, a new one is created and returned. If creating a new + * layer fails, NULL is returned. + * + * When a layer is obtained from the cache, it is removed and the total + * size of the cache goes down. + * + * @param size The dimensions of the desired layer + */ + Layer* get(LayerSize& size); + /** + * Adds the layer to the cache. The layer will not be added if there is + * not enough space available. + * + * @param size The dimensions of the layer + * @param layer The layer to add to the cache + * + * @return True if the layer was added, false otherwise. + */ + bool put(LayerSize& size, Layer* layer); + /** + * Clears the cache. This causes all layers to be deleted. + */ + void clear(); + + /** + * Sets the maximum size of the cache in bytes. + */ + void setMaxSize(uint32_t maxSize); + /** + * Returns the maximum size of the cache in bytes. + */ + uint32_t getMaxSize(); + /** + * Returns the current size of the cache in bytes. + */ + uint32_t getSize(); + +private: + void deleteLayer(Layer* layer); + + GenerationCache mCache; + uint32_t mIdGenerator; + + uint32_t mSize; + uint32_t mMaxSize; +}; // class LayerCache + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_LAYER_CACHE_H diff --git a/libs/hwui/Line.h b/libs/hwui/Line.h new file mode 100644 index 0000000000000000000000000000000000000000..64bdd6afb0606bc68b14f68e436381c2281459d6 --- /dev/null +++ b/libs/hwui/Line.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_LINE_H +#define ANDROID_UI_LINE_H + +#include + +#include + +#include + +#include "Patch.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Globals +/////////////////////////////////////////////////////////////////////////////// + +// Alpha8 texture used to perform texture anti-aliasing +static const uint8_t gLineTexture[] = { + 0, 0, 0, 0, 0, + 0, 255, 255, 255, 0, + 0, 255, 255, 255, 0, + 0, 255, 255, 255, 0, + 0, 0, 0, 0, 0 +}; +static const GLsizei gLineTextureWidth = 5; +static const GLsizei gLineTextureHeight = 5; +static const float gLineAABias = 1.0f; + +/////////////////////////////////////////////////////////////////////////////// +// Line +/////////////////////////////////////////////////////////////////////////////// + +class Line { +public: + Line(): mXDivsCount(2), mYDivsCount(2) { + mPatch = new Patch(mXDivsCount, mYDivsCount); + mXDivs = new int32_t[mXDivsCount]; + mYDivs = new int32_t[mYDivsCount]; + + mXDivs[0] = mYDivs[0] = 2; + mXDivs[1] = mYDivs[1] = 3; + + glGenTextures(1, &mTexture); + glBindTexture(GL_TEXTURE_2D, mTexture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, gLineTextureWidth, gLineTextureHeight, 0, + GL_ALPHA, GL_UNSIGNED_BYTE, gLineTexture); + } + + ~Line() { + delete mPatch; + delete[] mXDivs; + delete[] mYDivs; + + glDeleteTextures(1, &mTexture); + } + + inline float getLength(float x1, float y1, float x2, float y2) { + return sqrtf((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + + void update(float x1, float y1, float x2, float y2, float lineWidth, float& tx, float& ty) { + const float length = getLength(x1, y1, x2, y2); + const float half = lineWidth * 0.5f; + + mPatch->updateVertices(gLineTextureWidth, gLineTextureHeight, + -gLineAABias, -half - gLineAABias, length + gLineAABias, half + gLineAABias, + mXDivs, mYDivs, mXDivsCount, mYDivsCount); + + tx = -gLineAABias; + ty = lineWidth <= 1.0f ? -gLineAABias : -half - gLineAABias; + } + + inline GLvoid* getVertices() const { + return &mPatch->vertices[0].position[0]; + } + + inline GLvoid* getTexCoords() const { + return &mPatch->vertices[0].texture[0]; + } + + inline GLsizei getElementsCount() const { + return mPatch->verticesCount; + } + + inline GLuint getTexture() const { + return mTexture; + } + +private: + uint32_t mXDivsCount; + uint32_t mYDivsCount; + + int32_t* mXDivs; + int32_t* mYDivs; + + Patch* mPatch; + + GLuint mTexture; +}; // class Line + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_LINE_H diff --git a/libs/hwui/MODULE_LICENSE_APACHE2 b/libs/hwui/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/libs/hwui/Matrix.cpp b/libs/hwui/Matrix.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c698b5abb9c55c53a942b05321408a3003f9973f --- /dev/null +++ b/libs/hwui/Matrix.cpp @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include +#include +#include + +#include + +#include + +#include "Matrix.h" + +namespace android { +namespace uirenderer { + +void Matrix4::loadIdentity() { + data[kScaleX] = 1.0f; + data[kSkewY] = 0.0f; + data[2] = 0.0f; + data[kPerspective0] = 0.0f; + + data[kSkewX] = 0.0f; + data[kScaleY] = 1.0f; + data[6] = 0.0f; + data[kPerspective1] = 0.0f; + + data[8] = 0.0f; + data[9] = 0.0f; + data[kScaleZ] = 1.0f; + data[11] = 0.0f; + + data[kTranslateX] = 0.0f; + data[kTranslateY] = 0.0f; + data[kTranslateZ] = 0.0f; + data[kPerspective2] = 1.0f; + + mSimpleMatrix = true; +} + +void Matrix4::load(const float* v) { + memcpy(data, v, sizeof(data)); + mSimpleMatrix = false; +} + +void Matrix4::load(const Matrix4& v) { + memcpy(data, v.data, sizeof(data)); + mSimpleMatrix = v.mSimpleMatrix; +} + +void Matrix4::load(const SkMatrix& v) { + memset(data, 0, sizeof(data)); + + data[kScaleX] = v[SkMatrix::kMScaleX]; + data[kSkewX] = v[SkMatrix::kMSkewX]; + data[kTranslateX] = v[SkMatrix::kMTransX]; + + data[kSkewY] = v[SkMatrix::kMSkewY]; + data[kScaleY] = v[SkMatrix::kMScaleY]; + data[kTranslateY] = v[SkMatrix::kMTransY]; + + data[kPerspective0] = v[SkMatrix::kMPersp0]; + data[kPerspective1] = v[SkMatrix::kMPersp1]; + data[kPerspective2] = v[SkMatrix::kMPersp2]; + + data[kScaleZ] = 1.0f; + + mSimpleMatrix = (v.getType() <= SkMatrix::kScale_Mask); +} + +void Matrix4::copyTo(SkMatrix& v) const { + v.reset(); + + v.set(SkMatrix::kMScaleX, data[kScaleX]); + v.set(SkMatrix::kMSkewX, data[kSkewX]); + v.set(SkMatrix::kMTransX, data[kTranslateX]); + + v.set(SkMatrix::kMSkewY, data[kSkewY]); + v.set(SkMatrix::kMScaleY, data[kScaleY]); + v.set(SkMatrix::kMTransY, data[kTranslateY]); + + v.set(SkMatrix::kMPersp0, data[kPerspective0]); + v.set(SkMatrix::kMPersp1, data[kPerspective1]); + v.set(SkMatrix::kMPersp2, data[kPerspective2]); +} + +void Matrix4::loadInverse(const Matrix4& v) { + double scale = 1.0 / + (v.data[kScaleX] * ((double) v.data[kScaleY] * v.data[kPerspective2] - + (double) v.data[kTranslateY] * v.data[kPerspective1]) + + v.data[kSkewX] * ((double) v.data[kTranslateY] * v.data[kPerspective0] - + (double) v.data[kSkewY] * v.data[kPerspective2]) + + v.data[kTranslateX] * ((double) v.data[kSkewY] * v.data[kPerspective1] - + (double) v.data[kScaleY] * v.data[kPerspective0])); + + data[kScaleX] = (v.data[kScaleY] * v.data[kPerspective2] - + v.data[kTranslateY] * v.data[kPerspective1]) * scale; + data[kSkewX] = (v.data[kTranslateX] * v.data[kPerspective1] - + v.data[kSkewX] * v.data[kPerspective2]) * scale; + data[kTranslateX] = (v.data[kSkewX] * v.data[kTranslateY] - + v.data[kTranslateX] * v.data[kScaleY]) * scale; + + data[kSkewY] = (v.data[kTranslateY] * v.data[kPerspective0] - + v.data[kSkewY] * v.data[kPerspective2]) * scale; + data[kScaleY] = (v.data[kScaleX] * v.data[kPerspective2] - + v.data[kTranslateX] * v.data[kPerspective0]) * scale; + data[kTranslateY] = (v.data[kTranslateX] * v.data[kSkewY] - + v.data[kScaleX] * v.data[kTranslateY]) * scale; + + data[kPerspective0] = (v.data[kSkewY] * v.data[kPerspective1] - + v.data[kScaleY] * v.data[kPerspective0]) * scale; + data[kPerspective1] = (v.data[kSkewX] * v.data[kPerspective0] - + v.data[kScaleX] * v.data[kPerspective1]) * scale; + data[kPerspective2] = (v.data[kScaleX] * v.data[kScaleY] - + v.data[kSkewX] * v.data[kSkewY]) * scale; + + mSimpleMatrix = v.mSimpleMatrix; +} + +void Matrix4::copyTo(float* v) const { + memcpy(v, data, sizeof(data)); +} + +float Matrix4::getTranslateX() { + return data[kTranslateX]; +} + +float Matrix4::getTranslateY() { + return data[kTranslateY]; +} + +void Matrix4::multiply(float v) { + for (int i = 0; i < 16; i++) { + data[i] *= v; + } +} + +void Matrix4::loadTranslate(float x, float y, float z) { + loadIdentity(); + data[kTranslateX] = x; + data[kTranslateY] = y; + data[kTranslateZ] = z; +} + +void Matrix4::loadScale(float sx, float sy, float sz) { + loadIdentity(); + data[kScaleX] = sx; + data[kScaleY] = sy; + data[kScaleZ] = sz; +} + +void Matrix4::loadRotate(float angle, float x, float y, float z) { + data[kPerspective0] = 0.0f; + data[kPerspective1] = 0.0f; + data[11] = 0.0f; + data[kTranslateX] = 0.0f; + data[kTranslateY] = 0.0f; + data[kTranslateZ] = 0.0f; + data[kPerspective2] = 1.0f; + + angle *= float(M_PI / 180.0f); + float c = cosf(angle); + float s = sinf(angle); + + const float length = sqrtf(x * x + y * y + z * z); + float recipLen = 1.0f / length; + x *= recipLen; + y *= recipLen; + z *= recipLen; + + const float nc = 1.0f - c; + const float xy = x * y; + const float yz = y * z; + const float zx = z * x; + const float xs = x * s; + const float ys = y * s; + const float zs = z * s; + + data[kScaleX] = x * x * nc + c; + data[kSkewX] = xy * nc - zs; + data[8] = zx * nc + ys; + data[kSkewY] = xy * nc + zs; + data[kScaleY] = y * y * nc + c; + data[9] = yz * nc - xs; + data[2] = zx * nc - ys; + data[6] = yz * nc + xs; + data[kScaleZ] = z * z * nc + c; + + mSimpleMatrix = false; +} + +void Matrix4::loadMultiply(const Matrix4& u, const Matrix4& v) { + for (int i = 0 ; i < 4 ; i++) { + float x = 0; + float y = 0; + float z = 0; + float w = 0; + + for (int j = 0 ; j < 4 ; j++) { + const float e = v.get(i, j); + x += u.get(j, 0) * e; + y += u.get(j, 1) * e; + z += u.get(j, 2) * e; + w += u.get(j, 3) * e; + } + + set(i, 0, x); + set(i, 1, y); + set(i, 2, z); + set(i, 3, w); + } + + mSimpleMatrix = u.mSimpleMatrix && v.mSimpleMatrix; +} + +void Matrix4::loadOrtho(float left, float right, float bottom, float top, float near, float far) { + loadIdentity(); + data[kScaleX] = 2.0f / (right - left); + data[kScaleY] = 2.0f / (top - bottom); + data[kScaleZ] = -2.0f / (far - near); + data[kTranslateX] = -(right + left) / (right - left); + data[kTranslateY] = -(top + bottom) / (top - bottom); + data[kTranslateZ] = -(far + near) / (far - near); +} + +#define MUL_ADD_STORE(a, b, c) a = (a) * (b) + (c) + +void Matrix4::mapPoint(float& x, float& y) const { + if (mSimpleMatrix) { + MUL_ADD_STORE(x, data[kScaleX], data[kTranslateX]); + MUL_ADD_STORE(y, data[kScaleY], data[kTranslateY]); + return; + } + + float dx = x * data[kScaleX] + y * data[kSkewX] + data[kTranslateX]; + float dy = x * data[kSkewY] + y * data[kScaleY] + data[kTranslateY]; + float dz = x * data[kPerspective0] + y * data[kPerspective1] + data[kPerspective2]; + if (dz) dz = 1.0f / dz; + + x = dx * dz; + y = dy * dz; +} + +void Matrix4::mapRect(Rect& r) const { + if (mSimpleMatrix) { + MUL_ADD_STORE(r.left, data[kScaleX], data[kTranslateX]); + MUL_ADD_STORE(r.right, data[kScaleX], data[kTranslateX]); + MUL_ADD_STORE(r.top, data[kScaleY], data[kTranslateY]); + MUL_ADD_STORE(r.bottom, data[kScaleY], data[kTranslateY]); + return; + } + + float vertices[] = { + r.left, r.top, + r.right, r.top, + r.right, r.bottom, + r.left, r.bottom + }; + + float x, y, z; + + for (int i = 0; i < 8; i+= 2) { + float px = vertices[i]; + float py = vertices[i + 1]; + + x = px * data[kScaleX] + py * data[kSkewX] + data[kTranslateX]; + y = px * data[kSkewY] + py * data[kScaleY] + data[kTranslateY]; + z = px * data[kPerspective0] + py * data[kPerspective1] + data[kPerspective2]; + if (z) z = 1.0f / z; + + vertices[i] = x * z; + vertices[i + 1] = y * z; + } + + r.left = r.right = vertices[0]; + r.top = r.bottom = vertices[1]; + + for (int i = 2; i < 8; i += 2) { + x = vertices[i]; + y = vertices[i + 1]; + + if (x < r.left) r.left = x; + else if (x > r.right) r.right = x; + if (y < r.top) r.top = y; + else if (y > r.bottom) r.bottom = y; + } +} + +void Matrix4::dump() const { + LOGD("Matrix4[simple=%d", mSimpleMatrix); + LOGD(" %f %f %f %f", data[kScaleX], data[kSkewX], data[8], data[kTranslateX]); + LOGD(" %f %f %f %f", data[kSkewY], data[kScaleY], data[9], data[kTranslateY]); + LOGD(" %f %f %f %f", data[2], data[6], data[kScaleZ], data[kTranslateZ]); + LOGD(" %f %f %f %f", data[kPerspective0], data[kPerspective1], data[11], data[kPerspective2]); + LOGD("]"); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/Matrix.h b/libs/hwui/Matrix.h new file mode 100644 index 0000000000000000000000000000000000000000..0608efe988fe8b1b534b06791b62646357632e66 --- /dev/null +++ b/libs/hwui/Matrix.h @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_MATRIX_H +#define ANDROID_UI_MATRIX_H + +#include + +#include "Rect.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Classes +/////////////////////////////////////////////////////////////////////////////// + +class Matrix4 { +public: + float data[16]; + + enum Entry { + kScaleX = 0, + kSkewY = 1, + kPerspective0 = 3, + kSkewX = 4, + kScaleY = 5, + kPerspective1 = 7, + kScaleZ = 10, + kTranslateX = 12, + kTranslateY = 13, + kTranslateZ = 14, + kPerspective2 = 15 + }; + + Matrix4() { + loadIdentity(); + } + + Matrix4(const float* v) { + load(v); + } + + Matrix4(const Matrix4& v) { + load(v); + } + + Matrix4(const SkMatrix& v) { + load(v); + } + + void loadIdentity(); + + void load(const float* v); + void load(const Matrix4& v); + void load(const SkMatrix& v); + + void loadInverse(const Matrix4& v); + + void loadTranslate(float x, float y, float z); + void loadScale(float sx, float sy, float sz); + void loadRotate(float angle, float x, float y, float z); + void loadMultiply(const Matrix4& u, const Matrix4& v); + + void loadOrtho(float left, float right, float bottom, float top, float near, float far); + + void multiply(const Matrix4& v) { + Matrix4 u; + u.loadMultiply(*this, v); + load(u); + } + + void multiply(float v); + + void translate(float x, float y, float z) { + Matrix4 u; + u.loadTranslate(x, y, z); + multiply(u); + } + + void scale(float sx, float sy, float sz) { + Matrix4 u; + u.loadScale(sx, sy, sz); + multiply(u); + } + + void rotate(float angle, float x, float y, float z) { + Matrix4 u; + u.loadRotate(angle, x, y, z); + multiply(u); + } + + void copyTo(float* v) const; + void copyTo(SkMatrix& v) const; + + void mapRect(Rect& r) const; + void mapPoint(float& x, float& y) const; + + float getTranslateX(); + float getTranslateY(); + + void dump() const; + +private: + bool mSimpleMatrix; + + inline float get(int i, int j) const { + return data[i * 4 + j]; + } + + inline void set(int i, int j, float v) { + data[i * 4 + j] = v; + } +}; // class Matrix4 + +/////////////////////////////////////////////////////////////////////////////// +// Types +/////////////////////////////////////////////////////////////////////////////// + +typedef Matrix4 mat4; + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_MATRIX_H diff --git a/libs/hwui/NOTICE b/libs/hwui/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..c5b1efa7aac764ae6d8da63476a2d5cec02a6a5d --- /dev/null +++ b/libs/hwui/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..02b14258680b3e2e2097fec3ae9747b366ff9ff8 --- /dev/null +++ b/libs/hwui/OpenGLRenderer.cpp @@ -0,0 +1,1208 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include +#include +#include + +#include +#include + +#include + +#include "OpenGLRenderer.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Defines +/////////////////////////////////////////////////////////////////////////////// + +#define REQUIRED_TEXTURE_UNITS_COUNT 3 + +// Generates simple and textured vertices +#define FV(x, y, u, v) { { x, y }, { u, v } } + +#define RAD_TO_DEG (180.0f / 3.14159265f) +#define MIN_ANGLE 0.001f + +/////////////////////////////////////////////////////////////////////////////// +// Globals +/////////////////////////////////////////////////////////////////////////////// + +// This array is never used directly but used as a memcpy source in the +// OpenGLRenderer constructor +static const TextureVertex gMeshVertices[] = { + FV(0.0f, 0.0f, 0.0f, 0.0f), + FV(1.0f, 0.0f, 1.0f, 0.0f), + FV(0.0f, 1.0f, 0.0f, 1.0f), + FV(1.0f, 1.0f, 1.0f, 1.0f) +}; +static const GLsizei gMeshStride = sizeof(TextureVertex); +static const GLsizei gMeshCount = 4; + +/** + * Structure mapping Skia xfermodes to OpenGL blending factors. + */ +struct Blender { + SkXfermode::Mode mode; + GLenum src; + GLenum dst; +}; // struct Blender + +// In this array, the index of each Blender equals the value of the first +// entry. For instance, gBlends[1] == gBlends[SkXfermode::kSrc_Mode] +static const Blender gBlends[] = { + { SkXfermode::kClear_Mode, GL_ZERO, GL_ZERO }, + { SkXfermode::kSrc_Mode, GL_ONE, GL_ZERO }, + { SkXfermode::kDst_Mode, GL_ZERO, GL_ONE }, + { SkXfermode::kSrcOver_Mode, GL_ONE, GL_ONE_MINUS_SRC_ALPHA }, + { SkXfermode::kDstOver_Mode, GL_ONE_MINUS_DST_ALPHA, GL_ONE }, + { SkXfermode::kSrcIn_Mode, GL_DST_ALPHA, GL_ZERO }, + { SkXfermode::kDstIn_Mode, GL_ZERO, GL_SRC_ALPHA }, + { SkXfermode::kSrcOut_Mode, GL_ONE_MINUS_DST_ALPHA, GL_ZERO }, + { SkXfermode::kDstOut_Mode, GL_ZERO, GL_ONE_MINUS_SRC_ALPHA }, + { SkXfermode::kSrcATop_Mode, GL_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA }, + { SkXfermode::kDstATop_Mode, GL_ONE_MINUS_DST_ALPHA, GL_SRC_ALPHA }, + { SkXfermode::kXor_Mode, GL_ONE_MINUS_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA } +}; + +// This array contains the swapped version of each SkXfermode. For instance +// this array's SrcOver blending mode is actually DstOver. You can refer to +// createLayer() for more information on the purpose of this array. +static const Blender gBlendsSwap[] = { + { SkXfermode::kClear_Mode, GL_ZERO, GL_ZERO }, + { SkXfermode::kSrc_Mode, GL_ZERO, GL_ONE }, + { SkXfermode::kDst_Mode, GL_ONE, GL_ZERO }, + { SkXfermode::kSrcOver_Mode, GL_ONE_MINUS_DST_ALPHA, GL_ONE }, + { SkXfermode::kDstOver_Mode, GL_ONE, GL_ONE_MINUS_SRC_ALPHA }, + { SkXfermode::kSrcIn_Mode, GL_ZERO, GL_SRC_ALPHA }, + { SkXfermode::kDstIn_Mode, GL_DST_ALPHA, GL_ZERO }, + { SkXfermode::kSrcOut_Mode, GL_ZERO, GL_ONE_MINUS_SRC_ALPHA }, + { SkXfermode::kDstOut_Mode, GL_ONE_MINUS_DST_ALPHA, GL_ZERO }, + { SkXfermode::kSrcATop_Mode, GL_ONE_MINUS_DST_ALPHA, GL_SRC_ALPHA }, + { SkXfermode::kDstATop_Mode, GL_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA }, + { SkXfermode::kXor_Mode, GL_ONE_MINUS_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA } +}; + +static const GLenum gTextureUnits[] = { + GL_TEXTURE0, + GL_TEXTURE1, + GL_TEXTURE2 +}; + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +OpenGLRenderer::OpenGLRenderer(): mCaches(Caches::getInstance()) { + LOGD("Create OpenGLRenderer"); + + mShader = NULL; + mColorFilter = NULL; + mHasShadow = false; + + memcpy(mMeshVertices, gMeshVertices, sizeof(gMeshVertices)); + + mFirstSnapshot = new Snapshot; + + GLint maxTextureUnits; + glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits); + if (maxTextureUnits < REQUIRED_TEXTURE_UNITS_COUNT) { + LOGW("At least %d texture units are required!", REQUIRED_TEXTURE_UNITS_COUNT); + } + + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize); +} + +OpenGLRenderer::~OpenGLRenderer() { + LOGD("Destroy OpenGLRenderer"); +} + +/////////////////////////////////////////////////////////////////////////////// +// Setup +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::setViewport(int width, int height) { + glViewport(0, 0, width, height); + mOrthoMatrix.loadOrtho(0, width, height, 0, -1, 1); + + mWidth = width; + mHeight = height; +} + +void OpenGLRenderer::prepare() { + mSnapshot = new Snapshot(mFirstSnapshot, + SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + mSaveCount = 1; + + glViewport(0, 0, mWidth, mHeight); + + glDisable(GL_DITHER); + glDisable(GL_SCISSOR_TEST); + + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, mWidth, mHeight); + + mSnapshot->setClip(0.0f, 0.0f, mWidth, mHeight); +} + +void OpenGLRenderer::finish() { +#if DEBUG_OPENGL + GLenum status = GL_NO_ERROR; + while ((status = glGetError()) != GL_NO_ERROR) { + LOGD("GL error from OpenGLRenderer: 0x%x", status); + } +#endif +} + +void OpenGLRenderer::acquireContext() { + if (mCaches.currentProgram) { + if (mCaches.currentProgram->isInUse()) { + mCaches.currentProgram->remove(); + mCaches.currentProgram = NULL; + } + } +} + +void OpenGLRenderer::releaseContext() { + glViewport(0, 0, mWidth, mHeight); + + glEnable(GL_SCISSOR_TEST); + setScissorFromClip(); + + glDisable(GL_DITHER); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + if (mCaches.blend) { + glEnable(GL_BLEND); + glBlendFunc(mCaches.lastSrcMode, mCaches.lastDstMode); + glBlendEquation(GL_FUNC_ADD); + } else { + glDisable(GL_BLEND); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// State management +/////////////////////////////////////////////////////////////////////////////// + +int OpenGLRenderer::getSaveCount() const { + return mSaveCount; +} + +int OpenGLRenderer::save(int flags) { + return saveSnapshot(flags); +} + +void OpenGLRenderer::restore() { + if (mSaveCount > 1) { + restoreSnapshot(); + } +} + +void OpenGLRenderer::restoreToCount(int saveCount) { + if (saveCount < 1) saveCount = 1; + + while (mSaveCount > saveCount) { + restoreSnapshot(); + } +} + +int OpenGLRenderer::saveSnapshot(int flags) { + mSnapshot = new Snapshot(mSnapshot, flags); + return mSaveCount++; +} + +bool OpenGLRenderer::restoreSnapshot() { + bool restoreClip = mSnapshot->flags & Snapshot::kFlagClipSet; + bool restoreLayer = mSnapshot->flags & Snapshot::kFlagIsLayer; + + sp current = mSnapshot; + sp previous = mSnapshot->previous; + + mSaveCount--; + mSnapshot = previous; + + if (restoreLayer) { + composeLayer(current, previous); + } + + if (restoreClip) { + setScissorFromClip(); + } + + return restoreClip; +} + +/////////////////////////////////////////////////////////////////////////////// +// Layers +/////////////////////////////////////////////////////////////////////////////// + +int OpenGLRenderer::saveLayer(float left, float top, float right, float bottom, + const SkPaint* p, int flags) { + int count = saveSnapshot(flags); + + int alpha = 255; + SkXfermode::Mode mode; + + if (p) { + alpha = p->getAlpha(); + if (!mExtensions.hasFramebufferFetch()) { + const bool isMode = SkXfermode::IsMode(p->getXfermode(), &mode); + if (!isMode) { + // Assume SRC_OVER + mode = SkXfermode::kSrcOver_Mode; + } + } else { + mode = getXfermode(p->getXfermode()); + } + } else { + mode = SkXfermode::kSrcOver_Mode; + } + + createLayer(mSnapshot, left, top, right, bottom, alpha, mode, flags); + + return count; +} + +int OpenGLRenderer::saveLayerAlpha(float left, float top, float right, float bottom, + int alpha, int flags) { + if (alpha == 0xff) { + return saveLayer(left, top, right, bottom, NULL, flags); + } else { + SkPaint paint; + paint.setAlpha(alpha); + return saveLayer(left, top, right, bottom, &paint, flags); + } +} + +/** + * Layers are viewed by Skia are slightly different than layers in image editing + * programs (for instance.) When a layer is created, previously created layers + * and the frame buffer still receive every drawing command. For instance, if a + * layer is created and a shape intersecting the bounds of the layers and the + * framebuffer is draw, the shape will be drawn on both (unless the layer was + * created with the SkCanvas::kClipToLayer_SaveFlag flag.) + * + * A way to implement layers is to create an FBO for each layer, backed by an RGBA + * texture. Unfortunately, this is inefficient as it requires every primitive to + * be drawn n + 1 times, where n is the number of active layers. In practice this + * means, for every primitive: + * - Switch active frame buffer + * - Change viewport, clip and projection matrix + * - Issue the drawing + * + * Switching rendering target n + 1 times per drawn primitive is extremely costly. + * To avoid this, layers are implemented in a different way here. + * + * This implementation relies on the frame buffer being at least RGBA 8888. When + * a layer is created, only a texture is created, not an FBO. The content of the + * frame buffer contained within the layer's bounds is copied into this texture + * using glCopyTexImage2D(). The layer's region is then cleared(1) in the frame + * buffer and drawing continues as normal. This technique therefore treats the + * frame buffer as a scratch buffer for the layers. + * + * To compose the layers back onto the frame buffer, each layer texture + * (containing the original frame buffer data) is drawn as a simple quad over + * the frame buffer. The trick is that the quad is set as the composition + * destination in the blending equation, and the frame buffer becomes the source + * of the composition. + * + * Drawing layers with an alpha value requires an extra step before composition. + * An empty quad is drawn over the layer's region in the frame buffer. This quad + * is drawn with the rgba color (0,0,0,alpha). The alpha value offered by the + * quad is used to multiply the colors in the frame buffer. This is achieved by + * changing the GL blend functions for the GL_FUNC_ADD blend equation to + * GL_ZERO, GL_SRC_ALPHA. + * + * Because glCopyTexImage2D() can be slow, an alternative implementation might + * be use to draw a single clipped layer. The implementation described above + * is correct in every case. + * + * (1) The frame buffer is actually not cleared right away. To allow the GPU + * to potentially optimize series of calls to glCopyTexImage2D, the frame + * buffer is left untouched until the first drawing operation. Only when + * something actually gets drawn are the layers regions cleared. + */ +bool OpenGLRenderer::createLayer(sp snapshot, float left, float top, + float right, float bottom, int alpha, SkXfermode::Mode mode,int flags) { + LAYER_LOGD("Requesting layer %fx%f", right - left, bottom - top); + LAYER_LOGD("Layer cache size = %d", mCaches.layerCache.getSize()); + + // Window coordinates of the layer + Rect bounds(left, top, right, bottom); + mSnapshot->transform->mapRect(bounds); + + // Layers only make sense if they are in the framebuffer's bounds + bounds.intersect(*mSnapshot->clipRect); + bounds.snapToPixelBoundaries(); + + if (bounds.isEmpty() || bounds.getWidth() > mMaxTextureSize || + bounds.getHeight() > mMaxTextureSize) { + return false; + } + + LayerSize size(bounds.getWidth(), bounds.getHeight()); + Layer* layer = mCaches.layerCache.get(size); + if (!layer) { + return false; + } + + layer->mode = mode; + layer->alpha = alpha; + layer->layer.set(bounds); + + // Save the layer in the snapshot + snapshot->flags |= Snapshot::kFlagIsLayer; + snapshot->layer = layer; + + // Copy the framebuffer into the layer + glBindTexture(GL_TEXTURE_2D, layer->texture); + glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bounds.left, mHeight - bounds.bottom, + bounds.getWidth(), bounds.getHeight(), 0); + + if (flags & SkCanvas::kClipToLayer_SaveFlag) { + if (mSnapshot->clipTransformed(bounds)) setScissorFromClip(); + } + + // Enqueue the buffer coordinates to clear the corresponding region later + mLayers.push(new Rect(bounds)); + + return true; +} + +/** + * Read the documentation of createLayer() before doing anything in this method. + */ +void OpenGLRenderer::composeLayer(sp current, sp previous) { + if (!current->layer) { + LOGE("Attempting to compose a layer that does not exist"); + return; + } + + // Restore the clip from the previous snapshot + const Rect& clip = *previous->clipRect; + glScissor(clip.left, mHeight - clip.bottom, clip.getWidth(), clip.getHeight()); + + Layer* layer = current->layer; + const Rect& rect = layer->layer; + + if (layer->alpha < 255) { + drawColorRect(rect.left, rect.top, rect.right, rect.bottom, + layer->alpha << 24, SkXfermode::kDstIn_Mode, true); + } + + // Layers are already drawn with a top-left origin, don't flip the texture + resetDrawTextureTexCoords(0.0f, 1.0f, 1.0f, 0.0f); + + drawTextureMesh(rect.left, rect.top, rect.right, rect.bottom, layer->texture, + 1.0f, layer->mode, layer->blend, &mMeshVertices[0].position[0], + &mMeshVertices[0].texture[0], GL_TRIANGLE_STRIP, gMeshCount, true, true); + + resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f); + + LayerSize size(rect.getWidth(), rect.getHeight()); + // Failing to add the layer to the cache should happen only if the + // layer is too large + if (!mCaches.layerCache.put(size, layer)) { + LAYER_LOGD("Deleting layer"); + + glDeleteTextures(1, &layer->texture); + + delete layer; + } +} + +void OpenGLRenderer::clearLayerRegions() { + if (mLayers.size() == 0) return; + + for (uint32_t i = 0; i < mLayers.size(); i++) { + Rect* bounds = mLayers.itemAt(i); + + // Clear the framebuffer where the layer will draw + glScissor(bounds->left, mHeight - bounds->bottom, + bounds->getWidth(), bounds->getHeight()); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + delete bounds; + } + mLayers.clear(); + + // Restore the clip + setScissorFromClip(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Transforms +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::translate(float dx, float dy) { + mSnapshot->transform->translate(dx, dy, 0.0f); +} + +void OpenGLRenderer::rotate(float degrees) { + mSnapshot->transform->rotate(degrees, 0.0f, 0.0f, 1.0f); +} + +void OpenGLRenderer::scale(float sx, float sy) { + mSnapshot->transform->scale(sx, sy, 1.0f); +} + +void OpenGLRenderer::setMatrix(SkMatrix* matrix) { + mSnapshot->transform->load(*matrix); +} + +void OpenGLRenderer::getMatrix(SkMatrix* matrix) { + mSnapshot->transform->copyTo(*matrix); +} + +void OpenGLRenderer::concatMatrix(SkMatrix* matrix) { + mat4 m(*matrix); + mSnapshot->transform->multiply(m); +} + +/////////////////////////////////////////////////////////////////////////////// +// Clipping +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::setScissorFromClip() { + const Rect& clip = *mSnapshot->clipRect; + glScissor(clip.left, mHeight - clip.bottom, clip.getWidth(), clip.getHeight()); +} + +const Rect& OpenGLRenderer::getClipBounds() { + return mSnapshot->getLocalClip(); +} + +bool OpenGLRenderer::quickReject(float left, float top, float right, float bottom) { + Rect r(left, top, right, bottom); + mSnapshot->transform->mapRect(r); + return !mSnapshot->clipRect->intersects(r); +} + +bool OpenGLRenderer::clipRect(float left, float top, float right, float bottom, SkRegion::Op op) { + bool clipped = mSnapshot->clip(left, top, right, bottom, op); + if (clipped) { + setScissorFromClip(); + } + return !mSnapshot->clipRect->isEmpty(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Drawing +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::drawBitmap(SkBitmap* bitmap, float left, float top, const SkPaint* paint) { + const float right = left + bitmap->width(); + const float bottom = top + bitmap->height(); + + if (quickReject(left, top, right, bottom)) { + return; + } + + glActiveTexture(GL_TEXTURE0); + const Texture* texture = mCaches.textureCache.get(bitmap); + if (!texture) return; + const AutoTexture autoCleanup(texture); + + drawTextureRect(left, top, right, bottom, texture, paint); +} + +void OpenGLRenderer::drawBitmap(SkBitmap* bitmap, const SkMatrix* matrix, const SkPaint* paint) { + Rect r(0.0f, 0.0f, bitmap->width(), bitmap->height()); + const mat4 transform(*matrix); + transform.mapRect(r); + + if (quickReject(r.left, r.top, r.right, r.bottom)) { + return; + } + + glActiveTexture(GL_TEXTURE0); + const Texture* texture = mCaches.textureCache.get(bitmap); + if (!texture) return; + const AutoTexture autoCleanup(texture); + + drawTextureRect(r.left, r.top, r.right, r.bottom, texture, paint); +} + +void OpenGLRenderer::drawBitmap(SkBitmap* bitmap, + float srcLeft, float srcTop, float srcRight, float srcBottom, + float dstLeft, float dstTop, float dstRight, float dstBottom, + const SkPaint* paint) { + if (quickReject(dstLeft, dstTop, dstRight, dstBottom)) { + return; + } + + glActiveTexture(GL_TEXTURE0); + const Texture* texture = mCaches.textureCache.get(bitmap); + if (!texture) return; + const AutoTexture autoCleanup(texture); + + const float width = texture->width; + const float height = texture->height; + + const float u1 = srcLeft / width; + const float v1 = srcTop / height; + const float u2 = srcRight / width; + const float v2 = srcBottom / height; + + resetDrawTextureTexCoords(u1, v1, u2, v2); + + drawTextureRect(dstLeft, dstTop, dstRight, dstBottom, texture, paint); + + resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f); +} + +void OpenGLRenderer::drawPatch(SkBitmap* bitmap, Res_png_9patch* patch, + float left, float top, float right, float bottom, const SkPaint* paint) { + if (quickReject(left, top, right, bottom)) { + return; + } + + glActiveTexture(GL_TEXTURE0); + const Texture* texture = mCaches.textureCache.get(bitmap); + if (!texture) return; + const AutoTexture autoCleanup(texture); + + int alpha; + SkXfermode::Mode mode; + getAlphaAndMode(paint, &alpha, &mode); + + Patch* mesh = mCaches.patchCache.get(patch); + mesh->updateVertices(bitmap->width(), bitmap->height(),left, top, right, bottom, + &patch->xDivs[0], &patch->yDivs[0], patch->numXDivs, patch->numYDivs); + + // Specify right and bottom as +1.0f from left/top to prevent scaling since the + // patch mesh already defines the final size + drawTextureMesh(left, top, left + 1.0f, top + 1.0f, texture->id, alpha / 255.0f, + mode, texture->blend, &mesh->vertices[0].position[0], + &mesh->vertices[0].texture[0], GL_TRIANGLES, mesh->verticesCount); +} + +void OpenGLRenderer::drawLines(float* points, int count, const SkPaint* paint) { + int alpha; + SkXfermode::Mode mode; + getAlphaAndMode(paint, &alpha, &mode); + + uint32_t color = paint->getColor(); + const GLfloat a = alpha / 255.0f; + const GLfloat r = a * ((color >> 16) & 0xFF) / 255.0f; + const GLfloat g = a * ((color >> 8) & 0xFF) / 255.0f; + const GLfloat b = a * ((color ) & 0xFF) / 255.0f; + + const bool isAA = paint->isAntiAlias(); + if (isAA) { + GLuint textureUnit = 0; + setupTextureAlpha8(mLine.getTexture(), 0, 0, textureUnit, 0.0f, 0.0f, r, g, b, a, + mode, false, true, mLine.getVertices(), mLine.getTexCoords()); + } else { + setupColorRect(0.0f, 0.0f, 1.0f, 1.0f, r, g, b, a, mode, false); + } + + const float strokeWidth = paint->getStrokeWidth(); + const GLsizei elementsCount = isAA ? mLine.getElementsCount() : gMeshCount; + const GLenum drawMode = isAA ? GL_TRIANGLES : GL_TRIANGLE_STRIP; + + for (int i = 0; i < count; i += 4) { + float tx = 0.0f; + float ty = 0.0f; + + if (isAA) { + mLine.update(points[i], points[i + 1], points[i + 2], points[i + 3], + strokeWidth, tx, ty); + } else { + ty = strokeWidth <= 1.0f ? 0.0f : -strokeWidth * 0.5f; + } + + const float dx = points[i + 2] - points[i]; + const float dy = points[i + 3] - points[i + 1]; + const float mag = sqrtf(dx * dx + dy * dy); + const float angle = acos(dx / mag); + + mModelView.loadTranslate(points[i], points[i + 1], 0.0f); + if (angle > MIN_ANGLE || angle < -MIN_ANGLE) { + mModelView.rotate(angle * RAD_TO_DEG, 0.0f, 0.0f, 1.0f); + } + mModelView.translate(tx, ty, 0.0f); + if (!isAA) { + float length = mLine.getLength(points[i], points[i + 1], points[i + 2], points[i + 3]); + mModelView.scale(length, strokeWidth, 1.0f); + } + mCaches.currentProgram->set(mOrthoMatrix, mModelView, *mSnapshot->transform); + + if (mShader) { + mShader->updateTransforms(mCaches.currentProgram, mModelView, *mSnapshot); + } + + glDrawArrays(drawMode, 0, elementsCount); + } + + glDisableVertexAttribArray(mCaches.currentProgram->getAttrib("texCoords")); +} + +void OpenGLRenderer::drawColor(int color, SkXfermode::Mode mode) { + const Rect& clip = *mSnapshot->clipRect; + drawColorRect(clip.left, clip.top, clip.right, clip.bottom, color, mode, true); +} + +void OpenGLRenderer::drawRect(float left, float top, float right, float bottom, const SkPaint* p) { + if (quickReject(left, top, right, bottom)) { + return; + } + + SkXfermode::Mode mode; + if (!mExtensions.hasFramebufferFetch()) { + const bool isMode = SkXfermode::IsMode(p->getXfermode(), &mode); + if (!isMode) { + // Assume SRC_OVER + mode = SkXfermode::kSrcOver_Mode; + } + } else { + mode = getXfermode(p->getXfermode()); + } + + // Skia draws using the color's alpha channel if < 255 + // Otherwise, it uses the paint's alpha + int color = p->getColor(); + if (((color >> 24) & 0xff) == 255) { + color |= p->getAlpha() << 24; + } + + drawColorRect(left, top, right, bottom, color, mode); +} + +void OpenGLRenderer::drawText(const char* text, int bytesCount, int count, + float x, float y, SkPaint* paint) { + if (text == NULL || count == 0 || (paint->getAlpha() == 0 && paint->getXfermode() == NULL)) { + return; + } + paint->setAntiAlias(true); + + float length = -1.0f; + switch (paint->getTextAlign()) { + case SkPaint::kCenter_Align: + length = paint->measureText(text, bytesCount); + x -= length / 2.0f; + break; + case SkPaint::kRight_Align: + length = paint->measureText(text, bytesCount); + x -= length; + break; + default: + break; + } + + int alpha; + SkXfermode::Mode mode; + getAlphaAndMode(paint, &alpha, &mode); + + uint32_t color = paint->getColor(); + const GLfloat a = alpha / 255.0f; + const GLfloat r = a * ((color >> 16) & 0xFF) / 255.0f; + const GLfloat g = a * ((color >> 8) & 0xFF) / 255.0f; + const GLfloat b = a * ((color ) & 0xFF) / 255.0f; + + FontRenderer& fontRenderer = mCaches.fontRenderer.getFontRenderer(paint); + fontRenderer.setFont(paint, SkTypeface::UniqueID(paint->getTypeface()), + paint->getTextSize()); + if (mHasShadow) { + glActiveTexture(gTextureUnits[0]); + mCaches.dropShadowCache.setFontRenderer(fontRenderer); + const ShadowTexture* shadow = mCaches.dropShadowCache.get(paint, text, bytesCount, + count, mShadowRadius); + const AutoTexture autoCleanup(shadow); + + setupShadow(shadow, x, y, mode, a); + + // Draw the mesh + glDrawArrays(GL_TRIANGLE_STRIP, 0, gMeshCount); + glDisableVertexAttribArray(mCaches.currentProgram->getAttrib("texCoords")); + } + + GLuint textureUnit = 0; + glActiveTexture(gTextureUnits[textureUnit]); + + setupTextureAlpha8(fontRenderer.getTexture(), 0, 0, textureUnit, x, y, r, g, b, a, + mode, false, true); + + const Rect& clip = mSnapshot->getLocalClip(); + clearLayerRegions(); + fontRenderer.renderText(paint, &clip, text, 0, bytesCount, count, x, y); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDisableVertexAttribArray(mCaches.currentProgram->getAttrib("texCoords")); + + drawTextDecorations(text, bytesCount, length, x, y, paint); +} + +void OpenGLRenderer::drawPath(SkPath* path, SkPaint* paint) { + GLuint textureUnit = 0; + glActiveTexture(gTextureUnits[textureUnit]); + + const PathTexture* texture = mCaches.pathCache.get(path, paint); + if (!texture) return; + const AutoTexture autoCleanup(texture); + + const float x = texture->left - texture->offset; + const float y = texture->top - texture->offset; + + if (quickReject(x, y, x + texture->width, y + texture->height)) { + return; + } + + int alpha; + SkXfermode::Mode mode; + getAlphaAndMode(paint, &alpha, &mode); + + uint32_t color = paint->getColor(); + const GLfloat a = alpha / 255.0f; + const GLfloat r = a * ((color >> 16) & 0xFF) / 255.0f; + const GLfloat g = a * ((color >> 8) & 0xFF) / 255.0f; + const GLfloat b = a * ((color ) & 0xFF) / 255.0f; + + setupTextureAlpha8(texture, textureUnit, x, y, r, g, b, a, mode, true, true); + + clearLayerRegions(); + + // Draw the mesh + glDrawArrays(GL_TRIANGLE_STRIP, 0, gMeshCount); + glDisableVertexAttribArray(mCaches.currentProgram->getAttrib("texCoords")); +} + +/////////////////////////////////////////////////////////////////////////////// +// Shaders +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::resetShader() { + mShader = NULL; +} + +void OpenGLRenderer::setupShader(SkiaShader* shader) { + mShader = shader; + if (mShader) { + mShader->set(&mCaches.textureCache, &mCaches.gradientCache); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Color filters +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::resetColorFilter() { + mColorFilter = NULL; +} + +void OpenGLRenderer::setupColorFilter(SkiaColorFilter* filter) { + mColorFilter = filter; +} + +/////////////////////////////////////////////////////////////////////////////// +// Drop shadow +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::resetShadow() { + mHasShadow = false; +} + +void OpenGLRenderer::setupShadow(float radius, float dx, float dy, int color) { + mHasShadow = true; + mShadowRadius = radius; + mShadowDx = dx; + mShadowDy = dy; + mShadowColor = color; +} + +/////////////////////////////////////////////////////////////////////////////// +// Drawing implementation +/////////////////////////////////////////////////////////////////////////////// + +void OpenGLRenderer::setupShadow(const ShadowTexture* texture, float x, float y, + SkXfermode::Mode mode, float alpha) { + const float sx = x - texture->left + mShadowDx; + const float sy = y - texture->top + mShadowDy; + + const int shadowAlpha = ((mShadowColor >> 24) & 0xFF); + const GLfloat a = shadowAlpha < 255 ? shadowAlpha / 255.0f : alpha; + const GLfloat r = a * ((mShadowColor >> 16) & 0xFF) / 255.0f; + const GLfloat g = a * ((mShadowColor >> 8) & 0xFF) / 255.0f; + const GLfloat b = a * ((mShadowColor ) & 0xFF) / 255.0f; + + GLuint textureUnit = 0; + setupTextureAlpha8(texture, textureUnit, sx, sy, r, g, b, a, mode, true, false); +} + +void OpenGLRenderer::setupTextureAlpha8(const Texture* texture, GLuint& textureUnit, + float x, float y, float r, float g, float b, float a, SkXfermode::Mode mode, + bool transforms, bool applyFilters) { + setupTextureAlpha8(texture->id, texture->width, texture->height, textureUnit, + x, y, r, g, b, a, mode, transforms, applyFilters, + &mMeshVertices[0].position[0], &mMeshVertices[0].texture[0]); +} + +void OpenGLRenderer::setupTextureAlpha8(GLuint texture, uint32_t width, uint32_t height, + GLuint& textureUnit, float x, float y, float r, float g, float b, float a, + SkXfermode::Mode mode, bool transforms, bool applyFilters) { + setupTextureAlpha8(texture, width, height, textureUnit, + x, y, r, g, b, a, mode, transforms, applyFilters, + &mMeshVertices[0].position[0], &mMeshVertices[0].texture[0]); +} + +void OpenGLRenderer::setupTextureAlpha8(GLuint texture, uint32_t width, uint32_t height, + GLuint& textureUnit, float x, float y, float r, float g, float b, float a, + SkXfermode::Mode mode, bool transforms, bool applyFilters, + GLvoid* vertices, GLvoid* texCoords) { + // Describe the required shaders + ProgramDescription description; + description.hasTexture = true; + description.hasAlpha8Texture = true; + + if (applyFilters) { + if (mShader) { + mShader->describe(description, mExtensions); + } + if (mColorFilter) { + mColorFilter->describe(description, mExtensions); + } + } + + // Setup the blending mode + chooseBlending(true, mode, description); + + // Build and use the appropriate shader + useProgram(mCaches.programCache.get(description)); + + bindTexture(texture, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, textureUnit); + glUniform1i(mCaches.currentProgram->getUniform("sampler"), textureUnit); + + int texCoordsSlot = mCaches.currentProgram->getAttrib("texCoords"); + glEnableVertexAttribArray(texCoordsSlot); + + // Setup attributes + glVertexAttribPointer(mCaches.currentProgram->position, 2, GL_FLOAT, GL_FALSE, + gMeshStride, vertices); + glVertexAttribPointer(texCoordsSlot, 2, GL_FLOAT, GL_FALSE, + gMeshStride, texCoords); + + // Setup uniforms + if (transforms) { + mModelView.loadTranslate(x, y, 0.0f); + mModelView.scale(width, height, 1.0f); + } else { + mModelView.loadIdentity(); + } + mCaches.currentProgram->set(mOrthoMatrix, mModelView, *mSnapshot->transform); + glUniform4f(mCaches.currentProgram->color, r, g, b, a); + + textureUnit++; + if (applyFilters) { + // Setup attributes and uniforms required by the shaders + if (mShader) { + mShader->setupProgram(mCaches.currentProgram, mModelView, *mSnapshot, &textureUnit); + } + if (mColorFilter) { + mColorFilter->setupProgram(mCaches.currentProgram); + } + } +} + +// Same values used by Skia +#define kStdStrikeThru_Offset (-6.0f / 21.0f) +#define kStdUnderline_Offset (1.0f / 9.0f) +#define kStdUnderline_Thickness (1.0f / 18.0f) + +void OpenGLRenderer::drawTextDecorations(const char* text, int bytesCount, float length, + float x, float y, SkPaint* paint) { + // Handle underline and strike-through + uint32_t flags = paint->getFlags(); + if (flags & (SkPaint::kUnderlineText_Flag | SkPaint::kStrikeThruText_Flag)) { + float underlineWidth = length; + // If length is > 0.0f, we already measured the text for the text alignment + if (length <= 0.0f) { + underlineWidth = paint->measureText(text, bytesCount); + } + + float offsetX = 0; + switch (paint->getTextAlign()) { + case SkPaint::kCenter_Align: + offsetX = underlineWidth * 0.5f; + break; + case SkPaint::kRight_Align: + offsetX = underlineWidth; + break; + default: + break; + } + + if (underlineWidth > 0.0f) { + float textSize = paint->getTextSize(); + float height = textSize * kStdUnderline_Thickness; + + float left = x - offsetX; + float top = 0.0f; + float right = left + underlineWidth; + float bottom = 0.0f; + + if (flags & SkPaint::kUnderlineText_Flag) { + top = y + textSize * kStdUnderline_Offset; + bottom = top + height; + drawRect(left, top, right, bottom, paint); + } + + if (flags & SkPaint::kStrikeThruText_Flag) { + top = y + textSize * kStdStrikeThru_Offset; + bottom = top + height; + drawRect(left, top, right, bottom, paint); + } + } + } +} + +void OpenGLRenderer::drawColorRect(float left, float top, float right, float bottom, + int color, SkXfermode::Mode mode, bool ignoreTransform) { + clearLayerRegions(); + + // If a shader is set, preserve only the alpha + if (mShader) { + color |= 0x00ffffff; + } + + // Render using pre-multiplied alpha + const int alpha = (color >> 24) & 0xFF; + const GLfloat a = alpha / 255.0f; + const GLfloat r = a * ((color >> 16) & 0xFF) / 255.0f; + const GLfloat g = a * ((color >> 8) & 0xFF) / 255.0f; + const GLfloat b = a * ((color ) & 0xFF) / 255.0f; + + setupColorRect(left, top, right, bottom, r, g, b, a, mode, ignoreTransform); + + // Draw the mesh + glDrawArrays(GL_TRIANGLE_STRIP, 0, gMeshCount); +} + +void OpenGLRenderer::setupColorRect(float left, float top, float right, float bottom, + float r, float g, float b, float a, SkXfermode::Mode mode, bool ignoreTransform) { + GLuint textureUnit = 0; + + // Describe the required shaders + ProgramDescription description; + if (mShader) { + mShader->describe(description, mExtensions); + } + if (mColorFilter) { + mColorFilter->describe(description, mExtensions); + } + + // Setup the blending mode + chooseBlending(a < 1.0f || (mShader && mShader->blend()), mode, description); + + // Build and use the appropriate shader + useProgram(mCaches.programCache.get(description)); + + // Setup attributes + glVertexAttribPointer(mCaches.currentProgram->position, 2, GL_FLOAT, GL_FALSE, + gMeshStride, &mMeshVertices[0].position[0]); + + // Setup uniforms + mModelView.loadTranslate(left, top, 0.0f); + mModelView.scale(right - left, bottom - top, 1.0f); + if (!ignoreTransform) { + mCaches.currentProgram->set(mOrthoMatrix, mModelView, *mSnapshot->transform); + } else { + mat4 identity; + mCaches.currentProgram->set(mOrthoMatrix, mModelView, identity); + } + glUniform4f(mCaches.currentProgram->color, r, g, b, a); + + // Setup attributes and uniforms required by the shaders + if (mShader) { + mShader->setupProgram(mCaches.currentProgram, mModelView, *mSnapshot, &textureUnit); + } + if (mColorFilter) { + mColorFilter->setupProgram(mCaches.currentProgram); + } +} + +void OpenGLRenderer::drawTextureRect(float left, float top, float right, float bottom, + const Texture* texture, const SkPaint* paint) { + int alpha; + SkXfermode::Mode mode; + getAlphaAndMode(paint, &alpha, &mode); + + drawTextureMesh(left, top, right, bottom, texture->id, alpha / 255.0f, mode, + texture->blend, &mMeshVertices[0].position[0], &mMeshVertices[0].texture[0], + GL_TRIANGLE_STRIP, gMeshCount); +} + +void OpenGLRenderer::drawTextureRect(float left, float top, float right, float bottom, + GLuint texture, float alpha, SkXfermode::Mode mode, bool blend) { + drawTextureMesh(left, top, right, bottom, texture, alpha, mode, blend, + &mMeshVertices[0].position[0], &mMeshVertices[0].texture[0], + GL_TRIANGLE_STRIP, gMeshCount); +} + +void OpenGLRenderer::drawTextureMesh(float left, float top, float right, float bottom, + GLuint texture, float alpha, SkXfermode::Mode mode, bool blend, + GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount, + bool swapSrcDst, bool ignoreTransform) { + clearLayerRegions(); + + ProgramDescription description; + description.hasTexture = true; + if (mColorFilter) { + mColorFilter->describe(description, mExtensions); + } + + mModelView.loadTranslate(left, top, 0.0f); + mModelView.scale(right - left, bottom - top, 1.0f); + + chooseBlending(blend || alpha < 1.0f, mode, description, swapSrcDst); + + useProgram(mCaches.programCache.get(description)); + if (!ignoreTransform) { + mCaches.currentProgram->set(mOrthoMatrix, mModelView, *mSnapshot->transform); + } else { + mat4 m; + mCaches.currentProgram->set(mOrthoMatrix, mModelView, m); + } + + // Texture + bindTexture(texture, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0); + glUniform1i(mCaches.currentProgram->getUniform("sampler"), 0); + + // Always premultiplied + glUniform4f(mCaches.currentProgram->color, alpha, alpha, alpha, alpha); + + // Mesh + int texCoordsSlot = mCaches.currentProgram->getAttrib("texCoords"); + glEnableVertexAttribArray(texCoordsSlot); + glVertexAttribPointer(mCaches.currentProgram->position, 2, GL_FLOAT, GL_FALSE, + gMeshStride, vertices); + glVertexAttribPointer(texCoordsSlot, 2, GL_FLOAT, GL_FALSE, gMeshStride, texCoords); + + // Color filter + if (mColorFilter) { + mColorFilter->setupProgram(mCaches.currentProgram); + } + + glDrawArrays(drawMode, 0, elementsCount); + glDisableVertexAttribArray(texCoordsSlot); +} + +void OpenGLRenderer::chooseBlending(bool blend, SkXfermode::Mode mode, + ProgramDescription& description, bool swapSrcDst) { + blend = blend || mode != SkXfermode::kSrcOver_Mode; + if (blend) { + if (mode < SkXfermode::kPlus_Mode) { + if (!mCaches.blend) { + glEnable(GL_BLEND); + } + + GLenum sourceMode = swapSrcDst ? gBlendsSwap[mode].src : gBlends[mode].src; + GLenum destMode = swapSrcDst ? gBlendsSwap[mode].dst : gBlends[mode].dst; + + if (sourceMode != mCaches.lastSrcMode || destMode != mCaches.lastDstMode) { + glBlendFunc(sourceMode, destMode); + mCaches.lastSrcMode = sourceMode; + mCaches.lastDstMode = destMode; + } + } else { + // These blend modes are not supported by OpenGL directly and have + // to be implemented using shaders. Since the shader will perform + // the blending, turn blending off here + if (mExtensions.hasFramebufferFetch()) { + description.framebufferMode = mode; + description.swapSrcDst = swapSrcDst; + } + + if (mCaches.blend) { + glDisable(GL_BLEND); + } + blend = false; + } + } else if (mCaches.blend) { + glDisable(GL_BLEND); + } + mCaches.blend = blend; +} + +bool OpenGLRenderer::useProgram(Program* program) { + if (!program->isInUse()) { + if (mCaches.currentProgram != NULL) mCaches.currentProgram->remove(); + program->use(); + mCaches.currentProgram = program; + return false; + } + return true; +} + +void OpenGLRenderer::resetDrawTextureTexCoords(float u1, float v1, float u2, float v2) { + TextureVertex* v = &mMeshVertices[0]; + TextureVertex::setUV(v++, u1, v1); + TextureVertex::setUV(v++, u2, v1); + TextureVertex::setUV(v++, u1, v2); + TextureVertex::setUV(v++, u2, v2); +} + +void OpenGLRenderer::getAlphaAndMode(const SkPaint* paint, int* alpha, SkXfermode::Mode* mode) { + if (paint) { + if (!mExtensions.hasFramebufferFetch()) { + const bool isMode = SkXfermode::IsMode(paint->getXfermode(), mode); + if (!isMode) { + // Assume SRC_OVER + *mode = SkXfermode::kSrcOver_Mode; + } + } else { + *mode = getXfermode(paint->getXfermode()); + } + + // Skia draws using the color's alpha channel if < 255 + // Otherwise, it uses the paint's alpha + int color = paint->getColor(); + *alpha = (color >> 24) & 0xFF; + if (*alpha == 255) { + *alpha = paint->getAlpha(); + } + } else { + *mode = SkXfermode::kSrcOver_Mode; + *alpha = 255; + } +} + +SkXfermode::Mode OpenGLRenderer::getXfermode(SkXfermode* mode) { + if (mode == NULL) { + return SkXfermode::kSrcOver_Mode; + } + return mode->fMode; +} + +void OpenGLRenderer::bindTexture(GLuint texture, GLenum wrapS, GLenum wrapT, GLuint textureUnit) { + glActiveTexture(gTextureUnits[textureUnit]); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapT); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/OpenGLRenderer.h b/libs/hwui/OpenGLRenderer.h new file mode 100644 index 0000000000000000000000000000000000000000..eba0f41369be0d3ecab9fe06225c9cb37da6ef9c --- /dev/null +++ b/libs/hwui/OpenGLRenderer.h @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_OPENGL_RENDERER_H +#define ANDROID_UI_OPENGL_RENDERER_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Extensions.h" +#include "Matrix.h" +#include "Program.h" +#include "Rect.h" +#include "Snapshot.h" +#include "Vertex.h" +#include "SkiaShader.h" +#include "SkiaColorFilter.h" +#include "Caches.h" +#include "Line.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Defines +/////////////////////////////////////////////////////////////////////////////// + +// Debug +#define DEBUG_OPENGL 1 + +/////////////////////////////////////////////////////////////////////////////// +// Renderer +/////////////////////////////////////////////////////////////////////////////// + +/** + * OpenGL renderer used to draw accelerated 2D graphics. The API is a + * simplified version of Skia's Canvas API. + */ +class OpenGLRenderer { +public: + OpenGLRenderer(); + ~OpenGLRenderer(); + + void setViewport(int width, int height); + void prepare(); + void finish(); + void acquireContext(); + void releaseContext(); + + int getSaveCount() const; + int save(int flags); + void restore(); + void restoreToCount(int saveCount); + + int saveLayer(float left, float top, float right, float bottom, const SkPaint* p, int flags); + int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, int flags); + + void translate(float dx, float dy); + void rotate(float degrees); + void scale(float sx, float sy); + + void setMatrix(SkMatrix* matrix); + void getMatrix(SkMatrix* matrix); + void concatMatrix(SkMatrix* matrix); + + const Rect& getClipBounds(); + bool quickReject(float left, float top, float right, float bottom); + bool clipRect(float left, float top, float right, float bottom, SkRegion::Op op); + + void drawBitmap(SkBitmap* bitmap, float left, float top, const SkPaint* paint); + void drawBitmap(SkBitmap* bitmap, const SkMatrix* matrix, const SkPaint* paint); + void drawBitmap(SkBitmap* bitmap, float srcLeft, float srcTop, float srcRight, float srcBottom, + float dstLeft, float dstTop, float dstRight, float dstBottom, const SkPaint* paint); + void drawPatch(SkBitmap* bitmap, Res_png_9patch* patch, float left, float top, + float right, float bottom, const SkPaint* paint); + void drawColor(int color, SkXfermode::Mode mode); + void drawRect(float left, float top, float right, float bottom, const SkPaint* paint); + void drawPath(SkPath* path, SkPaint* paint); + void drawLines(float* points, int count, const SkPaint* paint); + + void resetShader(); + void setupShader(SkiaShader* shader); + + void resetColorFilter(); + void setupColorFilter(SkiaColorFilter* filter); + + void resetShadow(); + void setupShadow(float radius, float dx, float dy, int color); + + void drawText(const char* text, int bytesCount, int count, float x, float y, SkPaint* paint); + +private: + /** + * Saves the current state of the renderer as a new snapshot. + * The new snapshot is saved in mSnapshot and the previous snapshot + * is linked from mSnapshot->previous. + * + * @param flags The save flags; see SkCanvas for more information + * + * @return The new save count. This value can be passed to #restoreToCount() + */ + int saveSnapshot(int flags); + + /** + * Restores the current snapshot; mSnapshot becomes mSnapshot->previous. + * + * @return True if the clip was modified. + */ + bool restoreSnapshot(); + + /** + * Sets the clipping rectangle using glScissor. The clip is defined by + * the current snapshot's clipRect member. + */ + void setScissorFromClip(); + + /** + * Compose the layer defined in the current snapshot with the layer + * defined by the previous snapshot. + * + * The current snapshot *must* be a layer (flag kFlagIsLayer set.) + * + * @param curent The current snapshot containing the layer to compose + * @param previous The previous snapshot to compose the current layer with + */ + void composeLayer(sp current, sp previous); + + /** + * Creates a new layer stored in the specified snapshot. + * + * @param snapshot The snapshot associated with the new layer + * @param left The left coordinate of the layer + * @param top The top coordinate of the layer + * @param right The right coordinate of the layer + * @param bottom The bottom coordinate of the layer + * @param alpha The translucency of the layer + * @param mode The blending mode of the layer + * @param flags The layer save flags + * + * @return True if the layer was successfully created, false otherwise + */ + bool createLayer(sp snapshot, float left, float top, float right, float bottom, + int alpha, SkXfermode::Mode mode, int flags); + + /** + * Clears all the regions corresponding to the current list of layers. + * This method MUST be invoked before any drawing operation. + */ + void clearLayerRegions(); + + /** + * Draws a colored rectangle with the specified color. The specified coordinates + * are transformed by the current snapshot's transform matrix. + * + * @param left The left coordinate of the rectangle + * @param top The top coordinate of the rectangle + * @param right The right coordinate of the rectangle + * @param bottom The bottom coordinate of the rectangle + * @param color The rectangle's ARGB color, defined as a packed 32 bits word + * @param mode The Skia xfermode to use + * @param ignoreTransform True if the current transform should be ignored + * @paran ignoreBlending True if the blending is set by the caller + */ + void drawColorRect(float left, float top, float right, float bottom, + int color, SkXfermode::Mode mode, bool ignoreTransform = false); + + /** + * Setups shaders to draw a colored rect. + */ + void setupColorRect(float left, float top, float right, float bottom, + float r, float g, float b, float a, SkXfermode::Mode mode, bool ignoreTransform); + + /** + * Draws a textured rectangle with the specified texture. The specified coordinates + * are transformed by the current snapshot's transform matrix. + * + * @param left The left coordinate of the rectangle + * @param top The top coordinate of the rectangle + * @param right The right coordinate of the rectangle + * @param bottom The bottom coordinate of the rectangle + * @param texture The texture name to map onto the rectangle + * @param alpha An additional translucency parameter, between 0.0f and 1.0f + * @param mode The blending mode + * @param blend True if the texture contains an alpha channel + */ + void drawTextureRect(float left, float top, float right, float bottom, GLuint texture, + float alpha, SkXfermode::Mode mode, bool blend); + + /** + * Draws a textured rectangle with the specified texture. The specified coordinates + * are transformed by the current snapshot's transform matrix. + * + * @param left The left coordinate of the rectangle + * @param top The top coordinate of the rectangle + * @param right The right coordinate of the rectangle + * @param bottom The bottom coordinate of the rectangle + * @param texture The texture to use + * @param paint The paint containing the alpha, blending mode, etc. + */ + void drawTextureRect(float left, float top, float right, float bottom, + const Texture* texture, const SkPaint* paint); + + /** + * Draws a textured mesh with the specified texture. If the indices are omitted, the + * mesh is drawn as a simple quad. + * + * @param left The left coordinate of the rectangle + * @param top The top coordinate of the rectangle + * @param right The right coordinate of the rectangle + * @param bottom The bottom coordinate of the rectangle + * @param texture The texture name to map onto the rectangle + * @param alpha An additional translucency parameter, between 0.0f and 1.0f + * @param mode The blending mode + * @param blend True if the texture contains an alpha channel + * @param vertices The vertices that define the mesh + * @param texCoords The texture coordinates of each vertex + * @param indices The indices of the vertices, can be NULL + * @param elementsCount The number of elements in the mesh, required by indices + * @param swapSrcDst Whether or not the src and dst blending operations should be swapped + * @param ignoreTransform True if the current transform should be ignored + */ + void drawTextureMesh(float left, float top, float right, float bottom, GLuint texture, + float alpha, SkXfermode::Mode mode, bool blend, + GLvoid* vertices, GLvoid* texCoords, GLenum drawMode, GLsizei elementsCount, + bool swapSrcDst = false, bool ignoreTransform = false); + + /** + * Prepares the renderer to draw the specified shadow. + * + * @param texture The shadow texture + * @param x The x coordinate of the shadow + * @param y The y coordinate of the shadow + * @param mode The blending mode + * @param alpha The alpha value + */ + void setupShadow(const ShadowTexture* texture, float x, float y, SkXfermode::Mode mode, + float alpha); + + /** + * Prepares the renderer to draw the specified Alpha8 texture as a rectangle. + * + * @param texture The texture to render with + * @param textureUnit The texture unit to use, may be modified + * @param x The x coordinate of the rectangle to draw + * @param y The y coordinate of the rectangle to draw + * @param r The red component of the color + * @param g The green component of the color + * @param b The blue component of the color + * @param a The alpha component of the color + * @param mode The blending mode + * @param transforms True if the matrix passed to the shader should be multiplied + * by the model-view matrix + * @param applyFilters Whether or not to take color filters and + * shaders into account + */ + void setupTextureAlpha8(const Texture* texture, GLuint& textureUnit, float x, float y, + float r, float g, float b, float a, SkXfermode::Mode mode, bool transforms, + bool applyFilters); + + /** + * Prepares the renderer to draw the specified Alpha8 texture as a rectangle. + * + * @param texture The texture to render with + * @param width The width of the texture + * @param height The height of the texture + * @param textureUnit The texture unit to use, may be modified + * @param x The x coordinate of the rectangle to draw + * @param y The y coordinate of the rectangle to draw + * @param r The red component of the color + * @param g The green component of the color + * @param b The blue component of the color + * @param a The alpha component of the color + * @param mode The blending mode + * @param transforms True if the matrix passed to the shader should be multiplied + * by the model-view matrix + * @param applyFilters Whether or not to take color filters and + * shaders into account + */ + void setupTextureAlpha8(GLuint texture, uint32_t width, uint32_t height, + GLuint& textureUnit, float x, float y, float r, float g, float b, float a, + SkXfermode::Mode mode, bool transforms, bool applyFilters); + + /** + * Same as above setupTextureAlpha8() but specifies the mesh's vertices + * and texCoords pointers. + */ + void setupTextureAlpha8(GLuint texture, uint32_t width, uint32_t height, + GLuint& textureUnit, float x, float y, float r, float g, float b, float a, + SkXfermode::Mode mode, bool transforms, bool applyFilters, + GLvoid* vertices, GLvoid* texCoords); + + /** + * Draws text underline and strike-through if needed. + * + * @param text The text to decor + * @param bytesCount The number of bytes in the text + * @param length The length in pixels of the text, can be <= 0.0f to force a measurement + * @param x The x coordinate where the text will be drawn + * @param y The y coordinate where the text will be drawn + * @param paint The paint to draw the text with + */ + void drawTextDecorations(const char* text, int bytesCount, float length, + float x, float y, SkPaint* paint); + + /** + * Resets the texture coordinates stored in mMeshVertices. Setting the values + * back to default is achieved by calling: + * + * resetDrawTextureTexCoords(0.0f, 0.0f, 1.0f, 1.0f); + * + * @param u1 The left coordinate of the texture + * @param v1 The bottom coordinate of the texture + * @param u2 The right coordinate of the texture + * @param v2 The top coordinate of the texture + */ + void resetDrawTextureTexCoords(float u1, float v1, float u2, float v2); + + /** + * Gets the alpha and xfermode out of a paint object. If the paint is null + * alpha will be 255 and the xfermode will be SRC_OVER. + * + * @param paint The paint to extract values from + * @param alpha Where to store the resulting alpha + * @param mode Where to store the resulting xfermode + */ + inline void getAlphaAndMode(const SkPaint* paint, int* alpha, SkXfermode::Mode* mode); + + /** + * Binds the specified texture with the specified wrap modes. + */ + inline void bindTexture(GLuint texture, GLenum wrapS, GLenum wrapT, GLuint textureUnit = 0); + + /** + * Enable or disable blending as necessary. This function sets the appropriate + * blend function based on the specified xfermode. + */ + inline void chooseBlending(bool blend, SkXfermode::Mode mode, ProgramDescription& description, + bool swapSrcDst = false); + + /** + * Safely retrieves the mode from the specified xfermode. If the specified + * xfermode is null, the mode is assumed to be SkXfermode::kSrcOver_Mode. + */ + inline SkXfermode::Mode getXfermode(SkXfermode* mode); + + /** + * Use the specified program with the current GL context. If the program is already + * in use, it will not be bound again. If it is not in use, the current program is + * marked unused and the specified program becomes used and becomes the new + * current program. + * + * @param program The program to use + * + * @return true If the specified program was already in use, false otherwise. + */ + inline bool useProgram(Program* program); + + // Dimensions of the drawing surface + int mWidth, mHeight; + + // Matrix used for ortho projection in shaders + mat4 mOrthoMatrix; + + // Model-view matrix used to position/size objects + mat4 mModelView; + + // Number of saved states + int mSaveCount; + // Base state + sp mFirstSnapshot; + // Current state + sp mSnapshot; + + // Shaders + SkiaShader* mShader; + + // Color filters + SkiaColorFilter* mColorFilter; + + // Used to draw textured quads + TextureVertex mMeshVertices[4]; + + // GL extensions + Extensions mExtensions; + + // Drop shadow + bool mHasShadow; + float mShadowRadius; + float mShadowDx; + float mShadowDy; + int mShadowColor; + + // Various caches + Caches& mCaches; + + // List of rectangles to clear due to calls to saveLayer() + Vector mLayers; + + // Single object used to draw lines + Line mLine; + + // Misc + GLint mMaxTextureSize; + +}; // class OpenGLRenderer + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_OPENGL_RENDERER_H diff --git a/libs/hwui/Patch.cpp b/libs/hwui/Patch.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ca0a0b1691de7926771835e6d275842a0f67ac2c --- /dev/null +++ b/libs/hwui/Patch.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include "Patch.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +Patch::Patch(const uint32_t xCount, const uint32_t yCount) { + // 2 triangles per patch, 3 vertices per triangle + verticesCount = (xCount + 1) * (yCount + 1) * 2 * 3; + vertices = new TextureVertex[verticesCount]; +} + +Patch::~Patch() { + delete[] vertices; +} + +/////////////////////////////////////////////////////////////////////////////// +// Vertices management +/////////////////////////////////////////////////////////////////////////////// + +void Patch::updateVertices(const float bitmapWidth, const float bitmapHeight, + float left, float top, float right, float bottom, + const int32_t* xDivs, const int32_t* yDivs, const uint32_t width, const uint32_t height) { + const uint32_t xStretchCount = (width + 1) >> 1; + const uint32_t yStretchCount = (height + 1) >> 1; + + float stretchX = 0.0f; + float stretchY = 0.0; + + const float meshWidth = right - left; + + if (xStretchCount > 0) { + uint32_t stretchSize = 0; + for (uint32_t i = 1; i < width; i += 2) { + stretchSize += xDivs[i] - xDivs[i - 1]; + } + const float xStretchTex = stretchSize; + const float fixed = bitmapWidth - stretchSize; + const float xStretch = right - left - fixed; + stretchX = xStretch / xStretchTex; + } + + if (yStretchCount > 0) { + uint32_t stretchSize = 0; + for (uint32_t i = 1; i < height; i += 2) { + stretchSize += yDivs[i] - yDivs[i - 1]; + } + const float yStretchTex = stretchSize; + const float fixed = bitmapHeight - stretchSize; + const float yStretch = bottom - top - fixed; + stretchY = yStretch / yStretchTex; + } + + TextureVertex* vertex = vertices; + + float previousStepY = 0.0f; + + float y1 = 0.0f; + float v1 = 0.0f; + + for (uint32_t i = 0; i < height; i++) { + float stepY = yDivs[i]; + + float y2 = 0.0f; + if (i & 1) { + const float segment = stepY - previousStepY; + y2 = y1 + segment * stretchY; + } else { + y2 = y1 + stepY - previousStepY; + } + float v2 = fmax(0.0f, stepY - 0.5f) / bitmapHeight; + + generateRow(vertex, y1, y2, v1, v2, xDivs, width, stretchX, + right - left, bitmapWidth); + + y1 = y2; + v1 = (stepY + 0.5f) / bitmapHeight; + + previousStepY = stepY; + } + + generateRow(vertex, y1, bottom - top, v1, 1.0f, xDivs, width, stretchX, + right - left, bitmapWidth); +} + +inline void Patch::generateRow(TextureVertex*& vertex, float y1, float y2, float v1, float v2, + const int32_t xDivs[], uint32_t xCount, float stretchX, float width, float bitmapWidth) { + float previousStepX = 0.0f; + + float x1 = 0.0f; + float u1 = 0.0f; + + // Generate the row quad by quad + for (uint32_t i = 0; i < xCount; i++) { + float stepX = xDivs[i]; + + float x2 = 0.0f; + if (i & 1) { + const float segment = stepX - previousStepX; + x2 = x1 + segment * stretchX; + } else { + x2 = x1 + stepX - previousStepX; + } + float u2 = fmax(0.0f, stepX - 0.5f) / bitmapWidth; + + generateQuad(vertex, x1, y1, x2, y2, u1, v1, u2, v2); + + x1 = x2; + u1 = (stepX + 0.5f) / bitmapWidth; + + previousStepX = stepX; + } + + generateQuad(vertex, x1, y1, width, y2, u1, v1, 1.0f, v2); +} + +inline void Patch::generateQuad(TextureVertex*& vertex, float x1, float y1, float x2, float y2, + float u1, float v1, float u2, float v2) { + // Left triangle + TextureVertex::set(vertex++, x1, y1, u1, v1); + TextureVertex::set(vertex++, x2, y1, u2, v1); + TextureVertex::set(vertex++, x1, y2, u1, v2); + + // Right triangle + TextureVertex::set(vertex++, x1, y2, u1, v2); + TextureVertex::set(vertex++, x2, y1, u2, v1); + TextureVertex::set(vertex++, x2, y2, u2, v2); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/Patch.h b/libs/hwui/Patch.h new file mode 100644 index 0000000000000000000000000000000000000000..1d08c64fc40291f79fa5d4f4093c07f3f4de1aa7 --- /dev/null +++ b/libs/hwui/Patch.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_PATCH_H +#define ANDROID_UI_PATCH_H + +#include + +#include "Vertex.h" + +namespace android { +namespace uirenderer { + +/** + * Description of a patch. + */ +struct PatchDescription { + PatchDescription(): xCount(0), yCount(0) { } + PatchDescription(const uint32_t xCount, const uint32_t yCount): + xCount(xCount), yCount(yCount) { } + PatchDescription(const PatchDescription& description): + xCount(description.xCount), yCount(description.yCount) { } + + uint32_t xCount; + uint32_t yCount; + + bool operator<(const PatchDescription& rhs) const { + if (xCount == rhs.xCount) { + return yCount < rhs.yCount; + } + return xCount < rhs.xCount; + } + + bool operator==(const PatchDescription& rhs) const { + return xCount == rhs.xCount && yCount == rhs.yCount; + } +}; // struct PatchDescription + +/** + * An OpenGL patch. This contains an array of vertices and an array of + * indices to render the vertices. + */ +struct Patch { + Patch(const uint32_t xCount, const uint32_t yCount); + ~Patch(); + + void updateVertices(const float bitmapWidth, const float bitmapHeight, + float left, float top, float right, float bottom, + const int32_t* xDivs, const int32_t* yDivs, + const uint32_t width, const uint32_t height); + + TextureVertex* vertices; + uint32_t verticesCount; + +private: + static inline void generateRow(TextureVertex*& vertex, float y1, float y2, + float v1, float v2, const int32_t xDivs[], uint32_t xCount, + float stretchX, float width, float bitmapWidth); + static inline void generateQuad(TextureVertex*& vertex, + float x1, float y1, float x2, float y2, + float u1, float v1, float u2, float v2); +}; // struct Patch + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_PATCH_H diff --git a/libs/hwui/PatchCache.cpp b/libs/hwui/PatchCache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a7c0ccef89f844886b9bfdba2aaf08d535338667 --- /dev/null +++ b/libs/hwui/PatchCache.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include +#include + +#include "PatchCache.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +PatchCache::PatchCache(): mCache(DEFAULT_PATCH_CACHE_SIZE) { +} + +PatchCache::PatchCache(uint32_t maxEntries): mCache(maxEntries) { +} + +PatchCache::~PatchCache() { + clear(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Callbacks +/////////////////////////////////////////////////////////////////////////////// + +void PatchCache::operator()(PatchDescription& description, Patch*& mesh) { + if (mesh) delete mesh; +} + +/////////////////////////////////////////////////////////////////////////////// +// Caching +/////////////////////////////////////////////////////////////////////////////// + +void PatchCache::clear() { + mCache.setOnEntryRemovedListener(this); + mCache.clear(); + mCache.setOnEntryRemovedListener(NULL); +} + +Patch* PatchCache::get(const Res_png_9patch* patch) { + const uint32_t width = patch->numXDivs; + const uint32_t height = patch->numYDivs; + const PatchDescription description(width, height); + + Patch* mesh = mCache.get(description); + if (!mesh) { + PATCH_LOGD("Creating new patch mesh, w=%d h=%d", width, height); + mesh = new Patch(width, height); + mCache.put(description, mesh); + } + + return mesh; +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/PatchCache.h b/libs/hwui/PatchCache.h new file mode 100644 index 0000000000000000000000000000000000000000..6dad8314d069698b0da8749bdd504e1bd98fa2b6 --- /dev/null +++ b/libs/hwui/PatchCache.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_PATCH_CACHE_H +#define ANDROID_UI_PATCH_CACHE_H + +#include + +#include "Patch.h" +#include "GenerationCache.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Defines +/////////////////////////////////////////////////////////////////////////////// + +// Debug +#define DEBUG_PATCHES 0 + +// Debug +#if DEBUG_PATCHES + #define PATCH_LOGD(...) LOGD(__VA_ARGS__) +#else + #define PATCH_LOGD(...) +#endif + +/////////////////////////////////////////////////////////////////////////////// +// Cache +/////////////////////////////////////////////////////////////////////////////// + +class PatchCache: public OnEntryRemoved { +public: + PatchCache(); + PatchCache(uint32_t maxCapacity); + ~PatchCache(); + + /** + * Used as a callback when an entry is removed from the cache. + * Do not invoke directly. + */ + void operator()(PatchDescription& description, Patch*& mesh); + + Patch* get(const Res_png_9patch* patch); + void clear(); + +private: + GenerationCache mCache; +}; // class PatchCache + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_PATCH_CACHE_H diff --git a/libs/hwui/PathCache.cpp b/libs/hwui/PathCache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..70e06a1c3f7970b144f9f95be959277befcd1817 --- /dev/null +++ b/libs/hwui/PathCache.cpp @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include +#include + +#include + +#include "PathCache.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +PathCache::PathCache(): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(MB(DEFAULT_PATH_CACHE_SIZE)) { + char property[PROPERTY_VALUE_MAX]; + if (property_get(PROPERTY_PATH_CACHE_SIZE, property, NULL) > 0) { + LOGD(" Setting path cache size to %sMB", property); + setMaxSize(MB(atof(property))); + } else { + LOGD(" Using default path cache size of %.2fMB", DEFAULT_PATH_CACHE_SIZE); + } + init(); +} + +PathCache::PathCache(uint32_t maxByteSize): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(maxByteSize) { + init(); +} + +PathCache::~PathCache() { + Mutex::Autolock _l(mLock); + mCache.clear(); +} + +void PathCache::init() { + mCache.setOnEntryRemovedListener(this); + + GLint maxTextureSize; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize); + mMaxTextureSize = maxTextureSize; +} + +/////////////////////////////////////////////////////////////////////////////// +// Size management +/////////////////////////////////////////////////////////////////////////////// + +uint32_t PathCache::getSize() { + Mutex::Autolock _l(mLock); + return mSize; +} + +uint32_t PathCache::getMaxSize() { + Mutex::Autolock _l(mLock); + return mMaxSize; +} + +void PathCache::setMaxSize(uint32_t maxSize) { + Mutex::Autolock _l(mLock); + mMaxSize = maxSize; + while (mSize > mMaxSize) { + mCache.removeOldest(); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Callbacks +/////////////////////////////////////////////////////////////////////////////// + +void PathCache::operator()(PathCacheEntry& path, PathTexture*& texture) { + const uint32_t size = texture->width * texture->height; + mSize -= size; + + if (texture) { + glDeleteTextures(1, &texture->id); + delete texture; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Caching +/////////////////////////////////////////////////////////////////////////////// + +void PathCache::remove(SkPath* path) { + Mutex::Autolock _l(mLock); + // TODO: Linear search... + for (uint32_t i = 0; i < mCache.size(); i++) { + if (mCache.getKeyAt(i).path == path) { + mCache.removeAt(i); + } + } +} + +PathTexture* PathCache::get(SkPath* path, SkPaint* paint) { + PathCacheEntry entry(path, paint); + + mLock.lock(); + PathTexture* texture = mCache.get(entry); + mLock.unlock(); + + if (!texture) { + texture = addTexture(entry, path, paint); + } else if (path->getGenerationID() != texture->generation) { + mLock.lock(); + mCache.remove(entry); + mLock.unlock(); + texture = addTexture(entry, path, paint); + } + + return texture; +} + +PathTexture* PathCache::addTexture(const PathCacheEntry& entry, + const SkPath *path, const SkPaint* paint) { + const SkRect& bounds = path->getBounds(); + + const float pathWidth = bounds.width(); + const float pathHeight = bounds.height(); + + if (pathWidth > mMaxTextureSize || pathHeight > mMaxTextureSize) { + LOGW("Path too large to be rendered into a texture"); + return NULL; + } + + const float offset = entry.strokeWidth * 1.5f; + const uint32_t width = uint32_t(pathWidth + offset * 2.0 + 0.5); + const uint32_t height = uint32_t(pathHeight + offset * 2.0 + 0.5); + + const uint32_t size = width * height; + // Don't even try to cache a bitmap that's bigger than the cache + if (size < mMaxSize) { + mLock.lock(); + while (mSize + size > mMaxSize) { + mCache.removeOldest(); + } + mLock.unlock(); + } + + PathTexture* texture = new PathTexture; + texture->left = bounds.fLeft; + texture->top = bounds.fTop; + texture->offset = offset; + texture->width = width; + texture->height = height; + texture->generation = path->getGenerationID(); + + SkBitmap bitmap; + bitmap.setConfig(SkBitmap::kA8_Config, width, height); + bitmap.allocPixels(); + bitmap.eraseColor(0); + + SkCanvas canvas(bitmap); + canvas.translate(-bounds.fLeft + offset, -bounds.fTop + offset); + canvas.drawPath(*path, *paint); + + generateTexture(bitmap, texture); + + if (size < mMaxSize) { + mLock.lock(); + mSize += size; + mCache.put(entry, texture); + mLock.unlock(); + } else { + texture->cleanup = true; + } + + return texture; +} + +void PathCache::clear() { + Mutex::Autolock _l(mLock); + mCache.clear(); +} + +void PathCache::generateTexture(SkBitmap& bitmap, Texture* texture) { + SkAutoLockPixels alp(bitmap); + if (!bitmap.readyToDraw()) { + LOGE("Cannot generate texture from bitmap"); + return; + } + + glGenTextures(1, &texture->id); + + glBindTexture(GL_TEXTURE_2D, texture->id); + // Textures are Alpha8 + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + texture->blend = true; + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, texture->width, texture->height, 0, + GL_ALPHA, GL_UNSIGNED_BYTE, bitmap.getPixels()); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/PathCache.h b/libs/hwui/PathCache.h new file mode 100644 index 0000000000000000000000000000000000000000..bde0e7d845e96cd6369c38e0ddcf8c6ef4fafc32 --- /dev/null +++ b/libs/hwui/PathCache.h @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_PATH_CACHE_H +#define ANDROID_UI_PATH_CACHE_H + +#include +#include +#include + +#include "Texture.h" +#include "GenerationCache.h" + +namespace android { +namespace uirenderer { + +/** + * Describe a path in the path cache. + */ +struct PathCacheEntry { + PathCacheEntry() { + path = NULL; + join = SkPaint::kDefault_Join; + cap = SkPaint::kDefault_Cap; + style = SkPaint::kFill_Style; + miter = 4.0f; + strokeWidth = 1.0f; + } + + PathCacheEntry(const PathCacheEntry& entry): + path(entry.path), join(entry.join), cap(entry.cap), + style(entry.style), miter(entry.miter), + strokeWidth(entry.strokeWidth) { + } + + PathCacheEntry(SkPath* path, SkPaint* paint) { + this->path = path; + join = paint->getStrokeJoin(); + cap = paint->getStrokeCap(); + miter = paint->getStrokeMiter(); + strokeWidth = paint->getStrokeWidth(); + style = paint->getStyle(); + } + + SkPath* path; + SkPaint::Join join; + SkPaint::Cap cap; + SkPaint::Style style; + float miter; + float strokeWidth; + + bool operator<(const PathCacheEntry& rhs) const { + return memcmp(this, &rhs, sizeof(PathCacheEntry)) < 0; + } +}; // struct PathCacheEntry + +/** + * Alpha texture used to represent a path. + */ +struct PathTexture: public Texture { + PathTexture(): Texture() { + } + + /** + * Left coordinate of the path bounds. + */ + float left; + /** + * Top coordinate of the path bounds. + */ + float top; + /** + * Offset to draw the path at the correct origin. + */ + float offset; +}; // struct PathTexture + +/** + * A simple LRU path cache. The cache has a maximum size expressed in bytes. + * Any texture added to the cache causing the cache to grow beyond the maximum + * allowed size will also cause the oldest texture to be kicked out. + */ +class PathCache: public OnEntryRemoved { +public: + PathCache(); + PathCache(uint32_t maxByteSize); + ~PathCache(); + + /** + * Used as a callback when an entry is removed from the cache. + * Do not invoke directly. + */ + void operator()(PathCacheEntry& path, PathTexture*& texture); + + /** + * Returns the texture associated with the specified path. If the texture + * cannot be found in the cache, a new texture is generated. + */ + PathTexture* get(SkPath* path, SkPaint* paint); + /** + * Clears the cache. This causes all textures to be deleted. + */ + void clear(); + /** + * Removes an entry. + */ + void remove(SkPath* path); + + /** + * Sets the maximum size of the cache in bytes. + */ + void setMaxSize(uint32_t maxSize); + /** + * Returns the maximum size of the cache in bytes. + */ + uint32_t getMaxSize(); + /** + * Returns the current size of the cache in bytes. + */ + uint32_t getSize(); + +private: + /** + * Generates the texture from a bitmap into the specified texture structure. + */ + void generateTexture(SkBitmap& bitmap, Texture* texture); + + PathTexture* addTexture(const PathCacheEntry& entry, const SkPath *path, const SkPaint* paint); + + void init(); + + GenerationCache mCache; + + uint32_t mSize; + uint32_t mMaxSize; + GLuint mMaxTextureSize; + + /** + * Used to access mCache and mSize. All methods are accessed from a single + * thread except for remove(). + */ + mutable Mutex mLock; +}; // class PathCache + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_PATH_CACHE_H diff --git a/libs/hwui/Program.cpp b/libs/hwui/Program.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2e1b9a073e3d42a95680fcf9ded5286e1b0cfd9d --- /dev/null +++ b/libs/hwui/Program.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include "Program.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Shaders +/////////////////////////////////////////////////////////////////////////////// + +#define SHADER_SOURCE(name, source) const char* name = #source + +/////////////////////////////////////////////////////////////////////////////// +// Base program +/////////////////////////////////////////////////////////////////////////////// + +Program::Program(const char* vertex, const char* fragment) { + vertexShader = buildShader(vertex, GL_VERTEX_SHADER); + fragmentShader = buildShader(fragment, GL_FRAGMENT_SHADER); + + id = glCreateProgram(); + glAttachShader(id, vertexShader); + glAttachShader(id, fragmentShader); + glLinkProgram(id); + + GLint status; + glGetProgramiv(id, GL_LINK_STATUS, &status); + if (status != GL_TRUE) { + GLint infoLen = 0; + glGetProgramiv(id, GL_INFO_LOG_LENGTH, &infoLen); + if (infoLen > 1) { + GLchar log[infoLen]; + glGetProgramInfoLog(id, infoLen, 0, &log[0]); + LOGE("Error while linking shaders: %s", log); + } + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + glDeleteProgram(id); + } + + mUse = false; + + position = addAttrib("position"); + color = addUniform("color"); + transform = addUniform("transform"); +} + +Program::~Program() { + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + glDeleteProgram(id); +} + +int Program::addAttrib(const char* name) { + int slot = glGetAttribLocation(id, name); + attributes.add(name, slot); + return slot; +} + +int Program::getAttrib(const char* name) { + ssize_t index = attributes.indexOfKey(name); + if (index >= 0) { + return attributes.valueAt(index); + } + return addAttrib(name); +} + +int Program::addUniform(const char* name) { + int slot = glGetUniformLocation(id, name); + uniforms.add(name, slot); + return slot; +} + +int Program::getUniform(const char* name) { + ssize_t index = uniforms.indexOfKey(name); + if (index >= 0) { + return uniforms.valueAt(index); + } + return addUniform(name); +} + +GLuint Program::buildShader(const char* source, GLenum type) { + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, 0); + glCompileShader(shader); + + GLint status; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (status != GL_TRUE) { + // Some drivers return wrong values for GL_INFO_LOG_LENGTH + // use a fixed size instead + GLchar log[512]; + glGetShaderInfoLog(shader, sizeof(log), 0, &log[0]); + LOGE("Error while compiling shader: %s", log); + glDeleteShader(shader); + } + + return shader; +} + +void Program::set(const mat4& projectionMatrix, const mat4& modelViewMatrix, + const mat4& transformMatrix) { + mat4 t(projectionMatrix); + t.multiply(transformMatrix); + t.multiply(modelViewMatrix); + + glUniformMatrix4fv(transform, 1, GL_FALSE, &t.data[0]); +} + +void Program::use() { + glUseProgram(id); + mUse = true; + + glEnableVertexAttribArray(position); +} + +void Program::remove() { + mUse = false; + + glDisableVertexAttribArray(position); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/Program.h b/libs/hwui/Program.h new file mode 100644 index 0000000000000000000000000000000000000000..6531c7486591394f85d2b97a0b51ebd4e1216c10 --- /dev/null +++ b/libs/hwui/Program.h @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_PROGRAM_H +#define ANDROID_UI_PROGRAM_H + +#include +#include + +#include + +#include "Matrix.h" + +namespace android { +namespace uirenderer { + +/** + * A program holds a vertex and a fragment shader. It offers several utility + * methods to query attributes and uniforms. + */ +class Program { +public: + /** + * Creates a new program with the specified vertex and fragment + * shaders sources. + */ + Program(const char* vertex, const char* fragment); + virtual ~Program(); + + /** + * Binds this program to the GL context. + */ + virtual void use(); + + /** + * Marks this program as unused. This will not unbind + * the program from the GL context. + */ + virtual void remove(); + + /** + * Returns the OpenGL name of the specified attribute. + */ + int getAttrib(const char* name); + + /** + * Returns the OpenGL name of the specified uniform. + */ + int getUniform(const char* name); + + /** + * Indicates whether this program is currently in use with + * the GL context. + */ + inline bool isInUse() const { + return mUse; + } + + /** + * Binds the program with the specified projection, modelView and + * transform matrices. + */ + void set(const mat4& projectionMatrix, const mat4& modelViewMatrix, + const mat4& transformMatrix); + + /** + * Name of the position attribute. + */ + int position; + + /** + * Name of the color uniform. + */ + int color; + + /** + * Name of the transform uniform. + */ + int transform; + +protected: + /** + * Adds an attribute with the specified name. + * + * @return The OpenGL name of the attribute. + */ + int addAttrib(const char* name); + + /** + * Adds a uniform with the specified name. + * + * @return The OpenGL name of the uniform. + */ + int addUniform(const char* name); + +private: + /** + * Compiles the specified shader of the specified type. + * + * @return The name of the compiled shader. + */ + GLuint buildShader(const char* source, GLenum type); + + // Name of the OpenGL program + GLuint id; + + // Name of the shaders + GLuint vertexShader; + GLuint fragmentShader; + + // Keeps track of attributes and uniforms slots + KeyedVector attributes; + KeyedVector uniforms; + + bool mUse; +}; // class Program + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_PROGRAM_H diff --git a/libs/hwui/ProgramCache.cpp b/libs/hwui/ProgramCache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3e9412cf0c78fbf339568ee79089c8db22feedae --- /dev/null +++ b/libs/hwui/ProgramCache.cpp @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include "ProgramCache.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Vertex shaders snippets +/////////////////////////////////////////////////////////////////////////////// + +const char* gVS_Header_Attributes = + "attribute vec4 position;\n"; +const char* gVS_Header_Attributes_TexCoords = + "attribute vec2 texCoords;\n"; +const char* gVS_Header_Uniforms = + "uniform mat4 transform;\n"; +const char* gVS_Header_Uniforms_HasGradient[3] = { + // Linear + "uniform float gradientLength;\n" + "uniform vec2 gradient;\n" + "uniform vec2 gradientStart;\n" + "uniform mat4 screenSpace;\n", + // Circular + "uniform vec2 gradientStart;\n" + "uniform mat4 gradientMatrix;\n" + "uniform mat4 screenSpace;\n", + // Sweep + "uniform vec2 gradientStart;\n" + "uniform mat4 gradientMatrix;\n" + "uniform mat4 screenSpace;\n" +}; +const char* gVS_Header_Uniforms_HasBitmap = + "uniform mat4 textureTransform;\n" + "uniform vec2 textureDimension;\n"; +const char* gVS_Header_Varyings_HasTexture = + "varying vec2 outTexCoords;\n"; +const char* gVS_Header_Varyings_HasBitmap = + "varying vec2 outBitmapTexCoords;\n"; +const char* gVS_Header_Varyings_HasGradient[3] = { + // Linear + "varying float index;\n", + // Circular + "varying vec2 circular;\n", + // Sweep + "varying vec2 sweep;\n" +}; +const char* gVS_Main = + "\nvoid main(void) {\n"; +const char* gVS_Main_OutTexCoords = + " outTexCoords = texCoords;\n"; +const char* gVS_Main_OutGradient[3] = { + // Linear + " vec4 location = screenSpace * position;\n" + " index = dot(location.xy - gradientStart, gradient) * gradientLength;\n", + // Circular + " vec4 location = screenSpace * position;\n" + " circular = (gradientMatrix * vec4(location.xy - gradientStart, 0.0, 0.0)).xy;\n", + // Sweep + " vec4 location = screenSpace * position;\n" + " sweep = (gradientMatrix * vec4(location.xy - gradientStart, 0.0, 0.0)).xy;\n" +}; +const char* gVS_Main_OutBitmapTexCoords = + " vec4 bitmapCoords = textureTransform * position;\n" + " outBitmapTexCoords = bitmapCoords.xy * textureDimension;\n"; +const char* gVS_Main_Position = + " gl_Position = transform * position;\n"; +const char* gVS_Footer = + "}\n\n"; + +/////////////////////////////////////////////////////////////////////////////// +// Fragment shaders snippets +/////////////////////////////////////////////////////////////////////////////// + +const char* gFS_Header_Extension_FramebufferFetch = + "#extension GL_NV_shader_framebuffer_fetch : enable\n\n"; +const char* gFS_Header = + "precision mediump float;\n\n"; +const char* gFS_Uniforms_Color = + "uniform vec4 color;\n"; +const char* gFS_Uniforms_TextureSampler = + "uniform sampler2D sampler;\n"; +const char* gFS_Uniforms_GradientSampler[3] = { + // Linear + "uniform sampler2D gradientSampler;\n", + // Circular + "uniform float gradientRadius;\n" + "uniform sampler2D gradientSampler;\n", + // Sweep + "uniform sampler2D gradientSampler;\n" +}; +const char* gFS_Uniforms_BitmapSampler = + "uniform sampler2D bitmapSampler;\n"; +const char* gFS_Uniforms_ColorOp[4] = { + // None + "", + // Matrix + "uniform mat4 colorMatrix;\n" + "uniform vec4 colorMatrixVector;\n", + // Lighting + "uniform vec4 lightingMul;\n" + "uniform vec4 lightingAdd;\n", + // PorterDuff + "uniform vec4 colorBlend;\n" +}; +const char* gFS_Main = + "\nvoid main(void) {\n" + " lowp vec4 fragColor;\n"; +const char* gFS_Main_FetchColor = + " fragColor = color;\n"; +const char* gFS_Main_FetchTexture = + " fragColor = color * texture2D(sampler, outTexCoords);\n"; +const char* gFS_Main_FetchA8Texture = + " fragColor = color * texture2D(sampler, outTexCoords).a;\n"; +const char* gFS_Main_FetchGradient[3] = { + // Linear + " vec4 gradientColor = texture2D(gradientSampler, vec2(index, 0.5));\n", + // Circular + " float index = length(circular) * gradientRadius;\n" + " vec4 gradientColor = texture2D(gradientSampler, vec2(index, 0.5));\n", + // Sweep + " float index = atan(sweep.y, sweep.x) * 0.15915494309; // inv(2 * PI)\n" + " vec4 gradientColor = texture2D(gradientSampler, vec2(index - floor(index), 0.5));\n" +}; +const char* gFS_Main_FetchBitmap = + " vec4 bitmapColor = texture2D(bitmapSampler, outBitmapTexCoords);\n"; +const char* gFS_Main_FetchBitmapNpot = + " vec4 bitmapColor = texture2D(bitmapSampler, wrap(outBitmapTexCoords));\n"; +const char* gFS_Main_BlendShadersBG = + " fragColor = blendShaders(gradientColor, bitmapColor)"; +const char* gFS_Main_BlendShadersGB = + " fragColor = blendShaders(bitmapColor, gradientColor)"; +const char* gFS_Main_BlendShaders_Modulate = + " * fragColor.a;\n"; +const char* gFS_Main_GradientShader_Modulate = + " fragColor = gradientColor * fragColor.a;\n"; +const char* gFS_Main_BitmapShader_Modulate = + " fragColor = bitmapColor * fragColor.a;\n"; +const char* gFS_Main_FragColor = + " gl_FragColor = fragColor;\n"; +const char* gFS_Main_FragColor_Blend = + " gl_FragColor = blendFramebuffer(fragColor, gl_LastFragColor);\n"; +const char* gFS_Main_FragColor_Blend_Swap = + " gl_FragColor = blendFramebuffer(gl_LastFragColor, fragColor);\n"; +const char* gFS_Main_ApplyColorOp[4] = { + // None + "", + // Matrix + // TODO: Fix premultiplied alpha computations for color matrix + " fragColor *= colorMatrix;\n" + " fragColor += colorMatrixVector;\n" + " fragColor.rgb *= fragColor.a;\n", + // Lighting + " float lightingAlpha = fragColor.a;\n" + " fragColor = min(fragColor * lightingMul + (lightingAdd * lightingAlpha), lightingAlpha);\n" + " fragColor.a = lightingAlpha;\n", + // PorterDuff + " fragColor = blendColors(colorBlend, fragColor);\n" +}; +const char* gFS_Footer = + "}\n\n"; + +/////////////////////////////////////////////////////////////////////////////// +// PorterDuff snippets +/////////////////////////////////////////////////////////////////////////////// + +const char* gBlendOps[18] = { + // Clear + "return vec4(0.0, 0.0, 0.0, 0.0);\n", + // Src + "return src;\n", + // Dst + "return dst;\n", + // SrcOver + "return src + dst * (1.0 - src.a);\n", + // DstOver + "return dst + src * (1.0 - dst.a);\n", + // SrcIn + "return src * dst.a;\n", + // DstIn + "return dst * src.a;\n", + // SrcOut + "return src * (1.0 - dst.a);\n", + // DstOut + "return dst * (1.0 - src.a);\n", + // SrcAtop + "return vec4(src.rgb * dst.a + (1.0 - src.a) * dst.rgb, dst.a);\n", + // DstAtop + "return vec4(dst.rgb * src.a + (1.0 - dst.a) * src.rgb, src.a);\n", + // Xor + "return vec4(src.rgb * (1.0 - dst.a) + (1.0 - src.a) * dst.rgb, " + "src.a + dst.a - 2.0 * src.a * dst.a);\n", + // Add + "return min(src + dst, 1.0);\n", + // Multiply + "return src * dst;\n", + // Screen + "return src + dst - src * dst;\n", + // Overlay + "return clamp(vec4(mix(" + "2.0 * src.rgb * dst.rgb + src.rgb * (1.0 - dst.a) + dst.rgb * (1.0 - src.a), " + "src.a * dst.a - 2.0 * (dst.a - dst.rgb) * (src.a - src.rgb) + src.rgb * (1.0 - dst.a) + dst.rgb * (1.0 - src.a), " + "step(dst.a, 2.0 * dst.rgb)), " + "src.a + dst.a - src.a * dst.a), 0.0, 1.0);\n", + // Darken + "return vec4(src.rgb * (1.0 - dst.a) + (1.0 - src.a) * dst.rgb + " + "min(src.rgb * dst.a, dst.rgb * src.a), src.a + dst.a - src.a * dst.a);\n", + // Lighten + "return vec4(src.rgb * (1.0 - dst.a) + (1.0 - src.a) * dst.rgb + " + "max(src.rgb * dst.a, dst.rgb * src.a), src.a + dst.a - src.a * dst.a);\n", +}; + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructors +/////////////////////////////////////////////////////////////////////////////// + +ProgramCache::ProgramCache() { +} + +ProgramCache::~ProgramCache() { + clear(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Cache management +/////////////////////////////////////////////////////////////////////////////// + +void ProgramCache::clear() { + size_t count = mCache.size(); + for (size_t i = 0; i < count; i++) { + delete mCache.valueAt(i); + } + mCache.clear(); +} + +Program* ProgramCache::get(const ProgramDescription& description) { + programid key = description.key(); + ssize_t index = mCache.indexOfKey(key); + Program* program = NULL; + if (index < 0) { + description.log("Could not find program"); + program = generateProgram(description, key); + mCache.add(key, program); + } else { + program = mCache.valueAt(index); + } + return program; +} + +/////////////////////////////////////////////////////////////////////////////// +// Program generation +/////////////////////////////////////////////////////////////////////////////// + +Program* ProgramCache::generateProgram(const ProgramDescription& description, programid key) { + String8 vertexShader = generateVertexShader(description); + String8 fragmentShader = generateFragmentShader(description); + + Program* program = new Program(vertexShader.string(), fragmentShader.string()); + return program; +} + +String8 ProgramCache::generateVertexShader(const ProgramDescription& description) { + // Add attributes + String8 shader(gVS_Header_Attributes); + if (description.hasTexture) { + shader.append(gVS_Header_Attributes_TexCoords); + } + // Uniforms + shader.append(gVS_Header_Uniforms); + if (description.hasGradient) { + shader.append(gVS_Header_Uniforms_HasGradient[description.gradientType]); + } + if (description.hasBitmap) { + shader.append(gVS_Header_Uniforms_HasBitmap); + } + // Varyings + if (description.hasTexture) { + shader.append(gVS_Header_Varyings_HasTexture); + } + if (description.hasGradient) { + shader.append(gVS_Header_Varyings_HasGradient[description.gradientType]); + } + if (description.hasBitmap) { + shader.append(gVS_Header_Varyings_HasBitmap); + } + + // Begin the shader + shader.append(gVS_Main); { + if (description.hasTexture) { + shader.append(gVS_Main_OutTexCoords); + } + if (description.hasGradient) { + shader.append(gVS_Main_OutGradient[description.gradientType]); + } + if (description.hasBitmap) { + shader.append(gVS_Main_OutBitmapTexCoords); + } + // Output transformed position + shader.append(gVS_Main_Position); + } + // End the shader + shader.append(gVS_Footer); + + PROGRAM_LOGD("*** Generated vertex shader:\n\n%s", shader.string()); + + return shader; +} + +String8 ProgramCache::generateFragmentShader(const ProgramDescription& description) { + // Set the default precision + String8 shader; + + bool blendFramebuffer = description.framebufferMode >= SkXfermode::kPlus_Mode; + if (blendFramebuffer) { + shader.append(gFS_Header_Extension_FramebufferFetch); + } + + shader.append(gFS_Header); + + // Varyings + if (description.hasTexture) { + shader.append(gVS_Header_Varyings_HasTexture); + } + if (description.hasGradient) { + shader.append(gVS_Header_Varyings_HasGradient[description.gradientType]); + } + if (description.hasBitmap) { + shader.append(gVS_Header_Varyings_HasBitmap); + } + + + // Uniforms + shader.append(gFS_Uniforms_Color); + if (description.hasTexture) { + shader.append(gFS_Uniforms_TextureSampler); + } + if (description.hasGradient) { + shader.append(gFS_Uniforms_GradientSampler[description.gradientType]); + } + if (description.hasBitmap) { + shader.append(gFS_Uniforms_BitmapSampler); + } + shader.append(gFS_Uniforms_ColorOp[description.colorOp]); + + // Generate required functions + if (description.hasGradient && description.hasBitmap) { + generateBlend(shader, "blendShaders", description.shadersMode); + } + if (description.colorOp == ProgramDescription::kColorBlend) { + generateBlend(shader, "blendColors", description.colorMode); + } + if (blendFramebuffer) { + generateBlend(shader, "blendFramebuffer", description.framebufferMode); + } + if (description.isBitmapNpot) { + generateTextureWrap(shader, description.bitmapWrapS, description.bitmapWrapT); + } + + // Begin the shader + shader.append(gFS_Main); { + // Stores the result in fragColor directly + if (description.hasTexture) { + if (description.hasAlpha8Texture) { + shader.append(gFS_Main_FetchA8Texture); + } else { + shader.append(gFS_Main_FetchTexture); + } + } else { + shader.append(gFS_Main_FetchColor); + } + if (description.hasGradient) { + shader.append(gFS_Main_FetchGradient[description.gradientType]); + } + if (description.hasBitmap) { + if (!description.isBitmapNpot) { + shader.append(gFS_Main_FetchBitmap); + } else { + shader.append(gFS_Main_FetchBitmapNpot); + } + } + // Case when we have two shaders set + if (description.hasGradient && description.hasBitmap) { + if (description.isBitmapFirst) { + shader.append(gFS_Main_BlendShadersBG); + } else { + shader.append(gFS_Main_BlendShadersGB); + } + shader.append(gFS_Main_BlendShaders_Modulate); + } else { + if (description.hasGradient) { + shader.append(gFS_Main_GradientShader_Modulate); + } else if (description.hasBitmap) { + shader.append(gFS_Main_BitmapShader_Modulate); + } + } + // Apply the color op if needed + shader.append(gFS_Main_ApplyColorOp[description.colorOp]); + // Output the fragment + if (!blendFramebuffer) { + shader.append(gFS_Main_FragColor); + } else { + shader.append(!description.swapSrcDst ? + gFS_Main_FragColor_Blend : gFS_Main_FragColor_Blend_Swap); + } + } + // End the shader + shader.append(gFS_Footer); + + if (DEBUG_PROGRAM_CACHE) { + PROGRAM_LOGD("*** Generated fragment shader:\n\n"); + printLongString(shader); + } + + return shader; +} + +void ProgramCache::generateBlend(String8& shader, const char* name, SkXfermode::Mode mode) { + shader.append("\nvec4 "); + shader.append(name); + shader.append("(vec4 src, vec4 dst) {\n"); + shader.append(" "); + shader.append(gBlendOps[mode]); + shader.append("}\n"); +} + +void ProgramCache::generateTextureWrap(String8& shader, GLenum wrapS, GLenum wrapT) { + shader.append("\nvec2 wrap(vec2 texCoords) {\n"); + if (wrapS == GL_MIRRORED_REPEAT) { + shader.append(" float xMod2 = mod(texCoords.x, 2.0);\n"); + shader.append(" if (xMod2 > 1.0) xMod2 = 2.0 - xMod2;\n"); + } + if (wrapT == GL_MIRRORED_REPEAT) { + shader.append(" float yMod2 = mod(texCoords.y, 2.0);\n"); + shader.append(" if (yMod2 > 1.0) yMod2 = 2.0 - yMod2;\n"); + } + shader.append(" return vec2("); + switch (wrapS) { + case GL_CLAMP_TO_EDGE: + shader.append("texCoords.x"); + break; + case GL_REPEAT: + shader.append("mod(texCoords.x, 1.0)"); + break; + case GL_MIRRORED_REPEAT: + shader.append("xMod2"); + break; + } + shader.append(", "); + switch (wrapT) { + case GL_CLAMP_TO_EDGE: + shader.append("texCoords.y"); + break; + case GL_REPEAT: + shader.append("mod(texCoords.y, 1.0)"); + break; + case GL_MIRRORED_REPEAT: + shader.append("yMod2"); + break; + } + shader.append(");\n"); + shader.append("}\n"); +} + +void ProgramCache::printLongString(const String8& shader) const { + ssize_t index = 0; + ssize_t lastIndex = 0; + const char* str = shader.string(); + while ((index = shader.find("\n", index)) > -1) { + String8 line(str, index - lastIndex); + if (line.length() == 0) line.append("\n"); + PROGRAM_LOGD("%s", line.string()); + index++; + str += (index - lastIndex); + lastIndex = index; + } +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/ProgramCache.h b/libs/hwui/ProgramCache.h new file mode 100644 index 0000000000000000000000000000000000000000..4fa80111d358b0a46c1adf3afb4449625732dabf --- /dev/null +++ b/libs/hwui/ProgramCache.h @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_PROGRAM_CACHE_H +#define ANDROID_UI_PROGRAM_CACHE_H + +#include +#include +#include + +#include + +#include + +#include "Program.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Defines +/////////////////////////////////////////////////////////////////////////////// + +// Debug +#define DEBUG_PROGRAM_CACHE 0 + +// Debug +#if DEBUG_PROGRAM_CACHE + #define PROGRAM_LOGD(...) LOGD(__VA_ARGS__) +#else + #define PROGRAM_LOGD(...) +#endif + +#define PROGRAM_KEY_TEXTURE 0x1 +#define PROGRAM_KEY_A8_TEXTURE 0x2 +#define PROGRAM_KEY_BITMAP 0x4 +#define PROGRAM_KEY_GRADIENT 0x8 +#define PROGRAM_KEY_BITMAP_FIRST 0x10 +#define PROGRAM_KEY_COLOR_MATRIX 0x20 +#define PROGRAM_KEY_COLOR_LIGHTING 0x40 +#define PROGRAM_KEY_COLOR_BLEND 0x80 +#define PROGRAM_KEY_BITMAP_NPOT 0x100 +#define PROGRAM_KEY_SWAP_SRC_DST 0x2000 + +#define PROGRAM_KEY_BITMAP_WRAPS_MASK 0x600 +#define PROGRAM_KEY_BITMAP_WRAPT_MASK 0x1800 + +// Encode the xfermodes on 6 bits +#define PROGRAM_MAX_XFERMODE 0x1f +#define PROGRAM_XFERMODE_SHADER_SHIFT 26 +#define PROGRAM_XFERMODE_COLOR_OP_SHIFT 20 +#define PROGRAM_XFERMODE_FRAMEBUFFER_SHIFT 14 + +#define PROGRAM_BITMAP_WRAPS_SHIFT 9 +#define PROGRAM_BITMAP_WRAPT_SHIFT 11 + +#define PROGRAM_GRADIENT_TYPE_SHIFT 33 + +/////////////////////////////////////////////////////////////////////////////// +// Types +/////////////////////////////////////////////////////////////////////////////// + +typedef uint64_t programid; + +/////////////////////////////////////////////////////////////////////////////// +// Cache +/////////////////////////////////////////////////////////////////////////////// + +/** + * Describe the features required for a given program. The features + * determine the generation of both the vertex and fragment shaders. + * A ProgramDescription must be used in conjunction with a ProgramCache. + */ +struct ProgramDescription { + enum ColorModifier { + kColorNone, + kColorMatrix, + kColorLighting, + kColorBlend + }; + + enum Gradient { + kGradientLinear, + kGradientCircular, + kGradientSweep + }; + + ProgramDescription(): + hasTexture(false), hasAlpha8Texture(false), + hasBitmap(false), isBitmapNpot(false), hasGradient(false), + gradientType(kGradientLinear), + shadersMode(SkXfermode::kClear_Mode), isBitmapFirst(false), + bitmapWrapS(GL_CLAMP_TO_EDGE), bitmapWrapT(GL_CLAMP_TO_EDGE), + colorOp(kColorNone), colorMode(SkXfermode::kClear_Mode), + framebufferMode(SkXfermode::kClear_Mode), swapSrcDst(false) { + } + + // Texturing + bool hasTexture; + bool hasAlpha8Texture; + + // Shaders + bool hasBitmap; + bool isBitmapNpot; + + bool hasGradient; + Gradient gradientType; + + SkXfermode::Mode shadersMode; + + bool isBitmapFirst; + GLenum bitmapWrapS; + GLenum bitmapWrapT; + + // Color operations + int colorOp; + SkXfermode::Mode colorMode; + + // Framebuffer blending (requires Extensions.hasFramebufferFetch()) + // Ignored for all values < SkXfermode::kPlus_Mode + SkXfermode::Mode framebufferMode; + bool swapSrcDst; + + inline uint32_t getEnumForWrap(GLenum wrap) const { + switch (wrap) { + case GL_CLAMP_TO_EDGE: + return 0; + case GL_REPEAT: + return 1; + case GL_MIRRORED_REPEAT: + return 2; + } + return 0; + } + + programid key() const { + programid key = 0; + if (hasTexture) key |= PROGRAM_KEY_TEXTURE; + if (hasAlpha8Texture) key |= PROGRAM_KEY_A8_TEXTURE; + if (hasBitmap) { + key |= PROGRAM_KEY_BITMAP; + if (isBitmapNpot) { + key |= PROGRAM_KEY_BITMAP_NPOT; + key |= getEnumForWrap(bitmapWrapS) << PROGRAM_BITMAP_WRAPS_SHIFT; + key |= getEnumForWrap(bitmapWrapT) << PROGRAM_BITMAP_WRAPT_SHIFT; + } + } + if (hasGradient) key |= PROGRAM_KEY_GRADIENT; + key |= programid(gradientType) << PROGRAM_GRADIENT_TYPE_SHIFT; + if (isBitmapFirst) key |= PROGRAM_KEY_BITMAP_FIRST; + if (hasBitmap && hasGradient) { + key |= (shadersMode & PROGRAM_MAX_XFERMODE) << PROGRAM_XFERMODE_SHADER_SHIFT; + } + switch (colorOp) { + case kColorMatrix: + key |= PROGRAM_KEY_COLOR_MATRIX; + break; + case kColorLighting: + key |= PROGRAM_KEY_COLOR_LIGHTING; + break; + case kColorBlend: + key |= PROGRAM_KEY_COLOR_BLEND; + key |= (colorMode & PROGRAM_MAX_XFERMODE) << PROGRAM_XFERMODE_COLOR_OP_SHIFT; + break; + case kColorNone: + break; + } + key |= (framebufferMode & PROGRAM_MAX_XFERMODE) << PROGRAM_XFERMODE_FRAMEBUFFER_SHIFT; + if (swapSrcDst) key |= PROGRAM_KEY_SWAP_SRC_DST; + return key; + } + + void log(const char* message) const { + programid k = key(); + PROGRAM_LOGD("%s (key = 0x%.8x%.8x)", message, uint32_t(k >> 32), + uint32_t(k & 0xffffffff)); + } +}; // struct ProgramDescription + +/** + * Generates and caches program. Programs are generated based on + * ProgramDescriptions. + */ +class ProgramCache { +public: + ProgramCache(); + ~ProgramCache(); + + Program* get(const ProgramDescription& description); + + void clear(); + +private: + Program* generateProgram(const ProgramDescription& description, programid key); + String8 generateVertexShader(const ProgramDescription& description); + String8 generateFragmentShader(const ProgramDescription& description); + void generateBlend(String8& shader, const char* name, SkXfermode::Mode mode); + void generateTextureWrap(String8& shader, GLenum wrapS, GLenum wrapT); + + void printLongString(const String8& shader) const; + + KeyedVector mCache; +}; // class ProgramCache + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_PROGRAM_CACHE_H diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h new file mode 100644 index 0000000000000000000000000000000000000000..4e2f0914c8abb28d7eebbc684c76e41e1d30f7a9 --- /dev/null +++ b/libs/hwui/Properties.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_PROPERTIES_H +#define ANDROID_UI_PROPERTIES_H + +#include + +/** + * This file contains the list of system properties used to configure + * the OpenGLRenderer. + */ + +// These properties are defined in mega-bytes +#define PROPERTY_TEXTURE_CACHE_SIZE "ro.hwui.texture_cache_size" +#define PROPERTY_LAYER_CACHE_SIZE "ro.hwui.layer_cache_size" +#define PROPERTY_GRADIENT_CACHE_SIZE "ro.hwui.gradient_cache_size" +#define PROPERTY_PATH_CACHE_SIZE "ro.hwui.path_cache_size" +#define PROPERTY_DROP_SHADOW_CACHE_SIZE "ro.hwui.drop_shadow_cache_size" + +// These properties are defined in pixels +#define PROPERTY_TEXT_CACHE_WIDTH "ro.hwui.text_cache_width" +#define PROPERTY_TEXT_CACHE_HEIGHT "ro.hwui.text_cache_height" + +// Gamma (>= 1.0, <= 10.0) +#define PROPERTY_TEXT_GAMMA "ro.text_gamma" +#define PROPERTY_TEXT_BLACK_GAMMA_THRESHOLD "ro.text_gamma.black_threshold" +#define PROPERTY_TEXT_WHITE_GAMMA_THRESHOLD "ro.text_gamma.white_threshold" + +// Converts a number of mega-bytes into bytes +#define MB(s) s * 1024 * 1024 + +#define DEFAULT_TEXTURE_CACHE_SIZE 20.0f +#define DEFAULT_LAYER_CACHE_SIZE 6.0f +#define DEFAULT_PATH_CACHE_SIZE 6.0f +#define DEFAULT_PATCH_CACHE_SIZE 100 +#define DEFAULT_GRADIENT_CACHE_SIZE 0.5f +#define DEFAULT_DROP_SHADOW_CACHE_SIZE 2.0f + +#define DEFAULT_TEXT_GAMMA 1.4f +#define DEFAULT_TEXT_BLACK_GAMMA_THRESHOLD 64 +#define DEFAULT_TEXT_WHITE_GAMMA_THRESHOLD 192 + +#endif // ANDROID_UI_PROPERTIES_H diff --git a/libs/hwui/Rect.h b/libs/hwui/Rect.h new file mode 100644 index 0000000000000000000000000000000000000000..f03a602ba06e85ffa183f7cde8ee565e60209c0d --- /dev/null +++ b/libs/hwui/Rect.h @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_RECT_H +#define ANDROID_UI_RECT_H + +#include + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Structs +/////////////////////////////////////////////////////////////////////////////// + +struct Rect { + float left; + float top; + float right; + float bottom; + + Rect(): + left(0), + top(0), + right(0), + bottom(0) { + } + + Rect(float left, float top, float right, float bottom): + left(left), + top(top), + right(right), + bottom(bottom) { + } + + Rect(const Rect& r) { + set(r); + } + + Rect(Rect& r) { + set(r); + } + + Rect& operator=(const Rect& r) { + set(r); + return *this; + } + + Rect& operator=(Rect& r) { + set(r); + return *this; + } + + friend int operator==(const Rect& a, const Rect& b) { + return !memcmp(&a, &b, sizeof(a)); + } + + friend int operator!=(const Rect& a, const Rect& b) { + return memcmp(&a, &b, sizeof(a)); + } + + bool isEmpty() const { + return left >= right || top >= bottom; + } + + void setEmpty() { + memset(this, 0, sizeof(*this)); + } + + void set(float left, float top, float right, float bottom) { + this->left = left; + this->right = right; + this->top = top; + this->bottom = bottom; + } + + void set(const Rect& r) { + set(r.left, r.top, r.right, r.bottom); + } + + inline float getWidth() const { + return right - left; + } + + inline float getHeight() const { + return bottom - top; + } + + bool intersects(float left, float top, float right, float bottom) const { + return left < right && top < bottom && + this->left < this->right && this->top < this->bottom && + this->left < right && left < this->right && + this->top < bottom && top < this->bottom; + } + + bool intersects(const Rect& r) const { + return intersects(r.left, r.top, r.right, r.bottom); + } + + bool intersect(float left, float top, float right, float bottom) { + if (left < right && top < bottom && !this->isEmpty() && + this->left < right && left < this->right && + this->top < bottom && top < this->bottom) { + + if (this->left < left) this->left = left; + if (this->top < top) this->top = top; + if (this->right > right) this->right = right; + if (this->bottom > bottom) this->bottom = bottom; + + return true; + } + return false; + } + + bool intersect(const Rect& r) { + return intersect(r.left, r.top, r.right, r.bottom); + } + + bool unionWith(const Rect& r) { + if (r.left < r.right && r.top < r.bottom) { + if (left < right && top < bottom) { + if (left > r.left) left = r.left; + if (top > r.top) top = r.top; + if (right < r.right) right = r.right; + if (bottom < r.bottom) bottom = r.bottom; + return true; + } else { + left = r.left; + top = r.top; + right = r.right; + bottom = r.bottom; + return true; + } + } + return false; + } + + void snapToPixelBoundaries() { + left = floor(left); + top = floor(top); + right = ceil(right); + bottom = ceil(bottom); + } + + void dump() const { + LOGD("Rect[l=%f t=%f r=%f b=%f]", left, top, right, bottom); + } + +}; // struct Rect + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_RECT_H diff --git a/libs/hwui/SkiaColorFilter.cpp b/libs/hwui/SkiaColorFilter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fe57ae73b613ee86517f33e7dc5ec15fcfcb36a0 --- /dev/null +++ b/libs/hwui/SkiaColorFilter.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SkiaColorFilter.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Base color filter +/////////////////////////////////////////////////////////////////////////////// + +SkiaColorFilter::SkiaColorFilter(Type type, bool blend): mType(type), mBlend(blend) { +} + +SkiaColorFilter::~SkiaColorFilter() { +} + +/////////////////////////////////////////////////////////////////////////////// +// Color matrix filter +/////////////////////////////////////////////////////////////////////////////// + +SkiaColorMatrixFilter::SkiaColorMatrixFilter(float* matrix, float* vector): + SkiaColorFilter(kColorMatrix, true), mMatrix(matrix), mVector(vector) { +} + +SkiaColorMatrixFilter::~SkiaColorMatrixFilter() { + delete[] mMatrix; + delete[] mVector; +} + +void SkiaColorMatrixFilter::describe(ProgramDescription& description, + const Extensions& extensions) { + description.colorOp = ProgramDescription::kColorMatrix; +} + +void SkiaColorMatrixFilter::setupProgram(Program* program) { + glUniformMatrix4fv(program->getUniform("colorMatrix"), 1, GL_FALSE, &mMatrix[0]); + glUniform4fv(program->getUniform("colorMatrixVector"), 1, mVector); +} + +/////////////////////////////////////////////////////////////////////////////// +// Lighting color filter +/////////////////////////////////////////////////////////////////////////////// + +SkiaLightingFilter::SkiaLightingFilter(int multiply, int add): + SkiaColorFilter(kLighting, true) { + mMulR = ((multiply >> 16) & 0xFF) / 255.0f; + mMulG = ((multiply >> 8) & 0xFF) / 255.0f; + mMulB = ((multiply ) & 0xFF) / 255.0f; + + mAddR = ((add >> 16) & 0xFF) / 255.0f; + mAddG = ((add >> 8) & 0xFF) / 255.0f; + mAddB = ((add ) & 0xFF) / 255.0f; +} + +void SkiaLightingFilter::describe(ProgramDescription& description, const Extensions& extensions) { + description.colorOp = ProgramDescription::kColorLighting; +} + +void SkiaLightingFilter::setupProgram(Program* program) { + glUniform4f(program->getUniform("lightingMul"), mMulR, mMulG, mMulB, 1.0f); + glUniform4f(program->getUniform("lightingAdd"), mAddR, mAddG, mAddB, 0.0f); +} + +/////////////////////////////////////////////////////////////////////////////// +// Blend color filter +/////////////////////////////////////////////////////////////////////////////// + +SkiaBlendFilter::SkiaBlendFilter(int color, SkXfermode::Mode mode): + SkiaColorFilter(kBlend, true), mMode(mode) { + const int alpha = (color >> 24) & 0xFF; + mA = alpha / 255.0f; + mR = mA * ((color >> 16) & 0xFF) / 255.0f; + mG = mA * ((color >> 8) & 0xFF) / 255.0f; + mB = mA * ((color ) & 0xFF) / 255.0f; +} + +void SkiaBlendFilter::describe(ProgramDescription& description, const Extensions& extensions) { + description.colorOp = ProgramDescription::kColorBlend; + description.colorMode = mMode; +} + +void SkiaBlendFilter::setupProgram(Program* program) { + glUniform4f(program->getUniform("colorBlend"), mR, mG, mB, mA); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/SkiaColorFilter.h b/libs/hwui/SkiaColorFilter.h new file mode 100644 index 0000000000000000000000000000000000000000..865b6f0b06e76ea15b5cb682de84abde1248faac --- /dev/null +++ b/libs/hwui/SkiaColorFilter.h @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_SKIA_COLOR_FILTER_H +#define ANDROID_UI_SKIA_COLOR_FILTER_H + +#include + +#include "ProgramCache.h" +#include "Extensions.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Base color filter +/////////////////////////////////////////////////////////////////////////////// + +/** + * Represents a Skia color filter. A color filter modifies a ProgramDescription + * and sets uniforms on the resulting shaders. + */ +struct SkiaColorFilter { + /** + * Type of Skia color filter in use. + */ + enum Type { + kNone, + kColorMatrix, + kLighting, + kBlend, + }; + + SkiaColorFilter(Type type, bool blend); + virtual ~SkiaColorFilter(); + + virtual void describe(ProgramDescription& description, const Extensions& extensions) = 0; + virtual void setupProgram(Program* program) = 0; + + inline bool blend() const { + return mBlend; + } + + Type type() const { + return mType; + } + +protected: + Type mType; + bool mBlend; +}; // struct SkiaColorFilter + +/////////////////////////////////////////////////////////////////////////////// +// Implementations +/////////////////////////////////////////////////////////////////////////////// + +/** + * A color filter that multiplies the source color with a matrix and adds a vector. + */ +struct SkiaColorMatrixFilter: public SkiaColorFilter { + SkiaColorMatrixFilter(float* matrix, float* vector); + ~SkiaColorMatrixFilter(); + + void describe(ProgramDescription& description, const Extensions& extensions); + void setupProgram(Program* program); + +private: + float* mMatrix; + float* mVector; +}; // struct SkiaColorMatrixFilter + +/** + * A color filters that multiplies the source color with a fixed value and adds + * another fixed value. Ignores the alpha channel of both arguments. + */ +struct SkiaLightingFilter: public SkiaColorFilter { + SkiaLightingFilter(int multiply, int add); + + void describe(ProgramDescription& description, const Extensions& extensions); + void setupProgram(Program* program); + +private: + GLfloat mMulR, mMulG, mMulB; + GLfloat mAddR, mAddG, mAddB; +}; // struct SkiaLightingFilter + +/** + * A color filters that blends the source color with a specified destination color + * and PorterDuff blending mode. + */ +struct SkiaBlendFilter: public SkiaColorFilter { + SkiaBlendFilter(int color, SkXfermode::Mode mode); + + void describe(ProgramDescription& description, const Extensions& extensions); + void setupProgram(Program* program); + +private: + SkXfermode::Mode mMode; + GLfloat mR, mG, mB, mA; +}; // struct SkiaBlendFilter + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_SKIA_COLOR_FILTER_H diff --git a/libs/hwui/SkiaShader.cpp b/libs/hwui/SkiaShader.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9e1f6c2e862bfb09205f7ab3376d834d045e36d4 --- /dev/null +++ b/libs/hwui/SkiaShader.cpp @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include + +#include "SkiaShader.h" +#include "Texture.h" +#include "Matrix.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Support +/////////////////////////////////////////////////////////////////////////////// + +static const GLenum gTextureUnitsMap[] = { + GL_TEXTURE0, + GL_TEXTURE1, + GL_TEXTURE2 +}; + +static const GLint gTileModes[] = { + GL_CLAMP_TO_EDGE, // == SkShader::kClamp_TileMode + GL_REPEAT, // == SkShader::kRepeat_Mode + GL_MIRRORED_REPEAT // == SkShader::kMirror_TileMode +}; + +/////////////////////////////////////////////////////////////////////////////// +// Base shader +/////////////////////////////////////////////////////////////////////////////// + +SkiaShader::SkiaShader(Type type, SkShader* key, SkShader::TileMode tileX, + SkShader::TileMode tileY, SkMatrix* matrix, bool blend): + mType(type), mKey(key), mTileX(tileX), mTileY(tileY), mMatrix(matrix), mBlend(blend) { +} + +SkiaShader::~SkiaShader() { +} + +void SkiaShader::describe(ProgramDescription& description, const Extensions& extensions) { +} + +void SkiaShader::setupProgram(Program* program, const mat4& modelView, const Snapshot& snapshot, + GLuint* textureUnit) { +} + +void SkiaShader::bindTexture(GLuint texture, GLenum wrapS, GLenum wrapT, GLuint textureUnit) { + glActiveTexture(gTextureUnitsMap[textureUnit]); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapT); +} + +/////////////////////////////////////////////////////////////////////////////// +// Bitmap shader +/////////////////////////////////////////////////////////////////////////////// + +SkiaBitmapShader::SkiaBitmapShader(SkBitmap* bitmap, SkShader* key, SkShader::TileMode tileX, + SkShader::TileMode tileY, SkMatrix* matrix, bool blend): + SkiaShader(kBitmap, key, tileX, tileY, matrix, blend), mBitmap(bitmap), mTexture(NULL) { +} + +void SkiaBitmapShader::describe(ProgramDescription& description, const Extensions& extensions) { + const Texture* texture = mTextureCache->get(mBitmap); + if (!texture) return; + mTexture = texture; + + const float width = texture->width; + const float height = texture->height; + + description.hasBitmap = true; + // The driver does not support non-power of two mirrored/repeated + // textures, so do it ourselves + if (!extensions.hasNPot() && (!isPowerOfTwo(width) || !isPowerOfTwo(height)) && + (mTileX != SkShader::kClamp_TileMode || mTileY != SkShader::kClamp_TileMode)) { + description.isBitmapNpot = true; + description.bitmapWrapS = gTileModes[mTileX]; + description.bitmapWrapT = gTileModes[mTileY]; + } +} + +void SkiaBitmapShader::setupProgram(Program* program, const mat4& modelView, + const Snapshot& snapshot, GLuint* textureUnit) { + GLuint textureSlot = (*textureUnit)++; + glActiveTexture(gTextureUnitsMap[textureSlot]); + + const Texture* texture = mTexture; + mTexture = NULL; + if (!texture) return; + const AutoTexture autoCleanup(texture); + + const float width = texture->width; + const float height = texture->height; + + mat4 textureTransform; + if (mMatrix) { + SkMatrix inverse; + mMatrix->invert(&inverse); + textureTransform.load(inverse); + textureTransform.multiply(modelView); + } else { + textureTransform.load(modelView); + } + + // Uniforms + bindTexture(texture->id, gTileModes[mTileX], gTileModes[mTileY], textureSlot); + glUniform1i(program->getUniform("bitmapSampler"), textureSlot); + glUniformMatrix4fv(program->getUniform("textureTransform"), 1, + GL_FALSE, &textureTransform.data[0]); + glUniform2f(program->getUniform("textureDimension"), 1.0f / width, 1.0f / height); +} + +void SkiaBitmapShader::updateTransforms(Program* program, const mat4& modelView, + const Snapshot& snapshot) { + mat4 textureTransform; + if (mMatrix) { + SkMatrix inverse; + mMatrix->invert(&inverse); + textureTransform.load(inverse); + textureTransform.multiply(modelView); + } else { + textureTransform.load(modelView); + } + + glUniformMatrix4fv(program->getUniform("textureTransform"), 1, + GL_FALSE, &textureTransform.data[0]); +} + +/////////////////////////////////////////////////////////////////////////////// +// Linear gradient shader +/////////////////////////////////////////////////////////////////////////////// + +SkiaLinearGradientShader::SkiaLinearGradientShader(float* bounds, uint32_t* colors, + float* positions, int count, SkShader* key, SkShader::TileMode tileMode, + SkMatrix* matrix, bool blend): + SkiaShader(kLinearGradient, key, tileMode, tileMode, matrix, blend), + mBounds(bounds), mColors(colors), mPositions(positions), mCount(count) { +} + +SkiaLinearGradientShader::~SkiaLinearGradientShader() { + delete[] mBounds; + delete[] mColors; + delete[] mPositions; +} + +void SkiaLinearGradientShader::describe(ProgramDescription& description, + const Extensions& extensions) { + description.hasGradient = true; + description.gradientType = ProgramDescription::kGradientLinear; +} + +void SkiaLinearGradientShader::setupProgram(Program* program, const mat4& modelView, + const Snapshot& snapshot, GLuint* textureUnit) { + GLuint textureSlot = (*textureUnit)++; + glActiveTexture(gTextureUnitsMap[textureSlot]); + + Texture* texture = mGradientCache->get(mKey); + if (!texture) { + texture = mGradientCache->addLinearGradient(mKey, mColors, mPositions, mCount, mTileX); + } + + Rect start(mBounds[0], mBounds[1], mBounds[2], mBounds[3]); + if (mMatrix) { + mat4 shaderMatrix(*mMatrix); + shaderMatrix.mapPoint(start.left, start.top); + shaderMatrix.mapPoint(start.right, start.bottom); + } + snapshot.transform->mapRect(start); + + const float gradientX = start.right - start.left; + const float gradientY = start.bottom - start.top; + + mat4 screenSpace(*snapshot.transform); + screenSpace.multiply(modelView); + + // Uniforms + bindTexture(texture->id, gTileModes[mTileX], gTileModes[mTileY], textureSlot); + glUniform1i(program->getUniform("gradientSampler"), textureSlot); + glUniform2f(program->getUniform("gradientStart"), start.left, start.top); + glUniform2f(program->getUniform("gradient"), gradientX, gradientY); + glUniform1f(program->getUniform("gradientLength"), + 1.0f / (gradientX * gradientX + gradientY * gradientY)); + glUniformMatrix4fv(program->getUniform("screenSpace"), 1, GL_FALSE, &screenSpace.data[0]); +} + +void SkiaLinearGradientShader::updateTransforms(Program* program, const mat4& modelView, + const Snapshot& snapshot) { + mat4 screenSpace(*snapshot.transform); + screenSpace.multiply(modelView); + glUniformMatrix4fv(program->getUniform("screenSpace"), 1, GL_FALSE, &screenSpace.data[0]); +} + +/////////////////////////////////////////////////////////////////////////////// +// Circular gradient shader +/////////////////////////////////////////////////////////////////////////////// + +SkiaCircularGradientShader::SkiaCircularGradientShader(float x, float y, float radius, + uint32_t* colors, float* positions, int count, SkShader* key, SkShader::TileMode tileMode, + SkMatrix* matrix, bool blend): + SkiaSweepGradientShader(kCircularGradient, x, y, colors, positions, count, key, + tileMode, matrix, blend), + mRadius(radius) { +} + +void SkiaCircularGradientShader::describe(ProgramDescription& description, + const Extensions& extensions) { + description.hasGradient = true; + description.gradientType = ProgramDescription::kGradientCircular; +} + +void SkiaCircularGradientShader::setupProgram(Program* program, const mat4& modelView, + const Snapshot& snapshot, GLuint* textureUnit) { + SkiaSweepGradientShader::setupProgram(program, modelView, snapshot, textureUnit); + glUniform1f(program->getUniform("gradientRadius"), 1.0f / mRadius); +} + +/////////////////////////////////////////////////////////////////////////////// +// Sweep gradient shader +/////////////////////////////////////////////////////////////////////////////// + +SkiaSweepGradientShader::SkiaSweepGradientShader(float x, float y, uint32_t* colors, + float* positions, int count, SkShader* key, SkMatrix* matrix, bool blend): + SkiaShader(kSweepGradient, key, SkShader::kClamp_TileMode, + SkShader::kClamp_TileMode, matrix, blend), + mX(x), mY(y), mColors(colors), mPositions(positions), mCount(count) { +} + +SkiaSweepGradientShader::SkiaSweepGradientShader(Type type, float x, float y, uint32_t* colors, + float* positions, int count, SkShader* key, SkShader::TileMode tileMode, + SkMatrix* matrix, bool blend): + SkiaShader(type, key, tileMode, tileMode, matrix, blend), + mX(x), mY(y), mColors(colors), mPositions(positions), mCount(count) { +} + +SkiaSweepGradientShader::~SkiaSweepGradientShader() { + delete[] mColors; + delete[] mPositions; +} + +void SkiaSweepGradientShader::describe(ProgramDescription& description, + const Extensions& extensions) { + description.hasGradient = true; + description.gradientType = ProgramDescription::kGradientSweep; +} + +void SkiaSweepGradientShader::setupProgram(Program* program, const mat4& modelView, + const Snapshot& snapshot, GLuint* textureUnit) { + GLuint textureSlot = (*textureUnit)++; + glActiveTexture(gTextureUnitsMap[textureSlot]); + + Texture* texture = mGradientCache->get(mKey); + if (!texture) { + texture = mGradientCache->addLinearGradient(mKey, mColors, mPositions, mCount); + } + + float left = mX; + float top = mY; + + mat4 shaderMatrix; + if (mMatrix) { + shaderMatrix.load(*mMatrix); + shaderMatrix.mapPoint(left, top); + } + + mat4 copy(shaderMatrix); + shaderMatrix.loadInverse(copy); + + snapshot.transform->mapPoint(left, top); + + mat4 screenSpace(*snapshot.transform); + screenSpace.multiply(modelView); + + // Uniforms + bindTexture(texture->id, gTileModes[mTileX], gTileModes[mTileY], textureSlot); + glUniform1i(program->getUniform("gradientSampler"), textureSlot); + glUniformMatrix4fv(program->getUniform("gradientMatrix"), 1, GL_FALSE, &shaderMatrix.data[0]); + glUniform2f(program->getUniform("gradientStart"), left, top); + glUniformMatrix4fv(program->getUniform("screenSpace"), 1, GL_FALSE, &screenSpace.data[0]); +} + +void SkiaSweepGradientShader::updateTransforms(Program* program, const mat4& modelView, + const Snapshot& snapshot) { + mat4 screenSpace(*snapshot.transform); + screenSpace.multiply(modelView); + glUniformMatrix4fv(program->getUniform("screenSpace"), 1, GL_FALSE, &screenSpace.data[0]); +} + +/////////////////////////////////////////////////////////////////////////////// +// Compose shader +/////////////////////////////////////////////////////////////////////////////// + +SkiaComposeShader::SkiaComposeShader(SkiaShader* first, SkiaShader* second, + SkXfermode::Mode mode, SkShader* key): + SkiaShader(kCompose, key, SkShader::kClamp_TileMode, SkShader::kClamp_TileMode, + NULL, first->blend() || second->blend()), mFirst(first), mSecond(second), mMode(mode) { +} + +void SkiaComposeShader::set(TextureCache* textureCache, GradientCache* gradientCache) { + SkiaShader::set(textureCache, gradientCache); + mFirst->set(textureCache, gradientCache); + mSecond->set(textureCache, gradientCache); +} + +void SkiaComposeShader::describe(ProgramDescription& description, const Extensions& extensions) { + mFirst->describe(description, extensions); + mSecond->describe(description, extensions); + if (mFirst->type() == kBitmap) { + description.isBitmapFirst = true; + } + description.shadersMode = mMode; +} + +void SkiaComposeShader::setupProgram(Program* program, const mat4& modelView, + const Snapshot& snapshot, GLuint* textureUnit) { + mFirst->setupProgram(program, modelView, snapshot, textureUnit); + mSecond->setupProgram(program, modelView, snapshot, textureUnit); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/SkiaShader.h b/libs/hwui/SkiaShader.h new file mode 100644 index 0000000000000000000000000000000000000000..0023c46465af64ea7182e42830cbf8f7f2a2a2e3 --- /dev/null +++ b/libs/hwui/SkiaShader.h @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_SKIA_SHADER_H +#define ANDROID_UI_SKIA_SHADER_H + +#include +#include + +#include + +#include "Extensions.h" +#include "ProgramCache.h" +#include "TextureCache.h" +#include "GradientCache.h" +#include "Snapshot.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Base shader +/////////////////////////////////////////////////////////////////////////////// + +/** + * Represents a Skia shader. A shader will modify the GL context and active + * program to recreate the original effect. + */ +struct SkiaShader { + /** + * Type of Skia shader in use. + */ + enum Type { + kNone, + kBitmap, + kLinearGradient, + kCircularGradient, + kSweepGradient, + kCompose + }; + + SkiaShader(Type type, SkShader* key, SkShader::TileMode tileX, SkShader::TileMode tileY, + SkMatrix* matrix, bool blend); + virtual ~SkiaShader(); + + virtual void describe(ProgramDescription& description, const Extensions& extensions); + virtual void setupProgram(Program* program, const mat4& modelView, const Snapshot& snapshot, + GLuint* textureUnit); + + inline bool blend() const { + return mBlend; + } + + Type type() const { + return mType; + } + + virtual void set(TextureCache* textureCache, GradientCache* gradientCache) { + mTextureCache = textureCache; + mGradientCache = gradientCache; + } + + virtual void updateTransforms(Program* program, const mat4& modelView, + const Snapshot& snapshot) { + } + + void setMatrix(SkMatrix* matrix) { + mMatrix = matrix; + } + +protected: + inline void bindTexture(GLuint texture, GLenum wrapS, GLenum wrapT, GLuint textureUnit); + + Type mType; + SkShader* mKey; + SkShader::TileMode mTileX; + SkShader::TileMode mTileY; + SkMatrix* mMatrix; + bool mBlend; + + TextureCache* mTextureCache; + GradientCache* mGradientCache; +}; // struct SkiaShader + + +/////////////////////////////////////////////////////////////////////////////// +// Implementations +/////////////////////////////////////////////////////////////////////////////// + +/** + * A shader that draws a bitmap. + */ +struct SkiaBitmapShader: public SkiaShader { + SkiaBitmapShader(SkBitmap* bitmap, SkShader* key, SkShader::TileMode tileX, + SkShader::TileMode tileY, SkMatrix* matrix, bool blend); + + void describe(ProgramDescription& description, const Extensions& extensions); + void setupProgram(Program* program, const mat4& modelView, const Snapshot& snapshot, + GLuint* textureUnit); + void updateTransforms(Program* program, const mat4& modelView, const Snapshot& snapshot); + +private: + /** + * This method does not work for n == 0. + */ + inline bool isPowerOfTwo(unsigned int n) { + return !(n & (n - 1)); + } + + SkBitmap* mBitmap; + const Texture* mTexture; +}; // struct SkiaBitmapShader + +/** + * A shader that draws a linear gradient. + */ +struct SkiaLinearGradientShader: public SkiaShader { + SkiaLinearGradientShader(float* bounds, uint32_t* colors, float* positions, int count, + SkShader* key, SkShader::TileMode tileMode, SkMatrix* matrix, bool blend); + ~SkiaLinearGradientShader(); + + void describe(ProgramDescription& description, const Extensions& extensions); + void setupProgram(Program* program, const mat4& modelView, const Snapshot& snapshot, + GLuint* textureUnit); + void updateTransforms(Program* program, const mat4& modelView, const Snapshot& snapshot); + +private: + float* mBounds; + uint32_t* mColors; + float* mPositions; + int mCount; +}; // struct SkiaLinearGradientShader + +/** + * A shader that draws a sweep gradient. + */ +struct SkiaSweepGradientShader: public SkiaShader { + SkiaSweepGradientShader(float x, float y, uint32_t* colors, float* positions, int count, + SkShader* key, SkMatrix* matrix, bool blend); + ~SkiaSweepGradientShader(); + + virtual void describe(ProgramDescription& description, const Extensions& extensions); + virtual void setupProgram(Program* program, const mat4& modelView, const Snapshot& snapshot, + GLuint* textureUnit); + void updateTransforms(Program* program, const mat4& modelView, const Snapshot& snapshot); + +protected: + SkiaSweepGradientShader(Type type, float x, float y, uint32_t* colors, float* positions, + int count, SkShader* key, SkShader::TileMode tileMode, SkMatrix* matrix, bool blend); + + float mX, mY; + uint32_t* mColors; + float* mPositions; + int mCount; +}; // struct SkiaSweepGradientShader + +/** + * A shader that draws a circular gradient. + */ +struct SkiaCircularGradientShader: public SkiaSweepGradientShader { + SkiaCircularGradientShader(float x, float y, float radius, uint32_t* colors, float* positions, + int count, SkShader* key,SkShader::TileMode tileMode, SkMatrix* matrix, bool blend); + + void describe(ProgramDescription& description, const Extensions& extensions); + void setupProgram(Program* program, const mat4& modelView, const Snapshot& snapshot, + GLuint* textureUnit); + +private: + float mRadius; +}; // struct SkiaCircularGradientShader + +/** + * A shader that draws two shaders, composited with an xfermode. + */ +struct SkiaComposeShader: public SkiaShader { + SkiaComposeShader(SkiaShader* first, SkiaShader* second, SkXfermode::Mode mode, SkShader* key); + + void set(TextureCache* textureCache, GradientCache* gradientCache); + + void describe(ProgramDescription& description, const Extensions& extensions); + void setupProgram(Program* program, const mat4& modelView, const Snapshot& snapshot, + GLuint* textureUnit); + +private: + SkiaShader* mFirst; + SkiaShader* mSecond; + SkXfermode::Mode mMode; +}; // struct SkiaComposeShader + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_SKIA_SHADER_H diff --git a/libs/hwui/Snapshot.h b/libs/hwui/Snapshot.h new file mode 100644 index 0000000000000000000000000000000000000000..062c9868865cd70798fef532cc0e89b6e6868c34 --- /dev/null +++ b/libs/hwui/Snapshot.h @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_SNAPSHOT_H +#define ANDROID_UI_SNAPSHOT_H + +#include +#include + +#include + +#include +#include + +#include "Layer.h" +#include "Matrix.h" +#include "Rect.h" + +namespace android { +namespace uirenderer { + +/** + * A snapshot holds information about the current state of the rendering + * surface. A snapshot is usually created whenever the user calls save() + * and discarded when the user calls restore(). Once a snapshot is created, + * it can hold information for deferred rendering. + * + * Each snapshot has a link to a previous snapshot, indicating the previous + * state of the renderer. + */ +class Snapshot: public LightRefBase { +public: + Snapshot(): flags(0), previous(NULL), layer(NULL) { + transform = &mTransformRoot; + clipRect = &mClipRectRoot; + } + + /** + * Copies the specified snapshot/ The specified snapshot is stored as + * the previous snapshot. + */ + Snapshot(const sp& s, int saveFlags): + flags(0), previous(s), layer(NULL) { + if (saveFlags & SkCanvas::kMatrix_SaveFlag) { + mTransformRoot.load(*s->transform); + transform = &mTransformRoot; + } else { + transform = s->transform; + } + + if (saveFlags & SkCanvas::kClip_SaveFlag) { + mClipRectRoot.set(*s->clipRect); + clipRect = &mClipRectRoot; + } else { + clipRect = s->clipRect; + } + + if ((s->flags & Snapshot::kFlagClipSet) && + !(s->flags & Snapshot::kFlagDirtyLocalClip)) { + mLocalClip.set(s->mLocalClip); + } else { + flags |= Snapshot::kFlagDirtyLocalClip; + } + } + + /** + * Various flags set on #flags. + */ + enum Flags { + /** + * Indicates that the clip region was modified. When this + * snapshot is restored so must the clip. + */ + kFlagClipSet = 0x1, + /** + * Indicates that this snapshot was created when saving + * a new layer. + */ + kFlagIsLayer = 0x2, + /** + * Indicates that the local clip should be recomputed. + */ + kFlagDirtyLocalClip = 0x4, + }; + + /** + * Modifies the current clip with the new clip rectangle and + * the specified operation. The specified rectangle is transformed + * by this snapshot's trasnformation. + */ + bool clip(float left, float top, float right, float bottom, + SkRegion::Op op = SkRegion::kIntersect_Op) { + Rect r(left, top, right, bottom); + transform->mapRect(r); + return clipTransformed(r, op); + } + + /** + * Modifies the current clip with the new clip rectangle and + * the specified operation. The specified rectangle is considered + * already transformed. + */ + bool clipTransformed(const Rect& r, SkRegion::Op op = SkRegion::kIntersect_Op) { + bool clipped = false; + + // NOTE: The unimplemented operations require support for regions + // Supporting regions would require using a stencil buffer instead + // of the scissor. The stencil buffer itself is not too expensive + // (memory cost excluded) but on fillrate limited devices, managing + // the stencil might have a negative impact on the framerate. + switch (op) { + case SkRegion::kDifference_Op: + break; + case SkRegion::kIntersect_Op: + clipped = clipRect->intersect(r); + break; + case SkRegion::kUnion_Op: + clipped = clipRect->unionWith(r); + break; + case SkRegion::kXOR_Op: + break; + case SkRegion::kReverseDifference_Op: + break; + case SkRegion::kReplace_Op: + clipRect->set(r); + clipped = true; + break; + } + + if (clipped) { + flags |= Snapshot::kFlagClipSet | Snapshot::kFlagDirtyLocalClip; + } + + return clipped; + } + + /** + * Sets the current clip. + */ + void setClip(float left, float top, float right, float bottom) { + clipRect->set(left, top, right, bottom); + flags |= Snapshot::kFlagClipSet | Snapshot::kFlagDirtyLocalClip; + } + + const Rect& getLocalClip() { + if (flags & Snapshot::kFlagDirtyLocalClip) { + mat4 inverse; + inverse.loadInverse(*transform); + + mLocalClip.set(*clipRect); + inverse.mapRect(mLocalClip); + + flags &= ~Snapshot::kFlagDirtyLocalClip; + } + return mLocalClip; + } + + /** + * Dirty flags. + */ + int flags; + + /** + * Previous snapshot. + */ + sp previous; + + /** + * Only set when the flag kFlagIsLayer is set. + */ + Layer* layer; + + /** + * Local transformation. Holds the current translation, scale and + * rotation values. + */ + mat4* transform; + + /** + * Current clip region. The clip is stored in canvas-space coordinates, + * (screen-space coordinates in the regular case.) + */ + Rect* clipRect; + +private: + mat4 mTransformRoot; + Rect mClipRectRoot; + Rect mLocalClip; + +}; // class Snapshot + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_SNAPSHOT_H diff --git a/libs/hwui/TextDropShadowCache.cpp b/libs/hwui/TextDropShadowCache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9d54277eed499c2f11b143f946a1447487c85d80 --- /dev/null +++ b/libs/hwui/TextDropShadowCache.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include "TextDropShadowCache.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +TextDropShadowCache::TextDropShadowCache(): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(MB(DEFAULT_DROP_SHADOW_CACHE_SIZE)) { + char property[PROPERTY_VALUE_MAX]; + if (property_get(PROPERTY_DROP_SHADOW_CACHE_SIZE, property, NULL) > 0) { + LOGD(" Setting drop shadow cache size to %sMB", property); + setMaxSize(MB(atof(property))); + } else { + LOGD(" Using default drop shadow cache size of %.2fMB", DEFAULT_DROP_SHADOW_CACHE_SIZE); + } + + mCache.setOnEntryRemovedListener(this); +} + +TextDropShadowCache::TextDropShadowCache(uint32_t maxByteSize): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(maxByteSize) { + mCache.setOnEntryRemovedListener(this); +} + +TextDropShadowCache::~TextDropShadowCache() { + mCache.clear(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Size management +/////////////////////////////////////////////////////////////////////////////// + +uint32_t TextDropShadowCache::getSize() { + return mSize; +} + +uint32_t TextDropShadowCache::getMaxSize() { + return mMaxSize; +} + +void TextDropShadowCache::setMaxSize(uint32_t maxSize) { + mMaxSize = maxSize; + while (mSize > mMaxSize) { + mCache.removeOldest(); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Callbacks +/////////////////////////////////////////////////////////////////////////////// + +void TextDropShadowCache::operator()(ShadowText& text, ShadowTexture*& texture) { + const uint32_t size = texture->width * texture->height; + mSize -= size; + + if (texture) { + glDeleteTextures(1, &texture->id); + delete texture; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Caching +/////////////////////////////////////////////////////////////////////////////// + +void TextDropShadowCache::clear() { + mCache.clear(); +} + +ShadowTexture* TextDropShadowCache::get(SkPaint* paint, const char* text, uint32_t len, + int numGlyphs, uint32_t radius) { + ShadowText entry(paint, radius, len, text); + ShadowTexture* texture = mCache.get(entry); + + if (!texture) { + FontRenderer::DropShadow shadow = mRenderer->renderDropShadow(paint, text, 0, + len, numGlyphs, radius); + + texture = new ShadowTexture; + texture->left = shadow.penX; + texture->top = shadow.penY; + texture->width = shadow.width; + texture->height = shadow.height; + texture->generation = 0; + texture->blend = true; + + const uint32_t size = shadow.width * shadow.height; + // Don't even try to cache a bitmap that's bigger than the cache + if (size < mMaxSize) { + while (mSize + size > mMaxSize) { + mCache.removeOldest(); + } + } + + glGenTextures(1, &texture->id); + + glBindTexture(GL_TEXTURE_2D, texture->id); + // Textures are Alpha8 + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, texture->width, texture->height, 0, + GL_ALPHA, GL_UNSIGNED_BYTE, shadow.image); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + if (size < mMaxSize) { + mSize += size; + mCache.put(entry, texture); + } else { + texture->cleanup = true; + } + + // Cleanup shadow + delete[] shadow.image; + } + + return texture; +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/TextDropShadowCache.h b/libs/hwui/TextDropShadowCache.h new file mode 100644 index 0000000000000000000000000000000000000000..b65d62acc522219c8307135eebc68542a7a7ce54 --- /dev/null +++ b/libs/hwui/TextDropShadowCache.h @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_TEXT_DROP_SHADOW_CACHE_H +#define ANDROID_UI_TEXT_DROP_SHADOW_CACHE_H + +#include + +#include + +#include "GenerationCache.h" +#include "FontRenderer.h" +#include "Texture.h" + +namespace android { +namespace uirenderer { + +struct ShadowText { + ShadowText() { + text = NULL; + } + + ShadowText(SkPaint* paint, uint32_t radius, uint32_t len, const char* srcText): + radius(radius), len(len) { + text = new char[len]; + memcpy(text, srcText, len); + + textSize = paint->getTextSize(); + typeface = paint->getTypeface(); + + hash = 0; + uint32_t multiplier = 1; + for (uint32_t i = 0; i < len; i++) { + hash += text[i] * multiplier; + uint32_t shifted = multiplier << 5; + multiplier = shifted - multiplier; + } + } + + ShadowText(const ShadowText& shadow): + radius(shadow.radius), len(shadow.len), hash(shadow.hash), + textSize(shadow.textSize), typeface(shadow.typeface) { + text = new char[shadow.len]; + memcpy(text, shadow.text, shadow.len); + } + + ~ShadowText() { + delete[] text; + } + + uint32_t radius; + uint32_t len; + uint32_t hash; + float textSize; + SkTypeface* typeface; + char *text; + + bool operator<(const ShadowText& rhs) const { + if (hash < rhs.hash) return true; + else if (hash == rhs.hash) { + if (len < rhs.len) return true; + else if (len == rhs.len) { + if (radius < rhs.radius) return true; + else if (radius == rhs.radius) { + if (textSize < rhs.textSize) return true; + else if (textSize == rhs.textSize) { + if (typeface < rhs.typeface) return true; + else if (typeface == rhs.typeface) { + return strncmp(text, rhs.text, len) < 0; + } + } + } + } + } + return false; + } +}; // struct ShadowText + +/** + * Alpha texture used to represent a shadow. + */ +struct ShadowTexture: public Texture { + ShadowTexture(): Texture() { + } + + float left; + float top; +}; // struct ShadowTexture + +class TextDropShadowCache: public OnEntryRemoved { +public: + TextDropShadowCache(); + TextDropShadowCache(uint32_t maxByteSize); + ~TextDropShadowCache(); + + /** + * Used as a callback when an entry is removed from the cache. + * Do not invoke directly. + */ + void operator()(ShadowText& text, ShadowTexture*& texture); + + ShadowTexture* get(SkPaint* paint, const char* text, uint32_t len, + int numGlyphs, uint32_t radius); + + /** + * Clears the cache. This causes all textures to be deleted. + */ + void clear(); + + void setFontRenderer(FontRenderer& fontRenderer) { + mRenderer = &fontRenderer; + } + + /** + * Sets the maximum size of the cache in bytes. + */ + void setMaxSize(uint32_t maxSize); + /** + * Returns the maximum size of the cache in bytes. + */ + uint32_t getMaxSize(); + /** + * Returns the current size of the cache in bytes. + */ + uint32_t getSize(); + +private: + GenerationCache mCache; + + uint32_t mSize; + uint32_t mMaxSize; + FontRenderer* mRenderer; +}; // class TextDropShadowCache + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_TEXT_DROP_SHADOW_CACHE_H diff --git a/libs/hwui/Texture.h b/libs/hwui/Texture.h new file mode 100644 index 0000000000000000000000000000000000000000..817f1436bd3c161d340e5b7591482a9074afb251 --- /dev/null +++ b/libs/hwui/Texture.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_TEXTURE_H +#define ANDROID_UI_TEXTURE_H + +#include + +namespace android { +namespace uirenderer { + +/** + * Represents an OpenGL texture. + */ +struct Texture { + Texture() { + cleanup = false; + bitmapSize = 0; + } + + /** + * Name of the texture. + */ + GLuint id; + /** + * Generation of the backing bitmap, + */ + uint32_t generation; + /** + * Indicates whether the texture requires blending. + */ + bool blend; + /** + * Width of the backing bitmap. + */ + uint32_t width; + /** + * Height of the backing bitmap. + */ + uint32_t height; + /** + * Indicates whether this texture should be cleaned up after use. + */ + bool cleanup; + /** + * Optional, size of the original bitmap. + */ + uint32_t bitmapSize; +}; // struct Texture + +class AutoTexture { +public: + AutoTexture(const Texture* texture): mTexture(texture) { } + ~AutoTexture() { + if (mTexture && mTexture->cleanup) { + glDeleteTextures(1, &mTexture->id); + delete mTexture; + } + } + +private: + const Texture* mTexture; +}; // class AutoTexture + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_TEXTURE_H diff --git a/libs/hwui/TextureCache.cpp b/libs/hwui/TextureCache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..927070a54ec5a9fe5e27994da8db101b1cb3c17e --- /dev/null +++ b/libs/hwui/TextureCache.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "OpenGLRenderer" + +#include + +#include + +#include "TextureCache.h" +#include "Properties.h" + +namespace android { +namespace uirenderer { + +/////////////////////////////////////////////////////////////////////////////// +// Constructors/destructor +/////////////////////////////////////////////////////////////////////////////// + +TextureCache::TextureCache(): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(MB(DEFAULT_TEXTURE_CACHE_SIZE)) { + char property[PROPERTY_VALUE_MAX]; + if (property_get(PROPERTY_TEXTURE_CACHE_SIZE, property, NULL) > 0) { + LOGD(" Setting texture cache size to %sMB", property); + setMaxSize(MB(atof(property))); + } else { + LOGD(" Using default texture cache size of %.2fMB", DEFAULT_TEXTURE_CACHE_SIZE); + } + + init(); +} + +TextureCache::TextureCache(uint32_t maxByteSize): + mCache(GenerationCache::kUnlimitedCapacity), + mSize(0), mMaxSize(maxByteSize) { + init(); +} + +TextureCache::~TextureCache() { + Mutex::Autolock _l(mLock); + mCache.clear(); +} + +void TextureCache::init() { + mCache.setOnEntryRemovedListener(this); + + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize); + LOGD(" Maximum texture dimension is %d pixels", mMaxTextureSize); +} + +/////////////////////////////////////////////////////////////////////////////// +// Size management +/////////////////////////////////////////////////////////////////////////////// + +uint32_t TextureCache::getSize() { + Mutex::Autolock _l(mLock); + return mSize; +} + +uint32_t TextureCache::getMaxSize() { + Mutex::Autolock _l(mLock); + return mMaxSize; +} + +void TextureCache::setMaxSize(uint32_t maxSize) { + Mutex::Autolock _l(mLock); + mMaxSize = maxSize; + while (mSize > mMaxSize) { + mCache.removeOldest(); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Callbacks +/////////////////////////////////////////////////////////////////////////////// + +void TextureCache::operator()(SkBitmap*& bitmap, Texture*& texture) { + // This will be called already locked + if (texture) { + mSize -= texture->bitmapSize; + glDeleteTextures(1, &texture->id); + delete texture; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Caching +/////////////////////////////////////////////////////////////////////////////// + +Texture* TextureCache::get(SkBitmap* bitmap) { + mLock.lock(); + Texture* texture = mCache.get(bitmap); + mLock.unlock(); + + if (!texture) { + if (bitmap->width() > mMaxTextureSize || bitmap->height() > mMaxTextureSize) { + LOGW("Bitmap too large to be uploaded into a texture"); + return NULL; + } + + const uint32_t size = bitmap->rowBytes() * bitmap->height(); + // Don't even try to cache a bitmap that's bigger than the cache + if (size < mMaxSize) { + mLock.lock(); + while (mSize + size > mMaxSize) { + mCache.removeOldest(); + } + mLock.unlock(); + } + + texture = new Texture; + texture->bitmapSize = size; + generateTexture(bitmap, texture, false); + + if (size < mMaxSize) { + mLock.lock(); + mSize += size; + mCache.put(bitmap, texture); + mLock.unlock(); + } else { + texture->cleanup = true; + } + } else if (bitmap->getGenerationID() != texture->generation) { + generateTexture(bitmap, texture, true); + } + + return texture; +} + +void TextureCache::remove(SkBitmap* bitmap) { + Mutex::Autolock _l(mLock); + mCache.remove(bitmap); +} + +void TextureCache::clear() { + Mutex::Autolock _l(mLock); + mCache.clear(); +} + +void TextureCache::generateTexture(SkBitmap* bitmap, Texture* texture, bool regenerate) { + SkAutoLockPixels alp(*bitmap); + + if (!bitmap->readyToDraw()) { + LOGE("Cannot generate texture from bitmap"); + return; + } + + if (!regenerate) { + texture->generation = bitmap->getGenerationID(); + texture->width = bitmap->width(); + texture->height = bitmap->height(); + + glGenTextures(1, &texture->id); + } + + glBindTexture(GL_TEXTURE_2D, texture->id); + glPixelStorei(GL_UNPACK_ALIGNMENT, bitmap->bytesPerPixel()); + + switch (bitmap->getConfig()) { + case SkBitmap::kA8_Config: + texture->blend = true; + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, bitmap->rowBytesAsPixels(), texture->height, 0, + GL_ALPHA, GL_UNSIGNED_BYTE, bitmap->getPixels()); + break; + case SkBitmap::kRGB_565_Config: + texture->blend = false; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, bitmap->rowBytesAsPixels(), texture->height, 0, + GL_RGB, GL_UNSIGNED_SHORT_5_6_5, bitmap->getPixels()); + break; + case SkBitmap::kARGB_8888_Config: + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bitmap->rowBytesAsPixels(), texture->height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, bitmap->getPixels()); + // Do this after calling getPixels() to make sure Skia's deferred + // decoding happened + texture->blend = !bitmap->isOpaque(); + break; + default: + LOGW("Unsupported bitmap config"); + break; + } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +}; // namespace uirenderer +}; // namespace android diff --git a/libs/hwui/TextureCache.h b/libs/hwui/TextureCache.h new file mode 100644 index 0000000000000000000000000000000000000000..a63789a14eb0679b6a23775112ffa230c79caad8 --- /dev/null +++ b/libs/hwui/TextureCache.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_TEXTURE_CACHE_H +#define ANDROID_UI_TEXTURE_CACHE_H + +#include + +#include "Texture.h" +#include "GenerationCache.h" + +namespace android { +namespace uirenderer { + +/** + * A simple LRU texture cache. The cache has a maximum size expressed in bytes. + * Any texture added to the cache causing the cache to grow beyond the maximum + * allowed size will also cause the oldest texture to be kicked out. + */ +class TextureCache: public OnEntryRemoved { +public: + TextureCache(); + TextureCache(uint32_t maxByteSize); + ~TextureCache(); + + /** + * Used as a callback when an entry is removed from the cache. + * Do not invoke directly. + */ + void operator()(SkBitmap*& bitmap, Texture*& texture); + + /** + * Returns the texture associated with the specified bitmap. If the texture + * cannot be found in the cache, a new texture is generated. + */ + Texture* get(SkBitmap* bitmap); + /** + * Removes the texture associated with the specified bitmap. Returns NULL + * if the texture cannot be found. Upon remove the texture is freed. + */ + void remove(SkBitmap* bitmap); + /** + * Clears the cache. This causes all textures to be deleted. + */ + void clear(); + + /** + * Sets the maximum size of the cache in bytes. + */ + void setMaxSize(uint32_t maxSize); + /** + * Returns the maximum size of the cache in bytes. + */ + uint32_t getMaxSize(); + /** + * Returns the current size of the cache in bytes. + */ + uint32_t getSize(); + +private: + /** + * Generates the texture from a bitmap into the specified texture structure. + * + * @param regenerate If true, the bitmap data is reuploaded into the texture, but + * no new texture is generated. + */ + void generateTexture(SkBitmap* bitmap, Texture* texture, bool regenerate = false); + + void init(); + + GenerationCache mCache; + + uint32_t mSize; + uint32_t mMaxSize; + GLint mMaxTextureSize; + + /** + * Used to access mCache and mSize. All methods are accessed from a single + * thread except for remove(). + */ + mutable Mutex mLock; +}; // class TextureCache + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_TEXTURE_CACHE_H diff --git a/libs/hwui/Vertex.h b/libs/hwui/Vertex.h new file mode 100644 index 0000000000000000000000000000000000000000..1f540864f17f0df864d480f324263f5c27db744d --- /dev/null +++ b/libs/hwui/Vertex.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UI_VERTEX_H +#define ANDROID_UI_VERTEX_H + +namespace android { +namespace uirenderer { + +/** + * Simple structure to describe a vertex with a position and a texture. + */ +struct TextureVertex { + float position[2]; + float texture[2]; + + static inline void set(TextureVertex* vertex, float x, float y, float u, float v) { + vertex[0].position[0] = x; + vertex[0].position[1] = y; + vertex[0].texture[0] = u; + vertex[0].texture[1] = v; + } + + static inline void setUV(TextureVertex* vertex, float u, float v) { + vertex[0].texture[0] = u; + vertex[0].texture[1] = v; + } +}; // struct TextureVertex + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_UI_VERTEX_H diff --git a/libs/rs/Android.mk b/libs/rs/Android.mk index 98464a08bf411ebd11d7ba3696a7d28273fd561d..37c418bec7e97cdb63ecb515d5550dbbb42acdfe 100644 --- a/libs/rs/Android.mk +++ b/libs/rs/Android.mk @@ -76,34 +76,44 @@ ifneq ($(TARGET_SIMULATOR),true) LOCAL_SRC_FILES:= \ rsAdapter.cpp \ rsAllocation.cpp \ + rsAnimation.cpp \ rsComponent.cpp \ rsContext.cpp \ rsDevice.cpp \ rsElement.cpp \ - rsFileA3D.cpp \ + rsFileA3D.cpp \ + rsFont.cpp \ rsLight.cpp \ rsLocklessFifo.cpp \ rsObjectBase.cpp \ rsMatrix.cpp \ - rsMesh.cpp \ - rsNoise.cpp \ + rsMesh.cpp \ + rsMutex.cpp \ rsProgram.cpp \ rsProgramFragment.cpp \ - rsProgramFragmentStore.cpp \ + rsProgramStore.cpp \ rsProgramRaster.cpp \ rsProgramVertex.cpp \ rsSampler.cpp \ rsScript.cpp \ rsScriptC.cpp \ rsScriptC_Lib.cpp \ - rsShaderCache.cpp \ - rsSimpleMesh.cpp \ + rsScriptC_LibCL.cpp \ + rsScriptC_LibGL.cpp \ + rsShaderCache.cpp \ + rsSignal.cpp \ + rsStream.cpp \ rsThreadIO.cpp \ rsType.cpp \ rsVertexArray.cpp -LOCAL_SHARED_LIBRARIES += libcutils libutils libEGL libGLESv1_CM libGLESv2 libui libacc +LOCAL_SHARED_LIBRARIES += libcutils libutils libEGL libGLESv1_CM libGLESv2 libui libbcc + +LOCAL_STATIC_LIBRARIES := libft2 + +LOCAL_C_INCLUDES += external/freetype/include + LOCAL_LDLIBS := -lpthread -ldl LOCAL_MODULE:= libRS LOCAL_MODULE_TAGS := optional diff --git a/libs/rs/RenderScript.h b/libs/rs/RenderScript.h index d280f5053b037b552b5ae544530bb6fd7ad03efb..7902b9a2d1a6c3415440f687f81dddd5f3297f70 100644 --- a/libs/rs/RenderScript.h +++ b/libs/rs/RenderScript.h @@ -30,20 +30,23 @@ extern "C" { typedef void * RsAdapter1D; typedef void * RsAdapter2D; typedef void * RsAllocation; +typedef void * RsAnimation; typedef void * RsContext; typedef void * RsDevice; typedef void * RsElement; typedef void * RsFile; +typedef void * RsFont; typedef void * RsSampler; typedef void * RsScript; -typedef void * RsSimpleMesh; +typedef void * RsMesh; typedef void * RsType; typedef void * RsLight; +typedef void * RsObjectBase; typedef void * RsProgram; typedef void * RsProgramVertex; typedef void * RsProgramFragment; -typedef void * RsProgramFragmentStore; +typedef void * RsProgramStore; typedef void * RsProgramRaster; typedef void (* RsBitmapCallback_t)(void *); @@ -60,7 +63,6 @@ void rsDeviceSetConfig(RsDevice, RsDeviceParam, int32_t value); RsContext rsContextCreate(RsDevice, uint32_t version); RsContext rsContextCreateGL(RsDevice, uint32_t version, bool useDepth); void rsContextDestroy(RsContext); -void rsObjDestroyOOB(RsContext, void *); uint32_t rsContextGetMessage(RsContext, void *data, size_t *receiveLen, size_t bufferLen, bool wait); void rsContextInitToClient(RsContext); @@ -83,11 +85,17 @@ enum RsDataType { RS_TYPE_UNSIGNED_32, RS_TYPE_UNSIGNED_64, + RS_TYPE_BOOLEAN, + RS_TYPE_UNSIGNED_5_6_5, RS_TYPE_UNSIGNED_5_5_5_1, RS_TYPE_UNSIGNED_4_4_4_4, - RS_TYPE_ELEMENT, + RS_TYPE_MATRIX_4X4, + RS_TYPE_MATRIX_3X3, + RS_TYPE_MATRIX_2X2, + + RS_TYPE_ELEMENT = 1000, RS_TYPE_TYPE, RS_TYPE_ALLOCATION, RS_TYPE_SAMPLER, @@ -96,24 +104,17 @@ enum RsDataType { RS_TYPE_PROGRAM_FRAGMENT, RS_TYPE_PROGRAM_VERTEX, RS_TYPE_PROGRAM_RASTER, - RS_TYPE_PROGRAM_STORE + RS_TYPE_PROGRAM_STORE, }; enum RsDataKind { RS_KIND_USER, - RS_KIND_COLOR, - RS_KIND_POSITION, - RS_KIND_TEXTURE, - RS_KIND_NORMAL, - RS_KIND_INDEX, - RS_KIND_POINT_SIZE, - - RS_KIND_PIXEL_L, + + RS_KIND_PIXEL_L = 7, RS_KIND_PIXEL_A, RS_KIND_PIXEL_LA, RS_KIND_PIXEL_RGB, RS_KIND_PIXEL_RGBA, - }; enum RsSamplerParam { @@ -205,9 +206,71 @@ enum RsPrimitive { enum RsError { RS_ERROR_NONE, RS_ERROR_BAD_SHADER, - RS_ERROR_BAD_SCRIPT + RS_ERROR_BAD_SCRIPT, + RS_ERROR_BAD_VALUE, + RS_ERROR_OUT_OF_MEMORY }; +enum RsAnimationInterpolation { + RS_ANIMATION_INTERPOLATION_STEP, + RS_ANIMATION_INTERPOLATION_LINEAR, + RS_ANIMATION_INTERPOLATION_BEZIER, + RS_ANIMATION_INTERPOLATION_CARDINAL, + RS_ANIMATION_INTERPOLATION_HERMITE, + RS_ANIMATION_INTERPOLATION_BSPLINE +}; + +enum RsAnimationEdge { + RS_ANIMATION_EDGE_UNDEFINED, + RS_ANIMATION_EDGE_CONSTANT, + RS_ANIMATION_EDGE_GRADIENT, + RS_ANIMATION_EDGE_CYCLE, + RS_ANIMATION_EDGE_OSCILLATE, + RS_ANIMATION_EDGE_CYLE_RELATIVE +}; + +enum RsA3DClassID { + RS_A3D_CLASS_ID_UNKNOWN, + RS_A3D_CLASS_ID_MESH, + RS_A3D_CLASS_ID_TYPE, + RS_A3D_CLASS_ID_ELEMENT, + RS_A3D_CLASS_ID_ALLOCATION, + RS_A3D_CLASS_ID_PROGRAM_VERTEX, + RS_A3D_CLASS_ID_PROGRAM_RASTER, + RS_A3D_CLASS_ID_PROGRAM_FRAGMENT, + RS_A3D_CLASS_ID_PROGRAM_STORE, + RS_A3D_CLASS_ID_SAMPLER, + RS_A3D_CLASS_ID_ANIMATION, + RS_A3D_CLASS_ID_LIGHT, + RS_A3D_CLASS_ID_ADAPTER_1D, + RS_A3D_CLASS_ID_ADAPTER_2D, + RS_A3D_CLASS_ID_SCRIPT_C +}; + +enum RsCullMode { + RS_CULL_BACK, + RS_CULL_FRONT, + RS_CULL_NONE +}; + +typedef struct { + RsA3DClassID classID; + const char* objectName; +} RsFileIndexEntry; + +// Script to Script +typedef struct { + uint32_t xStart; + uint32_t xEnd; + uint32_t yStart; + uint32_t yEnd; + uint32_t zStart; + uint32_t zEnd; + uint32_t arrayStart; + uint32_t arrayEnd; + +} RsScriptCall; + #ifndef NO_RS_FUNCS #include "rsgApiFuncDecl.h" #endif diff --git a/libs/rs/RenderScriptEnv.h b/libs/rs/RenderScriptEnv.h index 99b8c0456f049e9a1b08b0101d40eab93a912152..c83ece45238227c1d05fc0d252463bf1f1d30868 100644 --- a/libs/rs/RenderScriptEnv.h +++ b/libs/rs/RenderScriptEnv.h @@ -9,10 +9,10 @@ typedef void * RsDevice; typedef void * RsElement; typedef void * RsSampler; typedef void * RsScript; -typedef void * RsSimpleMesh; +typedef void * RsMesh; typedef void * RsType; typedef void * RsProgramFragment; -typedef void * RsProgramFragmentStore; +typedef void * RsProgramStore; typedef void * RsLight; @@ -28,4 +28,4 @@ typedef struct { #define RS_PROGRAM_VERTEX_MODELVIEW_OFFSET 0 #define RS_PROGRAM_VERTEX_PROJECTION_OFFSET 16 #define RS_PROGRAM_VERTEX_TEXTURE_OFFSET 32 - +#define RS_PROGRAM_VERTEX_MVP_OFFSET 48 diff --git a/libs/rs/java/Film/res/drawable/p01.png b/libs/rs/java/Film/res/drawable/p01.png deleted file mode 100644 index a9b9bdbe5686dec0c297cada823c6b98351625cb..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p01.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p02.png b/libs/rs/java/Film/res/drawable/p02.png deleted file mode 100644 index 8162c82b629a8eba0a98f7508e4a98e6a83cfe73..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p02.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p03.png b/libs/rs/java/Film/res/drawable/p03.png deleted file mode 100644 index e3e26c0b3f1d389a5115aeacf9b5ce7b14cd1a9e..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p03.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p04.png b/libs/rs/java/Film/res/drawable/p04.png deleted file mode 100644 index daee603048ece171122eb8bcfaaf80f3da5b0dcd..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p04.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p05.png b/libs/rs/java/Film/res/drawable/p05.png deleted file mode 100644 index fac5248fb36b8cf7a2d11a1c98dc0b3bc8be254d..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p05.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p06.png b/libs/rs/java/Film/res/drawable/p06.png deleted file mode 100644 index 3b5126164ab267101392a7dda14cb22c8199df3c..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p06.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p07.png b/libs/rs/java/Film/res/drawable/p07.png deleted file mode 100644 index d8bd9383fbd3db5e37cfa6c56c02e362c95cdd07..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p07.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p08.png b/libs/rs/java/Film/res/drawable/p08.png deleted file mode 100644 index ef175e8f11273db51f467d1e7ee35b0dcc956b60..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p08.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p09.png b/libs/rs/java/Film/res/drawable/p09.png deleted file mode 100644 index 7bf387440b6d090a8a1976fb0fb5ab048abd35be..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p09.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p10.png b/libs/rs/java/Film/res/drawable/p10.png deleted file mode 100644 index 908827d243f4050a8f6371e842a16205c87c7fac..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p10.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p11.png b/libs/rs/java/Film/res/drawable/p11.png deleted file mode 100644 index 1289f7117f88277cc3d454a2e5ff50159ff35eb8..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p11.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p12.png b/libs/rs/java/Film/res/drawable/p12.png deleted file mode 100644 index e1af16a43b63dd291f9269b62f6c59d86de786ae..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p12.png and /dev/null differ diff --git a/libs/rs/java/Film/res/drawable/p13.png b/libs/rs/java/Film/res/drawable/p13.png deleted file mode 100644 index d08bcbee295c1658088e0dcf2a52fde35ef9a4c5..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Film/res/drawable/p13.png and /dev/null differ diff --git a/libs/rs/java/Film/res/raw/filmimage.c b/libs/rs/java/Film/res/raw/filmimage.c deleted file mode 100644 index d154c68bfa003777bcb0ebd048d29bb560027206..0000000000000000000000000000000000000000 --- a/libs/rs/java/Film/res/raw/filmimage.c +++ /dev/null @@ -1,110 +0,0 @@ -// Fountain test script - -#pragma version(1) -#pragma stateVertex(orthoWindow) -#pragma stateRaster(flat) -#pragma stateFragment(PgmFragBackground) -#pragma stateStore(MyBlend) - - -int main(void* con, int ft, int launchID) { - int count, touch, x, y, rate, maxLife, lifeShift; - int life; - int ct, ct2; - int newPart; - int drawCount; - int dx, dy, idx; - int posx,posy; - int c; - int srcIdx; - int dstIdx; - - count = loadI32(con, 0, 1); - touch = loadI32(con, 0, 2); - x = loadI32(con, 0, 3); - y = loadI32(con, 0, 4); - - rate = 4; - maxLife = (count / rate) - 1; - lifeShift = 0; - { - life = maxLife; - while (life > 255) { - life = life >> 1; - lifeShift ++; - } - } - - drawRect(con, 0, 256, 0, 512); - contextBindProgramFragment(con, NAMED_PgmFragParts); - - if (touch) { - newPart = loadI32(con, 2, 0); - for (ct2=0; ct2= count) { - newPart = 0; - } - } - storeI32(con, 2, 0, newPart); - } - - drawCount = 0; - for (ct=0; ct < count; ct++) { - srcIdx = ct * 5 + 1; - - dx = loadI32(con, 2, srcIdx); - dy = loadI32(con, 2, srcIdx + 1); - life = loadI32(con, 2, srcIdx + 2); - posx = loadI32(con, 2, srcIdx + 3); - posy = loadI32(con, 2, srcIdx + 4); - - if (life) { - if (posy < (480 << 16)) { - dstIdx = drawCount * 9; - c = 0xffafcf | ((life >> lifeShift) << 24); - - storeU32(con, 1, dstIdx, c); - storeI32(con, 1, dstIdx + 1, posx); - storeI32(con, 1, dstIdx + 2, posy); - - storeU32(con, 1, dstIdx + 3, c); - storeI32(con, 1, dstIdx + 4, posx + 0x10000); - storeI32(con, 1, dstIdx + 5, posy + dy * 4); - - storeU32(con, 1, dstIdx + 6, c); - storeI32(con, 1, dstIdx + 7, posx - 0x10000); - storeI32(con, 1, dstIdx + 8, posy + dy * 4); - drawCount ++; - } else { - if (dy > 0) { - dy = (-dy) >> 1; - } - } - - posx = posx + dx; - posy = posy + dy; - dy = dy + 0x400; - life --; - - //storeI32(con, 2, srcIdx, dx); - storeI32(con, 2, srcIdx + 1, dy); - storeI32(con, 2, srcIdx + 2, life); - storeI32(con, 2, srcIdx + 3, posx); - storeI32(con, 2, srcIdx + 4, posy); - } - } - - drawTriangleArray(con, NAMED_PartBuffer, drawCount); - return 1; -} diff --git a/libs/rs/java/Film/res/raw/filmstrip.c b/libs/rs/java/Film/res/raw/filmstrip.c deleted file mode 100644 index bf75675743a16a9f833f525aac7cda0b08c8387c..0000000000000000000000000000000000000000 --- a/libs/rs/java/Film/res/raw/filmstrip.c +++ /dev/null @@ -1,94 +0,0 @@ -// Fountain test script - -#pragma version(1) -#pragma stateVertex(PVBackground) -#pragma stateFragment(PFBackground) -#pragma stateStore(PSBackground) - -#define STATE_TRIANGLE_OFFSET_COUNT 0 -#define STATE_LAST_FOCUS 1 - - -// The script enviroment has 3 env allocations. -// bank0: (r) The enviroment structure -// bank1: (r) The position information -// bank2: (rw) The temporary texture state - -int lastFocus; - -int main(int index) -{ - float mat1[16]; - - float trans = Pos->translate; - float rot = Pos->rotate; - - matrixLoadScale(mat1, 2.f, 2.f, 2.f); - matrixTranslate(mat1, 0.f, 0.f, trans); - matrixRotate(mat1, 90.f, 0.f, 0.f, 1.f); - matrixRotate(mat1, rot, 1.f, 0.f, 0.f); - vpLoadModelMatrix(mat1); - - // Draw the lighting effect in the strip and fill the Z buffer. - drawSimpleMesh(NAMED_mesh); - - // Start of images. - bindProgramStore(NAMED_PSImages); - bindProgramFragment(NAMED_PFImages); - bindProgramVertex(NAMED_PVImages); - - float focusPos = Pos->focus; - int focusID = 0; - int lastFocusID = loadI32(2, STATE_LAST_FOCUS); - int imgCount = 13; - - if (trans > (-.3f)) { - focusID = -1.0f - focusPos; - if (focusID >= imgCount) { - focusID = -1; - } - } else { - focusID = -1; - } - - /* - if (focusID != lastFocusID) { - if (lastFocusID >= 0) { - uploadToTexture(con, env->tex[lastFocusID], 1); - } - if (focusID >= 0) { - uploadToTexture(con, env->tex[focusID], 0); - } - } - */ - lastFocus = focusID; - - int triangleOffsetsCount = Pos->triangleOffsetCount; - - int imgId = 0; - for (imgId=1; imgId <= imgCount; imgId++) { - float pos = focusPos + imgId + 0.4f; - int offset = (int)floorf(pos * 2.f); - pos = pos - 0.75f; - - offset = offset + triangleOffsetsCount / 2; - if (!((offset < 0) || (offset >= triangleOffsetsCount))) { - int start = offset -2; - int end = offset + 2; - - if (start < 0) { - start = 0; - } - if (end >= triangleOffsetsCount) { - end = triangleOffsetsCount-1; - } - - bindTexture(NAMED_PFImages, 0, loadI32(0, imgId - 1)); - matrixLoadTranslate(mat1, -pos - loadF(5, triangleOffsetsCount / 2), 0, 0); - vpLoadTextureMatrix(mat1); - drawSimpleMeshRange(NAMED_mesh, loadI32(4, start), (loadI32(4, end) - loadI32(4, start))); - } - } - return 0; -} - diff --git a/libs/rs/java/Film/src/com/android/film/FilmRS.java b/libs/rs/java/Film/src/com/android/film/FilmRS.java deleted file mode 100644 index 7d04502256be255c1a1645116a25dc64c6a0e2fb..0000000000000000000000000000000000000000 --- a/libs/rs/java/Film/src/com/android/film/FilmRS.java +++ /dev/null @@ -1,258 +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 com.android.film; - -import java.io.Writer; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.util.Log; - -import android.renderscript.*; - -public class FilmRS { - class StripPosition { - public float translate; - public float rotate; - public float focus; - public int triangleOffsetCount; - } - StripPosition mPos = new StripPosition(); - - - private final int STATE_LAST_FOCUS = 1; - - public FilmRS() { - } - - public void init(RenderScriptGL rs, Resources res, int width, int height) { - mRS = rs; - mRes = res; - initRS(); - } - - public void setFilmStripPosition(int x, int y) - { - if (x < 50) { - x = 50; - } - if (x > 270) { - x = 270; - } - - float anim = ((float)x-50) / 270.f; - mPos.translate = 2f * anim + 0.5f; // translation - mPos.rotate = (anim * 40); // rotation - mPos.focus = ((float)y) / 16.f - 10.f; // focusPos - mPos.triangleOffsetCount = mFSM.mTriangleOffsetsCount; - mAllocPos.data(mPos); - } - - - private Resources mRes; - private RenderScriptGL mRS; - private Script mScriptStrip; - private Script mScriptImage; - private Sampler mSampler; - private ProgramStore mPSBackground; - private ProgramStore mPSImages; - private ProgramFragment mPFBackground; - private ProgramFragment mPFImages; - private ProgramVertex mPVBackground; - private ProgramVertex mPVImages; - private ProgramVertex.MatrixAllocation mPVA; - private Type mStripPositionType; - - private Allocation mImages[]; - private Allocation mAllocIDs; - private Allocation mAllocPos; - private Allocation mAllocState; - private Allocation mAllocPV; - private Allocation mAllocOffsetsTex; - private Allocation mAllocOffsets; - - private SimpleMesh mMesh; - private Light mLight; - - private FilmStripMesh mFSM; - - private int[] mBufferIDs; - private float[] mBufferPos = new float[3]; - private int[] mBufferState; - - private void initPFS() { - ProgramStore.Builder b = new ProgramStore.Builder(mRS, null, null); - - b.setDepthFunc(ProgramStore.DepthFunc.LESS); - b.setDitherEnable(true); - b.setDepthMask(true); - mPSBackground = b.create(); - mPSBackground.setName("PSBackground"); - - b.setDepthFunc(ProgramStore.DepthFunc.EQUAL); - b.setDitherEnable(false); - b.setDepthMask(false); - b.setBlendFunc(ProgramStore.BlendSrcFunc.ONE, - ProgramStore.BlendDstFunc.ONE); - mPSImages = b.create(); - mPSImages.setName("PSImages"); - } - - private void initPF() { - Sampler.Builder bs = new Sampler.Builder(mRS); - bs.setMin(Sampler.Value.LINEAR);//_MIP_LINEAR); - bs.setMag(Sampler.Value.LINEAR); - bs.setWrapS(Sampler.Value.CLAMP); - bs.setWrapT(Sampler.Value.WRAP); - mSampler = bs.create(); - - ProgramFragment.Builder b = new ProgramFragment.Builder(mRS); - mPFBackground = b.create(); - mPFBackground.setName("PFBackground"); - - b = new ProgramFragment.Builder(mRS); - b.setTexture(ProgramFragment.Builder.EnvMode.REPLACE, - ProgramFragment.Builder.Format.RGBA, 0); - mPFImages = b.create(); - mPFImages.bindSampler(mSampler, 0); - mPFImages.setName("PFImages"); - } - - private void initPV() { - mLight = (new Light.Builder(mRS)).create(); - mLight.setPosition(0, -0.5f, -1.0f); - - ProgramVertex.Builder pvb = new ProgramVertex.Builder(mRS, null, null); - //pvb.addLight(mLight); - mPVBackground = pvb.create(); - mPVBackground.setName("PVBackground"); - - pvb = new ProgramVertex.Builder(mRS, null, null); - pvb.setTextureMatrixEnable(true); - mPVImages = pvb.create(); - mPVImages.setName("PVImages"); - } - - private void loadImages() { - mBufferIDs = new int[13]; - mImages = new Allocation[13]; - mAllocIDs = Allocation.createSized(mRS, - Element.createUser(mRS, Element.DataType.FLOAT_32), - mBufferIDs.length); - - Element ie = Element.createPixel(mRS, Element.DataType.UNSIGNED_5_6_5, Element.DataKind.PIXEL_RGB); - mImages[0] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p01, ie, true); - mImages[1] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p02, ie, true); - mImages[2] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p03, ie, true); - mImages[3] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p04, ie, true); - mImages[4] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p05, ie, true); - mImages[5] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p06, ie, true); - mImages[6] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p07, ie, true); - mImages[7] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p08, ie, true); - mImages[8] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p09, ie, true); - mImages[9] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p10, ie, true); - mImages[10] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p11, ie, true); - mImages[11] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p12, ie, true); - mImages[12] = Allocation.createFromBitmapResourceBoxed(mRS, mRes, R.drawable.p13, ie, true); - - int black[] = new int[1024]; - for(int ct=0; ct < mImages.length; ct++) { - Allocation.Adapter2D a = mImages[ct].createAdapter2D(); - - int size = 512; - int mip = 0; - while(size >= 2) { - a.subData(0, 0, 2, size, black); - a.subData(size-2, 0, 2, size, black); - a.subData(0, 0, size, 2, black); - a.subData(0, size-2, size, 2, black); - size >>= 1; - mip++; - a.setConstraint(Dimension.LOD, mip); - } - - mImages[ct].uploadToTexture(1); - mBufferIDs[ct] = mImages[ct].getID(); - } - mAllocIDs.data(mBufferIDs); - } - - private void initState() - { - mBufferState = new int[10]; - mAllocState = Allocation.createSized(mRS, - Element.createUser(mRS, Element.DataType.FLOAT_32), - mBufferState.length); - mBufferState[STATE_LAST_FOCUS] = -1; - mAllocState.data(mBufferState); - } - - private void initRS() { - mFSM = new FilmStripMesh(); - mMesh = mFSM.init(mRS); - mMesh.setName("mesh"); - - initPFS(); - initPF(); - initPV(); - - Log.e("rs", "Done loading named"); - - mStripPositionType = Type.createFromClass(mRS, StripPosition.class, 1); - - ScriptC.Builder sb = new ScriptC.Builder(mRS); - sb.setScript(mRes, R.raw.filmstrip); - sb.setRoot(true); - sb.setType(mStripPositionType, "Pos", 1); - mScriptStrip = sb.create(); - mScriptStrip.setClearColor(0.0f, 0.0f, 0.0f, 1.0f); - - mAllocPos = Allocation.createTyped(mRS, mStripPositionType); - - loadImages(); - initState(); - - mPVA = new ProgramVertex.MatrixAllocation(mRS); - mPVBackground.bindAllocation(mPVA); - mPVImages.bindAllocation(mPVA); - mPVA.setupProjectionNormalized(320, 480); - - - mScriptStrip.bindAllocation(mAllocIDs, 0); - mScriptStrip.bindAllocation(mAllocPos, 1); - mScriptStrip.bindAllocation(mAllocState, 2); - mScriptStrip.bindAllocation(mPVA.mAlloc, 3); - - - mAllocOffsets = Allocation.createSized(mRS, - Element.createUser(mRS, Element.DataType.SIGNED_32), mFSM.mTriangleOffsets.length); - mAllocOffsets.data(mFSM.mTriangleOffsets); - mScriptStrip.bindAllocation(mAllocOffsets, 4); - - mAllocOffsetsTex = Allocation.createSized(mRS, - Element.createUser(mRS, Element.DataType.FLOAT_32), mFSM.mTriangleOffsetsTex.length); - mAllocOffsetsTex.data(mFSM.mTriangleOffsetsTex); - mScriptStrip.bindAllocation(mAllocOffsetsTex, 5); - - setFilmStripPosition(0, 0); - mRS.contextBindRootScript(mScriptStrip); - } -} - - - diff --git a/libs/rs/java/Film/src/com/android/film/FilmStripMesh.java b/libs/rs/java/Film/src/com/android/film/FilmStripMesh.java deleted file mode 100644 index 448cce02745d7bfee535033d1314f1fc3bc8acf9..0000000000000000000000000000000000000000 --- a/libs/rs/java/Film/src/com/android/film/FilmStripMesh.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package com.android.film; - -import java.io.Writer; -import java.lang.Math; -import android.util.Log; - -import android.renderscript.RenderScript; -import android.renderscript.SimpleMesh; - - -class FilmStripMesh { - - class Vertex { - float nx; - float ny; - float nz; - float s; - float t; - float x; - float y; - float z; - - Vertex() { - nx = 0; - ny = 0; - nz = 0; - s = 0; - t = 0; - x = 0; - y = 0; - z = 0; - } - - void xyz(float _x, float _y, float _z) { - x = _x; - y = _y; - z = _z; - } - - void nxyz(float _x, float _y, float _z) { - nx = _x; - ny = _y; - nz = _z; - } - - void st(float _s, float _t) { - s = _s; - t = _t; - } - - void computeNorm(Vertex v1, Vertex v2) { - float dx = v1.x - v2.x; - float dy = v1.y - v2.y; - float dz = v1.z - v2.z; - float len = (float)java.lang.Math.sqrt(dx*dx + dy*dy + dz*dz); - dx /= len; - dy /= len; - dz /= len; - - nx = dx * dz; - ny = dy * dz; - nz = (float)java.lang.Math.sqrt(dx*dx + dy*dy); - - len = (float)java.lang.Math.sqrt(nx*nx + ny*ny + nz*nz); - nx /= len; - ny /= len; - nz /= len; - } - } - - int[] mTriangleOffsets; - float[] mTriangleOffsetsTex; - int mTriangleOffsetsCount; - - SimpleMesh init(RenderScript rs) - { - float vtx[] = new float[] { - 60.431003f, 124.482050f, - 60.862074f, 120.872604f, - 61.705303f, 117.336662f, - 62.949505f, 113.921127f, - 64.578177f, 110.671304f, - 66.569716f, 107.630302f, - 68.897703f, 104.838457f, - 71.531259f, 102.332803f, - 74.435452f, 100.146577f, - 77.571757f, 98.308777f, - 80.898574f, 96.843781f, - 84.371773f, 95.771023f, - 87.945283f, 95.104731f, - 98.958994f, 95.267098f, - 109.489523f, 98.497596f, - 118.699582f, 104.539366f, - 125.856872f, 112.912022f, - 130.392311f, 122.949849f, - 131.945283f, 133.854731f, - 130.392311f, 144.759613f, - 125.856872f, 154.797439f, - 118.699582f, 163.170096f, - 109.489523f, 169.211866f, - 98.958994f, 172.442364f, - 87.945283f, 172.604731f, - 72.507313f, 172.672927f, - 57.678920f, 168.377071f, - 44.668135f, 160.067134f, - 34.534908f, 148.420104f, - 28.104767f, 134.384831f, - 25.901557f, 119.104731f, - 28.104767f, 103.824631f, - 34.534908f, 89.789358f, - 44.668135f, 78.142327f, - 57.678920f, 69.832390f, - 72.507313f, 65.536534f, - 87.945283f, 65.604731f, - 106.918117f, 65.688542f, - 125.141795f, 60.409056f, - 141.131686f, 50.196376f, - 153.585137f, 35.882502f, - 161.487600f, 18.633545f, - 164.195283f, -0.145269f, - 161.487600f, -18.924084f, - 153.585137f, -36.173040f, - 141.131686f, -50.486914f, - 125.141795f, -60.699594f, - 106.918117f, -65.979081f, - 87.945283f, -65.895269f, - 80f, -65.895269f, - 60f, -65.895269f, - 40f, -65.895269f, - 20f, -65.895269f, - 0f, -65.895269f, - -20f, -65.895269f, - -40f, -65.895269f, - -60f, -65.895269f, - -80f, -65.895269f, - -87.945283f, -65.895269f, - -106.918117f, -65.979081f, - -125.141795f, -60.699594f, - -141.131686f, -50.486914f, - -153.585137f, -36.173040f, - -161.487600f, -18.924084f, - -164.195283f, -0.145269f, - -161.487600f, 18.633545f, - -153.585137f, 35.882502f, - -141.131686f, 50.196376f, - -125.141795f, 60.409056f, - -106.918117f, 65.688542f, - -87.945283f, 65.604731f, - -72.507313f, 65.536534f, - -57.678920f, 69.832390f, - -44.668135f, 78.142327f, - -34.534908f, 89.789358f, - -28.104767f, 103.824631f, - -25.901557f, 119.104731f, - -28.104767f, 134.384831f, - -34.534908f, 148.420104f, - -44.668135f, 160.067134f, - -57.678920f, 168.377071f, - -72.507313f, 172.672927f, - -87.945283f, 172.604731f, - -98.958994f, 172.442364f, - -109.489523f, 169.211866f, - -118.699582f, 163.170096f, - -125.856872f, 154.797439f, - -130.392311f, 144.759613f, - -131.945283f, 133.854731f, - -130.392311f, 122.949849f, - -125.856872f, 112.912022f, - -118.699582f, 104.539366f, - -109.489523f, 98.497596f, - -98.958994f, 95.267098f, - -87.945283f, 95.104731f, - -84.371773f, 95.771023f, - -80.898574f, 96.843781f, - -77.571757f, 98.308777f, - -74.435452f, 100.146577f, - -71.531259f, 102.332803f, - -68.897703f, 104.838457f, - -66.569716f, 107.630302f, - -64.578177f, 110.671304f, - -62.949505f, 113.921127f, - -61.705303f, 117.336662f, - -60.862074f, 120.872604f, - -60.431003f, 124.482050f - }; - - - mTriangleOffsets = new int[64]; - mTriangleOffsetsTex = new float[64]; - - mTriangleOffsets[0] = 0; - mTriangleOffsetsCount = 1; - - Vertex t = new Vertex(); - t.nxyz(1, 0, 0); - int count = vtx.length / 2; - - SimpleMesh.TriangleMeshBuilder tm = new SimpleMesh.TriangleMeshBuilder( - rs, 3, - SimpleMesh.TriangleMeshBuilder.NORMAL | SimpleMesh.TriangleMeshBuilder.TEXTURE_0); - - float runningS = 0; - for (int ct=0; ct < (count-1); ct++) { - t.x = -vtx[ct*2] / 100.f; - t.z = vtx[ct*2+1] / 100.f; - t.s = runningS; - t.nx = (vtx[ct*2+3] - vtx[ct*2 +1]); - t.ny = (vtx[ct*2+2] - vtx[ct*2 ]); - float len = (float)java.lang.Math.sqrt(t.nx * t.nx + t.ny * t.ny); - runningS += len / 100; - t.nx /= len; - t.ny /= len; - t.y = -0.5f; - t.t = 0; - tm.setNormal(t.nx, t.ny, t.nz); - tm.setTexture(t.s, t.t); - tm.addVertex(t.x, t.y, t.z); - //android.util.Log.e("rs", "vtx x="+t.x+" y="+t.y+" z="+t.z+" s="+t.s+" t="+t.t); - t.y = .5f; - t.t = 1; - tm.setTexture(t.s, t.t); - tm.addVertex(t.x, t.y, t.z); - //android.util.Log.e("rs", "vtx x="+t.x+" y="+t.y+" z="+t.z+" s="+t.s+" t="+t.t); - - if((runningS*2) > mTriangleOffsetsCount) { - mTriangleOffsets[mTriangleOffsetsCount] = ct*2 * 3; - mTriangleOffsetsTex[mTriangleOffsetsCount] = t.s; - mTriangleOffsetsCount ++; - } - } - - count = (count * 2 - 2); - for (int ct=0; ct < (count-2); ct+= 2) { - tm.addTriangle(ct, ct+1, ct+2); - tm.addTriangle(ct+1, ct+3, ct+2); - } - return tm.create(); - } - - -} - diff --git a/libs/rs/java/Fountain/Android.mk b/libs/rs/java/Fountain/Android.mk index f7e53a8f1244e6b6eddbf261b8bdd14b9a2e58de..71944b2184dc166d86e31bce3eac792b72081f8e 100644 --- a/libs/rs/java/Fountain/Android.mk +++ b/libs/rs/java/Fountain/Android.mk @@ -14,14 +14,18 @@ # limitations under the License. # +ifneq ($(TARGET_SIMULATOR),true) + LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional -LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-renderscript-files-under, src) #LOCAL_STATIC_JAVA_LIBRARIES := android.renderscript LOCAL_PACKAGE_NAME := Fountain include $(BUILD_PACKAGE) + +endif diff --git a/libs/rs/java/Fountain/res/drawable/gadgets_clock_mp3.png b/libs/rs/java/Fountain/res/drawable/gadgets_clock_mp3.png deleted file mode 100755 index e91bfb418a8e0c10d0e69b950e0ee39303a4a0b8..0000000000000000000000000000000000000000 Binary files a/libs/rs/java/Fountain/res/drawable/gadgets_clock_mp3.png and /dev/null differ diff --git a/libs/rs/java/Fountain/res/raw/fountain.c b/libs/rs/java/Fountain/res/raw/fountain.c deleted file mode 100644 index 73b819b3df864e82eb2c7483f738e9985e4b1670..0000000000000000000000000000000000000000 --- a/libs/rs/java/Fountain/res/raw/fountain.c +++ /dev/null @@ -1,52 +0,0 @@ -// Fountain test script -#pragma version(1) - -int newPart = 0; - -int main(int launchID) { - int ct; - int count = Control->count; - int rate = Control->rate; - float height = getHeight(); - struct point_s * p = (struct point_s *)point; - - if (rate) { - float rMax = ((float)rate) * 0.005f; - int x = Control->x; - int y = Control->y; - int color = ((int)(Control->r * 255.f)) | - ((int)(Control->g * 255.f)) << 8 | - ((int)(Control->b * 255.f)) << 16 | - (0xf0 << 24); - struct point_s * np = &p[newPart]; - - while (rate--) { - vec2Rand((float *)&np->delta.x, rMax); - np->position.x = x; - np->position.y = y; - np->color = color; - newPart++; - np++; - if (newPart >= count) { - newPart = 0; - np = &p[newPart]; - } - } - } - - for (ct=0; ct < count; ct++) { - float dy = p->delta.y + 0.15f; - float posy = p->position.y + dy; - if ((posy > height) && (dy > 0)) { - dy *= -0.3f; - } - p->delta.y = dy; - p->position.x += p->delta.x; - p->position.y = posy; - p++; - } - - uploadToBufferObject(NAMED_PartBuffer); - drawSimpleMesh(NAMED_PartMesh); - return 1; -} diff --git a/libs/rs/java/Fountain/res/raw/fountain2.rs b/libs/rs/java/Fountain/res/raw/fountain2.rs deleted file mode 100644 index 33011403f15a28d87a6516693544a1ebc96ba474..0000000000000000000000000000000000000000 --- a/libs/rs/java/Fountain/res/raw/fountain2.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Fountain test script -#pragma version(1) - -#include "rs_types.rsh" -#include "rs_math.rsh" -#include "rs_graphics.rsh" - -static int newPart = 0; - -typedef struct Control_s { - int x, y; - int rate; - int count; - float r, g, b; - rs_allocation partBuffer; - rs_mesh partMesh; -} Control_t; -Control_t *Control; - -typedef struct Point_s{ - float2 delta; - float2 position; - unsigned int color; -} Point_t; -Point_t *point; - -int main(int launchID) { - int ct; - int count = Control->count; - int rate = Control->rate; - float height = getHeight(); - Point_t * p = point; - - if (rate) { - float rMax = ((float)rate) * 0.005f; - int x = Control->x; - int y = Control->y; - int color = ((int)(Control->r * 255.f)) | - ((int)(Control->g * 255.f)) << 8 | - ((int)(Control->b * 255.f)) << 16 | - (0xf0 << 24); - Point_t * np = &p[newPart]; - - while (rate--) { - np->delta = vec2Rand(rMax); - np->position.x = x; - np->position.y = y; - np->color = color; - newPart++; - np++; - if (newPart >= count) { - newPart = 0; - np = &p[newPart]; - } - } - } - - for (ct=0; ct < count; ct++) { - float dy = p->delta.y + 0.15f; - float posy = p->position.y + dy; - if ((posy > height) && (dy > 0)) { - dy *= -0.3f; - } - p->delta.y = dy; - p->position.x += p->delta.x; - p->position.y = posy; - p++; - } - - uploadToBufferObject(Control->partBuffer); - drawSimpleMesh(Control->partMesh); - return 1; -} diff --git a/libs/rs/java/Fountain/src/com/android/fountain/FountainRS.java b/libs/rs/java/Fountain/src/com/android/fountain/FountainRS.java index 935657965e116ed5f61f7462c199834a422cfe0f..0b26cfd218a7da1e06451d2c98c080015a7d29e2 100644 --- a/libs/rs/java/Fountain/src/com/android/fountain/FountainRS.java +++ b/libs/rs/java/Fountain/src/com/android/fountain/FountainRS.java @@ -22,94 +22,50 @@ import android.util.Log; public class FountainRS { - public static final int PART_COUNT = 20000; - - static class SomeData { - public int x; - public int y; - public int rate; - public int count; - public float r; - public float g; - public float b; - } + public static final int PART_COUNT = 50000; public FountainRS() { } + private Resources mRes; + private RenderScriptGL mRS; + private ScriptC_fountain mScript; public void init(RenderScriptGL rs, Resources res, int width, int height) { mRS = rs; mRes = res; - initRS(); - } - - public void newTouchPosition(int x, int y, int rate) { - if (mSD.rate == 0) { - mSD.r = ((x & 0x1) != 0) ? 0.f : 1.f; - mSD.g = ((x & 0x2) != 0) ? 0.f : 1.f; - mSD.b = ((x & 0x4) != 0) ? 0.f : 1.f; - if ((mSD.r + mSD.g + mSD.b) < 0.9f) { - mSD.r = 0.8f; - mSD.g = 0.5f; - mSD.b = 1.f; - } - } - mSD.rate = rate; - mSD.x = x; - mSD.y = y; - mIntAlloc.data(mSD); - } - - - ///////////////////////////////////////// - - private Resources mRes; - - private RenderScriptGL mRS; - private Allocation mIntAlloc; - private SimpleMesh mSM; - private SomeData mSD; - private Type mSDType; - private void initRS() { - mSD = new SomeData(); - mSDType = Type.createFromClass(mRS, SomeData.class, 1, "SomeData"); - mIntAlloc = Allocation.createTyped(mRS, mSDType); - mSD.count = PART_COUNT; - mIntAlloc.data(mSD); + ProgramFragment.Builder pfb = new ProgramFragment.Builder(rs); + pfb.setVaryingColor(true); + rs.contextBindProgramFragment(pfb.create()); - Element.Builder eb = new Element.Builder(mRS); - eb.add(Element.createVector(mRS, Element.DataType.FLOAT_32, 2), "delta"); - eb.add(Element.createAttrib(mRS, Element.DataType.FLOAT_32, Element.DataKind.POSITION, 2), "position"); - eb.add(Element.createAttrib(mRS, Element.DataType.UNSIGNED_8, Element.DataKind.COLOR, 4), "color"); - Element primElement = eb.create(); + ScriptField_Point points = new ScriptField_Point(mRS, PART_COUNT); + Mesh.AllocationBuilder smb = new Mesh.AllocationBuilder(mRS); + smb.addVertexAllocation(points.getAllocation()); + smb.addIndexType(Primitive.POINT); + Mesh sm = smb.create(); - SimpleMesh.Builder smb = new SimpleMesh.Builder(mRS); - int vtxSlot = smb.addVertexType(primElement, PART_COUNT); - smb.setPrimitive(Primitive.POINT); - mSM = smb.create(); - mSM.setName("PartMesh"); - - Allocation partAlloc = mSM.createVertexAllocation(vtxSlot); - partAlloc.setName("PartBuffer"); - mSM.bindVertexAllocation(partAlloc, 0); + mScript = new ScriptC_fountain(mRS, mRes, R.raw.fountain, true); + mScript.set_partMesh(sm); + mScript.bind_point(points); + mRS.contextBindRootScript(mScript); + } - // All setup of named objects should be done by this point - // because we are about to compile the script. - ScriptC.Builder sb = new ScriptC.Builder(mRS); - sb.setScript(mRes, R.raw.fountain); - sb.setRoot(true); - sb.setType(mSDType, "Control", 0); - sb.setType(mSM.getVertexType(0), "point", 1); - Script script = sb.create(); - script.setClearColor(0.0f, 0.0f, 0.0f, 1.0f); + boolean holdingColor[] = new boolean[10]; + public void newTouchPosition(float x, float y, float pressure, int id) { + if (id > holdingColor.length) { + return; + } + int rate = (int)(pressure * pressure * 500.f); + if(rate > 500) { + rate = 500; + } + if (rate > 0) { + mScript.invoke_addParticles(rate, x, y, id, !holdingColor[id]); + holdingColor[id] = true; + } else { + holdingColor[id] = false; + } - script.bindAllocation(mIntAlloc, 0); - script.bindAllocation(partAlloc, 1); - mRS.contextBindRootScript(script); } - } - - diff --git a/libs/rs/java/Fountain/src/com/android/fountain/FountainView.java b/libs/rs/java/Fountain/src/com/android/fountain/FountainView.java index dfd6a49d09a1d600495e04aea29fbfa87690d090..c1411656b010d4f1bd0ea4e8b921512eb628510b 100644 --- a/libs/rs/java/Fountain/src/com/android/fountain/FountainView.java +++ b/libs/rs/java/Fountain/src/com/android/fountain/FountainView.java @@ -71,17 +71,33 @@ public class FountainView extends RSSurfaceView { @Override public boolean onTouchEvent(MotionEvent ev) { - int act = ev.getAction(); + int act = ev.getActionMasked(); if (act == ev.ACTION_UP) { - mRender.newTouchPosition(0, 0, 0); + mRender.newTouchPosition(0, 0, 0, ev.getPointerId(0)); return false; + } else if (act == MotionEvent.ACTION_POINTER_UP) { + // only one pointer going up, we can get the index like this + int pointerIndex = ev.getActionIndex(); + int pointerId = ev.getPointerId(pointerIndex); + mRender.newTouchPosition(0, 0, 0, pointerId); } - float rate = (ev.getPressure() * 50.f); - rate *= rate; - if(rate > 2000.f) { - rate = 2000.f; + int count = ev.getHistorySize(); + int pcount = ev.getPointerCount(); + + for (int p=0; p < pcount; p++) { + int id = ev.getPointerId(p); + mRender.newTouchPosition(ev.getX(p), + ev.getY(p), + ev.getPressure(p), + id); + + for (int i=0; i < count; i++) { + mRender.newTouchPosition(ev.getHistoricalX(p, i), + ev.getHistoricalY(p, i), + ev.getHistoricalPressure(p, i), + id); + } } - mRender.newTouchPosition((int)ev.getX(), (int)ev.getY(), (int)rate); return true; } } diff --git a/libs/rs/java/Fountain/src/com/android/fountain/fountain.rs b/libs/rs/java/Fountain/src/com/android/fountain/fountain.rs new file mode 100644 index 0000000000000000000000000000000000000000..812cb7ab33f32373dd7fc961bc87f48244be0142 --- /dev/null +++ b/libs/rs/java/Fountain/src/com/android/fountain/fountain.rs @@ -0,0 +1,72 @@ +// Fountain test script +#pragma version(1) + +#pragma rs java_package_name(com.android.fountain) + +#pragma stateFragment(parent) + +#include "rs_graphics.rsh" + +static int newPart = 0; +rs_mesh partMesh; + +typedef struct __attribute__((packed, aligned(4))) Point { + float2 delta; + float2 position; + uchar4 color; +} Point_t; +Point_t *point; + +#pragma rs export_var(point, partMesh) +#pragma rs export_func(addParticles) + +int root() { + float dt = min(rsGetDt(), 0.1f); + rsgClearColor(0.f, 0.f, 0.f, 1.f); + const float height = rsgGetHeight(); + const int size = rsAllocationGetDimX(rsGetAllocation(point)); + float dy2 = dt * (10.f); + Point_t * p = point; + for (int ct=0; ct < size; ct++) { + p->delta.y += dy2; + p->position += p->delta; + if ((p->position.y > height) && (p->delta.y > 0)) { + p->delta.y *= -0.3f; + } + p++; + } + + rsgDrawMesh(partMesh); + return 1; +} + +static float4 partColor[10]; +void addParticles(int rate, float x, float y, int index, bool newColor) +{ + if (newColor) { + partColor[index].x = rsRand(0.5f, 1.0f); + partColor[index].y = rsRand(1.0f); + partColor[index].z = rsRand(1.0f); + } + float rMax = ((float)rate) * 0.02f; + int size = rsAllocationGetDimX(rsGetAllocation(point)); + uchar4 c = rsPackColorTo8888(partColor[index]); + + Point_t * np = &point[newPart]; + float2 p = {x, y}; + while (rate--) { + float angle = rsRand(3.14f * 2.f); + float len = rsRand(rMax); + np->delta.x = len * sin(angle); + np->delta.y = len * cos(angle); + np->position = p; + np->color = c; + newPart++; + np++; + if (newPart >= size) { + newPart = 0; + np = &point[newPart]; + } + } +} + diff --git a/libs/rs/java/ImageProcessing/Android.mk b/libs/rs/java/ImageProcessing/Android.mk index 833427b269ae839fc3e2dcb0af8cc31a7262812c..7fa30d0b8bdd5124d8b98c2c0ea896c1db17d291 100644 --- a/libs/rs/java/ImageProcessing/Android.mk +++ b/libs/rs/java/ImageProcessing/Android.mk @@ -14,14 +14,19 @@ # limitations under the License. # +ifneq ($(TARGET_SIMULATOR),true) + LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional -LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_SRC_FILES := $(call all-java-files-under, src) \ + $(call all-renderscript-files-under, src) #LOCAL_STATIC_JAVA_LIBRARIES := android.renderscript LOCAL_PACKAGE_NAME := ImageProcessing include $(BUILD_PACKAGE) + +endif diff --git a/libs/rs/java/ImageProcessing/AndroidManifest.xml b/libs/rs/java/ImageProcessing/AndroidManifest.xml index b48d2082ae38de0af43ad79f829975fbfc2ff996..d6a2db4a46ef872936f0b2ab598f35cd0aeabf0f 100644 --- a/libs/rs/java/ImageProcessing/AndroidManifest.xml +++ b/libs/rs/java/ImageProcessing/AndroidManifest.xml @@ -6,7 +6,8 @@ - + diff --git a/libs/rs/java/ImageProcessing/res/drawable/data.jpg b/libs/rs/java/ImageProcessing/res/drawable/data.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81a87b1726c2c137abb35fefdd19b00fce097bdf Binary files /dev/null and b/libs/rs/java/ImageProcessing/res/drawable/data.jpg differ diff --git a/libs/rs/java/ImageProcessing/res/layout/main.xml b/libs/rs/java/ImageProcessing/res/layout/main.xml index 6770c18838c9061fc375d8af970600dd7658dfa1..c6ec729652aa64f437e13d4f4fdb45dead6adb9e 100644 --- a/libs/rs/java/ImageProcessing/res/layout/main.xml +++ b/libs/rs/java/ImageProcessing/res/layout/main.xml @@ -25,9 +25,147 @@ android:id="@+id/display" android:layout_width="320dip" android:layout_height="266dip" /> - + +