diff --git a/Android.mk b/Android.mk index 0978bec05c0f177daf61174fc6b9e430aad6de86..f7219302a17a94b6c989b40f6bda6dd6ccec6383 100644 --- a/Android.mk +++ b/Android.mk @@ -1,73 +1,251 @@ +# Local modifications: +# * All location/maps code has been removed from the incallui. +# * Precompiled AutoValue classes have been included. +# * Precompiled Dagger classes have been included. +# * All autovalue imports and annotations have been stripped. +# * Precompiled proto classes have been included. LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) -LOCAL_MODULE_TAGS := optional - -incallui_dir := InCallUI -contacts_common_dir := ../ContactsCommon -phone_common_dir := ../PhoneCommon - ifeq ($(TARGET_BUILD_APPS),) support_library_root_dir := frameworks/support else support_library_root_dir := prebuilts/sdk/current/support endif -src_dirs := src \ - $(incallui_dir)/src \ - $(contacts_common_dir)/src \ - $(phone_common_dir)/src +# The base directory for Dialer sources. +BASE_DIR := java/com/android -res_dirs := res \ - $(incallui_dir)/res \ - $(contacts_common_dir)/res \ - $(contacts_common_dir)/icons/res \ - $(phone_common_dir)/res +# Primary dialer module sources. +SRC_DIRS := \ + $(BASE_DIR)/contacts/common \ + $(BASE_DIR)/dialer \ + $(BASE_DIR)/incallui \ + $(BASE_DIR)/voicemailomtp -src_dirs += \ - src-N +# All Dialers resources. +# find . -type d -name "res" | uniq | sort +RES_DIRS := \ + assets/product/res \ + assets/quantum/res \ + $(BASE_DIR)/contacts/common/res \ + $(BASE_DIR)/dialer/app/res \ + $(BASE_DIR)/dialer/app/voicemail/error/res \ + $(BASE_DIR)/dialer/blocking/res \ + $(BASE_DIR)/dialer/callcomposer/camera/camerafocus/res \ + $(BASE_DIR)/dialer/callcomposer/cameraui/res \ + $(BASE_DIR)/dialer/callcomposer/res \ + $(BASE_DIR)/dialer/common/res \ + $(BASE_DIR)/dialer/dialpadview/res \ + $(BASE_DIR)/dialer/interactions/res \ + $(BASE_DIR)/dialer/phonenumberutil/res \ + $(BASE_DIR)/dialer/shortcuts/res \ + $(BASE_DIR)/dialer/theme/res \ + $(BASE_DIR)/dialer/util/res \ + $(BASE_DIR)/dialer/voicemailstatus/res \ + $(BASE_DIR)/dialer/widget/res \ + $(BASE_DIR)/incallui/answer/impl/affordance/res \ + $(BASE_DIR)/incallui/answer/impl/answermethod/res \ + $(BASE_DIR)/incallui/answer/impl/hint/res \ + $(BASE_DIR)/incallui/answer/impl/res \ + $(BASE_DIR)/incallui/audioroute/res \ + $(BASE_DIR)/incallui/autoresizetext/res \ + $(BASE_DIR)/incallui/commontheme/res \ + $(BASE_DIR)/incallui/contactgrid/res \ + $(BASE_DIR)/incallui/hold/res \ + $(BASE_DIR)/incallui/incall/impl/res \ + $(BASE_DIR)/incallui/res \ + $(BASE_DIR)/incallui/sessiondata/res \ + $(BASE_DIR)/incallui/video/impl/res \ + $(BASE_DIR)/incallui/wifi/res \ + $(BASE_DIR)/voicemailomtp/res -LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs)) -LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) \ - $(support_library_root_dir)/v7/cardview/res \ - $(support_library_root_dir)/v7/recyclerview/res \ - $(support_library_root_dir)/v7/appcompat/res \ - $(support_library_root_dir)/design/res \ - $(support_library_root_dir)/transition/res +# Dialer manifest files to merge. +# find . -type f -name "AndroidManifest.xml" | uniq | sort +DIALER_MANIFEST_FILES += \ + $(BASE_DIR)/contacts/common/AndroidManifest.xml \ + $(BASE_DIR)/dialer/app/AndroidManifest.xml \ + $(BASE_DIR)/dialer/app/manifests/activities/AndroidManifest.xml \ + $(BASE_DIR)/dialer/app/voicemail/error/AndroidManifest.xml \ + $(BASE_DIR)/dialer/backup/AndroidManifest.xml \ + $(BASE_DIR)/dialer/blocking/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/camera/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/cameraui/AndroidManifest.xml \ + $(BASE_DIR)/dialer/common/AndroidManifest.xml \ + $(BASE_DIR)/dialer/debug/AndroidManifest.xml \ + $(BASE_DIR)/dialer/debug/impl/AndroidManifest.xml \ + $(BASE_DIR)/dialer/dialpadview/AndroidManifest.xml \ + $(BASE_DIR)/dialer/interactions/AndroidManifest.xml \ + $(BASE_DIR)/dialer/phonenumberutil/AndroidManifest.xml \ + $(BASE_DIR)/dialer/shortcuts/AndroidManifest.xml \ + $(BASE_DIR)/dialer/simulator/impl/AndroidManifest.xml \ + $(BASE_DIR)/dialer/theme/AndroidManifest.xml \ + $(BASE_DIR)/dialer/util/AndroidManifest.xml \ + $(BASE_DIR)/dialer/voicemailstatus/AndroidManifest.xml \ + $(BASE_DIR)/dialer/widget/AndroidManifest.xml \ + $(BASE_DIR)/incallui/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/affordance/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/answermethod/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/hint/AndroidManifest.xml \ + $(BASE_DIR)/incallui/audioroute/AndroidManifest.xml \ + $(BASE_DIR)/incallui/autoresizetext/AndroidManifest.xml \ + $(BASE_DIR)/incallui/commontheme/AndroidManifest.xml \ + $(BASE_DIR)/incallui/contactgrid/AndroidManifest.xml \ + $(BASE_DIR)/incallui/hold/AndroidManifest.xml \ + $(BASE_DIR)/incallui/incall/impl/AndroidManifest.xml \ + $(BASE_DIR)/incallui/sessiondata/AndroidManifest.xml \ + $(BASE_DIR)/incallui/video/impl/AndroidManifest.xml \ + $(BASE_DIR)/incallui/wifi/AndroidManifest.xml \ + $(BASE_DIR)/voicemailomtp/AndroidManifest.xml +# Merge all manifest files. +LOCAL_FULL_LIBS_MANIFEST_FILES := \ + $(addprefix $(LOCAL_PATH)/, $(DIALER_MANIFEST_FILES)) +LOCAL_SRC_FILES := $(call all-java-files-under, $(SRC_DIRS)) +LOCAL_RESOURCE_DIR := \ + $(addprefix $(LOCAL_PATH)/, $(RES_DIRS)) \ + $(support_library_root_dir)/design/res \ + $(support_library_root_dir)/v7/appcompat/res \ + $(support_library_root_dir)/v7/cardview/res \ + $(support_library_root_dir)/v7/recyclerview/res + +# We specify each package explicitly to glob resource files. LOCAL_AAPT_FLAGS := \ - --auto-add-overlay \ - --extra-packages android.support.v7.appcompat \ - --extra-packages android.support.v7.cardview \ - --extra-packages android.support.v7.recyclerview \ - --extra-packages android.support.design \ - --extra-packages android.support.transition \ - --extra-packages com.android.incallui \ - --extra-packages com.android.contacts.common \ - --extra-packages com.android.phone.common + --auto-add-overlay \ + --extra-packages android.support.design \ + --extra-packages android.support.transition \ + --extra-packages android.support.v7.appcompat \ + --extra-packages android.support.v7.cardview \ + --extra-packages android.support.v7.recyclerview \ + --extra-packages com.android.contacts.common \ + --extra-packages com.android.dialer.app \ + --extra-packages com.android.dialer.app.voicemail.error \ + --extra-packages com.android.dialer.blocking \ + --extra-packages com.android.dialer.callcomposer \ + --extra-packages com.android.dialer.callcomposer \ + --extra-packages com.android.dialer.callcomposer.camera \ + --extra-packages com.android.dialer.callcomposer.camera.camerafocus \ + --extra-packages com.android.dialer.callcomposer.cameraui \ + --extra-packages com.android.dialer.common \ + --extra-packages com.android.dialer.dialpadview \ + --extra-packages com.android.dialer.interactions \ + --extra-packages com.android.dialer.phonenumberutil \ + --extra-packages com.android.dialer.shortcuts \ + --extra-packages com.android.dialer.util \ + --extra-packages com.android.dialer.voicemailstatus \ + --extra-packages com.android.dialer.widget \ + --extra-packages com.android.incallui \ + --extra-packages com.android.incallui.answer.impl \ + --extra-packages com.android.incallui.answer.impl.affordance \ + --extra-packages com.android.incallui.answer.impl.affordance \ + --extra-packages com.android.incallui.answer.impl.answermethod \ + --extra-packages com.android.incallui.answer.impl.hint \ + --extra-packages com.android.incallui.audioroute \ + --extra-packages com.android.incallui.autoresizetext \ + --extra-packages com.android.incallui.commontheme \ + --extra-packages com.android.incallui.contactgrid \ + --extra-packages com.android.incallui.hold \ + --extra-packages com.android.incallui.incall.impl \ + --extra-packages com.android.incallui.sessiondata \ + --extra-packages com.android.incallui.video \ + --extra-packages com.android.incallui.video.impl \ + --extra-packages com.android.incallui.wifi \ + --extra-packages com.android.phone.common \ + --extra-packages com.android.voicemailomtp \ + --extra-packages com.android.voicemailomtp.settings \ + --extra-packages me.leolin.shortcutbadger LOCAL_STATIC_JAVA_LIBRARIES := \ - android-common \ - android-support-v13 \ - android-support-v4 \ - android-support-v7-appcompat \ - android-support-v7-cardview \ - android-support-v7-recyclerview \ - android-support-design \ - android-support-transition \ - com.android.vcard \ - guava \ - libphonenumber + android-common \ + android-support-design \ + android-support-v13 \ + android-support-v4 \ + android-support-v7-appcompat \ + android-support-v7-cardview \ + android-support-v7-recyclerview \ + com.android.vcard \ + dailer-dagger2-compiler \ + dialer-dagger2 \ + dialer-dagger2-producers \ + dialer-glide \ + dialer-javax-annotation-api \ + dialer-javax-inject \ + dialer-libshortcutbadger \ + jsr305 \ + libphonenumber \ + libprotobuf-java-nano \ + org.apache.http.legacy.boot \ + volley + +LOCAL_JAVA_LIBRARIES := \ + android-support-annotations \ + android-support-transition \ + dailer-dagger2-compiler \ + dialer-dagger2 \ + dialer-dagger2-producers \ + dialer-glide \ + dialer-guava \ + dialer-javax-annotation-api \ + dialer-javax-inject \ + dialer-libshortcutbadger \ + jsr305 \ + libprotobuf-java-nano + +# Libraries needed by the compiler (JACK) to generate code. +PROCESSOR_LIBRARIES_TARGET := \ + dailer-dagger2-compiler \ + dialer-dagger2 \ + dialer-dagger2-producers \ + dialer-guava \ + dialer-javax-annotation-api \ + dialer-javax-inject + +# TODO: Include when JACK properly supports AutoValue b/35360557 +# (builders not generated successfully, javac duplicate issues) in +# LOCAL_STATIC_JAVA_LIBRARIES, LOCAL_JAVA_LIBRARIES, PROCESSOR_LIBRARIES_TARGET +# dialer-auto-value + +# Resolve the jar paths. +PROCESSOR_JARS := $(call java-lib-deps, $(PROCESSOR_LIBRARIES_TARGET)) +LOCAL_ADDITIONAL_DEPENDENCIES += $(PROCESSOR_JARS) + +LOCAL_JACK_FLAGS += --processorpath $(call normalize-path-list,$(PROCESSOR_JARS)) + +LOCAL_PROGUARD_FLAG_FILES := proguard.flags $(incallui_dir)/proguard.flags +LOCAL_SDK_VERSION := current +LOCAL_MODULE_TAGS := optional LOCAL_PACKAGE_NAME := Dialer LOCAL_CERTIFICATE := shared LOCAL_PRIVILEGED_MODULE := true +include $(BUILD_PACKAGE) -LOCAL_PROGUARD_FLAG_FILES := proguard.flags $(incallui_dir)/proguard.flags +# Cleanup local state +BASE_DIR := +SRC_DIRS := +RES_DIRS := +DIALER_MANIFEST_FILES := +PROCESSOR_LIBRARIES_TARGET := +PROCESSOR_JARS := -LOCAL_SDK_VERSION := current +# Create references to prebuilt libraries. +include $(CLEAR_VARS) -include $(BUILD_PACKAGE) +LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \ + dailer-dagger2-compiler:../../../prebuilts/tools/common/m2/repository/com/google/dagger/dagger-compiler/2.6/dagger-compiler-2.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-auto-common:../../../prebuilts/tools/common/m2/repository/com/google/auto/auto-common/0.6/auto-common-0.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-auto-value:../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.3/auto-value-1.3$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-dagger2:../../../prebuilts/tools/common/m2/repository/com/google/dagger/dagger/2.6/dagger-2.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-dagger2-producers:../../../prebuilts/tools/common/m2/repository/com/google/dagger/dagger-producers/2.6/dagger-producers-2.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-glide:../../../prebuilts/maven_repo/bumptech/com/github/bumptech/glide/glide/4.0.0-SNAPSHOT/glide-4.0.0-SNAPSHOT$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-guava:../../../prebuilts/tools/common/m2/repository/com/google/guava/guava/20.0/guava-20.0$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-javax-annotation-api:../../../prebuilts/tools/common/m2/repository/javax/annotation/javax.annotation-api/1.2/javax.annotation-api-1.2$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-javax-inject:../../../prebuilts/tools/common/m2/repository/javax/inject/javax.inject/1/javax.inject-1$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-libshortcutbadger:../../../prebuilts/tools/common/m2/repository/me/leolin/ShortcutBadger/1.1.13/ShortcutBadger-1.1.13$(COMMON_JAVA_PACKAGE_SUFFIX) -# Use the following include to make our test apk. -include $(call all-makefiles-under,$(LOCAL_PATH)) +include $(BUILD_MULTI_PREBUILT) + +include $(CLEAR_VARS) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d83080376872d60d11be29a2bd198403c8844450..85ed1981c86c5edbecffdd1670b994a3e676e271 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + coreApp="true" + package="com.android.dialer" + android:versionCode="90000" + android:versionName="9.0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000000000000000000000000000000000000..2827b7d3fa277e2daab95ea3cfaff1c2bfc1389e --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,27 @@ +Want to contribute? Great! First, read this page (including the small print at the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement] +(https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. + +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the +[Software Grant and Corporate Contributor License Agreement] +(https://cla.developers.google.com/about/google-corporate). diff --git a/InCallUI/AndroidManifest.xml b/InCallUI/AndroidManifest.xml deleted file mode 100644 index 5c758edaa980d23f182cd465e4ee4ff58546f8f7..0000000000000000000000000000000000000000 --- a/InCallUI/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/InCallUI/build.gradle b/InCallUI/build.gradle deleted file mode 100644 index de472519935358f4c76e80962c2077fbbc61fee3..0000000000000000000000000000000000000000 --- a/InCallUI/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -apply plugin: 'com.android.library' - -android { - sourceSets.main { - manifest.srcFile 'AndroidManifest.xml' - res.srcDirs = ['res'] - } -} - -dependencies { - compile 'com.android.support:support-v4:23.1.+' - compile project(':phonecommon') - compile project(':contactscommon') -} diff --git a/InCallUI/proguard.flags b/InCallUI/proguard.flags deleted file mode 100644 index 4e8310ca9e263b6ad332112096cf4d3bef0df868..0000000000000000000000000000000000000000 --- a/InCallUI/proguard.flags +++ /dev/null @@ -1,14 +0,0 @@ --keep class com.android.incallui.widget.multiwaveview.* { - *; -} - -# Keep names that are used only by animation framework. --keepclasseswithmembers class com.android.incallui.AnimationUtils$CrossFadeDrawable { - *** setCrossFadeAlpha(...); -} - -# Any class or method annotated with NeededForTesting or NeededForReflection. --keepclassmembers class * { -@com.android.contacts.common.test.NeededForTesting *; -@com.android.incallui.NeededForReflection *; -} diff --git a/InCallUI/res/anim/activity_open_enter.xml b/InCallUI/res/anim/activity_open_enter.xml deleted file mode 100644 index 303b9ddc0af9f6b82f66f6e1f1e27d6d6cdc293f..0000000000000000000000000000000000000000 --- a/InCallUI/res/anim/activity_open_enter.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/anim/activity_open_exit.xml b/InCallUI/res/anim/activity_open_exit.xml deleted file mode 100644 index afa7c5e725996f1f5c5e8ec1408314d2483120da..0000000000000000000000000000000000000000 --- a/InCallUI/res/anim/activity_open_exit.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/InCallUI/res/anim/call_status_pulse.xml b/InCallUI/res/anim/call_status_pulse.xml deleted file mode 100644 index abda25b7349cf0836fbdc49fb2e80be5218b01cd..0000000000000000000000000000000000000000 --- a/InCallUI/res/anim/call_status_pulse.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/InCallUI/res/color/selectable_icon_tint.xml b/InCallUI/res/color/selectable_icon_tint.xml deleted file mode 100644 index b8aad130362ab0b1ca75de220ef4ba16dafba9c3..0000000000000000000000000000000000000000 --- a/InCallUI/res/color/selectable_icon_tint.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - diff --git a/InCallUI/res/drawable-hdpi/fab_blue.png b/InCallUI/res/drawable-hdpi/fab_blue.png deleted file mode 100644 index 8ff3d291859d94a80770ca04d8aa94add5ffc1ad..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_ic_call.png b/InCallUI/res/drawable-hdpi/fab_ic_call.png deleted file mode 100644 index 548a391a6986d4463f7e47897ff54087e213044c..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_ic_end_call.png b/InCallUI/res/drawable-hdpi/fab_ic_end_call.png deleted file mode 100644 index b7f54d3bb97c5048feda146669f93806a6bc20ab..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_ic_message.png b/InCallUI/res/drawable-hdpi/fab_ic_message.png deleted file mode 100644 index a1cf2ad82fbc61d3a9fa0da6a780b8584153746b..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_red.png b/InCallUI/res/drawable-hdpi/fab_red.png deleted file mode 100644 index 497cc7916b3c4e70eabdbe8afe28df591bf09a74..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_business_white_24dp.png deleted file mode 100644 index d10ebb766f4ba84a4f218de01c4c1fefb6368058..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_call_white_24dp.png deleted file mode 100644 index 1902e721bd79536e85cb1ead8398bf5862a8c4cc..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-hdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index 983c45e2c38c52bfb90cc15689e2646684d57e04..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_question_mark.png b/InCallUI/res/drawable-hdpi/ic_question_mark.png deleted file mode 100644 index adab6c13fae87672d40079492c169767649ecf43..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png deleted file mode 100644 index 06603f21ce1b807ba8052f97d23da3f4aa0e5a9c..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-hdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index ea02daad200be6ee75a0ddbd03cc1bd36476e8d4..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-hdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index 05e19bc25f804b0d85f32d71f5a2640e60e5e03e..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-hdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 413fdff2644768a8beb4c426443c08383f3143a5..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-hdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index 90ee1fb5fae94fe0d3c5ff281fee0037dec920d5..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png deleted file mode 100644 index 69ece11bee2eb25e0214cc0ab3bfddaba202a57c..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-hdpi/ic_toolbar_hold.png deleted file mode 100644 index f32d6d5520445a4ed91910fe1e03b2c38721703f..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-hdpi/ic_toolbar_merge.png deleted file mode 100644 index 2871555e4b3c093cdb41e0af92fa9d9228eb8bd7..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png deleted file mode 100644 index b142ca869a32ecd26005cdf2598071d2652c0107..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-hdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index c934b13447e9ab13e591ed08c8044ba981e4ab49..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-hdpi/ic_toolbar_swap.png deleted file mode 100644 index e673f3251e4256fd94b36a34635ca90b8cbe3187..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_video.png b/InCallUI/res/drawable-hdpi/ic_toolbar_video.png deleted file mode 100644 index cef47aaff2dbab80f81404eaddf5b6572d7dd634..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-hdpi/ic_toolbar_video_off.png deleted file mode 100644 index 968ded7d8dbab1755b35d0ee8337884af9c57b2e..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-hdpi/ic_toolbar_video_switch.png deleted file mode 100644 index cdd623dc01f99f9f173f78029bfafe252699ac96..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-land/rounded_call_card_background.xml b/InCallUI/res/drawable-land/rounded_call_card_background.xml deleted file mode 100644 index f41ecda79aa2db60bf15920e3edde57f830a2ea4..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable-land/rounded_call_card_background.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable-mdpi/fab_blue.png b/InCallUI/res/drawable-mdpi/fab_blue.png deleted file mode 100644 index 2ca6b4bdf40ceddf8d4475986cbbc497509e36a5..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_ic_call.png b/InCallUI/res/drawable-mdpi/fab_ic_call.png deleted file mode 100644 index ff7b345e1f8630eaf466d4766612011d1e42e1fe..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_ic_end_call.png b/InCallUI/res/drawable-mdpi/fab_ic_end_call.png deleted file mode 100644 index 76ce3973d37f43fe1f01b9bba2b2d795fb8bbf1d..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_ic_message.png b/InCallUI/res/drawable-mdpi/fab_ic_message.png deleted file mode 100644 index 74876fe77d1d362417befb588407fff1becdcd44..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_red.png b/InCallUI/res/drawable-mdpi/fab_red.png deleted file mode 100644 index c9e76a057b5db286500e3dff526623116e602d74..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_business_white_24dp.png deleted file mode 100644 index 7b9227c0674c839b3383586590a7bfe6e6b13fdc..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_call_white_24dp.png deleted file mode 100644 index d4e5f5d7de18303ba326d8442b66afd6ed1a6231..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-mdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index 056c3f175471310c5d152c3964f2efa5be999c4f..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_question_mark.png b/InCallUI/res/drawable-mdpi/ic_question_mark.png deleted file mode 100644 index cfe64f696da8078e891c13664aafd26e6bbda2ef..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png deleted file mode 100644 index 1ee2fb1f52ec076de418d4a96ba6b890dce27b3b..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-mdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index c39990deba53252668fdbd20dfed24eb695d1a26..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-mdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index a6634ed662f7877274f1f0d534b0afb702d8ba96..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-mdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index b387e850ad7335f0427f8359c1725f6c167e591c..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-mdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index b4d887cf34054c0f0312ca61187f7101178aa887..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png deleted file mode 100644 index 9baa21b951023094d06deacd79e67c693024b240..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-mdpi/ic_toolbar_hold.png deleted file mode 100644 index c8372738b2de65c90275312a0bd68ec13eff1751..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-mdpi/ic_toolbar_merge.png deleted file mode 100644 index 2fba86514598889c71db643645c489a00b03a2f9..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png deleted file mode 100644 index c6b02b82c53c6c91a765b6393f4b83439138c769..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-mdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index 008e245f843bf836ba9677388e2fee461c7c91de..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-mdpi/ic_toolbar_swap.png deleted file mode 100644 index acc9850d520cf1960e20bfe3d052b91ae1780d87..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_video.png b/InCallUI/res/drawable-mdpi/ic_toolbar_video.png deleted file mode 100644 index 3f13f9c311dc5b6524b46c3e9beb364e70c9304f..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-mdpi/ic_toolbar_video_off.png deleted file mode 100644 index 64a69f2a7b7c475264cbf0d9614097ace3ab2868..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-mdpi/ic_toolbar_video_switch.png deleted file mode 100644 index 6d097c9e74a12a109237c75ea9372b0a4c9c6f7f..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_blue.png b/InCallUI/res/drawable-xhdpi/fab_blue.png deleted file mode 100644 index 300b07eb4b056d00c6f6409af1d1a0279457af0b..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_ic_call.png b/InCallUI/res/drawable-xhdpi/fab_ic_call.png deleted file mode 100644 index 2bff65e0a75313ecf9f81c5fe7352518d5709aca..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_ic_end_call.png b/InCallUI/res/drawable-xhdpi/fab_ic_end_call.png deleted file mode 100644 index 1c95e175a665bca039cfacb37973172c5f767c64..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_ic_message.png b/InCallUI/res/drawable-xhdpi/fab_ic_message.png deleted file mode 100644 index 5e3334ae0dff0c19da8d09dc8ad8fb3a3c95e99d..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_red.png b/InCallUI/res/drawable-xhdpi/fab_red.png deleted file mode 100644 index 373e49e8f51c2a37f9fac8ed15cb64c85b4e882c..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_business_white_24dp.png deleted file mode 100644 index e5630455a9759548446c804e92739f71bc0b4a68..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png deleted file mode 100644 index cde9cea3a25994430a401874a29b496c4d339153..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-xhdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index cbd039afd5cda65e02ed583a79708c8c2c79416f..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_question_mark.png b/InCallUI/res/drawable-xhdpi/ic_question_mark.png deleted file mode 100644 index 8da4870885e6042b346b718613a1734c2ea5a3e2..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_add_call.png deleted file mode 100644 index b251d6bd862eeff3422cccab1263da4fadf7b049..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index cdaa79d37d8468008c1bc3dac28bc199810ddb20..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index 88f6bb9453cdd133d27d56d38600a7caae14bcb0..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 1acfcafbd2717285a1edbd1e30808c607f25f9a9..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index 0ba8f1e3e3430c41ad881ae27493148a9b0adef2..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png deleted file mode 100644 index cf803d1c1d0a81f406ddd9b0c3eb9ff66b0b6cbd..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png deleted file mode 100644 index 8fecf7514e0aef89ce55f68f9464fce8bdfa01b5..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png deleted file mode 100644 index 777483eb0465e3d531163d248d589e0455a01c6a..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png deleted file mode 100644 index cf2041ad69a5c3bf4929bf54b1d93c02a82a9b4a..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index 5b5831cc0bb14b29970460f56bc4531a1d90ed12..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png deleted file mode 100644 index 38917cb88da7419ef53a5ded6f4ea5bc41a4cf28..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_video.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_video.png deleted file mode 100644 index b20f504985d0e1f2b4f18512ac2fb384f9c1233f..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_video_off.png deleted file mode 100644 index 1b269a6a7b488600e7b6c13b8d6ebcb2b23f687d..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_video_switch.png deleted file mode 100644 index fae6bfdb16a0e2aa4c82bcd0248a92d2d4aae624..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_blue.png b/InCallUI/res/drawable-xxhdpi/fab_blue.png deleted file mode 100644 index 76d68ac6a21f7812e97e1730fd09cbfb846e4675..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_ic_call.png b/InCallUI/res/drawable-xxhdpi/fab_ic_call.png deleted file mode 100644 index a756b95adc35f09f5fe3e63585f4d2ed4942b722..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png b/InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png deleted file mode 100644 index 37e8264029e8a5cd7a968f35c859fd400295b4a8..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_ic_message.png b/InCallUI/res/drawable-xxhdpi/fab_ic_message.png deleted file mode 100644 index 66984b1e3f996abb0e2c6ebc50e4cc495fbe38cb..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_red.png b/InCallUI/res/drawable-xxhdpi/fab_red.png deleted file mode 100644 index 92eb979d5a9866ff9b3b7b634cae53365335e8c4..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_business_white_24dp.png deleted file mode 100644 index 7dfc8dc527c94768b086c12846022573de69fdac..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_call_white_24dp.png deleted file mode 100644 index b761bc466d6e758401ee550dfd7d5e0c5317ba61..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-xxhdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index c0edd91c8346759ad80480c75c94f0f268e8b90f..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_question_mark.png b/InCallUI/res/drawable-xxhdpi/ic_question_mark.png deleted file mode 100644 index b9b6b00e78b78e40ecb641cfdb19de36af0225b0..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_add_call.png deleted file mode 100644 index 6e343c74ec62f7c326b278871ac87343f076aaf0..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index 737704018308d93028aed201342cc888799c1d22..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index b8a385d1476cd8eabde1d6bdd290f1d96a2907b7..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 62d0ae331047171ed64df9301780f239ab88aeaa..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index 0e88501d66b2cae2be88fbbad4bb0ca4c6f996e9..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_dialpad.png deleted file mode 100644 index a754f68759f2ad9b1d8d1307a9b98494d16d3513..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png deleted file mode 100644 index f3757a8b5344fe05a60b5e59f2ba01d994623c48..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png deleted file mode 100644 index 5d046008c7de5ce16d2b4106134274383f649f48..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_mic_off.png deleted file mode 100644 index ae41d5c35409f74c12516328e5b3a1ca26ab2314..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index d1bbb0947f012022b6e2adcd6d7a5b04a2411803..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png deleted file mode 100644 index ea9127ee239d050d9f74736aeab72a36e304fe65..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png deleted file mode 100644 index 5c52dd6c6c3c46ff3eef281142a220cac0f70c24..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_off.png deleted file mode 100644 index 898b7c04d03887a338f8f0bc568ac9e43b607b0b..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_switch.png deleted file mode 100644 index 4380a47ca4e9925bcd9b79df272784954b0be567..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_blue.png b/InCallUI/res/drawable-xxxhdpi/fab_blue.png deleted file mode 100644 index 1dd8a9260fbc0890b736e48d9a1d35de009534e7..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png b/InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png deleted file mode 100644 index aabdadec2b73b0895295cf96fa18fc63eed7b034..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_ic_message.png b/InCallUI/res/drawable-xxxhdpi/fab_ic_message.png deleted file mode 100644 index c5a108abab42635dba2121b72ad8994d638aff14..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_red.png b/InCallUI/res/drawable-xxxhdpi/fab_red.png deleted file mode 100644 index f1b36f70baa9e03f2c2845ed01feafbed1b59486..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_business_white_24dp.png deleted file mode 100644 index c9aea72ceb49bc4581d8223d240c223cbbddb6a1..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_question_mark.png b/InCallUI/res/drawable-xxxhdpi/ic_question_mark.png deleted file mode 100644 index 7ba34242ca5f589d803fca0944b84842f08d7118..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_add_call.png deleted file mode 100644 index c97e4bb15131daf0314e9f165fa2a907cb3eaa85..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index 1c11c5d0f1de155a1149ce42403b1e819a6436d1..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index f7fa12c8b79e4b21e9b142c051fa5e6402f71fc1..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 8199701ce41b6ff97ba86a8825106fbea9b37b28..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index ee14ea67a41191bd9f97da1186740fc1374d1f26..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_dialpad.png deleted file mode 100644 index e537112fb0006f54093fe67838bdb27cdf31c2c2..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png deleted file mode 100644 index 883d0d609d4cffa00eaa3021c3aa0e7fc5d94c08..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png deleted file mode 100644 index 4b643750702112fe370594a7ff4b934390650dfa..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_mic_off.png deleted file mode 100644 index 2d8f279da5163ec682b8845fb6ed03bf7fbbe747..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index 0560bb262e2eca1df4ec95ab3c9bcefce8bb4dc3..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png deleted file mode 100644 index 6f03b3f6625c589a65be124ce4393015879998b8..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png deleted file mode 100644 index 0797fd01904be7bbf3af3ca1083e22f5bc82a33e..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_off.png deleted file mode 100644 index 63f742befb042d81a8c9d55ff4c16fe3ca2c36da..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_switch.png deleted file mode 100644 index 77ff73cdb361477fb68c2bd08f7648a1405b5a28..0000000000000000000000000000000000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable/btn_add.xml b/InCallUI/res/drawable/btn_add.xml deleted file mode 100644 index 7d5e90f71737fdca6550d6e25a0fa4a5cba51ad3..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_add.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_background.xml b/InCallUI/res/drawable/btn_background.xml deleted file mode 100644 index 5978858033527cbe13c0f3123d63ea85137f4933..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_background.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_change_to_video.xml b/InCallUI/res/drawable/btn_change_to_video.xml deleted file mode 100644 index a26cee3e9fc551a398738ac517c1e985a7a2db09..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_change_to_video.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_change_to_voice.xml b/InCallUI/res/drawable/btn_change_to_voice.xml deleted file mode 100644 index 86a7f21d548f51d71f0598a7543f75981bdef1a9..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_change_to_voice.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_compound_audio.xml b/InCallUI/res/drawable/btn_compound_audio.xml deleted file mode 100644 index 25a64a6abe528cfa5c9783a7884ad9491fa6f61c..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_compound_audio.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_background.xml b/InCallUI/res/drawable/btn_compound_background.xml deleted file mode 100644 index 20e2a3056c927296797002ae6ccd82fc5b5346b3..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_compound_background.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_dialpad.xml b/InCallUI/res/drawable/btn_compound_dialpad.xml deleted file mode 100644 index 1b78ead44038ef18dea82042430231f6357378e1..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_compound_dialpad.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_hold.xml b/InCallUI/res/drawable/btn_compound_hold.xml deleted file mode 100644 index 7974efae557eb53a2fc9c93e5634ee02a9b5690e..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_compound_hold.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_mute.xml b/InCallUI/res/drawable/btn_compound_mute.xml deleted file mode 100644 index 86708fb0c8f3712af437da47c2343187b5f1c5ab..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_compound_mute.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_video_off.xml b/InCallUI/res/drawable/btn_compound_video_off.xml deleted file mode 100644 index b942cd0c37e39872de85dbf553e7d4342515dbdc..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_compound_video_off.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_video_switch.xml b/InCallUI/res/drawable/btn_compound_video_switch.xml deleted file mode 100644 index f8111866ecce6181944cbbae185b59fb34c1a475..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_compound_video_switch.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_merge.xml b/InCallUI/res/drawable/btn_merge.xml deleted file mode 100644 index 2b4818a47b11e4d3737176245c3d6c96acac3926..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_merge.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_overflow.xml b/InCallUI/res/drawable/btn_overflow.xml deleted file mode 100644 index 2eb26cc1470a1a502ebb86d7a7746937b9fdb269..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_overflow.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_selected.xml b/InCallUI/res/drawable/btn_selected.xml deleted file mode 100644 index 1446e41633c03539609df4f1fa337e30ca7728de..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_selected.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_selected_focused.xml b/InCallUI/res/drawable/btn_selected_focused.xml deleted file mode 100644 index 2eda9bf8b983b491c2966d863d73141745de40e1..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_selected_focused.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_swap.xml b/InCallUI/res/drawable/btn_swap.xml deleted file mode 100644 index 5d6c8ecafad93c5dca0655743b994e2ce66bb93e..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_swap.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_unselected.xml b/InCallUI/res/drawable/btn_unselected.xml deleted file mode 100644 index aed995cecdc7855fdceb195e4de3d8ba7412621a..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_unselected.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_unselected_focused.xml b/InCallUI/res/drawable/btn_unselected_focused.xml deleted file mode 100644 index 66075d4272192cd7c9cdd3a6a143336d0798e258..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/btn_unselected_focused.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/conference_ripple.xml b/InCallUI/res/drawable/conference_ripple.xml deleted file mode 100644 index 4e4a2130476ff07dc00284cc5fe5057e86dfea61..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/conference_ripple.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/end_call_background.xml b/InCallUI/res/drawable/end_call_background.xml deleted file mode 100644 index c43deac4f09efbd51df88e1bd36ec4049cf28257..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/end_call_background.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_incall_audio_handle.xml b/InCallUI/res/drawable/ic_incall_audio_handle.xml deleted file mode 100644 index 2e71a5b7043e580e798423d147beef467fdedb0b..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_incall_audio_handle.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_incall_video_handle.xml b/InCallUI/res/drawable/ic_incall_video_handle.xml deleted file mode 100644 index a24e305c4f6bb04466bcdae03263e7400df82ac4..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_incall_video_handle.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer.xml b/InCallUI/res/drawable/ic_lockscreen_answer.xml deleted file mode 100644 index 3184111fb355429842513a6504d00d83bed82014..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_activated_layer.xml deleted file mode 100644 index f22b87e34a4230a9ac568cc18fe27028e45f3689..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_activated_layer.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_normal_layer.xml deleted file mode 100644 index 31b884f999a4e8f53bfbfbab0de5ed2ae2faa0ca..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_normal_layer.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_video.xml b/InCallUI/res/drawable/ic_lockscreen_answer_video.xml deleted file mode 100644 index 05577979aba97a14a77a4e7efa838fe4415b39be..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_video.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_video_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_video_activated_layer.xml deleted file mode 100644 index 7895e1b6d87f4ee377d64fc607804663088d5bbd..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_video_activated_layer.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_video_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_video_normal_layer.xml deleted file mode 100644 index 793a36e10c6e7fc097b1fbb8f1f26c143c1e2a1f..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_video_normal_layer.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline.xml b/InCallUI/res/drawable/ic_lockscreen_decline.xml deleted file mode 100644 index 6643816d91d83ae37160dfc86cdc0e0278309ceb..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_activated_layer.xml deleted file mode 100644 index 096c32b4af2a2a229d015de8eb77627a12ac8259..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_activated_layer.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_normal_layer.xml deleted file mode 100644 index 4da5f8d66c7da6f34004e5ec626e1ae44fb033d3..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_normal_layer.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_video.xml b/InCallUI/res/drawable/ic_lockscreen_decline_video.xml deleted file mode 100644 index cedd49757c338c06b1ee0ef367e601290571bf9a..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_video.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_video_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_video_activated_layer.xml deleted file mode 100644 index 0790aed19e712ffee6e8b698aa6bb26a5895fd85..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_video_activated_layer.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_video_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_video_normal_layer.xml deleted file mode 100644 index e3b89b9471f40806679504b9d1cd5b7801200b74..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_video_normal_layer.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_outerring.xml b/InCallUI/res/drawable/ic_lockscreen_outerring.xml deleted file mode 100644 index 489515fbcd8786a28ca790188b9806d2767109c7..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_outerring.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_text.xml b/InCallUI/res/drawable/ic_lockscreen_text.xml deleted file mode 100644 index f9caac8181cfa100ba3bebbc06693fc17a1928a2..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_text.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_text_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_text_activated_layer.xml deleted file mode 100644 index a74e36b31f66f86f9fab203c1ed15f1532e3329a..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_text_activated_layer.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_text_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_text_normal_layer.xml deleted file mode 100644 index be32d0baace5fea6e1f082abf2cee3af9f06eadd..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/ic_lockscreen_text_normal_layer.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/incoming_sms_background.xml b/InCallUI/res/drawable/incoming_sms_background.xml deleted file mode 100644 index 81ff21c61ddc177a9a97667a5067904ec29439b5..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/incoming_sms_background.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/InCallUI/res/drawable/outgoing_sms_background.xml b/InCallUI/res/drawable/outgoing_sms_background.xml deleted file mode 100644 index e4f868fead69611425a39fdb2788c9820cf9eb3d..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/outgoing_sms_background.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/InCallUI/res/drawable/spam_notification_icon.xml b/InCallUI/res/drawable/spam_notification_icon.xml deleted file mode 100644 index c8bafe0857d85fcfd3526b462b880a98fa1e5790..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/spam_notification_icon.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/subject_bubble.xml b/InCallUI/res/drawable/subject_bubble.xml deleted file mode 100644 index adab6783369565a5ca2b7b90cb4602299c7cfb6c..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/subject_bubble.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/unknown_notification_icon.xml b/InCallUI/res/drawable/unknown_notification_icon.xml deleted file mode 100644 index 85c50752ce4b39585ef1eb028c08359386405554..0000000000000000000000000000000000000000 --- a/InCallUI/res/drawable/unknown_notification_icon.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout-h400dp/call_card_fragment.xml b/InCallUI/res/layout-h400dp/call_card_fragment.xml deleted file mode 100644 index 2ef6e52daed1b706c31ef3e17e52571ecdcd5d09..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout-h400dp/call_card_fragment.xml +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout-h600dp/manage_conference_call_button.xml b/InCallUI/res/layout-h600dp/manage_conference_call_button.xml deleted file mode 100644 index 9a83313ac156eabae8c86fbb62be6af2e4d9e24f..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout-h600dp/manage_conference_call_button.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout-w500dp-land/call_card_fragment.xml b/InCallUI/res/layout-w500dp-land/call_card_fragment.xml deleted file mode 100644 index c71cf07a69b681cd8c3ec2a35c2a19bad3961d00..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout-w500dp-land/call_card_fragment.xml +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout-w600dp-land/manage_conference_call_button.xml b/InCallUI/res/layout-w600dp-land/manage_conference_call_button.xml deleted file mode 100644 index 9a83313ac156eabae8c86fbb62be6af2e4d9e24f..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout-w600dp-land/manage_conference_call_button.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/accessible_answer_fragment.xml b/InCallUI/res/layout/accessible_answer_fragment.xml deleted file mode 100644 index 90fe577888da098d9eb28eb4106ea53bce8d1a03..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/accessible_answer_fragment.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/answer_fragment.xml b/InCallUI/res/layout/answer_fragment.xml deleted file mode 100644 index ec6ef30ac0f64e5df2ee147f885a24a8adf67f3a..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/answer_fragment.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - diff --git a/InCallUI/res/layout/business_contact_context_list_header.xml b/InCallUI/res/layout/business_contact_context_list_header.xml deleted file mode 100644 index 90521188e5673f00631f7112b4ebc49c48623214..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/business_contact_context_list_header.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/business_context_info_list_item.xml b/InCallUI/res/layout/business_context_info_list_item.xml deleted file mode 100644 index 616d219d97b0a0a348babf252ef1857935fcfb7c..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/business_context_info_list_item.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/call_button_fragment.xml b/InCallUI/res/layout/call_button_fragment.xml deleted file mode 100644 index 802e3de6246f260cf8443f85bfec07904a3b3e8c..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/call_button_fragment.xml +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/call_card_fragment.xml b/InCallUI/res/layout/call_card_fragment.xml deleted file mode 100644 index fabde378af4a86cb0c6c3f7e73e8adda052f1e14..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/call_card_fragment.xml +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/caller_in_conference.xml b/InCallUI/res/layout/caller_in_conference.xml deleted file mode 100644 index ac78096f6fedce41959bc572e326a9405a2f01db..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/caller_in_conference.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/conference_manager_fragment.xml b/InCallUI/res/layout/conference_manager_fragment.xml deleted file mode 100644 index 7350bee14b83f1e8956aca843c67aa6a41a21b54..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/conference_manager_fragment.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/layout/incall_dialpad_fragment.xml b/InCallUI/res/layout/incall_dialpad_fragment.xml deleted file mode 100644 index b567dbbf2bf5fa25285b96e7c515d4c692a0c5dd..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/incall_dialpad_fragment.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/InCallUI/res/layout/incall_screen.xml b/InCallUI/res/layout/incall_screen.xml deleted file mode 100644 index 3922ea073c0bb3d0e15e25fddf98857ab912fe4d..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/incall_screen.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/InCallUI/res/layout/manage_conference_call_button.xml b/InCallUI/res/layout/manage_conference_call_button.xml deleted file mode 100644 index 01ca1bdc3a19bd495a8becb531a796cfaf44f17e..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/manage_conference_call_button.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/outgoing_call_animation.xml b/InCallUI/res/layout/outgoing_call_animation.xml deleted file mode 100644 index 69ba3d3c69f163ffa7bd19fdd70db3a4f85fe5ee..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/outgoing_call_animation.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/person_context_info_list_item.xml b/InCallUI/res/layout/person_context_info_list_item.xml deleted file mode 100644 index 4f973d56486f80877acfc24a7a61df36cba744cc..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/person_context_info_list_item.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/primary_call_info.xml b/InCallUI/res/layout/primary_call_info.xml deleted file mode 100644 index cae915224196846940705a94914289393b79bc90..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/primary_call_info.xml +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/secondary_call_info.xml b/InCallUI/res/layout/secondary_call_info.xml deleted file mode 100644 index e866795a6a8afb565e5800bf13c358284ea1b157..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/secondary_call_info.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/video_call_fragment.xml b/InCallUI/res/layout/video_call_fragment.xml deleted file mode 100644 index d5e11ef4ababbf2f4775f2611106dcb24043b960..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/video_call_fragment.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/video_call_views.xml b/InCallUI/res/layout/video_call_views.xml deleted file mode 100644 index d514f6df19843c1b50f730e634ae77e485d31ee5..0000000000000000000000000000000000000000 --- a/InCallUI/res/layout/video_call_views.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/menu/incall_audio_mode_menu.xml b/InCallUI/res/menu/incall_audio_mode_menu.xml deleted file mode 100644 index 070c1813a6604a6764718875fed957a52912fb41..0000000000000000000000000000000000000000 --- a/InCallUI/res/menu/incall_audio_mode_menu.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/values-af/strings.xml b/InCallUI/res/values-af/strings.xml deleted file mode 100644 index 181ffce66316248791764a92c81424ecb07ff6cb..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-af/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Foon" - "Hou aan" - "Onbekend" - "Privaat nommer" - "Telefoonhokkie" - "Konferensie-oproep" - "Oproep is ontkoppel" - "Luidspreker" - "Selfoonoorfoon" - "Bedraade kopfoon" - "Bluetooth" - "Stuur die volgende luitone?\n" - "Stuur luitone\n" - "Stuur" - "Ja" - "Nee" - "Vervang die plekhouerkarakter met" - "Konferensie-oproep %s" - "Stemboodskapnommer" - "Bel" - "Bel tans weer" - "Konferensie-oproep" - "Inkomende oproep" - "Inkomende werkoproep" - "Oproep beëindig" - "Hou aan" - "Lui af" - "In oproep" - "My nommer is %s" - "Koppel tans video" - "Video-oproep" - "Versoek tans video" - "Kan nie video-oproep koppel nie" - "Videoversoek is verwerp" - "Jou terugbelnommer\n %1$s" - "Jou noodterugbelnommer\n %1$s" - "Bel" - "Gemiste oproep" - "Gemiste oproepe" - "%s gemiste oproepe" - "Gemiste oproep vanaf %s" - "Voortdurende oproep" - "Voortdurende werkoproep" - "Voortdurende Wi-Fi-oproep" - "Voortdurende Wi-Fi-werkoproep" - "Hou aan" - "Inkomende oproep" - "Inkomende werkoproep" - "Inkomende Wi-Fi-oproep" - "Inkomende Wi-Fi-werkoproep" - "Inkomende video-oproep" - "Inkomende verdagte strooipos-oproep" - "Inkomende videoversoek" - "Nuwe stemboodskap" - "Nuwe stemboodskap (%d)" - "Bel %s" - "Stemboodskapnommer is onbekend" - "Geen diens nie" - "Gekose netwerk (%s) is nie beskikbaar nie" - "Antwoord" - "Lui af" - "Video" - "Stem" - "Aanvaar" - "Maak toe" - "Bel terug" - "Boodskap" - "Voortgesette oproep op \'n ander toestel" - "Sit oproep deur" - "Skakel vliegtuigmodus eers af om \'n oproep te maak." - "Nie geregistreer op netwerk nie." - "Sellulêre netwerk is nie beskikbaar nie." - "Voer \'n geldige nommer in om \'n oproep te maak." - "Kan nie bel nie." - "Begin tans MMI-volgorde …" - "Diens word nie gesteun nie." - "Kan nie oproepe wissel nie." - "Kan nie oproep skei nie." - "Kan nie deurskakel nie." - "Kan nie konferensie-oproep maak nie." - "Kan nie oproep weier nie." - "Kan nie oproep(e) vrystel nie." - "SIP-oproep" - "Noodoproep" - "Skakel tans radio aan …" - "Geen sein nie. Probeer tans weer …" - "Kan nie bel nie. %s is nie \'n noodnommer nie." - "Kan nie bel nie. Bel \'n noodnommer." - "Gebruik sleutelbord om te bel" - "Hou oproep" - "Hervat oproep" - "Beëindig oproep" - "Wys belblad" - "Versteek belblad" - "Demp" - "Ontdemp" - "Voeg oproep by" - "Smelt oproepe saam" - "Ruil" - "Bestuur oproepe" - "Bestuur konferensie-oproep" - "Konferensie-oproep" - "Bestuur" - "Oudio" - "Video-oproep" - "Verander na stemoproep" - "Wissel kamera" - "Skakel kamera aan" - "Skakel kamera af" - "Nog opsies" - "Speler het begin" - "Speler het gestop" - "Kamera is nie gereed nie" - "Kamera is gereed" - "Onbekende oproepsessiegebeurtenis" - "Diens" - "Opstelling" - "<Nie gestel nie>" - "Ander oproepinstellings" - "Bel via %s" - "Inkomend via %s" - "kontakfoto" - "gaan privaat" - "kies kontak" - "Skryf jou eie …" - "Kanselleer" - "Stuur" - "Antwoord" - "Stuur SMS" - "Weier" - "Antwoord as video-oproep" - "Antwoord as oudio-oproep" - "Aanvaar videoversoek" - "Weier videoversoek" - "Aanvaar videoversendversoek" - "Weier videoversendversoek" - "Aanvaar video-ontvangversoek" - "Weier video-ontvangversoek" - "Gly op vir %s." - "Gly links vir %s." - "Gly regs vir %s." - "Gly af vir %s." - "Vibreer" - "Vibreer" - "Klank" - "Verstekklank (%1$s)" - "Foonluitoon" - "Vibreer wanneer dit lui" - "Luitoon en vibreer" - "Bestuur konferensie-oproep" - "Noodnommer" - "Profielfoto" - "Kamera is af" - "via %s" - "Nota is gestuur" - "Onlangse boodskappe" - "Besigheidinligting" - "%.1f myl ver" - "%.1f km ver" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Maak môre om %s oop" - "Maak vandag om %s oop" - "Maak om %s toe" - "Het vandag om %s toegemaak" - "Nou oop" - "Nou gesluit" - "Verdagte strooiposbeller" - "Oproep het geëindig %1$s" - "Dit is die eerste keer wat hierdie nommer jou bel." - "Ons vermoed dat hierdie oproep strooipos was." - "Blokkeer/gee strooipos aan" - "Voeg kontak by" - "Nie strooipos nie" - diff --git a/InCallUI/res/values-am/strings.xml b/InCallUI/res/values-am/strings.xml deleted file mode 100644 index 99516cfec17dd730b8c27cd4db6975afa7737d7f..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-am/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ስልክ" - "ያዝና ቆይ" - "ያልታወቀ" - "የግል ቁጥር" - "የሕዝብ ስልክ" - "የስብሰባ ጥሪ" - "ጥሪው ተቋርጧል" - "ድምፅ ማጉያ" - "የስልክ ጆሮ ማዳመጫ" - "ባለ ገመድ የጆሮ ማዳመጫ" - "ብሉቱዝ" - "የሚከተሉትን የጥሪ ድምፆች ላክ?\n" - "የጥሪ ድምፆች በመላክ ላይ \n" - "ላክ" - "አዎ" - "አይ" - "የልቅ ምልክት ተካ በ" - "የስብሰባ ጥሪ %s" - "የድምፅ መልእክት ቁጥር" - "በመደወል ላይ" - "ዳግም በመደወል ላይ" - "የስብሰባ ጥሪ" - "ገቢ ጥሪ" - "ገቢ የሥራ ጥሪ" - "ጥሪ አብቅቷል" - "ያዝና ቆይ" - "በመዝጋት ላይ" - "ጥሪ ላይ" - "ቁጥሬ %s ነው" - "ቪድዮ በማገናኘት ላይ" - "የቪዲዮ ጥሪ" - "ቪድዮ በመጠየቅ ላይ" - "የቪዲዮ ጥሪን ማገናኘት አይቻልም" - "የቪዲዮ ጥያቄ ውድቅ ተደርጓል" - "የእርስዎ የመልሶ መደወያ ቁጥር\n%1$s" - "የእርስዎ የድንገተኛ አደጋ መልሶ መደወያ ቁጥር\n%1$s" - "በመደወል ላይ" - "ያመለጠ ጥሪ" - "ያመለጡ ጥሪዎች" - "%s ያመለጡ ጥሪዎች" - "ያልተመለሰ ጥሪ ከ%s" - "በሂደት ላይ ያለ ጥሪ" - "በሂደት ላይ ያለ የሥራ ጥሪ" - "በሂደት ላይ ያለ የWi-Fi ጥሪ" - "በሂደት ላይ ያለ የWi-Fi የሥራ ጥሪ" - "ያዝና ቆይ" - "ገቢ ጥሪ" - "ገቢ የሥራ ጥሪ" - "ገቢ የWi-Fi ጥሪ" - "ገቢ የWi-Fi የሥራ ጥሪ" - "ገቢ የቪዲዮ ጥሪ" - "መጪ የተጠረጠረ የአይፈለጌ መልዕክት ጥሪ" - "ገቢ የቪዲዮ ጥያቄ" - "አዲስ የድምፅ መልእክት" - "አዲስ የድምፅ መልእክት (%d)" - "ደውል %s" - "የማይታወቅ የድምፅ መልእክት ቁጥር" - "ምንም አገልግሎት የለም" - "የተመረጠ አውታረመረብ (%s) አይገኝም" - "መልስ" - "ዝጋ" - "ቪዲዮ" - "ድምፅ" - "ተቀበል" - "አስወግድ" - "መልሰህ ደውል" - "መልእክት" - "በሌላ መሳሪያ ጥሪ በመካሄድ ላይ ነው" - "ጥሪ አስተላልፍ" - "ለመደወል፣ መጀመሪያ የአውሮፕላኑን ሁኔታ ያጥፉ።" - "በአውታረ መረቡ ላይ አልተመዘገበም።" - "የተንቀሳቃሽ ስልክ አውታረ መረብ አይገኝም።" - "አንድ ጥሪ ለማድረግ የሚሠራ ቁጥር ያስገቡ።" - "መደወል አይቻልም።" - "የMMI ቅደመ ተከተል በማስጀመር ላይ…" - "አገልግሎት አይደገፍም።" - "ጥሪዎችን መቀያየር አይቻልም።" - "ጥሪን መለየት አይቻልም።" - "ማስተላለፍ አይቻልም።" - "የጉባዔ ጥሪ ማድረግ አይቻልም።" - "ጥሪውን መዝጋት አይቻልም።" - "ጥሪ(ዎች)ን መልቀቅ አይቻልም።" - "የSIP ጥሪ" - "የአስቸኳይ ጊዜ ጥሪ" - "ሬዲዮ በማብራት ላይ…" - "ምንም አገልግሎት የለም። ዳግም በመሞከር ላይ…" - "መደወል አይቻልም። %s የአስቸኳይ አደጋ ቁጥር አይደለም።" - "መደወል አይቻልም። ወደ የአስቸኳይ አደጋ ቁጥር ይደውሉ።" - "ለመደወል የሰሌዳ ቁልፍ ተጠቀም" - "ጥሪ አቆይ" - "ጥሪ ቀጥል" - "ጥሪ ጨርስ" - "የመደወያ ሰሌዳ አሳይ" - "የመደወያ ሰሌዳ ደብቅ" - "ድምፅ-ከል አድርግ" - "ድምፅ አታጥፋ" - "ጥሪ ያክሉ" - "ጥሪዎችን አዋህድ" - "በውዝ" - "ጥሪዎችን አደራጅ" - "የስብሰባ ስልክ ጥሪ አደራጅ" - "የስብሰባ ጥሪ" - "ያስተዳድሩ" - "ኦዲዮ" - "የቪዲዮ ጥሪ" - "ወደ ድምፅ ጥሪ ይለውጡ" - "ካሜራ ቀይር" - "ካሜራ ያብሩ" - "ካሜራ ያጥፉ" - "ተጨማሪ አማራጮች" - "አጫዋች ጀምሯል" - "አጫዋች ቆሟል" - "ካሜራ ዝግጁ አይደለም" - "ካሜራ ዝግጁ ነው" - "ያልታወቀ የጥሪ ክፍለጊዜ ክስተት" - "አገልግሎት" - "አዋቅር" - "<አልተዘጋጀም>" - "ሌሎች የጥሪ ቅንብሮች" - "በ%s በኩል በመደወል ላይ" - "በ%s በኩል የመጣ" - "የዕውቂያ ፎቶ" - "ወደ ግላዊነት ሂድ" - "ዕውቂያ ይምረጡ" - "የእራስዎን ይጻፉ..." - "ተወው" - "ላክ" - "መልስ" - "ኤስኤምኤስ ላክ" - "ውድቅ አድርግ" - "እንደ ቪድዮ ጥሪ ይመልሱ" - "እንደ ድምፅ ጥሪ ይመልሱ" - "የቪዲዮ ጥያቄ ተቀበል" - "የቪዲዮ ጥያቄ ውድቅ አድርግ" - "የቪዲዮ አስተላልፍ ጥያቄን ተቀበል" - "የቪዲዮ አስተላልፍ ጥያቄን ውድቅ አድርግ" - "የቪዲዮ ተቀበል ጥያቄን ተቀበል" - "የቪዲዮ ተቀበል ጥያቄን ውድቅ አድርግ" - "ለ%s ወደ ላይ ያንሸራትቱ።" - "ለ%s ወደ ግራ ያንሸራትቱ።" - "ለ%s ወደ ቀኝ ያንሸራትቱ።" - "ለ%s ወደ ታች ያንሸራትቱ።" - "ንዘር" - "ንዘር" - "ድምፅ" - "ነባሪ ድምፅ (%1$s)" - "የስልክ ጥሪ ቅላጼ" - "በሚደወልበት ጊዜ ንዘር" - "የጥሪ ቅላጼ እና ንዘረት" - "የስብሰባ ስልክ ጥሪ አደራጅ" - "የአደጋ ጊዜ ቁጥር" - "የመገለጫ ፎቶ" - "ካሜራ ጠፍቷል" - "በ%s በኩል" - "ማስታወሻ ተልኳል" - "የቅርብ ጊዜ መልእክቶች" - "የንግድ መረጃ" - "%.1f ማይል ርቀት ላይ" - "%.1f ኪሜ ርቀት ላይ" - "%1$s%2$s" - "%1$s - %2$s" - "%1$s%2$s" - "ነገ %s ላይ ይከፈታል" - "ዛሬ %s ላይ ይከፈታል" - "%s ላይ ይዘጋል" - "ዛሬ %s ላይ ተዘግቷል" - "አሁን ክፍት ነው" - "አሁን ዝግ ነው" - "የተጠረጠረ አይፈለጌ ጠሪ" - "ጥሪው አብቅቷል %1$s" - "ይህ ቁጥር ለእርስዎ ሲደውል ይህ የመጀመሪያው ነው።" - "ይህ ቁጥር አይፈለጌ ላኪ ነው ብለን እንገምታለን።" - "አይፈለጌ አግድ/ሪፖርት አድርግ" - "እውቂያ ያክሉ" - "አይፈለጌ አይደለም" - diff --git a/InCallUI/res/values-ar/strings.xml b/InCallUI/res/values-ar/strings.xml deleted file mode 100644 index e89f026b6f5f510322eece286a28303d2ca9ae4d..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ar/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "الهاتف" - "معلقة" - "غير معروف" - "رقم خاص" - "هاتف بالعملة" - "مكالمة جماعية" - "تم قطع المكالمة" - "السماعة" - "سماعة الأذن للهاتف" - "سماعة رأس سلكية" - "بلوتوث" - "هل تريد إرسال النغمات التالية؟\n" - "إرسال النغمات\n" - "إرسال" - "نعم" - "لا" - "استبدال حرف البدل بـ" - "مكالمة جماعية %s" - "رقم البريد الصوتي" - "جارٍ الطلب" - "جارٍ إعادة الطلب" - "مكالمة جماعية" - "مكالمة واردة" - "مكالمة عمل واردة" - "تم إنهاء الاتصال" - "معلقة" - "جارٍ وقف المكالمة" - "بصدد مكالمة" - "رقمي %s" - "جارٍ الاتصال بالفيديو" - "مكالمة فيديو" - "جارٍ طلب الفيديو" - "يتعذر الاتصال بمكالمة فيديو" - "تم رفض طلب الفيديو" - "رقم معاودة الاتصال\n %1$s" - "رقم معاودة اتصال الطوارئ\n %1$s" - "جارٍ الطلب" - "مكالمة فائتة" - "المكالمات الفائتة" - "%s من المكالمات الفائتة" - "مكالمة فائتة من %s" - "مكالمة حالية" - "مكالمة عمل جارية" - "‏مكالمة جارية عبر Wi-Fi" - "‏مكالمة عمل جارية عبر Wi-Fi" - "معلقة" - "مكالمة واردة" - "مكالمة عمل واردة" - "‏مكالمة واردة عبر Wi-Fi" - "‏مكالمة عمل واردة عبر اتصال Wi-Fi" - "مكالمة فيديو واردة" - "مكالمة واردة يشتبه في كونها غير مرغوب فيها" - "طلب فيديو وارد" - "بريد صوتي جديد" - "بريد صوتي جديد (%d)" - "طلب %s" - "رقم البريد الصوتي غير معروف" - "لا تتوفر خدمة" - "الشبكة المحددة (%s) غير متاحة" - "رد" - "قطع الاتصال" - "فيديو" - "صوت" - "قبول" - "تجاهل" - "معاودة الاتصال" - "رسالة" - "مكالمة جارية على جهاز آخر" - "تحويل الاتصال" - "لإجراء مكالمة، أوقف تشغيل وضع الطائرة أولاً." - "غير مسجل على الشبكة." - "شبكة الجوّال غير متاحة." - "لإجراء مكالمة، أدخل رقمًا صالحًا." - "يتعذر الاتصال." - "‏جارٍ بدء تسلسل MMI…" - "الخدمة ليست متوفرة." - "يتعذر تبديل المكالمات." - "يتعذر فصل المكالمة." - "يتعذر النقل." - "يتعذر إجراء مكالمة جماعية." - "يتعذر رفض المكالمة." - "يتعذر تحرير المكالمات." - "‏مكالمة SIP" - "مكالمة طوارئ" - "جارٍ تشغيل اللاسلكي..." - "لا تتوفر خدمة. جارٍ إعادة المحاولة…" - "يتعذر الاتصال. لا يعد %s رقم طوارئ." - "يتعذر الاتصال. يمكنك طلب رقم طوارئ" - "استخدام لوحة المفاتيح للطلب" - "تعليق المكالمة" - "استئناف المكالمة" - "إنهاء المكالمة" - "عرض لوحة الاتصال" - "إخفاء لوحة الاتصال" - "تجاهل" - "إلغاء التجاهل" - "إضافة مكالمة" - "دمج المكالمات" - "تبديل" - "إدارة المكالمات" - "إدارة مكالمة جماعية" - "مكالمة جماعية" - "إدارة" - "صوت" - "مكالمة فيديو" - "التغيير إلى مكالمة صوتية" - "تبديل الكاميرا" - "تشغيل الكاميرا" - "إيقاف الكاميرا" - "خيارات أخرى" - "تم بدء المشغّل" - "تم إيقاف المشغّل" - "الكاميرا غير جاهزة" - "الكاميرا جاهزة" - "حدث جلسة اتصال غير معروف" - "الخدمة" - "الإعداد" - "‏<لم يتم التعيين>" - "إعدادات الاتصال الأخرى" - "الاتصال عبر %s" - "واردة عبر %s" - "صورة جهة الاتصال" - "انتقال إلى مكالمة خاصة" - "تحديد جهة اتصال" - "اكتب ردك…" - "إلغاء" - "إرسال" - "الرد" - "‏إرسال رسالة قصيرة SMS" - "الرفض" - "الرد بمكالمة فيديو" - "الرد بمكالمة صوتية" - "قبول طلب الفيديو" - "رفض طلب الفيديو" - "قبول طلب بث الفيديو" - "رفض طلب بث الفيديو" - "قبول طلب استلام مكالمة الفيديو" - "رفض طلب استلام مكالمة الفيديو" - "تمرير لأعلى لـ %s." - "تمرير لليسار لـ %s." - "تمرير لليمين لـ %s." - "تمرير لأسفل لـ %s." - "اهتزاز" - "اهتزاز" - "الصوت" - "الصوت الافتراضي (%1$s)" - "نغمة رنين الهاتف" - "اهتزاز عند الرنين" - "نغمة الرنين والاهتزاز" - "إدارة مكالمة جماعية" - "رقم الطوارئ" - "صورة الملف الشخصي" - "تم إيقاف الكاميرا" - "عبر %s" - "تم إرسال الملاحظة" - "الرسائل الأخيرة" - "معلومات النشاط التجاري" - "على بُعد %.1f ميل" - "على بُعد %.1f كم" - "%1$s، %2$s" - "%1$s - %2$s" - "%1$s، %2$s" - "مفتوح غدًا في %s" - "مفتوح اليوم في %s" - "مغلق في %s" - "مغلق اليوم في %s" - "مفتوح الآن" - "مغلق الآن" - "اشتباه في متصل غير مرغوب فيه" - "‏المكالمة انتهت %1$s" - "هذه هي المرة الأولى التي تتلقى فيها اتصالاً من هذا الرقم." - "لدينا شك أن هذه المكالمة واردة من متصل غير مرغوب فيه." - "حظر/إبلاغ عن رقم غير مرغوب فيه" - "إضافة جهة اتصال" - "ليس رقمًا غير مرغوب فيه" - diff --git a/InCallUI/res/values-az/strings.xml b/InCallUI/res/values-az/strings.xml deleted file mode 100644 index 4bb9652b92307208ad1229288f2e8dfa0063565c..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-az/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Gözləmə mövqeyində" - "Naməlum" - "Şəxsi nömrə" - "Taksofon" - "Konfrans zəngi" - "Zəng bitdi" - "Dinamik" - "Dəstək qulaqlığı" - "Simli qulaqlıq" - "Bluetooth" - "Aşağıdakı tonlar göndərilsin?\n" - "Tonlar göndərilir\n" - "Göndər" - "Bəli" - "Xeyr" - "Joker simvolları əvəz edin" - "Konfrans zəngi %s" - "Səsli poçt nömrəsi" - "Nömrə yığılır" - "Yenidən yığır" - "Konfrans zəngi" - "Gələn zəng" - "Daxil olan iş çağrısı" - "Zəng sona çatdı" - "Gözləmə mövqeyində" - "Dəstək asılır" - "Çağrıda" - "Mənim nömrəm %s" - "Video qoşulur" - "Video zəng" - "Video sorğusu göndərilir" - "Video zəngə qoşulmaq mümkün deyil" - "Video sorğusu rədd edildi" - "Cavab zəngi nömrəniz\n %1$s" - "Təcili cavab zəngi nömrəniz\n %1$s" - "Nömrə yığılır" - "Buraxılmış zəng" - "Buraxılmış zənglər" - "%s buraxılmış zənglər" - "%s tərəfindən zəng buraxılıb" - "Davam edən çağrı" - "Davam edən iş çağrısı" - "Davam edən Wi-Fi zəngi" - "Davam edən Wi-Fi iş çağrısı" - "Gözləmə mövqeyində" - "Gələn zəng" - "Daxil olan iş çağrısı" - "Gələn Wi-Fi zəngi" - "Daxil olan Wi-Fi iş çağrısı" - "Gələn video zəng" - "Şübhəli spam zəngi" - "Gələn video çağrı" - "Yeni səsli poçt" - "Yeni səsli poçt (%d)" - "Yığın %s" - "Səsli poçt nömrəsi naməlumdur" - "Xidmət yoxdur" - "Seçilmiş (%s) şəbəkə əlçatmazdır" - "Cavab" - "Dəstəyi qoyun" - "Videolar" - "Səs" - "Qəbul edin" - "Rədd edin" - "Geriyə zəng" - "Mesaj" - "Digər cihazda davam etməkdə olan zəng" - "Zəngi Transfer edin" - "Zəng etmək üçün ilk olaraq Uçuş Rejimini söndürün." - "Şəbəkədə qeydə alınmayıb." - "Mobil şəbəkə əlçatan deyil" - "Zəngi yerləşdirmək üçün düzgün nömrə daxil edin." - "Zəng etmək mümkün deyil." - "MMI başlanma ardıcıllığı…" - "Xidmət dəstəklənmir." - "Zəngləri keçirmək mümkün deyil." - "Zəngi ayırmaq mümkün deyil." - "Ötürmək mümkün deyil." - "Konfrans keçirmək mümkün deyil." - "Zəngi rədd etmək mümkün deyil." - "Zəngləri buraxmaq mümkün deyil." - "SIP çağrısıs" - "Təcili zəng" - "Radio yandırılır ..." - "Xidmət yoxdur. Yenidən cəhd edilir…" - "Zəng etmək mümkün deyil. %s fövqəladə nömrə deyil." - "Zəng etmək mümkün deyil. Fövqəladə nömrəni yı" - "Nömrə yığmaq üçün klaviaturadan istifadə ediin" - "Zəngi gözlədin" - "Zəngə davam edin" - "Zəngi bitirin" - "Yığım düymələrini göstərin" - "Yığım düymələrini gizlədin" - "Susdurun" - "Susdurmayın" - "Zəng əlavə edin" - "Zəngləri birləşdirin" - "Dəyişdirin" - "Zəngləri idarə edin" - "Konfrans çağrısını idarə edin" - "Konfrans zəngi" - "İdarə edin" - "Audio" - "Video zəng" - "Səsli çağrıya dəyişin" - "Kameraya keçin" - "Kameranı yandırın" - "Kameranı söndürün" - "Daha çox seçim" - "Pleyer Başladıldı" - "Pleyer Dayandırıldı" - "Kamera hazır deyil" - "Kamera hazırdır" - "Naməlum zəng sessiyası" - "Xidmət" - "Quraşdırma" - "<Təyin edilməyib>" - "Digər zəng parametrləri" - "%s vasitəsi ilə zəng edilir" - "%s vasitəsilə gələn" - "kontakt fotosu" - "şəxsi rejimə keçin" - "kontakt seçin" - "Özünüzünkünü yazın" - "Ləğv edin" - "Göndər" - "Cavab" - "SMS göndərin" - "İmtina edin" - "Video çağrı olaraq cavab verin" - "Audio çağrı olaraq cavab verin" - "Video sorğusunu qəbul edin" - "Video sorğusunu ləğv edin" - "Video ötürmə sorğusunu qəbul edin" - "Video ötürmə sorğusunu ləğv edin" - "Video qəbuletmə sorğusunu qəbul edin" - "Video qəbuletmə sorğusunu ləğv edin" - "%s üçün yuxarı sürüşdürün." - "%s üçün sola sürüşdürün." - "%s üçün sağa sürüşdürün." - "%s üçün aşağı sürüşdürün." - "Vibrasiya" - "Vibrasiya" - "Səs" - "Defolt səs (%1$s)" - "Telefon zəng səsi" - "Zəng çalanda vibrasiya olsun" - "Zəng səsi & Vibrasiya" - "Konfrans çağrısını idarə edin" - "Təcili nömrə" - "Profil fotosu" - "Kamera deaktivdir" - "%s vasitəsilə" - "Qeyd göndərildi" - "Son mesajlar" - "Biznes məlumatı" - "%.1f mil uzaqlıqda" - "%.1f km uzaqlıqda" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Sabah saat %s açılır" - "Bu gün saat %s açılır" - "Saat %s bağlanır" - "Bu gün saat %s bağlanıb" - "İndi açın" - "İndi bağlandı" - "Şübhəli spam çağrıcısı" - "Zəng bitdi %1$s" - "Bu nömrə ilk dəfədir Sizə zəng edir." - "Bu zəngin spam olduğundan şübhələnirik." - "Spamı blok edin/bildirin" - "Kontakt əlavə edin" - "Spam deyil" - diff --git a/InCallUI/res/values-b+sr+Latn/strings.xml b/InCallUI/res/values-b+sr+Latn/strings.xml deleted file mode 100644 index 9770f90a924a9f4c9c02cf8aa0d49cf660a9b64c..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-b+sr+Latn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Na čekanju" - "Nepoznat" - "Privatan broj" - "Telefonska govornica" - "Konferencijski poziv" - "Poziv je prekinut" - "Zvučnik" - "Slušalica telefona" - "Žičane naglavne slušalice" - "Bluetooth" - "Želite li da pošaljete sledeće tonove?\n" - "Tonovi se šalju\n" - "Pošalji" - "Da" - "Ne" - "Zamenite džoker znak sa" - "Konferencijski poziv %s" - "Broj govorne pošte" - "Poziva se" - "Ponovo se bira" - "Konferencijski poziv" - "Dolazni poziv" - "Dolazni poziv za Work" - "Poziv je završen" - "Na čekanju" - "Veza se prekida" - "Poziv je u toku" - "Moj broj je %s" - "Povezuje se video poziv" - "Video poziv" - "Zahteva se video poziv" - "Povezivanje video poziva nije uspelo" - "Zahtev za video poziv je odbijen" - "Broj za povratni poziv\n %1$s" - "Broj za hitan povratni poziv\n %1$s" - "Poziva se" - "Propušten poziv" - "Propušteni pozivi" - "Broj propuštenih poziva: %s" - "Propušten poziv od: %s" - "Tekući poziv" - "Tekući poziv za Work" - "Tekući Wi-Fi poziv" - "Tekući poziv za Work preko Wi-Fi-ja" - "Na čekanju" - "Dolazni poziv" - "Dolazni poziv za Work" - "Dolazni Wi-Fi poziv" - "Dolazni poziv za Work preko Wi-Fi-ja" - "Dolazni video poziv" - "Sumnja na nepoželjan dolazni poziv" - "Zahtev za dolazni video poziv" - "Nova poruka govorne pošte" - "Nova poruka govorne pošte (%d)" - "Pozovi %s" - "Nepoznat broj govorne pošte" - "Mobilna mreža nije dostupna" - "Izabrana mreža (%s) nije dostupna" - "Odgovori" - "Prekini vezu" - "Video" - "Glasovni" - "Prihvatam" - "Odbaci" - "Uzvrati poziv" - "Pošalji SMS" - "Poziv je u toku na drugom uređaju" - "Prebaci poziv" - "Da biste uputili poziv, prvo isključite režim rada u avionu." - "Nije registrovano na mreži." - "Mobilna mreža nije dostupna." - "Da biste uputili poziv, unesite važeći broj." - "Poziv nije uspeo." - "Pokreće se MMI sekvenca..." - "Usluga nije podržana." - "Zamena poziva nije uspela." - "Razdvajanje poziva nije uspelo." - "Prebacivanje nije uspelo." - "Konferencijski poziv nije uspeo." - "Odbijanje poziva nije uspelo." - "Uspostavljanje poziva nije uspelo." - "SIP poziv" - "Hitni poziv" - "Uključuje se radio…" - "Mobilna mreža nije dostupna. Pokušavamo ponovo…" - "Poziv nije uspeo. %s nije broj za hitne slučajeve." - "Poziv nije uspeo. Pozovite broj za hitne slučajeve." - "Koristite tastaturu za pozivanje" - "Stavi poziv na čekanje" - "Nastavi poziv" - "Završi poziv" - "Prikaži numeričku tastaturu" - "Sakrij numeričku tastaturu" - "Isključi zvuk" - "Uključi zvuk" - "Dodaj poziv" - "Objedini pozive" - "Zameni" - "Upravljaj pozivima" - "Upravljaj konferencijskim pozivom" - "Konferencijski poziv" - "Upravljaj" - "Audio" - "Video poziv" - "Promeni u glasovni poziv" - "Promeni kameru" - "Uključi kameru" - "Isključi kameru" - "Još opcija" - "Plejer je pokrenut" - "Plejer je zaustavljen" - "Kamera nije spremna" - "Kamera je spremna" - "Nepoznat događaj sesije poziva" - "Usluga" - "Podešavanje" - "<Nije podešeno>" - "Druga podešavanja poziva" - "Poziva se preko dobavljača %s" - "Dolazni poziv preko %s" - "slika kontakta" - "idi na privatno" - "izaberite kontakt" - "Napišite sami…" - "Otkaži" - "Pošalji" - "Odgovori" - "Pošalji SMS" - "Odbij" - "Odgovori video pozivom" - "Odgovori audio-pozivom" - "Prihvati zahtev za video" - "Odbij zahtev za video" - "Prihvati zahtev za odlazni video poziv" - "Odbij zahtev za odlazni video poziv" - "Prihvati zahtev za dolazni video poziv" - "Odbij zahtev za dolazni video poziv" - "Prevucite nagore za %s." - "Prevucite ulevo za %s." - "Prevucite udesno za %s." - "Prevucite nadole za %s." - "Vibracija" - "Vibracija" - "Zvuk" - "Podrazumevani zvuk (%1$s)" - "Melodija zvona telefona" - "Vibriraj kada zvoni" - "Melodija zvona i vibracija" - "Upravljaj konferencijskim pozivom" - "Broj za hitne slučajeve" - "Slika profila" - "Kamera je isključena" - "na %s" - "Beleška je poslata" - "Nedavne poruke" - "Informacije o preduzeću" - "Udaljenost je %.1f mi" - "Udaljenost je %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Otvara se sutra u %s" - "Otvara se danas u %s" - "Zatvara se u %s" - "Zatvorilo se danas u %s" - "Trenutno otvoreno" - "Trenutno zatvoreno" - "Nepoželjan pozivalac" - "Poziv se završio u %1$s" - "Ovo je bio prvi poziv sa ovog broja." - "Sumnjamo da je ovaj poziv nepoželjan." - "Blokiraj/prijavi" - "Dodaj kontakt" - "Nije nepoželjan" - diff --git a/InCallUI/res/values-be/strings.xml b/InCallUI/res/values-be/strings.xml deleted file mode 100644 index 70145fb7ce5727c10bb552354862ec15e6db88d9..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-be/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Тэлефон" - "На ўтрыманні" - "Невядомы" - "Прыватны нумар" - "Таксафон" - "Канферэнц-выклік" - "Выклік абарваўся" - "Дынамік" - "Дынамік тэлефона" - "Правадная гарнітура" - "Bluetooth" - "Адправіць гэтыя тоны?\n" - "Адпраўка тонаў\n" - "Адправiць" - "Так" - "Не" - "Замяніце знак падстаноўкі на" - "Канферэнц-выклік у %s" - "Нумар галасавой пошты" - "Набор нумара" - "Паўторны набор" - "Канферэнц-выклік" - "Уваходны выклік" - "Уваходны выклік па працы" - "Выклік скончаны" - "На ўтрыманні" - "Завяршэнне выкліку" - "У выкліку" - "Мой нумар - %s" - "Падлучэнне відэа" - "Відэавыклік" - "Запыт на відэа" - "Немагчыма падлучыць відэавыклік" - "Запыт на відэа адхілены" - "Ваш нумар зваротнага выкліку\n %1$s" - "Ваш нумар экстраннага зваротнага выкліку\n %1$s" - "Набор нумара" - "Прапушчаны выклік" - "Прапушчаныя выклікі" - "Прапушчаных выклікаў: %s" - "Прапушчаны выклiк ад %s" - "Бягучы выклік" - "Бягучы выклік па працы" - "Бягучы выклік праз Wi-Fi" - "Бягучы выклік па працы праз Wi-Fi" - "На ўтрыманні" - "Уваходны выклік" - "Уваходны выклік па працы" - "Уваходны выклік праз Wi-Fi" - "Уваходны выклік па працы праз Wi-Fi" - "Уваходны відэавыклік" - "Уваходны выклiк ад абанента, якога падазраваюць у спаме" - "Уваходны запыт на відэавыклік" - "Новая галасавая пошта" - "Новыя паведамленнi галасавой пошты (%d)" - "Набраць %s" - "Невядомы нумар галасавой пошты" - "Не абслугоўваецца" - "Выбраная сетка (%s) недаступная" - "Адказ" - "Сконч. разм." - "Відэа" - "Галасавы" - "Прыняць" - "Адхіліць" - "Звар. выклік" - "Паведамленне" - "Бягучы выклік на іншай прыладзе" - "Перадаць выклік" - "Каб зрабіць выклік, спачатку выключыце рэжым палёту." - "Не зарэгістраваны ў сетцы." - "Мабільная сетка недаступная." - "Каб зрабіць выклік, увядзіце сапраўдны нумар." - "Выклік немагчымы." - "Пачатак паслядоўнасці MMI…" - "Служба не падтрымліваецца." - "Немагчыма пераключыць выклікі." - "Немагчыма аддзяліць выклік." - "Немагчыма перадаць выклік." - "Немагчыма зрабіць канферэнц-выклік." - "Немагчыма адхіліць выклік." - "Немагчыма скончыць выклік(і)." - "SIP-выклік" - "Экстранны выклік" - "Уключэнне радыё…" - "Не абслугоўваецца. Паўтор спробы…" - "Выклік немагчымы. %s не з\'яўляецца нумарам экстраннай службы." - "Выклік немагчымы. Набярыце нумар экстраннай службы." - "Набраць нумар з клавіятуры" - "Паставіць выклік на ўтрыманне" - "Узнавіць выклік" - "Завяршыць выклік" - "Паказаць панэль набору" - "Схаваць панэль набору" - "Адключыць мікрафон" - "Уключыць мікрафон" - "Дадаць выклік" - "Аб\'яднаць выклікі" - "Пераключыць" - "Кіраваць выклікамі" - "Кіраванне канферэнц-выклікам" - "Канферэнц-выклік" - "Кіраванне" - "Аўдыя" - "Відэавыклік" - "Змяніць на галасавы выклік" - "Пераключыць камеру" - "Уключыць камеру" - "Адключыць камеру" - "Дадатковыя параметры" - "Прайгравальнік запушчаны" - "Прайгравальнік спынены" - "Камера не гатовая" - "Камера гатовая" - "Невядомая падзея сеансу выкліку" - "Сэрвіс" - "Наладка" - "<Не зададзены>" - "Іншыя налады выклікаў" - "Выклікі праз правайдара %s" - "Уваходны выклік праз %s" - "фаіаграфія кантакту" - "перайсці да прыватнай гаворкі" - "выбраць кантакт" - "Напiшыце сваё…" - "Скасаваць" - "Адправiць" - "Адказ" - "Адправiць SMS" - "Адхіліць" - "Адказаць відэавыклікам" - "Адказаць аўдыявыклікам" - "Прыняць запыт на відэа" - "Адхіліць запыт на відэа" - "Прыняць запыт на перадачу відэа" - "Адхіліць запыт на перадачу відэа" - "Прыняць запыт на атрыманне відэа" - "Адхіліць запыт на атрыманне відэа" - "Правядзіце пальцам уверх, каб %s." - "Правядзіце пальцам улева, каб %s." - "Правядзіце пальцам управа, каб %s." - "Правядзіце пальцам уніз, каб %s." - "Вібрацыя" - "Вібрацыя" - "Гук" - "Стандартны гук (%1$s)" - "Рынгтон тэлефона" - "Вібрацыя падчас званка" - "Рынгтон і вiбрацыя" - "Кіраванне канферэнц-выклікам" - "Нумар экстраннай службы" - "Фота профілю" - "Камера адключана" - "праз %s" - "Нататка адпраўлена" - "Апошнія паведамленні" - "Бізнес-інфармацыя" - "Адлеглаць у мілях: %.1f" - "Адлегласць %.1f км" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Адкрываецца заўтра ў %s" - "Адкрываецца сёння ў %s" - "Закрываецца ў %s" - "Закрыта сёння ў %s" - "Адкрыць зараз" - "Зараз закрыта" - "Падазраваецца ў спаме" - "Выклік завершаны %1$s" - "Вы атрымліваеце выклік з гэтага нумара ўпершыню." - "Існуе падазрэнне, што гэты выклік – спамерскі." - "Забл./павед.пра спам" - "Дадаць кантакт" - "Не спам" - diff --git a/InCallUI/res/values-bg/strings.xml b/InCallUI/res/values-bg/strings.xml deleted file mode 100644 index adf504777d1a91e762f941f5c2bf35e8df02d9f2..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-bg/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "Задържано" - "Неизвестно лице" - "Частен номер" - "Обществен телефон" - "Конферентно обаждане" - "Обаждането бе прекъснато" - "Високоговорител" - "Телефонна слушалка" - "Слушалки с кабел" - "Bluetooth" - "Да се изпратят ли следните мелодии? \n" - "Мелодиите се изпращат\n" - "Изпращане" - "Да" - "Не" - "Замяна на заместващия символ със:" - "Конферентно обаждане – %s" - "Номер за гласова поща" - "Набира се" - "Набира се отново" - "Конферентно обаждане" - "Входящо обаждане" - "Входящо служебно обаждане" - "Обаждането завърши" - "Задържано" - "Разговорът се приключва" - "Извършва се обаждане" - "Номерът ми е %s" - "Установява се видеовръзка" - "Видеообаждане" - "Заявява се видеовръзка" - "Видеообаждането не може да се осъществи" - "Заявката за видеовръзка е отхвърлена" - "Номерът ви за обратно обаждане\n– %1$s" - "Номерът ви за спешно обратно обаждане\n– %1$s" - "Набиране" - "Пропуснато обаждане" - "Пропуснати обаждания" - "%s пропуснати обаждания" - "Пропуснато обаждане от %s" - "Текущо обаждане" - "Текущо служебно обаждане" - "Текущо обаждане през Wi-Fi" - "Текущо служебно обаждане през Wi-Fi" - "Задържано" - "Входящо обаждане" - "Входящо служебно обаждане" - "Входящо обаждане през Wi-Fi" - "Входящо служебно обаждане през Wi-Fi" - "Входящо видеообаждане" - "Входящо обаждане – възможен спам" - "Заявка за входящо видеообаждане" - "Нова гласова поща" - "Нова гласова поща (%d)" - "Набиране на %s" - "Неизвестен номер за гласова поща" - "Няма покритие" - "Избраната мрежа (%s) не е налице" - "Приемане" - "Затваряне" - "Видеообажд." - "Гл. обаждане" - "Приемане" - "Отхвърляне" - "Обр. обажд." - "Съобщение" - "Текущо обаждане на друго устройство" - "Прехвърляне на обаждането" - "За да осъществите обаждане, първо изключете самолетния режим." - "Няма регистрация в мрежата." - "Няма достъп до клетъчната мрежа." - "За да извършите обаждане, въведете валиден номер." - "Не може да се извърши обаждане." - "Стартира се последователността MMI…" - "Услугата не се поддържа." - "Обажданията не могат да се превключат." - "Обаждането не може да се отдели." - "Не може да се прехвърли." - "Не може да се извърши конферентно обаждане." - "Обаждането не може да се отхвърли." - "Обаждането или съответно обажданията не могат да се освободят." - "Обаждане чрез SIP" - "Спешно обаждане" - "Радиомодулът се включва…" - "Няма услуга. Извършва се нов опит…" - "Не може да се извърши обаждане. %s не е номер за спешни случаи." - "Не може да се извърши обаждане. Наберете номер за спешни случаи." - "Използвайте клавиатурата за набиране" - "Задържане на обаждането" - "Възобновяване на обаждането" - "Край на обаждането" - "Показване на клавиатурата за набиране" - "Скриване на клавиатурата за набиране" - "Заглушаване" - "Пускане" - "Добавяне на обаждане" - "Обединяване на обаждания" - "Размяна" - "Управление на обажданията" - "Управление на конф. обаждане" - "Конферентно обаждане" - "Управление" - "Аудио" - "Видеообажд." - "Преминаване към гласово обаждане" - "Превключване на камерата" - "Включване на камерата" - "Изключване на камерата" - "Още опции" - "Плейърът е стартиран" - "Плейърът е спрян" - "Камерата не е в готовност" - "Камерата е в готовност" - "Неизвестно събитие в сесията на обаждане" - "Услуга" - "Настройване" - "<Не е зададено>" - "Други настройки за обаждане" - "Обаждане чрез %s" - "Входящо обаждане чрез %s" - "снимка на контакта" - "превключване към частно обаждане" - "избиране на контакта" - "Напишете свой собствен..." - "Отказ" - "Изпращане" - "Приемане" - "Изпращане на SMS" - "Отхвърляне" - "Приемане като видеообаждане" - "Приемане като аудиообаждане" - "Приемане на заявката за видеовръзка" - "Отхвърляне на заявката за видеовръзка" - "Приемане на заявката за предаване на видео" - "Отхвърляне на заявката за предаване на видео" - "Приемане на заявката за получаване на видеообаждане" - "Отхвърляне на заявката за получаване на видео" - "Плъзнете нагоре за %s." - "Плъзнете наляво за %s." - "Плъзнете надясно за %s." - "Плъзнете надолу за %s." - "Вибриране" - "Вибриране" - "Звук" - "Стандартен звук (%1$s)" - "Мелодия на телефона" - "Вибриране при звънене" - "Мелодия и вибриране" - "Управление на конферентното обаждане" - "Спешен номер" - "Снимка на потребителския профил" - "Камерата е изключена" - "чрез %s" - "Бележката е изпратена" - "Скорошни съобщения" - "Бизнес информация" - "На %.1f мили" - "На %.1f км" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s; %2$s" - "Отваря утре в %s" - "Отваря днес в %s" - "Затваря в %s" - "Затворено днес в %s" - "В момента работи" - "В момента не работи" - "Възможен спам" - "Обаждането завърши %1$s" - "За първи път ви се обаждат от този номер." - "Подозирахме, че това обаждане може да е от разпространител на спам." - "Блокиране/спам" - "Добавяне на контакт" - "Не е спам" - diff --git a/InCallUI/res/values-bn/strings.xml b/InCallUI/res/values-bn/strings.xml deleted file mode 100644 index 2f561ce6e10b507f0706a6c3ca1de195d3e062d7..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-bn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ফোন" - "হোল্ডে রয়েছে" - "অজানা" - "ব্যক্তিগত নম্বর" - "অর্থের বিনিময়ে কল করার ফোন" - "কনফারেন্স কল" - "কল সমাপ্ত হয়েছে" - "স্পিকার" - "হ্যান্ডসেট ইয়ারপিস" - "তারযুক্ত হেডসেট" - "ব্লুটুথ" - "নিম্নলিখিত টোনগুলি পাঠাবেন?\n" - "টোনগুলি পাঠানো হচ্ছে\n" - "পাঠান" - "হ্যাঁ" - "না" - "ওয়াইল্ড অক্ষরগুলিকে এর মাধ্যমে প্রতিস্থাপিত করুন" - "কনফারেন্স কল %s" - "ভয়েসমেল নম্বর" - "ডায়াল করা হচ্ছে" - "আবার ডায়াল করা হচ্ছে" - "কনফারেন্স কল" - "আগত কল" - "আগত কাজের কল" - "কল সমাপ্ত হয়েছে" - "হোল্ডে রয়েছে" - "কল নামিয়ে রাখা হচ্ছে" - "কলের সময়ে" - "আমার নম্বর হল %s" - "ভিডিও সংযুক্ত করছে" - "ভিডিও কল" - "ভিডিওর অনুরোধ করছে" - "ভিডিও কলে সংযোগ করা যাচ্ছে না" - "ভিডিওর অনুরোধ প্রত্যাখ্যান করা হয়েছে" - "আপনার কলব্যাক নম্বর\n%1$s" - "আপনার জরুরী কলব্যাক নম্বর\n%1$s" - "ডায়াল করা হচ্ছে" - "মিসড কল" - "মিসড কলগুলি" - "%sটি মিসড কল" - "%s এর থেকে মিসড কল" - "চলমান কল" - "চলমান কাজের কল" - "চলমান ওয়াই-ফাই কল" - "চলমান ওয়াই-ফাই কাজের কল" - "হোল্ডে রয়েছে" - "আগত কল" - "আগত কাজের কল" - "আগত ওয়াই-ফাই কল" - "আগত ওয়াই-ফাই কাজের কল" - "আগত ভিডিও কল" - "আগত সন্দেহভাজন স্প্যাম কল" - "আগত ভিডিও অনুরোধ" - "নতুন ভয়েসমেল" - "নতুন ভয়েসমেল (%dটি)" - "%s ডায়াল করুন" - "ভয়েসমেল নম্বর অজানা" - "কোনো পরিষেবা নেই" - "নির্বাচিত নেটওয়ার্ক (%s) অনুপলব্ধ" - "উত্তর" - "কল নামিয়ে রাখুন" - "ভিডিও" - "ভয়েস" - "স্বীকার করুন" - "খারিজ করুন" - "ঘুরিয়ে কল করুন" - "বার্তা" - "অন্য ডিভাইসে চালু থাকা কল" - "কল স্থানান্তর করুন" - "একটি কল করতে, প্রথমে বিমানমোড বন্ধ করুন৷" - "নেটওয়ার্কে নিবন্ধিত নয়৷" - "সেলুলার নেটওয়ার্ক উপলব্ধ নয়।" - "কোনো কল স্থাপন করতে, একটি বৈধ নম্বর লিখুন৷" - "কল করা যাবে না৷" - "MMI ক্রম চালু হচ্ছে…" - "পরিষেবা সমর্থিত নয়৷" - "কলগুলি স্যুইচ করা যাবে না৷" - "কল আলাদা করা যাবে না৷" - "হস্তান্তর করা যাবে না৷" - "কনফারেন্স করা যাবে না৷" - "কল প্রত্যাখ্যান কলা যাবে না৷" - "কল(গুলি) কাটা যাবে না৷" - "SIP কল" - "জরুরি কল" - "রেডিও চালু করা হচ্ছে…" - "কোন পরিষেবা নেই৷ আবার চেষ্টা করা হচ্ছে..." - "কল করা যাবে না৷ %s কোনো জরুরী নম্বর নয়৷" - "কল করা যাবে না৷ কোনো জরুরী নম্বর ডায়াল করুন৷" - "ডায়াল করতে কীবোর্ড ব্যবহার করুন" - "কল হোল্ডে রাখুন" - "কল আবার শুরু করুন" - "কল শেষ করুন" - "ডায়ালপ্যাড দেখান" - "ডায়ালপ্যাড লুকান" - "নিঃশব্দ করুন" - "সশব্দ করুন" - "কল যোগ করুন" - "কলগুলি মার্জ করুন" - "সোয়াপ করুন" - "কলগুলি পরিচালনা করুন" - "কনফারেন্স কল পরিচালনা করুন" - "কনফারেন্স কল" - "পরিচালনা করুন" - "অডিও" - "ভিডিও কল" - "ভয়েস কলে পরিবর্তন করুন" - "ক্যামেরা স্যুইচ করুন" - "ক্যামেরা চালু করুন" - "ক্যামেরা বন্ধ করুন" - "আরো বিকল্প" - "প্লেয়ার শুরু হয়েছে" - "প্লেয়ার বন্ধ হয়ে গেছে" - "ক্যামেরা রেডি নয়" - "ক্যামেরা রেডি" - "অজানা কল অধিবেশনের ইভেন্ট" - "পরিষেবা" - "সেটআপ" - "<সেট করা নেই>" - "অন্যান্য কল সেটিংস" - "%s এর মাধ্যমে কল করা হচ্ছে" - "%s এর মাধ্যমে ইনকামিং কল" - "পরিচিতির ফটো" - "ব্যক্তিগতভাবে কাজ করুন" - "পরিচিতি নির্বাচন করুন" - "আপনার নিজের পছন্দ মতো লিখুন…" - "বাতিল করুন" - "পাঠান" - "উত্তর" - "SMS পাঠান" - "অস্বীকার করুন" - "ভিডিও কল হিসেবে উত্তর দিন" - "অডিও কল হিসেবে উত্তর দিন" - "ভিডিওর অনুরোধ গ্রহণ করুন" - "ভিডিওর অনুরোধ প্রত্যাখ্যান করুন" - "ভিডিও প্রেরণ করার অনুরোধ স্বীকার করুন" - "ভিডিও প্রেরণ করার অনুরোধ প্রত্যাখ্যান করুন" - "ভিডিও গ্রহণ করার অনুরোধ স্বীকার করুন" - "ভিডিও গ্রহণ করার অনুরোধ প্রত্যাখ্যান করুন" - "%s এর জন্য উপরের দিকে স্লাইড করুন৷" - "%s এর জন্য বাঁ দিকে স্লাইড করুন৷" - "%s এর জন্য ডান দিকে স্লাইড করুন৷" - "%s এর জন্য নীচের দিকে স্লাইড করুন৷" - "কম্পন" - "কম্পন" - "শব্দ" - "ডিফল্ট শব্দ (%1$s)" - "ফোন রিংটোন" - "রিং হওয়ার সময় কম্পন হবে" - "রিংটোন ও কম্পন" - "কনফারেন্স কল পরিচালনা করুন" - "জরুরি নম্বর" - "প্রোফাইল ফটো" - "ক্যামেরা বন্ধ" - "%s এর মাধ্যমে" - "নোট পাঠানো হয়েছে" - "সাম্প্রতিক বার্তাগুলি" - "ব্যবসার তথ্য" - "%.1f মাইল দূরে" - "%.1f কিলোমিটার দূরে" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "আগামীকাল %s\'টায় খুলবে" - "আজ %s\'টায় খুলবে" - "%s\'টায় বন্ধ হয়" - "আজ %s\'টায় বন্ধ হয়েছে" - "এখন খোলা রয়েছে" - "এখন বন্ধ রয়েছে" - "সন্দেহভাজন স্প্যাম কলার" - "কল সমাপ্ত হয়েছে %1$s" - "এই প্রথমবার এই নম্বর থেকে আপনাকে কল করা হয়েছে৷" - "এটি কোনো স্প্যামারের কল হতে পারে বলে আমাদের মনে হচ্ছে৷" - "অবরুদ্ধ করুন/স্প্যাম হিসাবে অভিযোগ করুন" - "পরিচিতি যোগ করুন" - "স্প্যাম নয়" - diff --git a/InCallUI/res/values-bs/strings.xml b/InCallUI/res/values-bs/strings.xml deleted file mode 100644 index 9db727c980306d09ff8b4baae042ca5c78e2659e..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-bs/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Na čekanju" - "Nepoznato" - "Privatni broj" - "Telefonska govornica" - "Konferencijski poziv" - "Poziv je prekinut" - "Zvučnik" - "Slušalice telefona" - "Žičane slušalice" - "Bluetooth" - "Poslati sljedeće tonove?\n" - "Slanje tonova\n" - "Pošalji" - "Da" - "Ne" - "Zamijeni zamjenski znak sa" - "Konferencijski poziv %s" - "Broj govorne pošte" - "Poziva se" - "Ponovno pozivanje" - "Konferencijski poziv" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Poziv je završen" - "Na čekanju" - "Prekid veze" - "Poziv u toku" - "Moj broj je %s" - "Uspostavljanje videopoziva" - "Videopoziv" - "Zahtijevanje videopoziva" - "Nije moguće uspostaviti videopoziv" - "Zahtjev za videopoziv je odbijen" - "Vaš broj za povratni poziv\n %1$s" - "Vaš broj za hitni povratni poziv\n %1$s" - "Poziva se" - "Propušteni poziv" - "Propušteni pozivi" - "Propušteni pozivi: %s" - "Propušteni poziv od kontakta %s" - "Poziv u toku" - "Poslovni poziv u toku" - "Wi-Fi poziv u toku" - "Wi-Fi poslovni poziv u toku" - "Na čekanju" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Dolazni Wi-Fi poziv" - "Dolazni Wi-Fi poslovni poziv" - "Dolazni videopoziv" - "Mogući neželjeni dolazni poziv" - "Zahtjev za dolazni videopoziv" - "Nova govorna pošta" - "Nova govorna pošta (%d)" - "Pozovi %s" - "Nepoznat broj govorne pošte" - "Nema mreže" - "Odabrana mreža (%s) je nedostupna" - "Odgovori" - "Prekini vezu" - "Videopoziv" - "Glasovni poziv" - "Prihvati" - "Odbaci" - "Povr. poziv" - "Poruka" - "Poziv u toku na drugom uređaju" - "Prenesi poziv" - "Da uputite poziv, isključite Način rada u avionu." - "Nije registrirano na mreži." - "Mobilna mreža nije dostupna." - "Da uputite poziv, upišite važeći broj." - "Nije moguće pozvati." - "Pokretanje MMI sekvence u toku…" - "Usluga nije podržana." - "Nije moguće prebacivanje poziva." - "Nije moguće odvojiti poziv." - "Prijenos nije moguć." - "Konferencijski poziv nije uspio." - "Nije moguće odbiti poziv." - "Nije moguće uputiti poziv(e)." - "SIP poziv" - "Hitni poziv" - "Uključivanje radija u toku…" - "Nema mreže. Ponovni pokušaj u toku…" - "Nije moguće pozvati. %s nije broj za htine slučajeve." - "Nije moguće pozvati. Pozovite broj za hitne slučajeve." - "Koristi tastaturu za biranje" - "Stavi poziv na čekanje" - "Nastavi poziv" - "Prekini poziv" - "Prikaži telefonsku tipkovnicu" - "Sakrij telefonsku tipkovnicu" - "Isključi zvuk" - "Uključi zvuk" - "Dodaj poziv" - "Spoji pozive" - "Zamijeni" - "Upravljaj pozivima" - "Upravljaj konf. pozivom" - "Konferencijski poziv" - "Upravljaj" - "Zvuk" - "Videopoziv" - "Promijeni na glasovni poziv" - "Promijeni kameru" - "Uključi kameru" - "Isključi kameru" - "Više opcija" - "Plejer je pokrenut" - "Plejer je zaustavljen" - "Kamera nije spremna" - "Kamera je spremna" - "Nepoznati događaj sesije poziva" - "Usluga" - "Postavljanje" - "<Nije postavljeno>" - "Ostale postavke poziva" - "Pozivanje putem %s" - "Dolazni poziv putem %s" - "fotografija kontakta" - "idi na privatno" - "odaberi kontakt" - "Napišite svoj..." - "Otkaži" - "Pošalji" - "Odgovori" - "Pošalji SMS" - "Odbij" - "Odgovori videopozivom" - "Prihvati kao audiopoziv" - "Prihvati zahtjev za videopoziv" - "Odbij zahtjev za videopoziv" - "Prihvati zahtjev za slanje videopoziva" - "Odbij zahtjev za slanje videopoziva" - "Prihvati zahtjev za primanje videopoziva" - "Odbij zahtjev za primanje videopoziva" - "Kliznite nagore za %s." - "Kliznite lijevo za %s." - "Kliznite nadesno za %s." - "Kliznite nadolje za %s." - "Vibracija" - "Vibracija" - "Zvuk" - "Zadani zvuk (%1$s)" - "Melodija zvona telefona" - "Vibriraj kada zvoni" - "Melodija zvona i vibracija" - "Upravljaj konferencijskim pozivom" - "Broj za hitne slučajeve" - "Fotografija profila" - "Kamera je isključena" - "putem %s" - "Bilješka je poslana" - "Nedavne poruke" - "Informacije o preduzeću" - "Udaljenost u miljama: %.1f" - "Udaljenost u km: %.1f" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Otvara se sutra u %s" - "Otvara se danas u %s" - "Zatvara se u %s" - "Zatvoreno danas u %s" - "Otvori sad" - "Zatvoreno sada" - "Neželjeni pozivalac" - "Poziv je završen %1$s" - "Ovo je prvi poziv koji ste primili s ovog broja." - "Sumnjamo da je ovaj poziv neželjen sadržaj." - "Blokiraj/prijavi" - "Dodaj kontakt" - "Nije neželjeno" - diff --git a/InCallUI/res/values-ca/strings.xml b/InCallUI/res/values-ca/strings.xml deleted file mode 100644 index 103f15b639e3edacb2af255fb5920e4a00f4c6c3..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ca/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telèfon" - "En espera" - "Desconegut" - "Número privat" - "Telèfon públic" - "Conferència" - "La trucada s\'ha interromput" - "Altaveu" - "Auricular" - "Auricular amb cable" - "Bluetooth" - "Vols enviar els tons següents?\n" - "S\'estan enviant els tons\n" - "Envia" - "Sí" - "No" - "Substitueix el caràcter comodí per" - "Conferència, %s" - "Número del missatge de veu" - "S\'està marcant" - "S\'està tornant a marcar" - "Conferència" - "Trucada entrant" - "Trucada de feina entrant" - "Ha finalitzat la trucada" - "En espera" - "S\'està penjant" - "En una trucada" - "El meu número és %s" - "S\'està connectant el vídeo" - "Videotrucada" - "S\'està sol·licitant el vídeo" - "No es pot connectar la videotrucada" - "S\'ha rebutjat la sol·licitud de vídeo" - "Número de devolució de trucada\n %1$s" - "Número de devolució de trucada d\'emergència\n %1$s" - "S\'està marcant" - "Trucada perduda" - "Trucades perdudes" - "%s trucades perdudes" - "Trucada perduda de %s" - "Trucada en curs" - "Trucada de feina en curs" - "Trucada per Wi-Fi en curs" - "Trucada de feina per Wi-Fi en curs" - "En espera" - "Trucada entrant" - "Trucada de feina entrant" - "Trucada per Wi-Fi entrant" - "Trucada de feina per Wi-Fi entrant" - "Videotrucada entrant" - "Presumpta trucada brossa entrant" - "Sol·licitud de vídeo entrant" - "Missatge de veu nou" - "Missatges de veu nous (%d)" - "Marca %s" - "Número del missatge de veu desconegut" - "Sense servei" - "La xarxa seleccionada (%s) no està disponible" - "Respon" - "Penja" - "Vídeo" - "Veu" - "Accepta" - "Ignora" - "Torna trucada" - "Missatge" - "Trucada en curs en un altre dispositiu" - "Transfereix la trucada" - "Per fer una trucada, primer desactiva el mode d\'avió." - "No s\'ha registrat a la xarxa." - "La xarxa mòbil no està disponible." - "Introdueix un número vàlid per fer una trucada." - "No es pot trucar." - "S\'està iniciant la seqüència MMI…" - "El servei no és compatible." - "No es pot canviar de trucada." - "No es pot separar la trucada." - "No es poden transferir trucades." - "No es pot establir la conferència." - "No es pot rebutjar la trucada." - "No es poden alliberar trucades." - "Trucada de SIP" - "Trucada d\'emergència" - "S\'està activant el senyal mòbil…" - "No hi ha servei. S\'està tornant a provar…" - "No es pot trucar. %s no és un número d\'emergència." - "No es pot trucar. Marca un número d\'emergència." - "Utilitza el teclat per marcar" - "Posa la trucada en espera" - "Reprèn la trucada" - "Finalitza la trucada" - "Mostra el teclat" - "Amaga el teclat" - "Silencia" - "Activa el so" - "Afegeix una trucada" - "Combina les trucades" - "Canvia" - "Gestiona les trucades" - "Gestiona la conferència" - "conferència" - "Gestiona" - "Àudio" - "Videotrucada" - "Canvia a trucada de veu" - "Canvia la càmera" - "Activa la càmera" - "Desactiva la càmera" - "Més opcions" - "S\'ha iniciat el reproductor" - "S\'ha aturat el reproductor" - "La càmera no està preparada" - "La càmera està preparada" - "Esdeveniment de sessió de trucada desconeguda" - "Servei" - "Configura" - "<No definit>" - "Altres opcions de configuració de les trucades" - "S\'està trucant amb %s" - "Trucada entrant mitjançant %s" - "foto de contacte" - "conferència privada" - "selecciona el contacte" - "Escriu la teva…" - "Cancel·la" - "Envia" - "Respon" - "Envia SMS" - "Rebutja" - "Respon amb una videotrucada" - "Respon amb una trucada d\'àudio" - "Accepta la sol·licitud de vídeo" - "Rebutja la sol·licitud de vídeo" - "Accepta la sol·licitud per transmetre vídeo" - "Rebutja la sol·licitud per transmetre vídeo" - "Accepta la sol·licitud per rebre vídeo" - "Rebutja la sol·licitud per rebre vídeo" - "Fes lliscar el dit cap amunt per %s." - "Fes lliscar el dit cap a l\'esquerra per %s." - "Fes lliscar el dit cap a la dreta per %s." - "Fes lliscar el dit cap avall per %s." - "Vibració" - "Vibració" - "So" - "So predeterminat (%1$s)" - "So de trucada" - "Vibrar en sonar" - "So de trucada i vibració" - "Gestiona la conferència" - "Número d\'emergència" - "Foto de perfil" - "La càmera està desactivada" - "mitjançant %s" - "S\'ha enviat la nota" - "Missatges recents" - "Informació de l\'empresa" - "A %.1f mi de distància" - "A %.1f km de distància" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Obre demà a les %s" - "Obre avui a les %s" - "Tanca a les %s" - "Avui ha tancat a les %s" - "Obert ara" - "Ara és tancat" - "Possible trucada brossa" - "La trucada amb el número %1$s ha finalitzat" - "És la primera vegada que aquest número t\'ha trucat." - "Sospitem que aquesta trucada prové d\'un emissor de contingut brossa." - "Bloqueja/marca brossa" - "Afegeix un contacte" - "No és brossa" - diff --git a/InCallUI/res/values-cs/strings.xml b/InCallUI/res/values-cs/strings.xml deleted file mode 100644 index f3e4a5ed3fb2959fdd6cd337e9e9919b2533efbb..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-cs/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Přidržený hovor" - "Neznámý volající" - "Soukromé číslo" - "Telefonní automat" - "Konferenční hovor" - "Volání zrušeno" - "Reproduktor" - "Sluchátko telefonu" - "Kabelová náhlavní soupr." - "Bluetooth" - "Odeslat následující tóny?\n" - "Odesílání tónů\n" - "Odeslat" - "Ano" - "Ne" - "Nahradit zástupné znaky jinými znaky" - "Konferenční hovor %s" - "Číslo hlasové schránky" - "Vytáčení" - "Opakované vytáčení" - "Konferenční hovor" - "Příchozí hovor" - "Příchozí pracovní hovor" - "Hovor ukončen" - "Přidržený hovor" - "Ukončování hovoru" - "Probíhá hovor" - "Moje číslo je %s" - "Navazování spojení pro video" - "Videohovor" - "Požadování videa" - "Videohovor nelze zahájit" - "Žádost o video byla zamítnuta" - "Vaše číslo pro zpětné volání\n%1$s" - "Vaše číslo pro tísňové zpětné volání\n%1$s" - "Vytáčení" - "Zmeškaný hovor" - "Zmeškané hovory" - "Zmeškané hovory: %s" - "Zmeškaný hovor od volajícího %s" - "Probíhající hovor" - "Probíhající pracovní hovor" - "Probíhající hovor přes Wi-Fi" - "Probíhající pracovní hovor přes Wi-Fi" - "Přidržený hovor" - "Příchozí hovor" - "Příchozí pracovní hovor" - "Příchozí hovor přes Wi-Fi" - "Příchozí pracovní hovor přes Wi-Fi" - "Příchozí videohovor" - "U příchozího hovoru máme podezření, že se jedná o spam" - "Příchozí žádost o videohovor" - "Nová hlasová zpráva" - "Nové hlasové zprávy (%d)" - "Volat hlasovou schránku %s" - "Číslo hlasové schránky není známé" - "Žádný signál" - "Vybraná síť (%s) není k dispozici" - "Přijmout" - "Zavěsit" - "Videohovor" - "Hlas. hovor" - "Přijmout" - "Odmítnout" - "Zavolat zpět" - "Posl. zprávu" - "Probíhá hovor na jiném zařízení" - "Převést hovor sem" - "Chcete-li telefonovat, nejprve vypněte režim Letadlo." - "Přihlášení k síti nebylo úspěšné." - "Mobilní síť je nedostupná." - "Chcete-li uskutečnit hovor, zadejte platné telefonní číslo." - "Hovor nelze uskutečnit." - "Spouštění sekvence MMI..." - "Služba není podporována." - "Hovory nelze přepnout." - "Hovor nelze rozdělit." - "Hovor nelze předat." - "Konferenční hovor nelze uskutečnit." - "Hovor nelze odmítnout." - "Hovor nelze ukončit." - "Volání SIP" - "Tísňové volání" - "Zapínání bezdrátového modulu..." - "Žádný signál. Probíhá další pokus…" - "Hovor nelze uskutečnit. %s není číslo tísňového volání." - "Hovor nelze uskutečnit. Vytočte číslo tísňového volání." - "Vytočte číslo pomocí klávesnice" - "Podržet hovor" - "Obnovit hovor" - "Ukončit hovor" - "Zobrazit číselník" - "Skrýt číselník" - "Vypnout zvuk" - "Zapnout zvuk" - "Přidat hovor" - "Spojit hovory" - "Zaměnit" - "Spravovat hovory" - "Spravovat konferenční hovor" - "Konferenční hovor" - "Spravovat" - "Zvuk" - "Videohovor" - "Změnit na hlasové volání" - "Přepnout kameru" - "Zapnout kameru" - "Vypnout kameru" - "Další možnosti" - "Přehrávač spuštěn" - "Přehrávač zastaven" - "Fotoaparát není připraven" - "Fotoaparát je připraven" - "Neznámá událost relace volání" - "Služba" - "Nastavení" - "<Nenastaveno>" - "Další nastavení hovorů" - "Volání prostřednictvím poskytovatele %s" - "Příchozí hovor přes poskytovatele %s" - "fotografie kontaktu" - "přepnout na soukromé" - "vybrat kontakt" - "Napsat vlastní odpověď..." - "Zrušit" - "Odeslat" - "Přijmout" - "Odeslat SMS" - "Odmítnout" - "Přijmout jako videohovor" - "Přijmout jako hlasový hovor" - "Přijmout žádost o videhovor" - "Odmítnout žádost o videohovor" - "Přijmout žádost o odesílání videa" - "Odmítnout žádost o odesílání videa" - "Přijmout žádost o příjem videa" - "Odmítnout žádost o příjem videa" - "%s – přejeďte prstem nahoru" - "%s – přejeďte prstem doleva" - "%s – přejeďte prstem doprava" - "%s – přejeďte prstem dolů" - "Vibrace" - "Vibrace" - "Zvuk" - "Výchozí zvuk (%1$s)" - "Vyzváněcí tón telefonu" - "Vibrace při vyzvánění" - "Vyzvánění a vibrace" - "Správa konferenčního hovoru" - "Číslo tísňové linky" - "Profilová fotka" - "Fotoaparát je vypnutý" - "pomocí čísla %s" - "Poznámka byla odeslána" - "Nejnovější zprávy" - "Informace o firmě" - "Vzdálenost: %.1f mi" - "Vzdálenost: %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Zítra otevírá v %s" - "Dnes otevírá v %s" - "Zavírá v %s" - "Dnes zavřeno od %s" - "Otevřeno" - "Nyní zavřeno" - "Podezření na spam" - "Hovor skončil v %1$s" - "Toto číslo vám volalo poprvé." - "Máme podezření, že tento hovor byl spam." - "Blok./nahlásit spam" - "Přidat kontakt" - "Nešlo o spam" - diff --git a/InCallUI/res/values-da/strings.xml b/InCallUI/res/values-da/strings.xml deleted file mode 100644 index 82d773c1c6197cca4c4b4273b88e2bb87597105b..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-da/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Afventer" - "Ukendt" - "Privat nummer" - "Mønttelefon" - "Telefonmøde" - "Opkaldet blev afbrudt." - "Højttaler" - "Ørestykke til håndsæt" - "Headset med ledning" - "Bluetooth" - "Vil du sende følgende toner?\n" - "Sender toner\n" - "Send" - "Ja" - "Nej" - "Erstat jokertegnet med" - "Telefonmøde %s" - "Telefonsvarernummer" - "Ringer op" - "Ringer op igen" - "Telefonmøde" - "Indgående opkald" - "Indgående arbejdsopkald" - "Opkaldet er afsluttet" - "Afventer" - "Lægger på" - "Opkald i gang" - "Mit nummer er %s" - "Opretter videoforbindelse" - "Videoopkald" - "Anmoder om video" - "Kan ikke forbinde videoopkald" - "Videoanmodningen blev afvist" - "Dit tilbagekaldsnummer\n %1$s" - "Dit tilbagekaldsnummer til nødopkald\n %1$s" - "Ringer op" - "Ubesvaret opkald" - "Ubesvarede opkald" - "%s ubesvarede opkald" - "Ubesvaret opkald fra %s" - "Igangværende opkald" - "Igangværende opkald i forbindelse med arbejde" - "Igangværende opkald via Wi-Fi" - "Igangværende Wi-Fi-opkald i forbindelse med arbejde" - "Afventer" - "Indgående opkald" - "Indgående arbejdsopkald" - "Indgående Wi-Fi-opkald" - "Indgående Wi-Fi-opkald i forbindelse med arbejde" - "Indgående videoopkald" - "Indgående formodet spamopkald" - "Indgående videoanmodning" - "Ny telefonsvarerbesked" - "Nye telefonsvarerbeskeder (%d)" - "Ring til %s" - "Telefonsvarernummeret er ukendt" - "Ingen dækning" - "Det valgte netværk (%s) er ikke tilgængeligt" - "Besvar" - "Læg på" - "Video" - "Tale" - "Acceptér" - "Afvis" - "Ring tilbage" - "Besked" - "Igangværende opkald på en anden enhed" - "Overfør opkald" - "Slå Flytilstand fra først for at foretage et opkald." - "Ikke registreret på netværket." - "Mobilnetværket er ikke tilgængeligt." - "Indtast et gyldigt nummer for at foretage et opkald." - "Der kan ikke ringes op." - "Starter MMI-sekvens…" - "Tjenesten er ikke understøttet." - "Der kan ikke skiftes opkald." - "Opkaldet kan ikke adskilles." - "Der kan ikke viderestilles." - "Der kan ikke oprettes telefonmøde." - "Opkaldet kan ikke afvises." - "Et eller flere opkald kan ikke frigives." - "SIP-opkald" - "Nødopkald" - "Tænder for radio…" - "Ingen tjeneste. Prøver igen…" - "Der kan ikke ringes op. %s er ikke et alarmnummer." - "Der kan ikke ringes op. Ring til et alarmnummer." - "Brug tastaturet til at ringe op" - "Sæt opkald i venteposition" - "Genoptag opkald" - "Afslut opkald" - "Vis numerisk tastatur" - "Skjul numerisk tastatur" - "Slå lyden fra" - "Slå lyden til" - "Tilføj opkald" - "Slå opkald sammen" - "Skift" - "Administrer opkald" - "Administrer telefonmøde" - "Telefonmøde" - "Administrer" - "Lyd" - "Videoopkald" - "Skift til taleopkald" - "Skift kamera" - "Slå kameraet til" - "Slå kameraet fra" - "Flere valgmuligheder" - "Afspilleren er startet" - "Afspilleren er stoppet" - "Kameraet er ikke klar" - "Kameraet er klar" - "Ukendt opkaldsbegivenhed" - "Tjeneste" - "Konfiguration" - "<Ikke angivet>" - "Andre indstillinger for opkald" - "Opkald via %s" - "Indgående opkald via %s" - "billede af kontaktperson" - "gør privat" - "vælg kontaktperson" - "Skriv dit eget svar…" - "Annuller" - "Send" - "Besvar" - "Send sms" - "Afvis" - "Besvar som videoopkald" - "Besvar som taleopkald" - "Acceptér anmodning om video" - "Afvis videoanmodning" - "Acceptér anmodning om udgående video" - "Afvis anmodning om udgående video" - "Acceptér anmodning om indgående video" - "Afvis anmodning om indgående video" - "Skub op for at %s." - "Skub til venstre for at %s." - "Skub til højre for at %s." - "Skub ned for at %s." - "Vibration" - "Vibration" - "Lyd" - "Standardlyd (%1$s)" - "Ringetone ved opkald" - "Vibrer ved opringning" - "Ringetone og vibration" - "Administrer telefonmøde" - "Alarmnummer" - "Profilbillede" - "Kameraet er slukket" - "via %s" - "Noten er sendt" - "Seneste beskeder" - "Virksomhedsoplysninger" - "%.1f mil væk" - "%.1f km væk" - "%1$s, %2$s" - "%1$s-%2$s" - "%1$s, %2$s" - "Åbner i morgen kl. %s" - "Åbner i dag kl. %s" - "Lukker kl. %s" - "Lukkede i dag kl. %s" - "Åbent nu" - "Lukket for i dag" - "Formodet spammer" - "Opkaldet blev afsluttet %1$s" - "Dette er første gang, at dette nummer har ringet til dig." - "Vi har mistanke om, at dette er et spamopkald." - "Bloker/rap. spam" - "Tilføj kontaktperson" - "Ikke spam" - diff --git a/InCallUI/res/values-de/strings.xml b/InCallUI/res/values-de/strings.xml deleted file mode 100644 index b3ecffc4c236de7218989b3e3aabd6e47634c733..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-de/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Gehaltener Anruf" - "Unbekannt" - "Private Nummer" - "Münztelefon" - "Telefonkonferenz" - "Verbindung unterbrochen" - "Lautsprecher" - "Mobilgerät-Kopfhörer" - "Kabelgebundenes Headset" - "Bluetooth" - "Folgende Töne senden?\n" - "Töne werden gesendet\n" - "Senden" - "Ja" - "Nein" - "Platzhalter ersetzen durch" - "Telefonkonferenz %s" - "Mailboxnummer" - "Rufaufbau" - "Wahlwiederholung" - "Telefonkonferenz" - "Eingehender Anruf" - "Eingeh. geschäftl. Anruf" - "Anruf beendet" - "Gehaltener Anruf" - "Auflegen" - "Im Gespräch" - "Meine Nummer lautet %s" - "Videoverbindung wird hergestellt" - "Videoanruf" - "Videoanfrage wird gesendet" - "Videoanruf kann nicht verbunden werden" - "Videoanfrage abgelehnt" - "Deine Rückrufnummer lautet:\n %1$s" - "Deine Notrufnummer lautet:\n %1$s" - "Rufaufbau" - "Verpasster Anruf" - "Entgangene Anrufe" - "%s entgangene Anrufe" - "Verpasster Anruf von %s" - "Aktiver Anruf" - "Aktiver geschäftlicher Anruf" - "Aktiver WLAN-Anruf" - "Aktiver geschäftlicher WLAN-Anruf" - "Gehaltener Anruf" - "Eingehender Anruf" - "Eingehender geschäftlicher Anruf" - "Eingehender WLAN-Anruf" - "Eingehender geschäftlicher WLAN-Anruf" - "Eingehender Videoanruf" - "Verdacht auf eingehenden Spam-Anruf" - "Eingehende Videoanfrage" - "Neue Mailbox-Nachricht" - "Neue Mailbox-Nachricht (%d)" - "%s wählen" - "Mailboxnummer unbekannt" - "Kein Service" - "Ausgewähltes Netzwerk (%s) nicht verfügbar" - "Annehmen" - "Beenden" - "Videoanruf" - "Sprachanruf" - "Akzeptieren" - "Ablehnen" - "Zurückrufen" - "Nachricht" - "Aktiver Anruf auf anderem Gerät" - "Anruf übertragen" - "Deaktiviere zunächst den Flugmodus, um einen Anruf zu tätigen." - "Nicht in Netzwerk registriert." - "Mobilfunknetz nicht verfügbar." - "Gib eine gültige Nummer ein, um einen Anruf zu tätigen." - "Anruf nicht möglich." - "MMI-Sequenz wird gestartet…" - "Dienst wird nicht unterstützt." - "Anruf kann nicht gewechselt werden." - "Anruf kann nicht getrennt werden." - "Anruf kann nicht übergeben werden." - "Konferenzschaltung nicht möglich." - "Anruf kann nicht abgelehnt werden." - "Anrufe können nicht freigegeben werden." - "SIP-Anruf" - "Notruf" - "Mobilfunkverbindung wird aktiviert…" - "Kein Service. Neuer Versuch…" - "Anruf nicht möglich. %s ist keine Notrufnummer." - "Anruf nicht möglich. Wähle eine Notrufnummer." - "Zum Wählen Tastatur verwenden" - "Anruf halten" - "Anruf fortsetzen" - "Anruf beenden" - "Wähltasten einblenden" - "Wähltasten ausblenden" - "Stummschalten" - "Stummschaltung aufheben" - "Anruf hinzufügen" - "Anrufe verbinden" - "Wechseln" - "Anrufe verwalten" - "Telefonkonferenz verwalten" - "Telefonkonferenz" - "Verwalten" - "Audio" - "Videoanruf" - "Zu Sprachanruf wechseln" - "Kamera wechseln" - "Kamera einschalten" - "Kamera ausschalten" - "Weitere Optionen" - "Videoübertragung gestartet" - "Videoübertragung gestoppt" - "Kamera nicht bereit" - "Kamera bereit" - "Unbekanntes Ereignis während eines Anrufs" - "Dienst" - "Einrichtung" - "<Nicht festgelegt>" - "Sonstige Anrufeinstellungen" - "Anruf über %s" - "Eingehender Anruf über %s" - "Kontaktbild" - "privat sprechen" - "Kontakt auswählen" - "Eigene Antwort schreiben…" - "Abbrechen" - "Senden" - "Annehmen" - "SMS senden" - "Ablehnen" - "Als Videoanruf annehmen" - "Als normalen Anruf annehmen" - "Videoanfrage akzeptieren" - "Videoanfrage ablehnen" - "Anfrage für ausgehenden Videoanruf akzeptieren" - "Anfrage für ausgehenden Videoanruf ablehnen" - "Anfrage für eingehenden Videoanruf akzeptieren" - "Anfrage für eingehenden Videoanruf ablehnen" - "Zum %s nach oben schieben." - "Zum %s nach links schieben." - "Zum %s nach rechts schieben." - "Zum %s nach unten schieben." - "Vibrieren" - "Vibrieren" - "Ton" - "Standardklingelton (%1$s)" - "Klingelton" - "Beim Klingeln vibrieren" - "Klingelton & Vibration" - "Telefonkonferenz verwalten" - "Notrufnummer" - "Profilbild" - "Kamera aus" - "über %s" - "Notiz gesendet" - "Zuletzt eingegangene Nachrichten" - "Geschäftsinformationen" - "%.1f Meilen entfernt" - "%.1f Kilometer entfernt" - "%1$s, %2$s" - "%1$s bis %2$s" - "%1$s, %2$s" - "Öffnet morgen um %s" - "Öffnet heute um %s" - "Schließt um %s" - "Hat heute um %s geschlossen" - "Jetzt geöffnet" - "Jetzt geschlossen" - "Verdacht auf Spam" - "Anruf beendet %1$s" - "Du wurdest das erste Mal von dieser Nummer angerufen." - "Dieser Anruf schien ein Spam-Anruf zu sein." - "Blockieren/Spam melden" - "Kontakt hinzufügen" - "Kein Spam" - diff --git a/InCallUI/res/values-el/strings.xml b/InCallUI/res/values-el/strings.xml deleted file mode 100644 index 99381329065a34a4ad0992a66717d84588ce478f..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-el/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Τηλέφωνο" - "Σε αναμονή" - "Άγνωστος" - "Απόκρυψη αριθμού" - "Τηλέφωνο με χρέωση" - "Κλήση συνδιάσκεψης" - "Η κλήση απορρίφθηκε" - "Ηχείο" - "Ακουστικό" - "Ενσύρματο ακουστικό" - "Bluetooth" - "Αποστολή των παρακάτω ήχων;\n" - "Ήχοι αποστολής\n" - "Αποστολή" - "Ναι" - "Όχι" - "Αντικατάσταση του χαρακτήρα μπαλαντέρ με" - "Κλήση συνδιάσκεψης %s" - "Αριθμός αυτόματου τηλεφωνητή" - "Κλήση" - "Επανάκληση" - "Κλήση συνδιάσκεψης" - "Εισερχόμενη κλήση" - "Εισερχόμ. κλήση εργασίας" - "Η κλήση τερματίστηκε" - "Σε αναμονή" - "Κλείσιμο γραμμής" - "Σε κλήση" - "Ο αριθμός μου είναι %s" - "Σύνδεση βίντεο" - "Βιντεοκλήση" - "Αίτημα βίντεο" - "Δεν είναι δυνατή η σύνδεση βιντεοκλήσης" - "Το αίτημα βίντεο απορρίφθηκε" - "Αριθμός επανάκλησης\n %1$s" - "Αριθμός επανάκλησης έκτακτης ανάγκης\n %1$s" - "Κλήση" - "Αναπάντητη κλήση" - "Αναπάντητες κλήσεις" - "%s αναπάντητες κλήσεις" - "Αναπάντητη κλήση από %s" - "Κλήση σε εξέλιξη" - "Κλήση εργασίας σε εξέλιξη" - "Κλήση Wi-Fi σε εξέλιξη" - "Κλήση εργασίας μέσω Wi-Fi σε εξέλιξη" - "Σε αναμονή" - "Εισερχόμενη κλήση" - "Εισερχόμενη κλήση εργασίας" - "Εισερχόμενη κλήση μέσω Wi-Fi" - "Εισερχόμενη κλήση εργασίας μέσω Wi-Fi" - "Εισερχόμενη βιντεοκλήση" - "Πιθανώς ανεπιθύμητη εισερχόμενη κλήση" - "Αίτημα εισερχόμενου βίντεο" - "Νέο μήνυμα στον αυτόματο τηλεφωνητή" - "Νέο μήνυμα στον αυτόματο τηλεφωνητή (%d)" - "Καλέστε στο %s" - "Άγνωστος αριθμός αυτόματου τηλεφωνητή" - "Δίκτυο μη διαθέσιμο" - "Το επιλεγμένο δίκτυο (%s) δεν είναι διαθέσιμο" - "Απάντηση" - "Τερμ. κλήσης" - "Βίντεο" - "Φωνή" - "Αποδοχή" - "Παράβλεψη" - "Επανάκληση" - "Μήνυμα" - "Κλήση σε εξέλιξη σε άλλη συσκευή" - "Μεταφορά κλήσης" - "Για να πραγματοποιήσετε μια κλήση, απενεργοποιήστε πρώτα τη λειτουργία πτήσης." - "Δεν έχετε εγγραφεί στο δίκτυο." - "Το δίκτυο κινητής τηλεφωνίας δεν είναι διαθέσιμο." - "Για να πραγματοποιήσετε κλήση, εισαγάγετε έναν έγκυρο αριθμό." - "Δεν είναι δυνατή η κλήση." - "Έναρξη ακολουθίας MMI…" - "Η υπηρεσία δεν υποστηρίζεται." - "Δεν είναι δυνατή η εναλλαγή κλήσεων." - "Δεν είναι δυνατός ο διαχωρισμός της κλήσης." - "Δεν είναι δυνατή η μεταφορά." - "Δεν είναι δυνατή η συνδιάσκεψη." - "Δεν είναι δυνατή η απόρριψη της κλήσης." - "Δεν είναι δυνατή η πραγματοποίηση κλήσεων." - "Κλήση SIP" - "Κλήση έκτακτης ανάγκης" - "Ενεργοποίηση πομπού…" - "Δεν υπάρχει υπηρεσία. Νέα προσπάθεια…" - "Δεν είναι δυνατή η κλήση. Το %s δεν είναι αριθμός έκτακτης ανάγκης." - "Δεν είναι δυνατή η κλήση. Πληκτρολογήστε έναν αριθμό έκτακτης ανάγκης." - "Χρησιμοποιήστε το πληκτρολόγιο για να πραγματοποιήσετε μια κλήση" - "Αναμονή κλήσης" - "Συνέχιση κλήσης" - "Τερματισμός κλήσης" - "Εμφάνιση πληκτρολογίου κλήσης" - "Απόκρυψη πληκτρολογίου κλήσης" - "Σίγαση" - "Κατάργηση σίγασης" - "Προσθήκη κλήσης" - "Συγχώνευση κλήσεων" - "Ανταλλαγή" - "Διαχείριση κλήσεων" - "Διαχείριση κλήσης συνδιάσκεψης" - "Κλήση διάσκεψης" - "Διαχείριση" - "Ήχος" - "Βιντεοκλ." - "Αλλαγή σε φωνητική κλήση" - "Αλλαγή κάμερας" - "Ενεργοποίηση κάμερας" - "Απενεργοποίηση κάμερας" - "Περισσότερες επιλογές" - "Το πρόγραμμα αναπαραγωγής βίντεο ξεκίνησε" - "Το πρόγραμμα αναπαραγωγής βίντεο διακόπηκε" - "Η κάμερα δεν είναι έτοιμη" - "Η κάμερα είναι έτοιμη" - "Άγνωστο συμβάν περιόδου σύνδεσης κλήσης" - "Υπηρεσία" - "Ρύθμιση" - "<Δεν έχει οριστεί>" - "Άλλες ρυθμίσεις κλήσης" - "Κλήση μέσω %s" - "Εισερχόμενη κλήση μέσω %s" - "φωτογραφία επαφής" - "ιδιωτική χρήση" - "επιλογή επαφής" - "Συντάξτε τη δική σας…" - "Ακύρωση" - "Αποστολή" - "Απάντηση" - "Αποστολή SMS" - "Απόρριψη" - "Απάντηση ως βιντεοκλήση" - "Απάντηση ως κλήση ήχου" - "Αποδοχή αιτήματος βίντεο" - "Απόρριψη αιτήματος βίντεο" - "Αποδοχή αιτήματος μετάδοσης βίντεο" - "Απόρριψη αιτήματος μετάδοσης βίντεο" - "Αποδοχή αιτήματος λήψης βίντεο" - "Απόρριψη αιτήματος λήψης βίντεο" - "Κύλιση προς τα επάνω για %s." - "Κύλιση προς τα αριστερά για %s." - "Κύλιση προς τα δεξιά για %s." - "Κύλιση προς τα κάτω για %s." - "Δόνηση" - "Δόνηση" - "Ήχος" - "Προεπιλεγμένος ήχος (%1$s)" - "Ήχος κλήσης τηλεφώνου" - "Δόνηση κατά το κουδούνισμα" - "Ήχος κλήσης και δόνηση" - "Διαχείριση κλήσης συνδιάσκεψης" - "Αριθμός έκτακτης ανάγκης" - "Φωτογραφία προφίλ" - "Απενεργοποίηση κάμερας" - "μέσω %s" - "Η σημείωση εστάλη" - "Πρόσφατα μηνύματα" - "Πληροφορίες επιχείρησης" - "%.1f μίλια μακριά" - "%.1f χιλιόμετρα μακριά" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Ανοίγει αύριο στις %s" - "Ανοίγει σήμερα στις %s" - "Κλείνει στις %s" - "Έκλεισε σήμερα στις %s" - "Ανοιχτό τώρα" - "Κλειστό τώρα" - "Πιθανώς ανεπιθύμητος" - "Η κλήση τερματίστηκε %1$s" - "Αυτή είναι η πρώτη φορά που σας καλεί αυτός ο αριθμός." - "Έχουμε υποψίες ότι αυτή κλήση είναι ανεπιθύμητη." - "Αποκλ./αναφ. ανεπιθ." - "Προσθήκη επαφής" - "Μη ανεπιθύμητος" - diff --git a/InCallUI/res/values-en-rAU/strings.xml b/InCallUI/res/values-en-rAU/strings.xml deleted file mode 100644 index 013aa9685eac96cb59b90617fdd2112c1adedca0..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-en-rAU/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Phone" - "On hold" - "unknown" - "Private number" - "Payphone" - "Conference call" - "Call cut off" - "Speaker" - "Handset earpiece" - "Wired headset" - "Bluetooth" - "Send the following tones?\n" - "Sending tones\n" - "Send" - "yes" - "no" - "Replace wild character with" - "Conference call %s" - "Voicemail number" - "Dialling" - "Redialling" - "Conference call" - "Incoming call" - "Incoming work call" - "Call ended" - "On hold" - "Hanging up" - "In call" - "My number is %s" - "Connecting video" - "Video call" - "Requesting video" - "Can\'t connect video call" - "Video request rejected" - "Your callback number\n %1$s" - "Your emergency callback number\n %1$s" - "Dialling" - "Missed call" - "Missed calls" - "%s missed calls" - "Missed call from %s" - "On-going call" - "Ongoing work call" - "Ongoing Wi-Fi call" - "Ongoing Wi-Fi work call" - "On hold" - "Incoming call" - "Incoming work call" - "Incoming Wi-Fi call" - "Incoming Wi-Fi work call" - "Incoming video call" - "Incoming suspected spam call" - "Incoming video request" - "New voicemail" - "New voicemail (%d)" - "Dial %s" - "Voicemail number unknown" - "No service" - "Selected network (%s) unavailable" - "Answer" - "Hang up" - "In-stream video" - "Voice" - "Accept" - "Dismiss" - "Call back" - "Message" - "Ongoing call on another device" - "Transfer call" - "To place a call, first turn off Aeroplane mode." - "Not registered on network." - "Mobile network not available." - "To place a call, enter a valid number." - "Can\'t call." - "Starting MMI sequence…" - "Service not supported." - "Can\'t switch calls." - "Can\'t separate call." - "Can\'t transfer." - "Can\'t conference." - "Can\'t reject call." - "Can\'t release call(s)." - "SIP call" - "Emergency call" - "Turning on radio…" - "No network. Trying again…" - "Can\'t call. %s is not an emergency number." - "Can\'t call. Dial an emergency number." - "Use keyboard to dial" - "Hold Call" - "Resume Call" - "End Call" - "Show dial pad" - "Hide dial pad" - "Mute" - "Unmute" - "Add call" - "Merge calls" - "Swap" - "Manage calls" - "Manage conference call" - "Conference call" - "Manage" - "Audio" - "Video call" - "Change to voice call" - "Switch camera" - "Turn on camera" - "Turn off camera" - "More options" - "Player Started" - "Player Stopped" - "Camera not ready" - "Camera ready" - "Unknown call session event" - "Service" - "Set up" - "<Not set>" - "Other call settings" - "Calling via %s" - "Incoming via %s" - "contact photo" - "go private" - "select contact" - "Write your own..." - "cancel" - "Send" - "Answer" - "Send SMS" - "Decline" - "Answer as video call" - "Answer as audio call" - "Accept video request" - "Decline video request" - "Accept video transmit request" - "Decline video transmit request" - "Accept video receive request" - "Decline video receive request" - "Slide up for %s." - "Slide left for %s." - "Slide right for %s." - "Slide down for %s." - "Vibrate" - "Vibrate" - "Sound" - "Default sound (%1$s)" - "Phone ringtone" - "Vibrate when ringing" - "Ringtone & Vibrate" - "Manage conference call" - "Emergency number" - "Profile photo" - "Camera off" - "via %s" - "Note sent" - "Recent messages" - "Business info" - "%.1f mi away" - "%.1f km away" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opens tomorrow at %s" - "Opens today at %s" - "Closes at %s" - "Closed today at %s" - "Open now" - "Closed now" - "Suspected spam caller" - "Call ended %1$s" - "This is the first time that this number has called you." - "We suspected that this call was from a spammer." - "Block/report spam" - "Add contact" - "Not spam" - diff --git a/InCallUI/res/values-en-rGB/strings.xml b/InCallUI/res/values-en-rGB/strings.xml deleted file mode 100644 index 013aa9685eac96cb59b90617fdd2112c1adedca0..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-en-rGB/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Phone" - "On hold" - "unknown" - "Private number" - "Payphone" - "Conference call" - "Call cut off" - "Speaker" - "Handset earpiece" - "Wired headset" - "Bluetooth" - "Send the following tones?\n" - "Sending tones\n" - "Send" - "yes" - "no" - "Replace wild character with" - "Conference call %s" - "Voicemail number" - "Dialling" - "Redialling" - "Conference call" - "Incoming call" - "Incoming work call" - "Call ended" - "On hold" - "Hanging up" - "In call" - "My number is %s" - "Connecting video" - "Video call" - "Requesting video" - "Can\'t connect video call" - "Video request rejected" - "Your callback number\n %1$s" - "Your emergency callback number\n %1$s" - "Dialling" - "Missed call" - "Missed calls" - "%s missed calls" - "Missed call from %s" - "On-going call" - "Ongoing work call" - "Ongoing Wi-Fi call" - "Ongoing Wi-Fi work call" - "On hold" - "Incoming call" - "Incoming work call" - "Incoming Wi-Fi call" - "Incoming Wi-Fi work call" - "Incoming video call" - "Incoming suspected spam call" - "Incoming video request" - "New voicemail" - "New voicemail (%d)" - "Dial %s" - "Voicemail number unknown" - "No service" - "Selected network (%s) unavailable" - "Answer" - "Hang up" - "In-stream video" - "Voice" - "Accept" - "Dismiss" - "Call back" - "Message" - "Ongoing call on another device" - "Transfer call" - "To place a call, first turn off Aeroplane mode." - "Not registered on network." - "Mobile network not available." - "To place a call, enter a valid number." - "Can\'t call." - "Starting MMI sequence…" - "Service not supported." - "Can\'t switch calls." - "Can\'t separate call." - "Can\'t transfer." - "Can\'t conference." - "Can\'t reject call." - "Can\'t release call(s)." - "SIP call" - "Emergency call" - "Turning on radio…" - "No network. Trying again…" - "Can\'t call. %s is not an emergency number." - "Can\'t call. Dial an emergency number." - "Use keyboard to dial" - "Hold Call" - "Resume Call" - "End Call" - "Show dial pad" - "Hide dial pad" - "Mute" - "Unmute" - "Add call" - "Merge calls" - "Swap" - "Manage calls" - "Manage conference call" - "Conference call" - "Manage" - "Audio" - "Video call" - "Change to voice call" - "Switch camera" - "Turn on camera" - "Turn off camera" - "More options" - "Player Started" - "Player Stopped" - "Camera not ready" - "Camera ready" - "Unknown call session event" - "Service" - "Set up" - "<Not set>" - "Other call settings" - "Calling via %s" - "Incoming via %s" - "contact photo" - "go private" - "select contact" - "Write your own..." - "cancel" - "Send" - "Answer" - "Send SMS" - "Decline" - "Answer as video call" - "Answer as audio call" - "Accept video request" - "Decline video request" - "Accept video transmit request" - "Decline video transmit request" - "Accept video receive request" - "Decline video receive request" - "Slide up for %s." - "Slide left for %s." - "Slide right for %s." - "Slide down for %s." - "Vibrate" - "Vibrate" - "Sound" - "Default sound (%1$s)" - "Phone ringtone" - "Vibrate when ringing" - "Ringtone & Vibrate" - "Manage conference call" - "Emergency number" - "Profile photo" - "Camera off" - "via %s" - "Note sent" - "Recent messages" - "Business info" - "%.1f mi away" - "%.1f km away" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opens tomorrow at %s" - "Opens today at %s" - "Closes at %s" - "Closed today at %s" - "Open now" - "Closed now" - "Suspected spam caller" - "Call ended %1$s" - "This is the first time that this number has called you." - "We suspected that this call was from a spammer." - "Block/report spam" - "Add contact" - "Not spam" - diff --git a/InCallUI/res/values-en-rIN/strings.xml b/InCallUI/res/values-en-rIN/strings.xml deleted file mode 100644 index 013aa9685eac96cb59b90617fdd2112c1adedca0..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-en-rIN/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Phone" - "On hold" - "unknown" - "Private number" - "Payphone" - "Conference call" - "Call cut off" - "Speaker" - "Handset earpiece" - "Wired headset" - "Bluetooth" - "Send the following tones?\n" - "Sending tones\n" - "Send" - "yes" - "no" - "Replace wild character with" - "Conference call %s" - "Voicemail number" - "Dialling" - "Redialling" - "Conference call" - "Incoming call" - "Incoming work call" - "Call ended" - "On hold" - "Hanging up" - "In call" - "My number is %s" - "Connecting video" - "Video call" - "Requesting video" - "Can\'t connect video call" - "Video request rejected" - "Your callback number\n %1$s" - "Your emergency callback number\n %1$s" - "Dialling" - "Missed call" - "Missed calls" - "%s missed calls" - "Missed call from %s" - "On-going call" - "Ongoing work call" - "Ongoing Wi-Fi call" - "Ongoing Wi-Fi work call" - "On hold" - "Incoming call" - "Incoming work call" - "Incoming Wi-Fi call" - "Incoming Wi-Fi work call" - "Incoming video call" - "Incoming suspected spam call" - "Incoming video request" - "New voicemail" - "New voicemail (%d)" - "Dial %s" - "Voicemail number unknown" - "No service" - "Selected network (%s) unavailable" - "Answer" - "Hang up" - "In-stream video" - "Voice" - "Accept" - "Dismiss" - "Call back" - "Message" - "Ongoing call on another device" - "Transfer call" - "To place a call, first turn off Aeroplane mode." - "Not registered on network." - "Mobile network not available." - "To place a call, enter a valid number." - "Can\'t call." - "Starting MMI sequence…" - "Service not supported." - "Can\'t switch calls." - "Can\'t separate call." - "Can\'t transfer." - "Can\'t conference." - "Can\'t reject call." - "Can\'t release call(s)." - "SIP call" - "Emergency call" - "Turning on radio…" - "No network. Trying again…" - "Can\'t call. %s is not an emergency number." - "Can\'t call. Dial an emergency number." - "Use keyboard to dial" - "Hold Call" - "Resume Call" - "End Call" - "Show dial pad" - "Hide dial pad" - "Mute" - "Unmute" - "Add call" - "Merge calls" - "Swap" - "Manage calls" - "Manage conference call" - "Conference call" - "Manage" - "Audio" - "Video call" - "Change to voice call" - "Switch camera" - "Turn on camera" - "Turn off camera" - "More options" - "Player Started" - "Player Stopped" - "Camera not ready" - "Camera ready" - "Unknown call session event" - "Service" - "Set up" - "<Not set>" - "Other call settings" - "Calling via %s" - "Incoming via %s" - "contact photo" - "go private" - "select contact" - "Write your own..." - "cancel" - "Send" - "Answer" - "Send SMS" - "Decline" - "Answer as video call" - "Answer as audio call" - "Accept video request" - "Decline video request" - "Accept video transmit request" - "Decline video transmit request" - "Accept video receive request" - "Decline video receive request" - "Slide up for %s." - "Slide left for %s." - "Slide right for %s." - "Slide down for %s." - "Vibrate" - "Vibrate" - "Sound" - "Default sound (%1$s)" - "Phone ringtone" - "Vibrate when ringing" - "Ringtone & Vibrate" - "Manage conference call" - "Emergency number" - "Profile photo" - "Camera off" - "via %s" - "Note sent" - "Recent messages" - "Business info" - "%.1f mi away" - "%.1f km away" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opens tomorrow at %s" - "Opens today at %s" - "Closes at %s" - "Closed today at %s" - "Open now" - "Closed now" - "Suspected spam caller" - "Call ended %1$s" - "This is the first time that this number has called you." - "We suspected that this call was from a spammer." - "Block/report spam" - "Add contact" - "Not spam" - diff --git a/InCallUI/res/values-es-rUS/strings.xml b/InCallUI/res/values-es-rUS/strings.xml deleted file mode 100644 index 915c90779e1dadf39f7282c3a736cbcec00b98f2..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-es-rUS/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Teléfono" - "En espera" - "Desconocido" - "Número privado" - "Teléfono pago" - "Llamada en conferencia" - "Se interrumpió la llamada" - "Altavoz" - "Auricular del dispositivo" - "Auriculares con cable" - "Bluetooth" - "¿Deseas enviar los siguientes tonos?\n" - "Enviando tonos\n" - "Enviar" - "Sí" - "No" - "Reemplazar el carácter comodín con" - "Llamada en conferencia: %s" - "Número de buzón de voz" - "Marcando" - "Volviendo a marcar" - "Llamada en conferencia" - "Llamada entrante" - "Llamada entrante: trabajo" - "Llamada finalizada" - "En espera" - "Colgando" - "En llamada" - "Mi número es %s" - "Conectando video" - "Videollamada" - "Solicitando video" - "No se puede conectar la videollamada" - "Se rechazó la solicitud de videollamada" - "Número de devolución de llamada\n %1$s" - "Número de devolución de llamada de emergencia\n %1$s" - "Marcando" - "Llamada perdida" - "Llamadas perdidas" - "%s llamadas perdidas" - "Llamada perdida de %s" - "Llamada en curso" - "Llamada en curso: trabajo" - "Llamada Wi-Fi en curso" - "Llamada Wi-Fi en curso: trabajo" - "En espera" - "Llamada entrante" - "Llamada entrante: trabajo" - "Llamada Wi-Fi entrante" - "Llamada Wi-Fi entrante: trabajo" - "Videollamada entrante" - "Posible llamada entrante de spam" - "Solicitud de videollamada entrante" - "Nuevo mensaje de buzón de voz" - "Buzón de voz nuevo (%d)" - "Marcar %s" - "Número de buzón de voz desconocido" - "Sin servicio" - "La red seleccionada (%s) no está disponible" - "Responder" - "Colgar" - "Video" - "Voz" - "Aceptar" - "Descartar" - "Llamar" - "Mensaje" - "Llamada en curso en otro dispositivo" - "Transferir llamada" - "Para realizar una llamada, primero debes desactivar el modo de avión." - "No está registrado en la red." - "La red móvil no está disponible." - "Para realizar una llamada, ingresa un número válido." - "No se puede realizar la llamada." - "Iniciando la secuencia de MMI…" - "El servicio no es compatible." - "No se pueden cambiar las llamadas." - "No se puede desviar la llamada." - "No se puede transferir." - "No se puede realizar la conferencia." - "No se puede rechazar la llamada." - "No se pueden liberar las llamadas." - "Llamada SIP" - "Llamada de emergencia" - "Encendiendo radio…" - "No hay servicio. Vuelve a intentarlo…" - "No se puede realizar la llamada. %s no es un número de emergencia." - "No se puede realizar la llamada. Marca un número de emergencia." - "Usar teclado para marcar" - "Retener llamada" - "Reanudar llamada" - "Finalizar llamada" - "Mostrar teclado" - "Ocultar teclado" - "Silenciar" - "Dejar de silenciar" - "Agregar llamada" - "Combinar llamadas" - "Cambiar" - "Administrar llamadas" - "Administrar conferencia" - "Llamada en conferencia" - "Administrar" - "Audio" - "Video" - "Cambiar a llamada de voz" - "Cambiar cámara" - "Activar la cámara" - "Desactivar la cámara" - "Más opciones" - "Se inició el reproductor" - "Se detuvo el reproductor" - "La cámara no está lista" - "Cámara lista" - "Evento de sesión de llamada desconocido" - "Servicio" - "Configuración" - "<Sin configurar>" - "Otras opciones de llamada" - "Llamada por medio de %s" - "Entrantes por medio de %s" - "foto de contacto" - "pasar a modo privado" - "seleccionar contacto" - "Escribe tu propia respuesta…" - "Cancelar" - "Enviar" - "Responder" - "Enviar SMS" - "Rechazar" - "Responder como videollamada" - "Responder como llamada de audio" - "Aceptar solicitud de videollamada" - "Rechazar solicitud de videollamada" - "Aceptar solicitud de transmisión de videollamada" - "Rechazar solicitud de transmisión de videollamada" - "Aceptar solicitud de recepción de videollamada" - "Rechazar solicitud de recepción de videollamada" - "Desliza el dedo hacia arriba para %s." - "Desliza el dedo hacia la izquierda para %s." - "Desliza el dedo hacia la derecha para %s." - "Desliza el dedo hacia abajo para %s." - "Vibrar" - "Vibrar" - "Sonido" - "Sonido predeterminado (%1$s)" - "Tono del teléfono" - "Vibrar al sonar" - "Tono y vibración" - "Administrar llamada en conferencia" - "Número de emergencia" - "Foto de perfil" - "Cámara desactivada" - "del %s" - "Se envió la nota" - "Mensajes recientes" - "Información de la empresa" - "A %.1f mi" - "A %.1f km" - "%1$s, %2$s" - "De %1$s a %2$s" - "%1$s y %2$s" - "Abre mañana a la hora %s" - "Abre hoy a la hora %s" - "Cierra a la hora %s" - "Cerró hoy a la hora %s" - "Abierto ahora" - "Cerrado ahora" - "Posible spam" - "Llamada finalizada %1$s" - "Es la primera vez que te llaman desde este número." - "Sospechamos que esta llamada era spam." - "Bloquear/denunciar" - "Agregar contacto" - "No es spam" - diff --git a/InCallUI/res/values-es/strings.xml b/InCallUI/res/values-es/strings.xml deleted file mode 100644 index ab570d593b3628fa537bc2053024f3490835e376..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-es/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Teléfono" - "En espera" - "Desconocida" - "Número privado" - "Teléfono público" - "Conferencia" - "Llamada perdida" - "Altavoz" - "Auricular" - "Auriculares con cable" - "Bluetooth" - "¿Quieres enviar los siguientes tonos?\n" - "Enviando tonos\n" - "Enviar" - "Sí" - "No" - "Sustituir el carácter comodín por" - "Conferencia %s" - "Número del mensaje de voz" - "Llamando" - "Llamando otra vez" - "Conferencia" - "Llamada entrante" - "Llamada trabajo entrante" - "Llamada finalizada" - "En espera" - "Colgando" - "Llamada entrante" - "Mi número es el %s" - "Conectando videollamada" - "Videollamada" - "Solicitando videollamada" - "No se puede establecer la videollamada" - "Solicitud de videollamada rechazada" - "Tu número de devolución de llamada\n %1$s" - "Tu número de devolución de llamada de emergencia\n %1$s" - "Llamando" - "Llamada perdida" - "Llamadas perdidas" - "%s llamadas perdidas" - "Llamada perdida de %s" - "Llamada en curso" - "Llamada de trabajo en curso" - "Llamada Wi-Fi en curso" - "Llamada Wi-Fi de trabajo en curso" - "En espera" - "Llamada entrante" - "Llamada de trabajo entrante" - "Llamada Wi-Fi entrante" - "Llamada Wi-Fi de trabajo entrante" - "Videollamada entrante" - "Llamada entrante sospechosa de spam" - "Solicitud de videollamada entrante" - "Nuevo mensaje de voz" - "Nuevo mensaje de voz (%d)" - "Marcar %s" - "Número del mensaje de voz desconocido" - "Sin servicio" - "La red seleccionada (%s) no está disponible" - "Responder" - "Colgar" - "Videollamada" - "Voz" - "Aceptar" - "Rechazar" - "Llamar" - "Mensaje" - "Llamada activa en otro dispositivo" - "Transferir llamada" - "Para realizar una llamada, primero debes desactivar el modo avión." - "No estás registrado en la red." - "La red móvil no está disponible." - "Para realizar una llamada, introduce un número válido." - "No se puede establecer la llamada." - "Iniciando secuencia MMI..." - "Servicio no admitido." - "No se pueden intercambiar llamadas." - "No se pueden separar llamadas." - "No se puede transferir." - "No se puede establecer la conferencia." - "No se puede rechazar la llamada." - "No se pueden hacer llamadas." - "Llamada SIP" - "Llamada de emergencia" - "Activando señal móvil…" - "Sin servicio. Reintentado…" - "No se puede establecer la llamada. %s no es un número de emergencia." - "No se puede establecer la llamada. Marca un número de emergencia." - "Usa el teclado para marcar" - "Retener llamada" - "Seguir con la llamada" - "Finalizar llamada" - "Mostrar teclado" - "Ocultar teclado" - "Silenciar" - "Activar sonido" - "Añadir llamada" - "Llamada a tres" - "Cambiar" - "Administrar llamadas" - "Administrar conferencia" - "Teleconferencia" - "Gestionar" - "Audio" - "Videollamada" - "Cambiar a llamada de voz" - "Cambiar cámara" - "Activar cámara" - "Desactivar cámara" - "Más opciones" - "Reproductor iniciado" - "Reproductor detenido" - "Cámara no preparada" - "Cámara preparada" - "Evento de sesión de llamada desconocido" - "Servicio" - "Configuración" - "<No definido>" - "Otra configuración de llamada" - "Llamada a través de %s" - "Recibidas a través de %s" - "foto de contacto" - "llamada privada" - "seleccionar contacto" - "Escribe tu propia respuesta..." - "Cancelar" - "Enviar" - "Responder" - "Enviar SMS" - "Rechazar" - "Responder como videollamada" - "Responder como llamada de audio" - "Aceptar solicitud de videollamada" - "Rechazar solicitud de videollamada" - "Aceptar solicitud de transmisión de videollamada" - "Rechazar solicitud de transmisión de videollamada" - "Aceptar solicitud de recepción de videollamada" - "Rechazar solicitud de recepción de videollamada" - "Desliza el dedo hacia arriba para %s." - "Desliza el dedo hacia la izquierda para %s." - "Desliza el dedo hacia la derecha para %s." - "Desliza el dedo hacia abajo para %s." - "Vibrar" - "Vibrar" - "Sonido" - "Sonido predeterminado (%1$s)" - "Tono de llamada del teléfono" - "Vibrar al sonar" - "Tono de llamada y vibración" - "Administrar videollamada" - "Número de emergencia" - "Foto de perfil" - "Cámara apagada" - "a través de %s" - "Nota enviada" - "Mensajes recientes" - "Información de la empresa" - "A %.1f mi" - "A %.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre mañana a las %s" - "Abre hoy a las %s" - "Cierra a las %s" - "Cerrado hoy a las %s" - "Abierto ahora" - "Cerrado ahora" - "Sospechoso de spam" - "Llamada de %1$s terminada" - "Es la primera vez que recibes una llamada de este número." - "Sospechábamos que esta llamada era de spam." - "Bloquear / Marcar como spam" - "Añadir contacto" - "No es spam" - diff --git a/InCallUI/res/values-et/strings.xml b/InCallUI/res/values-et/strings.xml deleted file mode 100644 index 5d2a0d8b0be89ad04dfb8fc8bec8818f2441385b..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-et/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Ootel" - "Tundmatu" - "Eranumber" - "Telefoniautomaat" - "Konverentskõne" - "Kõne katkes" - "Kõlar" - "Käsitelefoni kuular" - "Juhtmega peakomplekt" - "Bluetooth" - "Kas saata järgmised toonid?\n" - "Toonide saatmine\n" - "Saada" - "Jah" - "Ei" - "Asenda metamärk üksusega" - "Konverentskõne %s" - "Kõneposti number" - "Valimine" - "Uuesti valimine" - "Konverentskõne" - "Sissetulev kõne" - "Sissetulev töökõne" - "Kõne lõpetati" - "Ootel" - "Lõpetamine" - "Kõne on pooleli" - "Minu number on %s" - "Video ühendamine" - "Videokõne" - "Video taotlemine" - "Videokõnet ei õnnestu ühendada" - "Videokõne taotlus lükati tagasi" - "Teie tagasihelistamise number\n%1$s" - "Teie hädaabikõne tagasihelistamise number\n%1$s" - "Valimine" - "Vastamata kõne" - "Vastamata kõned" - "%s vastamata kõnet" - "Vastamata kõne helistajalt %s" - "Käimasolev kõne" - "Käimasolev töökõne" - "Käimasolev WiFi-kõne" - "Käimasolev töökõne WiFi kaudu" - "Ootel" - "Sissetulev kõne" - "Sissetulev töökõne" - "Sissetulev WiFi-kõne" - "Sissetulev töökõne WiFi kaudu" - "Sissetulev videokõne" - "Arvatav sissetulev rämpskõne" - "Sissetulev videokõne taotlus" - "Uus kõnepostisõnum" - "Uus kõnepostisõnum (%d)" - "Valige %s" - "Kõnepostinumber on tundmatu" - "Levi puudub" - "Valitud võrk (%s) pole saadaval" - "Vasta" - "Lõpeta kõne" - "Videokõne" - "Häälkõne" - "Nõustu" - "Loobu" - "Helista tagasi" - "Saada sõnum" - "Pooleliolev kõne teise seadmes" - "Kõne ülekandmine" - "Helistamiseks lülitage esmalt lennukirežiim välja." - "Ei ole võrgus registreeritud." - "Mobiilsidevõrk pole saadaval." - "Helistamiseks sisestage kehtiv number." - "Ei saa helistada." - "MMI-jada alustamine …" - "Teenust ei toetata." - "Kõnesid ei saa vahetada." - "Kõnet ei saa eraldada." - "Ei saa üle kanda." - "Konverentskõnet ei saa pidada." - "Kõnet ei saa tagasi lükata." - "Kõnesid ei saa vabastada." - "SIP-kõne" - "Hädaabikõne" - "Raadioside sisselülitamine …" - "Levi puudub. Uuesti proovimine …" - "Ei saa helistada. %s ei ole hädaabinumber." - "Ei saa helistada. Valige hädaabinumber." - "Kasutage valimiseks klaviatuuri" - "Kõne ootele" - "Jätka kõnet" - "Lõpeta kõne" - "Kuva valimisklahvistik" - "Peida valimisklahvistik" - "Vaigista" - "Tühista vaigistus" - "Lisa kõne" - "Ühenda kõned" - "Vaheta" - "Halda kõnesid" - "Halda konverentskõnet" - "Konverentskõne" - "Halda" - "Heli" - "Videokõne" - "Mine üle häälkõnele" - "Vaheta kaamerat" - "Lülita kaamera sisse" - "Lülita kaamera välja" - "Rohkem valikuid" - "Pleier käivitati" - "Pleier peatati" - "Kaamera pole valmis" - "Kaamera on valmis" - "Tundmatu kõneseansisündmus" - "Teenus" - "Seadistamine" - "<Määramata>" - "Muud kõneseaded" - "Kõne edastab %s" - "Sissetulev kõne teenusepakkuja %s kaudu" - "kontakti foto" - "aktiveeri privaatne kõne" - "vali kontakt" - "Kirjutage ise …" - "Tühista" - "Saada" - "Vastamine" - "Saada SMS" - "Tagasilükkamine" - "Vastamine videokõnena" - "Vastamine helikõnena" - "Video taotluse aktsepteerimine" - "Video taotluse tagasilükkamine" - "Video edastamise taotluse aktsepteerimine" - "Video edastamise taotluse tagasilükkamine" - "Video vastuvõtmise taotluse aktsepteerimine" - "Video vastuvõtmise taotluse tagasilükkamine" - "Lohistage üles: %s." - "Lohistage vasakule: %s." - "Lohistage paremale: %s." - "Lohistage alla: %s." - "Vibreerimine" - "Vibreerimine" - "Heli" - "Vaikeheli (%1$s)" - "Telefonihelin" - "Vibreerimine helina ajal" - "Helin ja vibratsioon" - "Konverentskõne haldamine" - "Hädaabinumber" - "Profiilifoto" - "Kaamera on välja lülitatud" - "numbri %s kaudu" - "Märkus on saadetud" - "Hiljutised sõnumid" - "Ettevõtte teave" - "%.1f miili kaugusel" - "%.1f km kaugusel" - "%1$s, %2$s" - "%1$s kuni %2$s" - "%1$s, %2$s" - "Avatakse homme kell %s" - "Avatakse täna kell %s" - "Suletakse kell %s" - "Suleti täna kell %s" - "Praegu avatud" - "Praegu suletud" - "Arvatav rämpskõne" - "Kõne lõppes: %1$s" - "Teile helistati sellelt numbrilt esimest korda." - "Kahtlustasime, et see võis olla rämpskõne." - "Blokeeri / teavita rämpskõnest" - "Lisa kontakt" - "Pole rämpskõne" - diff --git a/InCallUI/res/values-eu/strings.xml b/InCallUI/res/values-eu/strings.xml deleted file mode 100644 index c300c47cdb80f2573a508604ebd7e0a974b67bde..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-eu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefonoa" - "Zain" - "Ezezaguna" - "Zenbaki pribatua" - "Telefono publikoa" - "Konferentzia-deia" - "Eten egin da deia" - "Bozgorailua" - "Aurikularrak" - "Kabledun entzungailua" - "Bluetooth konexioa" - "Tonu hauek bidali nahi dituzu?\n" - "Tonuak bidaltzen\n" - "Bidali" - "Bai" - "Ez" - "Ordeztu komodina honekin:" - "Konferentzia-deiaren ordua: %s" - "Erantzungailuaren zenbakia" - "Deitzen" - "Berriro markatzen" - "Konferentzia-deia" - "Dei bat jaso duzu" - "Laneko dei bat jaso duzu" - "Amaitu da deia" - "Zain" - "Deia amaitzen" - "Deia abian" - "Nire zenbakia %s da" - "Bideoa konektatzen" - "Bideo-deia" - "Bideo-deia eskatzen" - "Ezin da konektatu bideo-deia" - "Baztertu egin da bideo-deia egiteko eskaera" - "Dei-erantzunetarako zenbakia:\n %1$s" - "Larrialdi-dei bidez erantzuteko zenbakia:\n %1$s" - "Deitzen" - "Dei bat galdu duzu" - "Dei batzuk galdu dituzu" - "%s dei galdu dituzu" - "Deitzaile honen dei bat galdu duzu: %s" - "Deia abian da" - "Laneko dei bat abian da" - "Wi-Fi bidezko deia abian da" - "Wi-Fi bidezko laneko dei bat abian da" - "Zain" - "Dei bat jaso duzu" - "Laneko dei bat jaso duzu" - "Wi-Fi bidezko dei bat jaso duzu" - "Wi-Fi bidezko laneko dei bat jaso duzu" - "Bideo-dei bat jaso duzu" - "Ustezko spam-deia jaso duzu" - "Bideo-dei bat egiteko eskaera bat jaso duzu" - "Ahots-mezu berria" - "Ahots-mezu berriak (%d)" - "Markatu %s" - "Erantzungailuaren zenbakia ezezaguna da" - "Ez dago zerbitzurik" - "Hautatutako sarea (%s) ez dago erabilgarri" - "Erantzun" - "Amaitu deia" - "Bideo-deia" - "Ahots-deia" - "Onartu" - "Baztertu" - "Itzuli deia" - "Bidali SMSa" - "Dei bat abian da beste gailu batean" - "Transferitu deia" - "Deitzeko, desaktibatu hegaldi modua." - "Ez dago sarean erregistratuta." - "Sare mugikorra ez dago erabilgarri." - "Deitzeko, idatzi balio duen zenbaki bat." - "Ezin da deitu." - "MMI sekuentzia hasten…" - "Ez da onartzen zerbitzua." - "Ezin aldatu beste dei batera." - "Ezin da bereizi deia." - "Ezin da transferitu." - "Ezin da egin konferentzia-deia." - "Ezin da baztertu deia." - "Ezin dira amaitu deiak." - "SIP deia" - "Larrialdi-deia" - "Irratia pizten…" - "Ez dago zerbitzurik. Berriro saiatzen…" - "Ezin da deitu. %s ez da larrialdietarako zenbakia." - "Ezin da deitu. Markatu larrialdietarako zenbakia." - "Erabili teklatua markatzeko" - "Utzi deia zain" - "Berrekin deiari" - "Amaitu deia" - "Erakutsi markagailua" - "Ezkutatu markagailua" - "Desaktibatu audioa" - "Aktibatu audioa" - "Gehitu deia" - "Bateratu deiak" - "Aldatu" - "Kudeatu deiak" - "Kudeatu konferentzia-deia" - "Konferentzia-deia" - "Kudeatu" - "Audioa" - "Bideo-deia" - "Aldatu ahots-deira" - "Aldatu kamera" - "Aktibatu kamera" - "Desaktibatu kamera" - "Aukera gehiago" - "Abian da erreproduzigailua" - "Gelditu da erreproduzigailua" - "Ez dago prest kamera" - "Prest dago kamera" - "Dei-saioko gertaera ezezaguna" - "Zerbitzua" - "Konfigurazioa" - "<Ezarri gabe>" - "Deien beste ezarpen batzuk" - "%s bidez deitzen" - "%s bidez jasotzen" - "kontaktuaren argazkia" - "bihurtu pribatu" - "hautatu kontaktua" - "Idatzi zeure erantzuna…" - "Utzi" - "Bidali" - "Erantzun" - "Bidali SMS mezua" - "Baztertu" - "Erantzun bideo-dei moduan" - "Erantzun audio-dei moduan" - "Onartu bideo-deia egiteko eskaera" - "Baztertu bideo-deia egiteko eskaera" - "Onartu bideoa transmititzeko eskaera" - "Baztertu bideoa transmititzeko eskaera" - "Onartu bideo-deia jasotzeko eskaera" - "Baztertu bideo-deia jasotzeko eskaera" - "Lerratu gora hau egiteko: %s." - "Lerratu ezkerrera hau egiteko: %s." - "Lerratu eskuinera hau egiteko: %s." - "Lerratu behera hau egiteko: %s." - "Dardara" - "Dardara" - "Soinua" - "Soinu lehenetsia (%1$s)" - "Telefonoaren tonua" - "Egin dar-dar tonuak jotzean" - "Tonua eta dardara" - "Kudeatu konferentzia-deia" - "Larrialdietarako zenbakia" - "Profileko argazkia" - "Desaktibatuta dago kamera" - "%s zenbakitik" - "Bidali da oharra" - "Azken mezuak" - "Enpresaren informazioa" - "Hemendik %.1f miliara" - "Hemendik %.1f km-ra" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "%s da biharko irekitze-ordua" - "%s da gaurko irekitze-ordua" - "%s da ixte-ordua" - "%s da gaurko itxiera-ordua" - "Irekita dago" - "Itxita dago" - "Ustezko spam-deitzailea" - "Deiaren amaiera: %1$s" - "Zenbaki honek deitu dizun lehen aldia izan da." - "Spam-igorle baten deia izan dela susmatu dugu." - "Salatu spama dela" - "Gehitu kontaktua" - "Ez da spama" - diff --git a/InCallUI/res/values-fa/strings.xml b/InCallUI/res/values-fa/strings.xml deleted file mode 100644 index f96b8956a170396affdada914b9a25faeac53c75..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-fa/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "تلفن" - "در انتظار" - "نامشخص" - "شماره خصوصی" - "تلفن عمومی" - "تماس کنفرانسی" - "تماس قطع شد" - "بلندگو" - "گوشی" - "هدست سیمی" - "بلوتوث" - "شماره‌های بعدی ارسال شود؟\n" - "تون‌های ارسالی\n" - "ارسال" - "بله" - "نه" - "جایگزینی نویسه عمومی با" - "تماس کنفرانسی %s" - "شماره پست صوتی" - "شماره‌گیری" - "درحال شماره‌گیری مجدد" - "تماس کنفرانسی" - "تماس ورودی" - "تماس کاری ورودی" - "تماس پایان یافت" - "در انتظار" - "قطع تماس" - "درحال تماس" - "شماره من %s است" - "درحال برقراری تماس ویدئویی" - "تماس ویدئویی" - "درحال درخواست تماس ویدئویی" - "برقراری تماس ویدئویی ممکن نیست" - "درخواست تماس ویدئویی رد شد" - "شماره پاسخ تماس شما\n %1$s" - "شماره پاسخ تماس اضطراری شما\n %1$s" - "شماره‌گیری" - "تماس بی‌پاسخ" - "تماس بی‌پاسخ" - "%s تماس بی‌پاسخ" - "تماس بی‌پاسخ از %s" - "تماس درحال انجام" - "تماس کاری درحال انجام" - "‏تماس درحال انجام ازطریق Wi-Fi" - "‏تماس کاری Wi-Fi درحال انجام" - "در انتظار" - "تماس ورودی" - "تماس کاری ورودی" - "‏تماس Wi-Fi ورودی" - "‏تماس کاری Wi-Fi ورودی" - "تماس ویدئویی ورودی" - "تماس هرزنامه احتمالی ورودی" - "درخواست تماس ویدئویی ورودی" - "پست صوتی جدید" - "پست صوتی جدید (%d)" - "شماره‌گیری %s" - "شماره پست صوتی ناشناس" - "بدون سرویس" - "شبکه انتخابی (%s) قابل دسترس نیست" - "پاسخ" - "پایان تماس" - "ویدئو" - "صدا" - "پذیرفتن" - "نپذیرفتن" - "پاسخ تماس" - "پیام" - "تماس در حال انجام در دستگاهی دیگر" - "انتقال تماس" - "برای برقراری تماس، ابتدا حالت هواپیما را خاموش کنید." - "در شبکه ثبت نشده است." - "شبکه تلفن همراه در دسترس نیست." - "برای برقراری تماس، شماره معتبری وارد کنید." - "تماس ممکن نیست." - "‏شروع ترتیب MMI…" - "سرویس پشتیبانی نمی‌شود." - "جابه‌جایی بین تماس‌ها ممکن نیست." - "جدا کردن تماس ممکن نیست." - "انتقال ممکن نیست." - "تماس کنفرانسی ممکن نیست." - "رد کردن تماس ممکن نیست." - "آزاد کردن تماس(ها) ممکن نیست." - "‏تماس SIP" - "تماس اضطراری" - "درحال روشن کردن رادیو…‏" - "سرویسی در دسترس نیست. درحال تلاش مجدد…‏" - "تماس ممکن نیست. %s شماره اضطراری نیست." - "تماس ممکن نیست. فقط شماره اضطراری." - "استفاده از صفحه‌کلید برای شماره‌گیری" - "در انتظار گذاشتن تماس" - "ازسرگیری تماس" - "پایان تماس" - "نمایش صفحه شماره‌گیری" - "پنهان کردن صفحه شماره‌گیری" - "بی‌صدا کردن" - "لغو نادیده گرفتن" - "افزودن تماس" - "ادغام تماس‌ها" - "تعویض" - "مدیریت تماس‌ها" - "مدیریت تماس کنفرانسی" - "تماس کنفرانسی" - "مدیریت" - "صوتی" - "تماس ویدئویی" - "تغییر به تماس صوتی" - "تعویض دوربین" - "روشن کردن دوربین" - "خاموش کردن دوربین" - "گزینه‌های بیشتر" - "پخش‌کننده راه‌اندازی شد" - "پخش‌کننده متوقف شد" - "دوربین آماده نیست" - "دوربین آماده است" - "رویداد جلسه تماس ناشناس" - "سرویس" - "راه‌اندازی" - "‏<تنظیم نشده>" - "سایر تنظیمات تماس" - "تماس با %s" - "تماس‌های ورودی ازطریق %s" - "عکس مخاطب" - "رفتن به حالت خصوصی" - "انتخاب مخاطب" - "پیام خودتان را بنویسید..." - "لغو" - "ارسال" - "پاسخ" - "ارسال پیامک" - "رد کردن" - "پاسخ به‌صورت تماس ویدئویی" - "پاسخ به‌صورت تماس صوتی" - "پذیرفتن درخواست تماس ویدئویی" - "نپذیرفتن درخواست تماس ویدئویی" - "پذیرفتن درخواست مخابره ویدئویی" - "نپذیرفتن درخواست مخابره ویدئویی" - "پذیرفتن درخواست دریافت ویدئویی" - "نپذیرفتن درخواست دریافت ویدئویی" - "برای %s به بالا بلغزانید." - "برای %s به چپ بلغزانید." - "برای %s به راست بلغزانید." - "برای %s به پایین بلغزانید." - "لرزش" - "لرزش" - "صدا" - "صدای پیش‌فرض (%1$s)" - "آهنگ زنگ تلفن" - "لرزش هنگام زنگ زدن" - "آهنگ‌ زنگ و لرزش" - "مدیریت تماس کنفرانسی" - "شماره اضطراری" - "عکس نمایه" - "دوربین خاموش" - "ازطریق %s" - "یادداشت ارسال شد" - "پیام‌های جدید" - "اطلاعات کسب و کار" - "%.1f مایل فاصله" - "%.1f کیلومتر فاصله" - "%1$s،‏ %2$s" - "%1$s تا %2$s" - "%1$s،‏ %2$s" - "فردا ساعت %s باز می‌شود" - "امروز ساعت %s باز می‌شود" - "ساعت %s بسته می‌شود" - "امروز ساعت %s بسته شد" - "اکنون باز است" - "اکنون بسته است" - "تماس‌گیرنده هرزنامه احتمالی" - "‏تماس به پایان رسید %1$s" - "این اولین بار است که این شماره با شما تماس گرفته است." - "ما به این تماس شک کردیم و احساس کردیم که ممکن است کلاهبردار باشد." - "مسدود کردن/گزارش دادن هرزنامه" - "افزودن مخاطب" - "هرزنامه نیست" - diff --git a/InCallUI/res/values-fi/strings.xml b/InCallUI/res/values-fi/strings.xml deleted file mode 100644 index 7553e7c251acdd4d4998601b63ae3ba310d17f0f..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-fi/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Puhelin" - "Pidossa" - "Tuntematon" - "Yksityinen numero" - "Maksupuhelin" - "Puhelinneuvottelu" - "Puhelu katkaistiin." - "Kaiutin" - "Puhelimen kaiutin" - "Kuulokemikrofoni" - "Bluetooth" - "Lähetetäänkö seuraavat äänet?\n" - "Lähetetään ääniä\n" - "Lähetä" - "Kyllä" - "Ei" - "Muuta jokerimerkiksi" - "Puhelinneuvottelu %s" - "Puhelinvastaajan numero" - "Soitetaan" - "Soitetaan uudelleen" - "Puhelinneuvottelu" - "Saapuva puhelu" - "Saapuva työpuhelu" - "Puhelu päättyi" - "Pidossa" - "Katkaistaan" - "Puhelu käynnissä" - "Numeroni on %s" - "Avataan videoyhteys" - "Videopuhelu" - "Videota pyydetään" - "Videopuhelua ei voi soittaa" - "Videopyyntö hylättiin" - "Takaisinsoittonumero:\n %1$s" - "Hätäpuhelujen takaisinsoittonumero:\n %1$s" - "Soitetaan" - "Vastaamaton puhelu" - "Vastaamattomia puheluita" - "%s vastaamatonta puhelua" - "Vastaamaton puhelu: %s" - "Käynnissä oleva puhelu" - "Käynnissä oleva työpuhelu" - "Käynnissä oleva Wi-Fi-puhelu" - "Käynnissä oleva Wi-Fi-työpuhelu" - "Pidossa" - "Saapuva puhelu" - "Saapuva työpuhelu" - "Saapuva Wi-Fi-puhelu" - "Saapuva Wi-Fi-työpuhelu" - "Saapuva videopuhelu" - "Tämä puhelu saattaa olla häirikköpuhelu." - "Saapuva videopyyntö" - "Uusi vastaajaviesti" - "Uusia vastaajaviestejä (%d)" - "Soita: %s" - "Puhelinvastaajan numero on tuntematon." - "Ei yhteyttä" - "Valittu verkko (%s) ei ole käytettävissä." - "Vastaa" - "Katkaise" - "Videopuhelu" - "Äänipuhelu" - "Hyväksy" - "Hylkää" - "Soita" - "Viesti" - "Puhelu on kesken toisella laitteella." - "Siirrä puhelu" - "Poista lentokonetila käytöstä ennen puhelun soittamista." - "Ei rekisteröity verkkoon" - "Matkapuhelinverkko ei ole käytettävissä." - "Soita antamalla kelvollinen numero." - "Puhelua ei voi soittaa." - "Aloitetaan MMI-koodisekvenssiä…" - "Yhteyttä ei tueta." - "Puhelua ei voi vaihtaa." - "Puhelua ei voi erottaa." - "Puhelua ei voi siirtää." - "Puheluja ei voi yhdistää." - "Puhelua ei voi hylätä." - "Puheluja ei voi katkaista." - "SIP-puhelu" - "Hätäpuhelu" - "Käynnistetään radiota…" - "Ei yhteyttä. Yritetään uudelleen…" - "Puhelua ei voi soittaa. %s ei ole hätänumero." - "Puhelua ei voi soittaa. Valitse hätänumero." - "Valitse numero näppäimistöllä." - "Aseta puhelu pitoon" - "Jatka puhelua" - "Lopeta puhelu" - "Avaa näppäimistö" - "Piilota näppäimistö" - "Mykistä" - "Poista mykistys" - "Lisää puhelu" - "Yhdistä puhelut" - "Vaihda" - "Hallinnoi puheluja" - "Hallinnoi puhelinneuvottelua" - "Puhelinneuvottelu" - "Hallinnoi" - "Ääni" - "Video" - "Muuta äänipuheluksi" - "Vaihda kameraa" - "Käynnistä kamera" - "Sammuta kamera" - "Lisäasetukset" - "Soitin käynnistettiin." - "Soitin pysäytettiin." - "Kamera ei ole valmis." - "Kamera on valmis." - "Tuntematon puheluistunnon tapahtuma" - "Palveluntarjoaja" - "Määritys" - "<Ei määritetty>" - "Muut puheluasetukset" - "Käytetään operaattoria %s" - "Saapuva puhelu (%s)" - "Yhteystiedon kuva" - "Muuta yksityiseksi." - "Valitse yhteystieto." - "Kirjoita oma…" - "Peruuta" - "Lähetä" - "Vastaa." - "Lähetä tekstiviesti." - "Hylkää." - "Vastaa ja aloita videopuhelu." - "Vastaa ja aloita äänipuhelu." - "Hyväksy videopyyntö." - "Hylkää videopyyntö." - "Hyväksy videon lähetyspyyntö." - "Hylkää videon lähetyspyyntö." - "Hyväksy videon vastaanottopyyntö." - "Hylkää videon vastaanottopyyntö." - "Valitse %s liu\'uttamalla ylös." - "Valitse %s liu\'uttamalla vasemmalle." - "Valitse %s liu\'uttamalla oikealle." - "Valitse %s liu\'uttamalla alas." - "Värinä" - "Värinä" - "Ääni" - "Oletusääni (%1$s)" - "Puhelimen soittoääni" - "Käytä värinää, kun puhelin soi" - "Soittoääni ja värinä" - "Hallinnoi puhelinneuvottelua" - "Hätänumero" - "Profiilikuva" - "Kamera on pois käytöstä." - "nron %s kautta" - "Muistiinpano lähetettiin." - "Viimeisimmät viestit" - "Yrityksen tiedot" - "Etäisyys: %.1f mailia" - "Etäisyys: %.1f kilometriä" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Avataan huomenna kello %s" - "Avataan tänään kello %s" - "Suljetaan tänään kello %s" - "Suljettiin tänään kello %s" - "Avoinna nyt" - "Suljettu nyt" - "Häirikkösoittaja" - "Puhelu loppui %1$s" - "Tämä oli ensimmäinen kerta, kun tästä numerosta soitettiin sinulle." - "Epäilemme, että tämä puhelu tuli häirikkösoittajalta." - "Estä / ilmoita" - "Lisää yhteystieto" - "Ei häirikkösoittaja" - diff --git a/InCallUI/res/values-fr-rCA/strings.xml b/InCallUI/res/values-fr-rCA/strings.xml deleted file mode 100644 index 2980646e884eeb72851649526a1c05b569f7e94f..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-fr-rCA/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Téléphone" - "En attente" - "Inconnue" - "Numéro privé" - "Cabine téléphonique" - "Conférence téléphonique" - "L\'appel a été interrompu" - "Haut-parleur" - "Écouteur du combiné" - "Écouteurs à fil" - "Bluetooth" - "Envoyer les tonalités suivantes?\n" - "Envoi des tonalités\n" - "Envoyer" - "Oui" - "Non" - "Remplacer le caractère générique par" - "Conférence téléphonique %s" - "Numéro de messagerie vocale" - "Composition..." - "Recomposition en cours..." - "Conférence téléphonique" - "Appel entrant" - "Appel entrant - travail" - "Appel terminé" - "En attente" - "Fin de l\'appel" - "En cours d\'appel" - "Mon numéro est le %s" - "Connexion de la vidéo en cours…" - "Appel vidéo" - "Demande de vidéo en cours" - "Impossible de connecter l\'appel vidéo" - "Demande vidéo refusée" - "Votre numéro de rappel :\n %1$s" - "Votre numéro de rappel d\'urgence :\n %1$s" - "Composition en cours..." - "Appel manqué" - "Appels manqués" - "%s appels manqués" - "Appel manqué de %s" - "Appel en cours" - "Appel en cours - travail" - "Appel Wi-Fi en cours" - "Appel Wi-Fi en cours - travail" - "En attente" - "Appel entrant" - "Appel entrant - travail" - "Appel Wi-Fi entrant" - "Appel Wi-Fi entrant - travail" - "Appel vidéo entrant" - "L\'appel entrant est suspect" - "Demande de vidéo reçue" - "Nouveau message vocal" - "Nouveaux messages vocaux (%d)" - "Composer le %s" - "Numéro de messagerie vocale inconnu" - "Aucun service" - "Réseau sélectionné (%s) non disponible" - "Répondre" - "Raccrocher" - "Vidéo" - "Voix" - "Accepter" - "Fermer" - "Rappeler" - "Message" - "Appel en cours sur un autre appareil" - "Transférer l\'appel" - "Pour faire un appel, d\'abord désactiver le mode Avion." - "Non enregistré sur le réseau." - "Réseau cellulaire non disponible." - "Pour faire un appel, entrez un numéro valide." - "Impossible d\'appeler." - "Lancement de la séquence IHM en cours…" - "Service non pris en charge." - "Impossible de faire des appels." - "Impossible de séparer les appels." - "Impossible de transférer." - "Impossible de créer la conférence." - "Impossible de refuser l\'appel." - "Impossible de libérer l\'appel ou les appels." - "Appel SIP" - "Appel d\'urgence" - "Activation du signal radio…" - "Aucun service. Nouvel essai en cours..." - "Appel impossible. %s n\'est pas un numéro d\'urgence." - "Appel impossible. Composez un numéro d\'urgence." - "Utilisez le clavier pour composer un numéro" - "Mettre l\'appel en attente" - "Reprendre l\'appel" - "Mettre fin à l\'appel" - "Afficher le clavier numérique" - "Masquer le clavier numérique" - "Désactiver le son" - "Réactiver le son" - "Ajouter un appel" - "Fusionner les appels" - "Permuter" - "Gérer les appels" - "Gérer la conférence" - "Conférence téléphonique" - "Gérer" - "Audio" - "Appel vidéo" - "Passer à un appel vocal" - "Changer d\'appareil photo" - "Activer la caméra" - "Désactiver la caméra" - "Plus d\'options" - "Le lecteur a démarré" - "Le lecteur a arrêté" - "L\'appareil photo n\'est pas prêt" - "L\'appareil photo est prêt" - "Événement inconnu de séance d\'appel" - "Service" - "Configuration" - "<Non défini>" - "Autres paramètres d\'appel" - "Appel par %s" - "Appel entrant par %s" - "photo du contact" - "mode privé" - "sélectionner un contact" - "Réponse personnalisée..." - "Annuler" - "Envoyer" - "Répondre" - "Envoyer un texto" - "Refuser" - "Répondre comme appel vidéo" - "Répondre comme appel audio" - "Accepter la demande vidéo" - "Refuser la demande vidéo" - "Accepter la demande de transmission vidéo" - "Refuser la demande de transmission vidéo" - "Accepter la demande de réception vidéo" - "Refuser la demande de réception vidéo" - "Faites glisser votre doigt vers le haut pour %s." - "Faites glisser votre doigt vers la gauche pour %s." - "Faites glisser votre doigt vers la droite pour %s." - "Faire glisser le doigt vers le bas : %s" - "Vibration" - "Vibration" - "Son" - "Son par défaut (%1$s)" - "Sonnerie du téléphone" - "Vibrer lorsque téléphone sonne" - "Sonnerie et vibreur" - "Gérer la conférence" - "Numéro d\'urgence" - "Photo de profil" - "Appareil photo désactivé" - "au moyen du %s" - "Note envoyée" - "Messages récents" - "Renseignements sur l\'entreprise" - %.1f mi" - %.1f km" - "%1$s, %2$s" - "De %1$s à %2$s" - "%1$s, %2$s" - "Ouvre demain à %s" - "Ouvre aujourd\'hui à %s" - "Ferme à %s" - "A fermé aujourd\'hui à %s" - "Ouvert" - "Fermé" - "Appel suspect" - "Appel terminé %1$s" - "C\'est la première fois que ce numéro vous appelle." - "Cet appel nous semblait suspect." - "Sign. appel suspect" - "Ajouter un contact" - "N\'est pas suspect" - diff --git a/InCallUI/res/values-fr/strings.xml b/InCallUI/res/values-fr/strings.xml deleted file mode 100644 index 521fedb64470893d2030a193fcf640f243c72ef3..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-fr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Téléphone" - "En attente" - "Inconnu" - "Numéro privé" - "Cabine téléphonique" - "Conférence téléphonique" - "Appel interrompu" - "Haut-parleur" - "Écouteur du combiné" - "Casque filaire" - "Bluetooth" - "Envoyer les tonalités suivantes ?\n" - "Envoi des tonalités…\n" - "Envoyer" - "Oui" - "Non" - "Remplacer le caractère générique par" - "Conférence téléphonique à %s" - "N° de la messagerie vocale" - "Appel…" - "Rappel…" - "Conférence téléphonique" - "Appel entrant" - "Appel profession. entrant" - "Appel terminé" - "En attente" - "Fin de l\'appel…" - "Appel en cours" - "Mon numéro est le %s" - "Connexion de la vidéo…" - "Appel vidéo" - "Demande de vidéo…" - "Impossible d\'établir la connexion de l\'appel vidéo." - "Demande d\'appel vidéo refusée" - "Votre numéro de rappel\n %1$s" - "Votre numéro de rappel d\'urgence\n %1$s" - "Appel…" - "Appel manqué" - "Appels manqués" - "%s appels manqués" - "Appel manqué de %s" - "Appel en cours" - "Appel professionnel en cours" - "Appel Wi-Fi en cours" - "Appel Wi-Fi professionnel en cours" - "En attente" - "Appel entrant" - "Appel professionnel entrant" - "Appel Wi-Fi entrant" - "Appel Wi-Fi professionnel entrant" - "Appel vidéo entrant" - "Appel entrant indésirable suspecté" - "Demande de vidéo reçue" - "Nouveau message vocal" - "Nouveaux messages vocaux (%d)" - "Composer le %s" - "Numéro de messagerie vocale inconnu" - "Aucun service" - "Réseau sélectionné (%s) non disponible" - "Répondre" - "Raccrocher" - "Vidéo" - "Appel vocal" - "Accepter" - "Fermer" - "Rappeler" - "Envoyer SMS" - "Appel en cours sur un autre appareil" - "Transférer l\'appel" - "Veuillez désactiver le mode Avion avant de passer un appel." - "Non enregistré sur le réseau." - "Réseau mobile indisponible." - "Pour émettre un appel, veuillez saisir un numéro valide." - "Impossible d\'émettre l\'appel." - "Lancement de la séquence IHM…" - "Service non compatible." - "Impossible de changer d\'appel." - "Impossible d\'isoler l\'appel." - "Transfert impossible." - "Impossible de lancer une conférence téléphonique." - "Impossible de refuser l\'appel." - "Impossible de lancer les appels." - "Appel SIP" - "Appel d\'urgence" - "Activation du signal radio…" - "Aucun service disponible. Nouvelle tentative…" - "Impossible d\'émettre l\'appel. %s n\'est pas un numéro d\'urgence." - "Impossible d\'émettre l\'appel. Veuillez composer un numéro d\'urgence." - "Utilisez le clavier pour composer un numéro." - "Mettre l\'appel en attente" - "Reprendre l\'appel" - "Mettre fin à l\'appel" - "Afficher le clavier" - "Masquer le clavier" - "Couper le son" - "Réactiver le son" - "Ajouter un appel" - "Fusionner les appels" - "Permuter" - "Gérer les appels" - "Gérer conférence téléphonique" - "Conférence téléphonique" - "Gérer" - "Audio" - "Appel vidéo" - "Passer à un appel vocal" - "Changer de caméra" - "Activer la caméra" - "Désactiver la caméra" - "Plus d\'options" - "Le lecteur a démarré." - "Le lecteur s\'est arrêté." - "La caméra n\'est pas prête" - "La caméra est prête" - "Événement de session d\'appel inconnu" - "Service" - "Configuration" - "<Non défini>" - "Autres paramètres d\'appel" - "Appel via %s" - "Appel entrant via %s" - "photo du contact" - "mode privé" - "sélectionner un contact" - "Réponse personnalisée…" - "Annuler" - "Envoyer" - "Répondre" - "Envoyer un SMS" - "Refuser" - "Répondre via un appel vidéo" - "Répondre via un appel audio" - "Accepter la demande d\'appel vidéo" - "Refuser la demande d\'appel vidéo" - "Accepter la demande de transmission d\'appel vidéo" - "Refuser la demande de transmission d\'appel vidéo" - "Accepter la demande de réception d\'appel vidéo" - "Refuser la demande de réception d\'appel vidéo" - "Faites glisser vers le haut pour %s" - "Faites glisser vers la gauche pour %s." - "Faites glisser vers la droite pour %s." - "Faites glisser vers le bas pour %s." - "Vibreur" - "Vibreur" - "Sonnerie" - "Sonnerie par défaut (%1$s)" - "Sonnerie du téléphone" - "Vibreur lorsque le tél. sonne" - "Sonnerie et vibreur" - "Gérer la conférence téléphonique" - "Numéro d\'urgence" - "Photo du profil" - "Caméra désactivée" - "via le %s" - "La note a bien été envoyée." - "Messages récents" - "Informations sur l\'établissement" - %.1f mi" - %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Ouvre demain à %s" - "Ouvre aujourd\'hui à %s" - "Ferme à %s" - "Fermé aujourd\'hui à %s" - "Ouvert" - "Fermé" - "Appel indésirable suspecté" - "Appel terminé %1$s" - "C\'est la première fois que vous recevez un appel de ce numéro." - "Nous suspectons cet appel de provenir d\'un spammeur." - "Bloquer/Signaler spam" - "Ajouter un contact" - "Numéro fiable" - diff --git a/InCallUI/res/values-gl/strings.xml b/InCallUI/res/values-gl/strings.xml deleted file mode 100644 index 8968946e634123a2c1f20494fcea147409ac8dbc..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-gl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Teléfono" - "En espera" - "Descoñecido" - "Número privado" - "Teléfono público" - "Conferencia telefónica" - "Chamada interrompida" - "Altofalante" - "Auricular do teléfono" - "Auriculares con cable" - "Bluetooth" - "Queres enviar os seguintes tons?\n" - "Enviando tons\n" - "Enviar" - "Si" - "Non" - "Substituír carácter comodín por" - "Conferencia telefónica ás %s" - "Número de correo de voz" - "Marcando" - "Marcando de novo" - "Conferencia telefónica" - "Chamada entrante" - "Chamada traballo entrante" - "Chamada finalizada" - "En espera" - "Desconectando" - "Chamada entrante" - "O meu número é o %s" - "Conectando vídeo" - "Videochamada" - "Solicitando vídeo" - "Non se pode conectar a videochamada" - "Rexeitouse a solicitude de vídeo" - "O teu número de devolución de chamada\n %1$s" - "O teu número de devolución de chamada de emerxencia\n %1$s" - "Marcando" - "Chamada perdida" - "Chamadas perdidas" - "%s chamadas perdidas" - "Chamada perdida de %s" - "Chamada en curso" - "Chamada de traballo saínte" - "Chamada por wifi saínte" - "Chamada por wifi de traballo saínte" - "En espera" - "Chamada entrante" - "Chamada de traballo entrante" - "Chamada por wifi entrante" - "Chamada wifi de traballo entrante" - "Videochamada entrante" - "Chamada entrante sospeitosa de spam" - "Solicitude de vídeo entrante" - "Correo de voz novo" - "Correo de voz novo (%d)" - "Marcar o %s" - "Número de correo de voz descoñecido" - "Sen servizo" - "A rede seleccionada (%s) non está dispoñible" - "Responder" - "Colgar" - "Vídeo" - "Voz" - "Aceptar" - "Ignorar" - "Dev. chamada" - "Mensaxe" - "Chamada en curso noutro dispositivo" - "Transferir chamada" - "Para realizar unha chamada, primeiro desactiva o modo avión." - "Sen rexistro na rede." - "Rede móbil non dispoñible." - "Para realizar unha chamada, introduce un número válido." - "Non se pode realizar a chamada." - "Iniciando secuencia MMI..." - "Servizo non compatible." - "Non se poden cambiar as chamadas." - "Non se pode separar a chamada." - "Non se pode transferir." - "Non se pode realizar a conferencia." - "Non se pode rexeitar a chamada." - "Non se poden desconectar as chamadas." - "Chamada SIP" - "Chamada de emerxencia" - "Activando radio..." - "Sen servizo. Tentándoo de novo…" - "Non se pode realizar a chamada. %s non é un número de emerxencia." - "Non se pode realizar a chamada. Marca un número de emerxencia." - "Utiliza o teclado para marcar" - "Poñer a chamada en espera" - "Retomar chamada" - "Finalizar chamada" - "Mostrar teclado de marcación" - "Ocultar teclado de marcación" - "Silenciar" - "Activar o son" - "Engadir chamada" - "Combinar chamadas" - "Cambiar" - "Xestionar chamadas" - "Xestionar confer. telefónica" - "Conferencia telefónica" - "Xestionar" - "Audio" - "Videocham." - "Cambiar para chamada de voz" - "Cambiar cámara" - "Acender cámara" - "Apagar cámara" - "Máis opcións" - "Iniciouse o reprodutor" - "Detívose o reprodutor" - "A cámara non está preparada" - "A cámara está preparada" - "Evento de sesión de chamada descoñecido" - "Servizo" - "Configuración" - "<Sen configurar>" - "Outras configuracións de chamada" - "Chamando a través de %s" - "Chamadas entrantes a través de %s" - "foto do contacto" - "activar o modo privado" - "seleccionar contacto" - "Escribe a túa propia..." - "Cancelar" - "Enviar" - "Responder" - "Enviar SMS" - "Rexeitar" - "Responde como videochamada" - "Responde como chamada de audio" - "Acepta a solicitude de vídeo" - "Rexeita a solicitude de vídeo" - "Acepta a solicitude de transmisión de vídeo" - "Rexeita a solicitude de transmisión de vídeo" - "Acepta a solicitude de recepción de vídeo" - "Rexeita a solicitude de recepción de vídeo" - "Pasa o dedo cara a arriba para %s." - "Pasa o dedo cara a esquerda para %s." - "Pasa o dedo cara a dereita para %s." - "Pasa o dedo cara a abaixo para %s." - "Vibrar" - "Vibrar" - "Son" - "Son predeterminado (%1$s)" - "Ton de chamada do teléfono" - "Vibrar ao soar" - "Ton de chamada e vibración" - "Xestionar conferencia telefónica" - "Número de emerxencia" - "Foto do perfil" - "A cámara está desactivada" - "a través do %s" - "Enviouse a nota" - "Mensaxes recentes" - "Información da empresa" - "A %.1f mi de distancia" - "A %.1f km de distancia" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre mañá ás %s" - "Abre hoxe ás %s" - "Pecha ás %s" - "Pechou hoxe ás %s" - "Aberto agora" - "Pechado agora" - "Chamada sospeitosa" - "Finalizouse a chamada %1$s" - "É a primeira vez que te chama este número." - "Sospeitamos que esta chamada era un xerador de spam." - "Bloquear/marcar spam" - "Engadir contacto" - "Non é spam" - diff --git a/InCallUI/res/values-gu/strings.xml b/InCallUI/res/values-gu/strings.xml deleted file mode 100644 index 017ccb691f7cee6cd401208ed218427a68a337cd..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-gu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ફોન" - "હોલ્ડ પર" - "અજાણ્યો" - "ખાનગી નંબર" - "પેફોન" - "કોન્ફરન્સ કૉલ" - "કૉલ કપાઇ ગયો" - "સ્પીકર" - "હેન્ડસેટ ઇયરપીસ" - "વાયર્ડ હેડસેટ" - "Bluetooth" - "નીચે આપેલ ટોન્સ મોકલીએ?\n" - "ટોન્સ મોકલી રહ્યાં છે\n" - "મોકલો" - "હા" - "નહીં" - "વાઇલ્ડ અક્ષરને આની સાથે બદલો" - "કોન્ફરન્સ કૉલ %s" - "વૉઇસમેઇલ નંબર" - "ડાયલ કરી રહ્યાં છે" - "ફરી ડાયલ કરી રહ્યાં છે" - "કોન્ફરન્સ કૉલ" - "ઇનકમિંગ કૉલ" - "ઇનકમિંગ કાર્ય કૉલ" - "કૉલ સમાપ્ત થયો" - "હોલ્ડ પર" - "સમાપ્ત કરી રહ્યાં છે" - "કૉલમાં" - "મારો નંબર %s છે" - "વિડિઓ કનેક્ટ કરી રહ્યાં છે" - "વિડિઓ કૉલ" - "વિડિઓની વિનંતી કરી રહ્યાં છે" - "વિડિઓ કૉલ કનેક્ટ કરી શકાતો નથી" - "વિડિઓ વિનંતી નકારી" - "તમારો કૉલબેક નંબર\n %1$s" - "તમારો કટોકટીનો કૉલબેક નંબર\n %1$s" - "ડાયલ કરી રહ્યાં છે" - "છૂટેલો કૉલ" - "છૂટેલા કૉલ્સ" - "%s છૂટેલા કૉલ" - "%s નો કૉલ ચૂકી ગયાં" - "ચાલી રહેલ કૉલ" - "ચાલી રહેલ કાર્ય કૉલ" - "ચાલી રહેલ Wi-Fi કૉલ" - "ચાલી રહેલ Wi-Fi કાર્ય કૉલ" - "હોલ્ડ પર" - "ઇનકમિંગ કૉલ" - "ઇનકમિંગ કાર્ય કૉલ" - "ઇનકમિંગ Wi-Fi કૉલ" - "ઇનકમિંગ Wi-Fi કાર્ય કૉલ" - "ઇનકમિંગ વિડિઓ કૉલ" - "ઇનકમિંગ શંકાસ્પદ સ્પામ કૉલ" - "ઇનકમિંગ વિડિઓ વિનંતી" - "નવો વૉઇસમેઇલ" - "નવો વૉઇસમેઇલ (%d)" - "%s ડાયલ કરો" - "વૉઇસમેઇલ નંબર અજાણ" - "કોઈ સેવા નથી" - "પસંદ કરેલ નેટવર્ક (%s) અનુપલબ્ધ" - "જવાબ" - "સમાપ્ત કરો" - "વિડિઓ" - "વૉઇસ" - "સ્વીકારો" - "છોડી દો" - "કૉલ બૅક કરો" - "સંદેશ" - "અન્ય ઉપકરણ પર ચાલી રહેલ કૉલ" - "કૉલ સ્થાનાંતરિત કરો" - "કૉલ કરવા માટે, પહેલા એરપ્લેન મોડને બંધ કરો." - "નેટવર્ક પર નોંધણી કરાયેલ નથી." - "સેલ્યુલર નેટવર્ક ઉપલબ્ધ નથી." - "કૉલ કરવા માટે, માન્ય નંબર દાખલ કરો." - "કૉલ કરી શકાતો નથી." - "MMI અનુક્રમ પ્રારંભ કરી રહ્યાં છે…" - "સેવા સમર્થિત નથી." - "કૉલ્સ સ્વિચ કરી શકાતાં નથી." - "અલગ કૉલ કરી શકાતો નથી." - "ટ્રાંસ્ફર કરી શકાતો નથી." - "કોન્ફરન્સ કરી શકાતી નથી." - "કૉલ નકારી શકાતો નથી." - "કૉલ(કૉલ્સ) રિલીઝ કરી શકતાં નથી." - "SIP કૉલ" - "કટોકટીનો કૉલ" - "રેડિઓ ચાલુ કરી રહ્યાં છે…" - "કોઈ સેવા નથી. ફરી પ્રયાસ કરી રહ્યાં છે…" - "કૉલ કરી શકાતો નથી. %s એ કટોકટીનો નંબર નથી." - "કૉલ કરી શકાતો નથી. કટોકટીનો નંબર ડાયલ કરો." - "ડાયલ કરવા માટે કીબોર્ડનો ઉપયોગ કરો" - "કૉલ હોલ્ડ કરો" - "કૉલ ફરી શરૂ કરો" - "કૉલ સમાપ્ત કરો" - "ડાયલપેડ બતાવો" - "ડાયલપેડ છુપાવો" - "મ્યૂટ કરો" - "અનમ્યૂટ કરો" - "કૉલ ઉમેરો" - "કૉલ્સ મર્જ કરો" - "સ્વેપ કરો" - "કૉલ્સ સંચાલિત કરો" - "કોન્ફરન્સ કૉલ સંચાલિત કરો" - "કોન્ફરન્સ કૉલ" - "સંચાલિત કરો" - "ઑડિઓ" - "વિડિઓ કૉલ" - "વૉઇસ કૉલ પર બદલો" - "કૅમેરા પર સ્વિચ કરો" - "કૅમેરો ચાલુ કરો" - "કૅમેરો બંધ કરો" - "વધુ વિકલ્પો" - "પ્લેયર પ્રારંભ કર્યું" - "પ્લેયર બંધ કર્યું" - "કૅમેરો તૈયાર નથી" - "કૅમેરો તૈયાર" - "અજાણી કૉલ સત્ર ઇવેન્ટ" - "સેવા" - "સેટઅપ" - "<સેટ કરેલ નથી>" - "અન્ય કૉલ સેટિંગ્સ" - "%s મારફતે કૉલ કરે છે" - "%s મારફતે ઇનકમિંગ" - "સંપર્ક ફોટો" - "ખાનગી થાઓ" - "સંપર્ક પસંદ કરો" - "તમારો પોતાનો લખો…" - "રદ કરો" - "મોકલો" - "જવાબ" - "SMS મોકલો" - "નકારો" - "વિડિઓ કૉલ તરીકે જવાબ આપો" - "ઑડિઓ કૉલ તરીકે જવાબ આપો" - "વિડિઓ વિનંતી સ્વીકારો" - "વિડિઓ વિનંતી નકારો" - "વિડિઓ ટ્રાંસ્મિટ વિનંતી સ્વીકારો" - "વિડિઓ ટ્રાંસ્મિટ વિનંતી નકારો" - "વિડિઓ પ્રાપ્તિ વિનંતી સ્વીકારો" - "વિડિઓ પ્રાપ્તિ વિનંતી નકારો" - "%s માટે ઉપર સ્લાઇડ કરો." - "%s માટે ડાબે સ્લાઇડ કરો." - "%s માટે જમણે સ્લાઇડ કરો." - "%s માટે નીચે સ્લાઇડ કરો." - "વાઇબ્રેટ" - "વાઇબ્રેટ" - "ધ્વનિ" - "ડિફોલ્ટ ધ્વનિ (%1$s)" - "ફોન રિંગટોન" - "જ્યારે રિંગ કરે ત્યારે વાઇબ્રેટ કરો" - "રિંગટોન અને વાઇબ્રેટ" - "કોન્ફરન્સ કૉલ સંચાલિત કરો" - "કટોકટીનો નંબર" - "પ્રોફાઇલ ફોટો" - "કૅમેરો બંધ" - "%s મારફતે" - "નોંધ મોકલી" - "તાજેતરનાં સંદેશા" - "વ્યવસાયની માહિતી" - "%.1f માઇલ દૂર" - "%.1f કિમી દૂર" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "આવતીકાલે %s વાગ્યે ખુલશે" - "આજે %s વાગ્યે ખુલશે" - "%s વાગ્યે બંધ થશે" - "આજે %s વાગ્યે બંધ થયેલું" - "હમણાં ખુલ્લું" - "હમણાં બંધ છે" - "શંકાસ્પદ સ્પામ કૉલર" - "%1$s નો કૉલ સમાપ્ત થયો" - "આ નંબરથી તમને પહેલી વાર કૉલ કરવામાં આવ્યો છે." - "આ કૉલ કોઈ સ્પામર હોવાની અમને શંકા છે." - "સ્પામની જાણ/અવરોધિત કરો" - "સંપર્ક ઉમેરો" - "સ્પામ નથી" - diff --git a/InCallUI/res/values-h400dp/dimens.xml b/InCallUI/res/values-h400dp/dimens.xml deleted file mode 100644 index dda755a3ea8d49b5148a8f3560ea44dd9fa13be7..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-h400dp/dimens.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - true - - 90dp - - 15dp - - -24dp - - 2dp - - 20dp - diff --git a/InCallUI/res/values-hi/strings.xml b/InCallUI/res/values-hi/strings.xml deleted file mode 100644 index dee8f464d59b9611848841734e7f4059bb9ad815..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-hi/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "फ़ोन" - "होल्ड पर" - "अज्ञात" - "निजी नंबर" - "पे-फ़ोन" - "कॉन्फ़्रेंस कॉल" - "कॉल कट गया" - "स्पीकर" - "हैंडसेट इयरपीस" - "वायर वाला हैडसेट" - "ब्लूटूथ" - "निम्न टोन भेजें?\n" - "भेजने वाली टोन\n" - "भेजें" - "हां" - "नहीं" - "वाइल्ड वर्ण को इससे बदलें:" - "कॉन्फ़्रेंस कॉल %s" - "वॉइसमेल नबंर" - "डायल किया जा रहा है" - "पुन: डायल हो रहा है" - "कॉन्फ़्रेंस कॉल" - "इनकमिंग कॉल" - "कार्यस्थल का इनकमिंग कॉल" - "कॉल समाप्त" - "होल्ड पर" - "कॉल समाप्त हो रहा है" - "कॉल में" - "मेरा नंबर %s है" - "वीडियो कनेक्ट हो रहा है" - "वीडियो कॉल" - "वीडियो का अनुरोध किया जा रहा है" - "वीडियो कॉल कनेक्ट नहीं किया जा सकता" - "वीडियो अनुरोध अस्वीकार किया गया" - "आपका कॉलबैक नंबर\n %1$s" - "आपका आपातकालीन कॉलबैक नंबर\n %1$s" - "डायल किया जा रहा है" - "छूटा कॉल" - "छूटे कॉल" - "%s छूटे कॉल" - "%s के छूटे कॉल" - "चल रहा कॉल" - "कार्यस्थल का जारी कॉल" - "चल रहा वाई-फ़ाई कॉल" - "कार्यस्थल का जारी वाई-फ़ाई कॉल" - "होल्ड पर" - "इनकमिंग कॉल" - "कार्यस्थल का इनकमिंग कॉल" - "इनकमिंग वाई-फ़ाई कॉल" - "कार्यस्थल का वाई-फ़ाई इनकमिंग कॉल" - "इनकमिंग वीडियो कॉल" - "संदिग्ध आवक स्पैम कॉल" - "इनकमिंग वीडियो अनुरोध" - "नया वॉइसमेल" - "नया वॉइसमेल (%d)" - "%s डायल करें" - "वॉइसमेल नंबर अज्ञात" - "कोई सेवा नहीं" - "चयनित नेटवर्क (%s) अनुपलब्ध" - "उत्तर दें" - "समाप्त करें" - "वीडियो" - "ध्वनि" - "स्वीकार करें" - "ख़ारिज करें" - "कॉल बैक करें" - "संदेश" - "दूसरे डिवाइस पर चल रहा कॉल" - "कॉल स्थानान्तरित करें" - "कॉल करने के लिए, पहले हवाई जहाज़ मोड बंद करें." - "नेटवर्क पर पंजीकृत नहीं." - "सेल्युलर नेटवर्क उपलब्ध नहीं." - "कॉल करने के लिए, मान्य नंबर डालें." - "कॉल नहीं किया जा सकता." - "MMI अनुक्रम प्रारंभ हो रहा है…" - "सेवा समर्थित नहीं है." - "कॉल स्विच नहीं किए जा सकते." - "कॉल अलग नहीं किया जा सकता." - "स्थानान्तरित नहीं किया जा सकता." - "कॉन्फ़्रेंस नहीं की जा सकती." - "कॉल अस्वीकार नहीं किया जा सकता." - "कॉल रिलीज़ नहीं किया जा सकता (किए जा सकते)." - "SIP कॉल" - "आपातकालीन कॉल" - "रेडियो चालू कर रहा है..." - "कोई सेवा नहीं. पुन: प्रयास किया जा रहा है…" - "कॉल नहीं किया जा सकता. %s एक आपातकालीन नंबर नहीं है." - "कॉल नहीं किया जा सकता. आपातकालीन नबर डायल करें." - "डायल करने के लिए कीबोर्ड का उपयोग करें" - "कॉल होल्ड करें" - "कॉल फिर से शुरू करें" - "कॉल समाप्त करें" - "डायलपैड दिखाएं" - "डायलपैड छिपाएं" - "म्यूट करें" - "अनम्यूट करें" - "कॉल जोड़ें" - "कॉल मर्ज करें" - "स्वैप करें" - "कॉल प्रबंधित करें" - "कॉन्फ़्रेंस कॉल प्रबंधित करें" - "कॉन्फ़्रेंस कॉल" - "प्रबंधित करें" - "ऑडियो" - "वीडियो कॉल" - "वॉइस कॉल में बदलें" - "कैमरा स्विच करें" - "कैमरा चालू करें" - "कैमरा बंद करें" - "अधिक विकल्प" - "प्लेयर प्रारंभ हो गया" - "प्लेयर रुक गया" - "कैमरा तैयार नहीं है" - "कैमरा तैयार है" - "अज्ञात कॉल सत्र इवेंट" - "सेवा" - "सेटअप" - "<सेट नहीं है>" - "अन्य कॉल सेटिंग" - "%s के माध्यम से कॉल किया जा रहा है" - "%s की ओर से इनकमिंग" - "संपर्क का फ़ोटो" - "निजी हो जाएं" - "संपर्क को चुनें" - "अपना स्वयं का लिखें..." - "अभी नहीं" - "भेजें" - "उत्तर दें" - "SMS भेजें" - "अस्वीकार करें" - "वीडियो कॉल के रूप में उत्तर दें" - "ऑडियो कॉल के रूप में उत्तर दें" - "वीडियो अनुरोध स्वीकार करें" - "वीडियो अनुरोध अस्वीकार करें" - "वीडियो प्रसारण अनुरोध स्वीकार करें" - "वीडियो प्रसारण अनुरोध अस्वीकार करें" - "वीडियो प्राप्ति अनुरोध स्वीकार करें" - "वीडियो प्राप्ति अनुरोध अस्वीकार करें" - "%s करने के लिए ऊपर स्लाइड करें." - "%s करने के लिए बाएं स्लाइड करें." - "%s करने के लिए दाएं स्लाइड करें." - "%s करने के लिए नीचे स्लाइड करें." - "कंपन" - "कंपन" - "ध्वनि" - "डिफ़ॉल्ट ध्वनि (%1$s)" - "फ़ोन रिंगटोन" - "रिंग आने पर कंपन करें" - "रिंगटोन और कंपन" - "कॉन्फ़्रेंस कॉल प्रबंधित करें" - "आपातकालीन नंबर" - "प्रोफ़ाइल फ़ोटो" - "कैमरा बंद है" - "%s के द्वारा" - "नोट भेज दिया गया है" - "हाल ही के संदेश" - "व्यवसाय जानकारी" - "%.1f मील दूर" - "%.1f किमी दूर" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "कल %s बजे खुलेगा" - "आज %s बजे खुलता है" - "%s बजे बंद होता है" - "आज %s बजे बंद हो गया" - "अभी खुला है" - "अभी बंद है" - "संदिग्ध स्पैम कॉलर" - "%1$s का कॉल समाप्त हो गया" - "इस नंबर से आपको पहली बार कॉल किया गया है." - "हमें आशंका है कि कॉल स्पैमकर्ता का हो सकता है." - "अवरुद्ध करें/स्पैम रिपोर्ट करें" - "संपर्क जोड़ें" - "स्पैम नहीं है" - diff --git a/InCallUI/res/values-hr/strings.xml b/InCallUI/res/values-hr/strings.xml deleted file mode 100644 index 9c11644c4ccb3bba99b027abd6070ed9475f807c..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-hr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Na čekanju" - "Nepoznato" - "Privatni broj" - "Javna telefonska govornica" - "Konferencijski poziv" - "Poziv je prekinut" - "Zvučnik" - "Slušalice" - "Žičane slušalice" - "Bluetooth" - "Poslati sljedeće tonove?\n" - "Slanje tonova\n" - "Pošalji" - "Da" - "Ne" - "Zamijeni zamjenski znak znakom" - "Konferencijski poziv %s" - "Broj govorne pošte" - "Biranje broja" - "Ponovno biranje" - "Konferencijski poziv" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Poziv je završio" - "Na čekanju" - "Prekidanje veze" - "Poziv u tijeku" - "Moj je broj %s" - "Uspostavljanje videopoziva" - "Videopoziv" - "Zahtijevanje videopoziva" - "Videopoziv nije uspostavljen" - "Zahtjev za videopoziv odbijen" - "Vaš broj za povratni poziv\n %1$s" - "Vaš broj za povratni poziv za hitne službe\n %1$s" - "Biranje broja" - "Propušteni poziv" - "Propušteni pozivi" - "Propušteni pozivi (%s)" - "Propušten poziv kontakta %s" - "Poziv u tijeku" - "Poslovni poziv u tijeku" - "Wi-Fi poziv u tijeku" - "Poslovni Wi-Fi poziv u tijeku" - "Na čekanju" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Dolazni Wi-Fi poziv" - "Dolazni poslovni Wi-Fi poziv" - "Dolazni videopoziv" - "Mogući neželjeni dolazni poziv" - "Dolazni zahtjev za videopoziv" - "Nova govorna pošta" - "Nova govorna pošta (%d)" - "Biraj %s" - "Nepoznat broj govorne pošte" - "Nema usluge" - "Odabrana mreža (%s) nije dostupna" - "Odgovori" - "Prekini vezu" - "Videopoziv" - "Glasovni poziv" - "Prihvati" - "Odbaci" - "Uzvrati" - "Poruka" - "Poziv u tijeku na drugom uređaju" - "Prijenos poziva" - "Da biste uspostavili poziv, isključite način rada u zrakoplovu." - "Nije registrirano na mreži." - "Mobilna mreža nije dostupna." - "Unesite važeći broj da biste uspostavili poziv." - "Pozivanje nije moguće." - "Pokretanje MMI sekvence…" - "Usluga nije podržana." - "Prebacivanje poziva nije moguće." - "Odvajanje poziva nije moguće." - "Prijenos nije moguć." - "Konferencijski poziv nije moguć." - "Odbijanje poziva nije moguće." - "Prekidanje poziva nije moguće." - "SIP poziv" - "Hitni poziv" - "Uključivanje radija…" - "Nema usluge. Pokušavamo ponovo…" - "Pozivanje nije moguće. %s nije broj hitne službe." - "Pozivanje nije moguće. Nazovite broj hitne službe." - "Upotrijebite tipkovnicu" - "Stavi poziv na čekanje" - "Nastavi poziv" - "Završi poziv" - "Prikaži površinu za biranje brojeva" - "Sakrij površinu za biranje brojeva" - "Zanemari" - "Prestani zanemarivati" - "Dodaj poziv" - "Spoji pozive" - "Zamijeni" - "Upravljaj pozivima" - "Upravljaj konf. pozivom" - "Konferencijski poziv" - "Upravljanje" - "Audio" - "Videopoziv" - "Prijeđi na glasovni poziv" - "Promijeni kameru" - "Uključivanje kamere" - "Isključivanje kamere" - "Više opcija" - "Player je pokrenut" - "Player je prekinut" - "Fotoaparat nije spreman" - "Fotoaparat je spreman" - "Nepoznati događaj sesije poziva" - "Usluga" - "Postavljanje" - "<Nije postavljeno>" - "Ostale postavke poziva" - "Pozivanje putem operatera %s" - "Dolazni pozivi putem usluge %s" - "fotografija kontakta" - "uputi na privatno" - "odabir kontakta" - "Napišite odgovor..." - "Odustani" - "Pošalji" - "Odgovori" - "Pošalji SMS" - "Odbij" - "Prihvati kao videopoziv" - "Prihvati kao audiopoziv" - "Prihvati zahtjev za videopoziv" - "Odbij zahtjev za videopoziv" - "Prihvati zahtjev za slanje videopoziva" - "Odbij zahtjev za slanje videopoziva" - "Prihvati zahtjev za primanje videopoziva" - "Odbij zahtjev za primanje videopoziva" - "Kliznite prema gore za %s." - "Kliznite lijevo za %s." - "Kliznite desno za %s." - "Kliznite prema dolje za %s." - "Vibriranje" - "Vibriranje" - "Zvuk" - "Zadani zvuk (%1$s)" - "Melodija zvona telefona" - "Vibracija tijekom zvonjenja" - "Melodija zvona i vibracija" - "Upravljaj konferencijskim pozivom" - "Broj hitne službe" - "Fotografija profila" - "Fotoaparat je isključen" - "putem broja %s" - "Bilješka je poslana" - "Nedavne poruke" - "Informacije o tvrtki" - "%.1f mi udaljenosti" - "%.1f km udaljenosti" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Otvara se sutra u %s" - "Otvara se danas u %s" - "Zatvara se u %s" - "Zatvoreno danas u %s" - "Trenutačno otvoreno" - "Trenutačno zatvoreno" - "Neželjeni pozivatelj" - "Poziv je završio %1$s" - "Prvi ste put primili poziv s tog broja." - "Sumnjamo da vas zove pošiljatelj neželjenih sadržaja." - "Blokiranje/prijava neželjenog broja" - "Dodavanje kontakta" - "Nije neželjeni broj" - diff --git a/InCallUI/res/values-hu/strings.xml b/InCallUI/res/values-hu/strings.xml deleted file mode 100644 index 81694260105fca642342cf822092462292409876..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-hu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Tartásban" - "Ismeretlen" - "Magántelefonszám" - "Nyilvános telefon" - "Konferenciahívás" - "A hívás megszakadt" - "Hangszóró" - "Kézibeszélő fülhallgatója" - "Vezetékes fülhallgató" - "Bluetooth" - "Elküldi a következő hangjelzéseket?\n" - "Hangjelzések küldése\n" - "Küldés" - "Igen" - "Nem" - "Helyettesítő karakter lecserélése a következőre:" - "Konferenciahívás – %s" - "Hangposta száma" - "Tárcsázás" - "Újratárcsázás" - "Konferenciahívás" - "Bejövő hívás" - "Bejövő munkahelyi hívás" - "A hívás befejeződött" - "Tartásban" - "Megszakítás" - "Hívásban" - "A számom: %s" - "Videókapcsolat létrehozása" - "Videóhívás" - "Videóhívást kér" - "A videóhívás létesítése sikertelen" - "Videókérelem elutasítva" - "Visszahívható telefonszáma:\n %1$s" - "Vészhelyzet esetén visszahívható telefonszáma:\n %1$s" - "Tárcsázás" - "Nem fogadott hívás" - "Nem fogadott hívások" - "%s nem fogadott hívás" - "Nem fogadott hívás: %s" - "Hívás folyamatban" - "Folyamatban lévő munkahelyi hívás" - "Folyamatban lévő Wi-Fi-hívás" - "Folyamatban lévő munkahelyi hívás Wi-Fi-hálózaton keresztül" - "Tartásban" - "Bejövő hívás" - "Bejövő munkahelyi hívás" - "Bejövő Wi-Fi-hívás" - "Bejövő munkahelyi hívás Wi-Fi-hálózaton keresztül" - "Bejövő videóhívás" - "Bejövő gyanús spamhívás" - "Bejövő videókérés" - "Új hangpostaüzenet" - "Új hangpostaüzenet (%d)" - "%s tárcsázása" - "A hangposta száma ismeretlen" - "Nincs szolgáltatás" - "A kiválasztott hálózat (%s) nem áll rendelkezésre" - "Fogadás" - "Hívás bontása" - "Videó" - "Hang" - "Elfogadás" - "Elvetés" - "Visszahívás" - "Üzenet" - "Folyamatban lévő hívás egy másik eszközön" - "Hívásátirányítás" - "Hívásindításhoz kapcsolja ki a Repülős üzemmódot." - "Nincs regisztrálva a hálózaton." - "A mobilhálózat nem áll rendelkezésre." - "Hívásindításhoz adjon meg egy érvényes számot." - "A hívás sikertelen." - "MMI-sorozat indítása…" - "A szolgáltatás nem támogatott." - "A hívások közötti váltás sikertelen." - "A híváselkülönítés sikertelen." - "Az átirányítás sikertelen." - "A konferenciahívás sikertelen." - "A híváselutasítás sikertelen." - "A tartásban lévő hívás(ok) folytatása sikertelen." - "SIP-hívás" - "Segélyhívás" - "Rádió bekapcsolása…" - "Nincs szolgáltatás. Újrapróbálkozás folyamatban…" - "A hívás sikertelen. A(z) %s szám nem segélyhívószám." - "A hívás sikertelen. Tárcsázzon segélyhívószámot." - "A tárcsázáshoz használja a billentyűzetet" - "Hívás tartása" - "Hívás folytatása" - "Hívás befejezése" - "Tárcsázó megjelenítése" - "Tárcsázó elrejtése" - "Némítás" - "Némítás feloldása" - "Hívás hozzáadása" - "Hívások egyesítése" - "Csere" - "Hívások kezelése" - "Konferenciahívás kezelése" - "Konferenciahívás" - "Kezelés" - "Hang" - "Videóhívás" - "Váltás hanghívásra" - "Váltás a kamerák között" - "Kamera bekapcsolása" - "Kamera kikapcsolása" - "További lehetőségek" - "A lejátszó elindult" - "A lejátszó leállt" - "A kamera nem áll készen" - "A kamera készen áll" - "Ismeretlen hívási esemény" - "Szolgáltatás" - "Beállítás" - "<Nincs megadva>" - "Egyéb hívásbeállítások" - "Hívás a(z) %s szolgáltatón keresztül" - "Bejövő hívás a következőn keresztül: %s" - "fotó a névjegyhez" - "magánbeszélgetés" - "névjegy kiválasztása" - "Saját válasz írása…" - "Mégse" - "Küldés" - "Fogadás" - "SMS küldése" - "Elutasítás" - "Fogadás videóhívásként" - "Fogadás hanghívásként" - "Videó kérésének elfogadása" - "Videó kérésének elutasítása" - "Videóküldési kérés elfogadása" - "Videóküldési kérés elutasítása" - "Videófogadási kérés elfogadása" - "Videófogadási kérés elutasítása" - "A(z) %s művelethez csúsztassa felfelé." - "A(z) %s művelethez csúsztassa balra." - "A(z) %s művelethez csúsztassa jobbra." - "A(z) %s művelethez csúsztassa lefelé." - "Rezgés" - "Rezgés" - "Hang" - "Alapértelmezett hang (%1$s)" - "Telefon csengőhangja" - "Csörgéskor rezegjen" - "Csengőhang és rezgés" - "Konferenciahívás kezelése" - "Segélyhívó szám" - "Profilfotó" - "Kamera ki" - "a következő számon keresztül: %s" - "Üzenet elküldve" - "Legutóbbi üzenetek" - "Cég adatai" - "%.1f mérföldre" - "%.1f kilométerre" - "%2$s, %1$s" - "%1$s%2$s" - "%1$s, %2$s" - "Holnap ekkor nyit: %s" - "Ma ekkor nyit: %s" - "Ekkor zár: %s" - "Ma ekkor zárt: %s" - "Jelenleg nyitva van" - "Jelenleg zárva van" - "Gyanús spamhívó" - "Hívás befejezve: %1$s" - "Ez az első alkalom, hogy erről a számról hívása érkezett." - "Azt gyanítjuk, hogy ez egy spamhívás." - "Letiltás/spam bejel." - "Névjegy hozzáadása" - "Nem spam" - diff --git a/InCallUI/res/values-hy/strings.xml b/InCallUI/res/values-hy/strings.xml deleted file mode 100644 index 899262432edd89786d38da9ee81d835b0dd4e134..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-hy/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Հեռախոս" - "Սպասում" - "Անհայտ" - "Գաղտնի համար" - "Հանրային հեռախոս" - "Կոնֆերանս զանգ" - "Զանգն ընդհատվեց" - "Բարձրախոս" - "Հեռախոսի ականջակալ" - "Լարային ականջակալ" - "Bluetooth" - "Ուղարկե՞լ հետևյալ ձայներանգները:\n" - "Ձայներանգների ուղարկում\n" - "Ուղարկել" - "Այո" - "Ոչ" - "Փոխարինել կոպիտ գրանշանը" - "Կոնֆերանս զանգ %s" - "Ձայնային փոստի համարը" - "Համարը հավաքվում է" - "Վերահամարարկում" - "Կոնֆերանս զանգ" - "Մուտքային զանգ" - "Մուտքային աշխատանքային զանգ" - "Զանգն ավարտվեց" - "Սպասում" - "Անջատում" - "Զանգը միացված է" - "Իմ համարը՝ %s" - "Տեսակապը միանում է" - "Տեսազանգ" - "Տեսակապի հայցում" - "Հնարավոր չէ միացնել տեսազանգը" - "Տեսազանգի հարցումը մերժվել է" - "Հետադարձ զանգի համարը՝\n%1$s" - "Արտակարգ իրավիճակների հետադարձ զանգի համարը՝\n%1$s" - "Համարը հավաքվում է" - "Բաց թողնված զանգ" - "Բաց թողնված զանգեր" - "%s բաց թողնված զանգ" - "Բաց թողնված զանգ %s-ից" - "Ընթացիկ զանգ" - "Ընթացիկ աշխատանքային զանգ" - "Ընթացիկ Wi-Fi զանգ" - "Ընթացիկ աշխատանքային Wi-Fi զանգ" - "Սպասում" - "Մուտքային զանգ" - "Մուտքային աշխատանքային զանգ" - "Մուտքային Wi-Fi զանգ" - "Մուտքային աշխատանքային Wi-Fi զանգ" - "Մուտքային տեսազանգ" - "Մուտքային զանգը հավանաբար լցոն է" - "Մուտքային տեսազանգի հայցում" - "Նոր ձայնային հաղորդագրություն" - "Նոր ձայնային հաղորդագրություն (%d)" - "Զանգել %s համարին" - "Ձայնային փոստի համարն անհայտ է" - "Ծառայություն չկա" - "Ընտրված ցանցը (%s) անհասանելի է" - "Պատասխանել" - "Անջատել" - "Տեսազանգ" - "Ձայնային" - "Ընդունել" - "Մերժել" - "Հետ զանգել" - "Հաղորդագրություն" - "Ընթացիկ զանգ այլ սարքում" - "Փոխանցել զանգը" - "Զանգ կատարելու համար նախ անջատեք Ինքնաթիռի ռեժիմը:" - "Ցանցում գրանցված չէ:" - "Բջջային ցանցն անհասանելի է:" - "Զանգ կատարելու համար մուտքագրեք ճիշտ համար:" - "Հնարավոր չէ զանգել:" - "Մեկնարկում է MMI հաջորդականությունը…" - "Ծառայությունը չի աջակցվում:" - "Հնարավոր չէ փոխարկել զանգերը:" - "Հնարավոր չէ առանձնացնել զանգը:" - "Հնարավոր չէ փոխանցել:" - "Հնարավոր չէ կոնֆերանս զանգ կատարել:" - "Հնարավոր չէ մերժել զանգը:" - "Հնարավոր չէ անջատել զանգ(եր)ը:" - "SIP զանգ" - "Շտապ կանչ" - "Ռադիոն միացվում է…" - "Ծառայությունը մատչելի չէ: Փորձը կրկնվում է…" - "Հնարավոր չէ զանգել: %s համարը արտակարգ իրավիճակի համար չէ:" - "Հնարավոր չէ զանգել: Հավաքեք արտակարգ իրավիճակի որևէ համար:" - "Օգտագործել ստեղնաշարը համար հավաքելու համար" - "Հետաձգել զանգը" - "Վերսկսել զանգը" - "Ավարտել զանգը" - "Ցուցադրել թվաշարը" - "Թաքցնել թվաշարը" - "Անջատել ձայնը" - "Չանտեսել" - "Ավելացնել զանգ" - "Միացնել զանգերը" - "Փոխանակել" - "Կառավարել զանգերը" - "Կառավարել կոնֆերանս զանգը" - "Կոնֆերանս զանգ" - "Կառավարել" - "Ձայնային" - "Տեսազանգ" - "Փոխարկել ձայնային կանչի" - "Փոխարկել խցիկը" - "Միացնել տեսախցիկը" - "Անջատել տեսախցիկը" - "Այլ ընտրանքներ" - "Նվագարկիչը մեկնարկել է" - "Նվագարկիչը դադարեցվել է" - "Տեսախցիկը պատրաստ չէ" - "Տեսախցիկը պատրաստ է" - "Զանգի աշխատաշրջանի անհայտ իրադարձություն" - "Ծառայություն" - "Կարգավորում" - "<Կարգավորված չէ>" - "Զանգերի այլ կարգավորումներ" - "Զանգում է %s-ի միջոցով" - "Մուտքային զանգ %s-ի միջոցով" - "կոնտակտի լուսանկարը" - "անցնել անձնականի" - "ընտրել կոնտակտ" - "Գրեք ձեր սեփականը…" - "Չեղարկել" - "Ուղարկել" - "Պատասխանել" - "Ուղարկել SMS" - "Մերժել" - "Պատասխանել տեսազանգով" - "Պատասխանել ձայնային զանգով" - "Ընդունել տեսազանգի հարցումը" - "Մերժել տեսազանգի հարցումը" - "Ընդունել տեսափոխանցման հարցումը" - "Մերժել տեսափոխանցման հարցումը" - "Ընդունել տեսազանգ ստանալու հարցումը" - "Մերժել տեսազանգ ստանալու հարցումը" - "Սահեցրեք վերև` %s գործառույթի համար:" - "Սահեցրեք ձախ` %s գործառույթի համար:" - "Սահեցրեք աջ` %s գործառույթի համար:" - "Սահեցրեք ցած՝ %s գործառույթի համար:" - "Թրթռոց" - "Թրթռոց" - "Ձայն" - "Կանխադրված ձայնը (%1$s)" - "Հեռախոսի զանգերանգ" - "Թրթռալ զանգի ժամանակ" - "Ձայներանգ և թրթռոց" - "Կառավարել կոնֆերանս զանգը" - "Արտակարգ իրավիճակի համար" - "Պրոֆիլի լուսանկար" - "Տեսախցիկն անջատված" - "%s-ի միջոցով" - "Գրառումն ուղարկվեց" - "Վերջին հաղորդագրությունները" - "Բիզնես տեղեկատվություն" - "%.1f մղոն հեռու" - "%.1f կմ հեռու" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Բացվում է վաղը ժամը %s-ին" - "Բացվում է այսօր ժամը %s-ին" - "Փակվում է ժամը %s-ին" - "Փակվել է այսօր ժամը %s-ին" - "Հիմա բաց է" - "Հիմա փակ է" - "Հավանաբար լցոն է" - "Զանգն ավարտվեց %1$s" - "Այս համարից առաջին անգամ եք զանգ ստանում:" - "Կասկածներ կային, որ այս զանգը լցոնողից է:" - "Արգելափակել/Նշել լցոն" - "Ավելացնել կոնտակտ" - "Լցոն չէ" - diff --git a/InCallUI/res/values-in/strings.xml b/InCallUI/res/values-in/strings.xml deleted file mode 100644 index a2cd31dc7f6048481089e4b385216ccabfd29b36..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-in/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telepon" - "Ditahan" - "Tidak dikenal" - "Nomor pribadi" - "Telepon Umum" - "Telewicara" - "Panggilan terputus" - "Pengeras suara" - "Earpiece handset" - "Headset berkabel" - "Bluetooth" - "Kirim nada berikut?\n" - "Mengirim nada\n" - "Kirim" - "Ya" - "Tidak" - "Ganti karakter acak dengan" - "Telewicara %s" - "Nomor pesan suara" - "Memanggil" - "Memanggil ulang" - "Telewicara" - "Panggilan masuk" - "Panggilan masuk di telepon kerja" - "Panggilan diakhiri" - "Ditahan" - "Menutup panggilan" - "Sedang dalam panggilan" - "Nomor saya %s" - "Menyambungkan video" - "Video call" - "Meminta video" - "Tidak dapat menyambungkan video call" - "Permintaan video ditolak" - "Nomor panggilan balik Anda\n %1$s" - "Nomor panggilan balik darurat Anda\n %1$s" - "Memanggil" - "Panggilan tak terjawab" - "Panggilan tak terjawab" - "%s panggilan tak terjawab" - "Panggilan tak terjawab dari %s" - "Panggilan yang berlangsung" - "Panggilan telepon kerja yang sedang berlangsung" - "Panggilan Wi-Fi keluar" - "Panggilan Wi-Fi kerja yang sedang berlangsung" - "Ditahan" - "Panggilan masuk" - "Panggilan masuk di telepon kerja" - "Panggilan Wi-Fi masuk" - "Panggilan Wi-Fi masuk di telepon kerja" - "Video call masuk" - "Panggilan masuk yang diduga spam" - "Permintaan video masuk" - "Pesan suara baru" - "Pesan suara baru (%d)" - "Telepon %s" - "Nomor pesan suara tidak dikenal" - "Tidak ada layanan" - "Jaringan yang dipilih (%s) tidak tersedia" - "Jawab" - "Akhiri" - "Video" - "Suara" - "Terima" - "Tutup" - "Telepon balik" - "Pesan" - "Panggilan yang berlangsung di perangkat lain" - "Transfer Panggilan" - "Untuk melakukan panggilan, terlebih dahulu nonaktifkan mode Pesawat." - "Tidak terdaftar pada jaringan." - "Jaringan seluler tidak tersedia." - "Untuk melakukan panggilan telepon, masukkan nomor yang valid." - "Tidak dapat menelepon." - "Memulai urutan MMI..." - "Layanan tidak didukung." - "Tidak dapat beralih panggilan." - "Tidak dapat memisahkan panggilan." - "Tidak dapat mentransfer." - "Tidak dapat melakukan telewicara." - "Tidak dapat menolak panggilan." - "Tidak dapat melepas panggilan." - "Panggilan SIP" - "Panggilan darurat" - "Menghidupkan radio..." - "Tidak ada layanan. Mencoba lagi…" - "Tidak dapat menelepon. %s bukan nomor darurat." - "Tidak dapat menelepon. Panggil nomor darurat." - "Gunakan keyboard untuk memanggil" - "Tahan Panggilan" - "Mulai Kembali Panggilan" - "Akhiri Panggilan" - "Tampilkan tombol nomor" - "Sembunyikan tombol nomor" - "Bisukan" - "Suarakan" - "Tambahkan panggilan" - "Gabungkan panggilan" - "Tukar" - "Kelola panggilan" - "Kelola telewicara" - "Konferensi telepon" - "Kelola" - "Audio" - "Video call" - "Ubah ke panggilan suara" - "Beralih kamera" - "Nyalakan kamera" - "Matikan kamera" - "Opsi lainnya" - "Pemutar Dimulai" - "Pemutar Dihentikan" - "Kamera tidak siap" - "Kamera siap" - "Sesi panggilan tidak dikenal" - "Layanan" - "Siapkan" - "<Tidak disetel>" - "Setelan panggilan lainnya" - "Memanggil via %s" - "Masuk melalui %s" - "foto kontak" - "aktifkan pribadi" - "pilih kontak" - "Tulis respons Anda sendiri…" - "Batal" - "Kirim" - "Jawab" - "Kirim SMS" - "Tolak" - "Jawab sebagai video call" - "Jawab sebagai panggilan audio" - "Terima permintaan video" - "Tolak permintaan video" - "Terima permintaan transmisi video" - "Tolak permintaan transmisi video" - "Terima permintaan menerima video" - "Tolak permintaan menerima video" - "Geser ke atas untuk %s." - "Geser ke kiri untuk %s." - "Geser ke kanan untuk %s." - "Geser ke bawah untuk %s." - "Getar" - "Getar" - "Suara" - "Suara default (%1$s)" - "Nada dering ponsel" - "Bergetar saat berdering" - "Nada dering & Getar" - "Kelola telewicara" - "Nomor darurat" - "Foto profil" - "Kamera tidak aktif" - "melalui %s" - "Catatan telah dikirim" - "Pesan terbaru" - "Info bisnis" - "%.1f mil" - "%.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Buka jam %s" - "Hari ini buka jam %s" - "Tutup pukul %s" - "Hari ini tutup pukul %s" - "Buka sekarang" - "Sekarang tutup" - "Diduga telepon spam" - "Panggilan diakhiri %1$s" - "Nomor ini baru pertama kali menghubungi Anda." - "Kami menduga panggilan ini dari pelaku spam." - "Blokir/laporkan spam" - "Tambahkan kontak" - "Bukan spam" - diff --git a/InCallUI/res/values-is/strings.xml b/InCallUI/res/values-is/strings.xml deleted file mode 100644 index 480fd6eaadb1a9d17dff2f687444250058d84cf5..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-is/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Sími" - "Í bið" - "Óþekkt" - "Óþekkt númer" - "Símasjálfsali" - "Símafundur" - "Símtali slitið" - "Hátalari" - "Símahátalari" - "Höfuðtól með snúru" - "Bluetooth" - "Senda eftirfarandi tóna?\n" - "Sendir tóna\n" - "Senda" - "Já" - "Nei" - "Skipta algildisstaf út fyrir" - "Símafundur %s" - "Talhólfsnúmer" - "Hringir" - "Hringir aftur" - "Símafundur" - "Móttekið símtal" - "Vinnusímtal berst" - "Lagt á" - "Í bið" - "Leggur" - "Í símtali" - "Númerið mitt er %s" - "Tengir myndskeið" - "Myndsímtal" - "Biður um myndskeið" - "Ekki tókst að tengja myndsímtal" - "Myndsímtalsbeiðni hafnað" - "Svarhringingarnúmer þitt\n %1$s" - "Svarhringingarnúmer þitt í neyðartilvikum\n %1$s" - "Hringir" - "Ósvarað símtal" - "Ósvöruð símtöl" - "%s ósvöruð símtöl" - "Ósvarað símtal frá %s" - "Samtal í gangi" - "Vinnusímtal í gangi" - "Wi-Fi símtal stendur yfir" - "Vinnusímtal í gangi um Wi-Fi" - "Í bið" - "Móttekið símtal" - "Vinnusímtal berst" - "Wi-Fi símtal berst" - "Vinnusímtal berst um Wi-Fi" - "Myndsímtal berst" - "Símtal sem berst er hugsanlega úr ruslnúmeri" - "Myndbeiðni berst" - "Ný skilaboð í talhólfinu" - "Ný skilaboð í talhólfinu (%d)" - "Hringja í %s" - "Talhólfsnúmer ekki þekkt" - "Ekkert símasamband" - "Valið símkerfi (%s) er ekki tiltækt" - "Svara" - "Leggja á" - "Myndskeið" - "Tal" - "Samþykkja" - "Hunsa" - "Hringja til baka" - "Skilaboð" - "Símtal í gangi í öðru tæki" - "Flytja símtal" - "Til að hringja símtal þarftu fyrst að slökkva á flugstillingu." - "Ekki skráð á símkerfi." - "Farsímakerfi ekki til staðar." - "Sláðu inn gilt númer til að hringja símtal." - "Ekki hægt að hringja." - "Ræsir MMI-runu…" - "Þjónustan er ekki studd." - "Ekki hægt að skipta milli símtala." - "Ekki hægt að aðskilja símtal." - "Ekki hægt að flytja." - "Ekki hægt að halda símafund." - "Ekki hægt að hafna símtali." - "Ekki hægt að leggja á." - "SIP-símtal" - "Neyðarsímtal" - "Kveikir á loftneti…" - "Ekkert samband. Reynir aftur…" - "Getur ekki hringt. %s er ekki neyðarsímanúmer." - "Ekki hægt að hringja. Hringdu í neyðarnúmer" - "Notaðu lyklaborðið til að hringja" - "Setja símtal í bið" - "Halda símtali áfram" - "Leggja á" - "Sýna símatakkaborð" - "Fela símatakkaborð" - "Slökkva á hljóði" - "Kveikja á hljóði" - "Bæta við símtali" - "Sameina símtöl" - "Skipta milli" - "Stjórna símtölum" - "Stjórna símafundi" - "Símafundur" - "Stjórna" - "Hljóð" - "Myndsímtal" - "Breyta í símtal" - "Skipta um myndavél" - "Kveikja á myndavél" - "Slökkva á myndavél" - "Fleiri valkostir" - "Spilari ræstur" - "Spilari stöðvaður" - "Myndavél ekki tilbúin" - "Myndavél tilbúin" - "Óþekkt atvik símtalslotu" - "Þjónusta" - "Uppsetning" - "<Ekki valið>" - "Aðrar símtalsstillingar" - "Hringt í gegnum %s" - "Berst í gegnum %s" - "mynd tengiliðar" - "tala í einrúmi" - "velja tengilið" - "Skrifaðu eigið svar…" - "Hætta við" - "Senda" - "Svara" - "Senda SMS-skilaboð" - "Hafna" - "Svara sem myndsímtali" - "Svara sem símtali" - "Samþykkja beiðni um myndsímtal" - "Hafna beiðni um myndsímtal" - "Samþykkja beiðni um sendingu myndsímtals" - "Hafna beiðni um sendingu myndsímtals" - "Samþykkja beiðni um móttöku myndsímtals" - "Hafna beiðni um móttöku myndsímtals" - "Strjúktu upp til að %s." - "Strjúktu til vinstri til að %s." - "Strjúktu til hægri til að %s." - "Strjúktu niður til að %s." - "Titra" - "Titra" - "Hljóð" - "Sjálfgefið hljóð (%1$s)" - "Hringitónn síma" - "Titra við hringingu" - "Hringitónn og titringur" - "Stjórna símafundi" - "Neyðarnúmer" - "Prófílmynd" - "Slökkt á myndavél" - "úr %s" - "Glósa send" - "Nýleg skilaboð" - "Fyrirtækjaupplýsingar" - %.1f míl. fjarlægð" - %.1f km fjarlægð" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opið á morgun frá kl. %s" - "Opið í dag frá kl. %s" - "Lokað kl. %s" - "Var lokað í dag kl. %s" - "Opið núna" - "Lokað núna" - "Grunur um svikasímtal" - "Símtali lokið %1$s" - "Þetta er í fyrsta sinn sem hringt er í þig úr þessu númeri." - "Okkur grunaði að þetta símtal væri úr ruslnúmeri." - "Bannlisti/tilkynna" - "Bæta tengilið við" - "Ekki ruslnúmer" - diff --git a/InCallUI/res/values-it/strings.xml b/InCallUI/res/values-it/strings.xml deleted file mode 100644 index c9b49501b919a4987ff662438466de1e825bd5bd..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-it/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefono" - "In attesa" - "Sconosciuto" - "Numero privato" - "Telefono pubblico" - "Audioconferenza" - "Chiamata persa" - "Altoparlante" - "Auricolare telefono" - "Auricolare con cavo" - "Bluetooth" - "Inviare i numeri successivi?\n" - "Invio toni\n" - "Invia" - "Sì" - "No" - "Sostituisci carattere jolly con" - "Audioconferenza: %s" - "Numero segreteria" - "Chiamata in corso" - "Ricomposizione" - "Audioconferenza" - "Chiamata in arrivo" - "Chiamata lavoro in arrivo" - "Chiamata terminata" - "In attesa" - "In fase di chiusura" - "Chiamata in corso" - "Il mio numero è: %s" - "Collegamento video" - "Videochiamata" - "Richiesta video in corso" - "Impossibile effettuare una videochiamata" - "Richiesta video rifiutata" - "Numero da richiamare:\n %1$s" - "Numero da richiamare in caso di emergenza:\n %1$s" - "Composizione in corso" - "Chiamata persa" - "Chiamate perse" - "%s chiamate perse" - "Chiamata senza risposta da %s" - "Chiamata in corso" - "Chiamata di lavoro in corso" - "Chiamata Wi-Fi in corso" - "Chiamata di lavoro tramite Wi-Fi in corso" - "In attesa" - "Chiamata in arrivo" - "Chiamata di lavoro in arrivo" - "Chiamata Wi-Fi in arrivo" - "Chiamata di lavoro in arrivo tramite Wi-Fi" - "Videochiamata in arrivo" - "Chiamata di presunto spam in arrivo" - "Richiesta video in arrivo" - "Nuovo messaggio vocale" - "Nuovo messaggio vocale (%d)" - "Componi %s" - "Numero segreteria sconosciuto" - "Nessun servizio" - "Rete selezionata (%s) non disponibile" - "Rispondi" - "Riaggancia" - "Video" - "Voce" - "Accetta" - "Ignora" - "Richiama" - "Messaggio" - "Chiamata in corso su un altro dispositivo" - "Trasferisci chiamata" - "Per fare una chiamata, disattiva la modalità aereo." - "Non registrato sulla rete." - "Rete dati non disponibile." - "Per fare una chiamata, inserisci un numero valido." - "Impossibile chiamare." - "Avvio sequenza MMI..." - "Servizio non supportato." - "Impossibile cambiare chiamata." - "Impossibile separare la chiamata." - "Impossibile trasferire." - "Impossibile fare una chiamata in conferenza." - "Impossibile rifiutare la chiamata." - "Impossibile riagganciare." - "Chiamata SIP" - "Chiamata di emergenza" - "Attivazione segnale cellulare..." - "Nessun servizio. Nuovo tentativo…" - "Impossibile chiamare. %s non è un numero di emergenza." - "Impossibile chiamare. Componi un numero di emergenza." - "Usa tastiera" - "Metti in attesa la chiamata" - "Riprendi chiamata" - "Termina chiamata" - "Mostra tastierino" - "Nascondi tastierino" - "Disattiva audio" - "Riattiva audio" - "Aggiungi chiamata" - "Unisci chiamate" - "Scambia" - "Gestisci chiamate" - "Gestisci audioconferenza" - "Audioconferenza" - "Gestisci" - "Audio" - "Videochiam" - "Passa a chiamata vocale" - "Cambia fotocamera" - "Attiva fotocamera" - "Disattiva fotocamera" - "Altre opzioni" - "Player avviato" - "Player interrotto" - "La fotocamera non è pronta" - "Fotocamera pronta" - "Evento sessione chiamata sconosciuto" - "Servizio" - "Configura" - "<Non impostato>" - "Altre impostazioni di chiamata" - "Chiamate tramite %s" - "In arrivo tramite %s" - "foto contatto" - "Privato" - "seleziona contatto" - "Scrivi risposta personale..." - "Annulla" - "Invia" - "Rispondi" - "Invia SMS" - "Rifiuta" - "Rispondi con videochiamata" - "Rispondi con chiamata audio" - "Accetta richiesta video" - "Rifiuta richiesta video" - "Accetta richiesta di trasmissione video" - "Rifiuta richiesta di trasmissione video" - "Accetta richiesta di ricevimento video" - "Rifiuta richiesta di ricevimento video" - "Scorri verso l\'alto per %s." - "Scorri verso sinistra per %s." - "Scorri verso destra per %s." - "Scorri verso il basso per %s." - "Vibrazione" - "Vibrazione" - "Suono" - "Suono predefinito (%1$s)" - "Suoneria telefono" - "Vibrazione quando squilla" - "Suoneria e vibrazione" - "Gestisci audioconferenza" - "Numero di emergenza" - "Foto del profilo" - "Fotocamera disattivata" - "tramite %s" - "Nota inviata" - "Messaggi recenti" - "Informazioni sull\'attività" - "Distante %.1f mi" - "Distante %.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Apre domani alle ore %s" - "Apre oggi alle ore %s" - "Chiude alle ore %s" - "Ha chiuso oggi alle ore %s" - "Aperto ora" - "Ora chiuso" - "Presunto spammer" - "Chiamata terminata %1$s" - "È la prima volta che questo numero ti chiama." - "Sospettavamo che questa chiamata provenisse da uno spammer." - "Blocca/Segnala spam" - "Aggiungi contatto" - "Non spam" - diff --git a/InCallUI/res/values-iw/strings.xml b/InCallUI/res/values-iw/strings.xml deleted file mode 100644 index c75c8da0d88b92fc534529ec82a24c7ac91fb6a4..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-iw/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "טלפון" - "בהמתנה" - "לא ידוע" - "מספר פרטי" - "טלפון ציבורי" - "שיחת ועידה" - "השיחה נותקה" - "רמקול" - "אוזנייה" - "אוזניות עם חיבור חוטי" - "Bluetooth" - "האם לשלוח את הצלילים הבאים?\n" - "שולח צלילים\n" - "שלח" - "כן" - "לא" - "החלף את התו הכללי ב" - "שיחת ועידה %s" - "המספר של הדואר הקולי" - "מחייג" - "מחייג שוב" - "שיחת ועידה" - "שיחה נכנסת" - "שיחת עבודה נכנסת" - "השיחה הסתיימה" - "בהמתנה" - "מנתק" - "בשיחה" - "המספר שלי הוא %s" - "מחבר וידאו" - "שיחת וידאו" - "מבקש וידאו" - "לא ניתן לחבר שיחת וידאו" - "בקשת וידאו נדחתה" - "המספר שלך להתקשרות חזרה\n %1$s" - "המספר שלך להתקשרות חזרה במצב חירום\n %1$s" - "מחייג" - "שיחה שלא נענתה" - "שיחות שלא נענו" - "%s שיחות שלא נענו" - "שיחה שלא נענתה מאת %s" - "שיחה פעילה" - "שיחת עבודה פעילה" - "‏שיחת Wi-Fi פעילה" - "‏שיחת עבודה פעילה ברשת WiFi" - "בהמתנה" - "שיחה נכנסת" - "שיחת עבודה נכנסת" - "‏שיחת Wi-Fi נכנסת" - "‏שיחת עבודה נכנסת ברשת WiFi" - "שיחת וידאו נכנסת" - "השיחה הנכנסת חשודה כספאם" - "בקשת וידאו נכנסת" - "דואר קולי חדש" - "דואר קולי חדש (%d)" - "‏חייג ‎%s‎" - "המספר של הדואר הקולי אינו ידוע" - "אין שירות" - "הרשת שנבחרה (%s) לא זמינה" - "ענה" - "נתק" - "וידאו" - "קול" - "אשר" - "בטל" - "התקשר חזרה" - "שלח הודעה" - "באחד מהמכשירים האחרים מתבצעת שיחה" - "העבר את השיחה" - "כדי להתקשר, כבה תחילה את מצב טיסה." - "לא רשום ברשת." - "רשת סלולרית אינה זמינה." - "כדי להתקשר, הזן מספר טלפון חוקי." - "לא ניתן להתקשר." - "‏מתחיל רצף MMI…" - "שירות לא נתמך." - "לא ניתן לעבור בין שיחות." - "לא ניתן להפריד שיחה." - "לא ניתן להעביר." - "לא ניתן לבצע שיחת ועידה." - "לא ניתן לדחות שיחה." - "לא ניתן להתקשר." - "‏שיחת SIP" - "שיחת חירום" - "מפעיל את הרדיו…" - "אין שירות. מנסה שוב..." - "לא ניתן להתקשר. %s אינו מספר חירום." - "לא ניתן להתקשר. חייג למספר חירום." - "השתמש במקלדת כדי לחייג" - "החזק שיחה" - "המשך בשיחה" - "סיים שיחה" - "הצגת לוח החיוג" - "הסתרת לוח החיוג" - "השתקה" - "ביטול ההשתקה" - "הוסף שיחה" - "מזג שיחות" - "החלף" - "נהל שיחות" - "נהל שיחת ועידה" - "שיחת ועידה" - "ניהול" - "אודיו" - "שיחת וידאו" - "שנה לשיחה קולית" - "החלף מצלמה" - "הפעל את המצלמה" - "כבה את המצלמה" - "אפשרויות נוספות" - "הנגן הופעל" - "הנגן הפסיק" - "המצלמה לא מוכנה" - "המצלמה מוכנה" - "אירוע הפעלת שיחה לא ידוע" - "שירות" - "הגדרות" - "<לא הוגדר>" - "הגדרות אחרות של שיחה" - "שיחה באמצעות %s" - "שיחה נכנסת באמצעות %s" - "תמונה של איש קשר" - "עבור לשיחה פרטית" - "בחר איש קשר" - "כתוב תגובה משלך..." - "בטל" - "שלח" - "ענה" - "‏שלח SMS" - "דחה" - "ענה כשיחת וידאו" - "ענה כשיחת אודיו" - "קבל בקשת וידאו" - "דחה בקשת וידאו" - "אשר את הבקשה לשידור וידאו" - "דחה את הבקשה לשידור וידאו" - "אשר את הבקשה לקבלת וידאו" - "דחה את הבקשה לקבלת וידאו" - "הסט למעלה כדי %s." - "הסט שמאלה כדי %s." - "הסט ימינה כדי %s." - "הסט למטה כדי %s." - "רטט" - "רטט" - "צליל" - "צליל ברירת מחדל (%1$s)" - "רינגטון של טלפון" - "רטט בעת צלצול" - "רינגטון ורטט" - "נהל שיחת ועידה" - "מספר חירום" - "תמונת פרופיל" - "המצלמה כבויה" - "באמצעות %s" - "ההערה נשלחה" - "הודעות אחרונות" - "פרטי עסק" - "במרחק %.1f מייל" - "במרחק %.1f ק\"מ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ייפתח מחר ב-%s" - "נפתח היום ב-%s" - "נסגר ב-%s" - "נסגר היום ב-%s" - "פתוח עכשיו" - "סגור עכשיו" - "חשד לשיחת ספאם" - "‏השיחה הסתיימה %1$s" - "זוהי הפעם הראשונה שמתקשרים אליך מהמספר הזה." - "אנו חושדים שהמספר הזה הוא של שולח ספאם." - "חסימה/דיווח על ספאם" - "הוספת איש קשר" - "לא ספאם" - diff --git a/InCallUI/res/values-ja/strings.xml b/InCallUI/res/values-ja/strings.xml deleted file mode 100644 index 551d896b55f36149b71e29c9501f5df7e877bf74..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ja/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "電話" - "保留中" - "不明" - "非通知" - "公衆電話" - "グループ通話" - "通話が遮断されました" - "スピーカー" - "モバイル端末のイヤホン" - "有線ヘッドセット" - "Bluetooth" - "次の番号を送信しますか?\n" - "番号を送信中\n" - "送信" - "はい" - "いいえ" - "ワイルド文字を置換:" - "グループ通話 %s" - "ボイスメールの番号" - "発信中" - "リダイヤル中" - "グループ通話" - "着信中" - "仕事の通話が着信中" - "通話終了" - "保留中" - "通話終了" - "通話中" - "この電話の番号: %s" - "ビデオハングアウトに接続中" - "ビデオハングアウト" - "ビデオハングアウトをリクエスト中" - "ビデオハングアウトの接続エラー" - "ビデオハングアウトのリクエスト不承認" - "コールバック先\n %1$s" - "緊急通報コールバック先\n %1$s" - "発信中" - "不在着信" - "不在着信" - "不在着信 %s 件" - "%s さんからの不在着信" - "通話中" - "仕事の通話中" - "Wi-Fi 通話中" - "仕事の Wi-Fi 通話中" - "保留中" - "着信中" - "仕事の通話が着信中" - "Wi-Fi 通話が着信中" - "仕事の Wi-Fi 通話が着信中" - "ビデオハングアウトが着信中" - "迷惑電話の疑いがある通話を着信しています" - "ビデオハングアウト リクエストが着信中" - "新着のボイスメール" - "新着のボイスメール(%d 件)" - "%s に発信" - "ボイスメールの番号が不明です" - "通信サービスはありません" - "選択したネットワーク(%s)が利用できません" - "電話に出る" - "通話終了" - "ビデオ" - "音声" - "受ける" - "拒否する" - "コールバック" - "メッセージ" - "別の端末で通話中" - "通話を転送" - "機内モードを OFF にしてから発信してください。" - "ご加入の通信サービスがありません。" - "モバイル ネットワークが利用できません。" - "発信するには、有効な番号を入力してください。" - "発信できません。" - "MMI シーケンスを開始しています..." - "サービスはサポートされていません。" - "通話を切り替えられません。" - "通話を分割できません。" - "転送できません。" - "グループ通話できません。" - "着信を拒否できません。" - "通話を解放できません。" - "SIP 通話" - "緊急通報" - "無線通信を ON にしています..." - "通信サービスはありません。もう一度お試しください…" - "発信できません。%s は緊急通報番号ではありません。" - "発信できません。緊急通報番号におかけください。" - "キーボードで番号を入力してください" - "通話を保留" - "通話を再開" - "通話を終了" - "ダイヤルパッドを表示" - "ダイヤルパッドを非表示" - "ミュート" - "ミュートを解除" - "通話を追加" - "グループ通話" - "切り替え" - "通話を管理" - "グループ通話オプション" - "グループ通話" - "管理" - "音声" - "ビデオ" - "音声通話に変更" - "カメラを切り替え" - "カメラを ON にする" - "カメラを OFF にする" - "その他のオプション" - "プレーヤーを開始しました" - "プレーヤーを停止しました" - "カメラが準備できていません" - "カメラが準備できました" - "不明な通話セッション イベントです" - "サービス" - "セットアップ" - "<未設定>" - "その他の通話設定" - "%s で発信中" - "%s で着信中" - "連絡先の写真" - "個別通話に切り替え" - "連絡先を選択" - "カスタム返信を作成..." - "キャンセル" - "送信" - "電話に出る" - "SMS を送信する" - "拒否" - "ビデオハングアウトで電話に出る" - "音声通話で電話に出る" - "ビデオハングアウト リクエストを承認する" - "ビデオハングアウト リクエストを拒否する" - "ビデオハングアウト送信リクエストを承認する" - "ビデオハングアウト送信リクエストを拒否する" - "ビデオハングアウト受信リクエストを承認する" - "ビデオハングアウト受信リクエストを拒否する" - "上にスライドして%sを行います。" - "左にスライドして%sを行います。" - "右にスライドして%sを行います。" - "下にスライドして%sを行います。" - "バイブレーション" - "バイブレーション" - "着信音" - "デフォルトの通知音(%1$s)" - "着信音" - "着信時のバイブレーション" - "着信音とバイブレーション" - "グループ通話オプション" - "緊急通報番号" - "プロフィール写真" - "カメラ OFF" - "%s に着信" - "メモを送信しました" - "最近のメッセージ" - "ビジネス情報" - "%.1f マイル内" - "%.1f km 内" - "%1$s%2$s" - "%1$s%2$s" - "%1$s%2$s" - "明日 %sに営業開始" - "本日 %sに営業開始" - "%sに営業終了" - "本日 %sに営業終了" - "現在営業中" - "営業終了" - "迷惑電話の疑いあり" - "通話が終了しました %1$s" - "この番号からの通話を受信したのはこれが初めてです。" - "この通話は迷惑電話の可能性があります。" - "ブロック / 報告" - "連絡先に追加" - "迷惑電話ではない" - diff --git a/InCallUI/res/values-ka/strings.xml b/InCallUI/res/values-ka/strings.xml deleted file mode 100644 index b0100656166524109bea61bd33be1031eb10cc89..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ka/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ტელეფონი" - "მოცდის რეჟიმში" - "უცნობი" - "დაფარული ნომერი" - "ტელეფონ-ავტომატი" - "საკონფერენციო ზარი" - "ზარი შეწყდა" - "დინამიკი" - "ყურსაცვამის საყურისი" - "სადენიანი ყურსაცვამი" - "Bluetooth" - "გსურთ შემდეგი ტონების გაგზავნა?\n" - "ტონების გაგზავნა\n" - "გაგზავნა" - "დიახ" - "არა" - "ჩანაცვლების სიმბოლო ჩანაცვლდეს შემდეგით:" - "საკონფერენციო ზარი: %s" - "ხმოვანი ფოსტის ნომერი" - "მიმდინარეობს აკრეფა" - "იკრიფება ხელახლა" - "საკონფერენციო ზარი" - "შემომავალი ზარი" - "შემომავალი ზარი (სამსახური)" - "ზარი დასრულდა" - "მოცდის რეჟიმში" - "მიმდინარეობს გათიშვა" - "საუბრის რეჟიმში" - "ჩემი ნომერია %s" - "მიმდინარეობს ვიდეოს დაკავშირება" - "ვიდეო ზარი" - "მიმდინარეობს ვიდეოს მოთხოვნა" - "ვიდეო ზარის დაკავშირება ვერ მოხერხდა" - "ვიდეოს მოთხოვნა უარყოფილია" - "თქვენი ნომერი გადმორეკვისთვის\n %1$s" - "თქვენი ნომერი გადაუდებელი გადმორეკვისთვის\n %1$s" - "მიმდინარეობს აკრეფა" - "გამოტოვებული ზარი" - "გამოტოვებული ზარები" - "%s გამოტოვებული ზარი" - "გამოტოვებული ზარი %s-ისგან" - "მიმდინარე ზარი" - "მიმდინარე ზარი (სამსახური)" - "მიმდინარე Wi-Fi ზარი" - "მიმდინარე Wi-Fi ზარი (სამსახური)" - "მოცდის რეჟიმში" - "შემომავალი ზარი" - "შემომავალი ზარი (სამსახური)" - "შემომავალი Wi-Fi ზარი" - "შემომავალი Wi-Fi ზარი (სამსახური)" - "შემომავალი ვიდეო ზარი" - "შემომავალი ზარი - სავარაუდოდ სპამი" - "შემომავალი ვიდეოს მოთხოვნა" - "ახალი ხმოვანი შეტყობინება" - "ახალი ხმოვანი შეტყობინება (%d)" - "%s-ზე დარეკვა" - "ხმოვანი ფოსტის ნომერი უცნობია" - "სერვისი არ არის" - "არჩეული ქსელი (%s) მიუწვდომელია" - "პასუხი" - "გათიშვა" - "ვიდეო" - "ხმოვანი" - "მიღება" - "დახურვა" - "გადარეკვა" - "შეტყობინება" - "სხვა მოწყობილობაზე მიმდინარე ზარი" - "ზარის ტრანსფერი" - "ზარის განსახორციელებლად, ჯერ გამორთეთ თვითმფრინავის რეჟიმი." - "არ არის რეგისტრირებული ქსელში." - "ფიჭური ქსელი მიუწვდომელია." - "ზარის განსახორციელებლად, შეიყვანეთ სწორი ნომერი." - "დარეკვა ვერ ხერხდება." - "MMI თანმიმდევრობის დაწყება…" - "სერვისი არ არის მხარდაჭერილი." - "ზარების გადართვა ვერ ხერხდება." - "ზარის განცალკევება ვერ ხერხდება." - "გადამისამართება ვერ ხერხდება." - "საკონფერენციო ზარის განხორციელება ვერ ხერხდება." - "ზარის უარყოფა ვერ ხერხდება." - "ზარ(ებ)ის გათიშვა ვერ ხერხდება." - "SIP ზარი" - "გადაუდებელი ზარი" - "მიმდინარეობს რადიოს ჩართვა…" - "სერვისი არ არის. მიმდინარეობს ხელახლა ცდა…" - "დარეკვა ვერ ხერხდება. %s არ არის გადაუდებელი დახმარების ნომერი." - "დარეკვა ვერ ხერხდება. აკრიფეთ გადაუდებელი დახმარების ნომერი." - "ნომრის ასაკრეფად გამოიყენეთ კლავიატურა" - "მოცდის რეჟიმზე გადაყვანა" - "ზარის განახლება" - "ზარის დასრულება" - "ციფერბლატის ჩვენება" - "ციფერბლატის დამალვა" - "დადუმება" - "დადუმების გაუქმება" - "ზარის დამატება" - "ზარების გაერთიანება" - "ჩანაცვლება" - "ზარების მართვა" - "საკონფერენციო ზარის მართვა" - "საკონფერენციო ზარი" - "მართვა" - "აუდიო" - "ვიდეო ზარი" - "ხმოვან ზარზე გადართვა" - "კამერის გადართვა" - "კამერის ჩართვა" - "კამერის გამორთვა" - "სხვა ვარიანტები" - "დამკვრელი ჩაირთო" - "დამკვრელი გამოირთო" - "კამერა არ არის მზად" - "კამერა მზადაა" - "ზარის სესიის უცნობი მოვლენა" - "სერვისი" - "დაყენება" - "<არ არის დაყენებული>" - "ზარის სხვა პარამეტრები" - "მიმდინარეობს დარეკვა %s-ის მეშვეობით" - "შემომავალი ზარი %s-დან" - "კონტაქტის ფოტო" - "პირადი რეჟიმი" - "კონტაქტის არჩევა" - "საკუთარის შექმნა..." - "გაუქმება" - "გაგზავნა" - "პასუხი" - "SMS-ის გაგზავნა" - "უარყოფა" - "პასუხი ვიდეო ზარის სახით" - "პასუხი ხმოვანი ზარის სახით" - "ვიდეოს მოთხოვნის მიღება" - "ვიდეოს მოთხოვნის უარყოფა" - "ვიდეოს გადაცემის მოთხოვნის მიღება" - "ვიდეოს გადაცემის მოთხოვნის უარყოფა" - "ვიდეოს მიღების მოთხოვნაზე დათანხმება" - "ვიდეოს მიღების მოთხოვნის უარყოფა" - "გაასრიალეთ ზემოთ, რათა შესრულდეს %s." - "გაასრიალეთ მარცხნივ, რათა შესრულდეს %s." - "გაასრიალეთ მარჯვნივ, რათა შესრულდეს %s." - "გაასრიალეთ ქვემოთ, რათა შესრულდეს %s." - "ვიბრაცია" - "ვიბრაცია" - "ხმა" - "ნაგულისხმები ხმა (%1$s)" - "ტელეფონის ზარი" - "ვიბრაცია დარეკვისას" - "ზარის მელოდია და ვიბრაცია" - "საკონფერენციო ზარის მართვა" - "გადაუდებელი დახმარების ნომერი" - "პროფილის ფოტო" - "კამერა გამორთულია" - "%s-დან" - "შენიშვნა გაიგზავნა" - "ბოლო შეტყობინებები" - "ბიზნეს-ინფორმაცია" - "%.1f მილში" - "%.1f კმ-ში" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "იხსნება ხვალ %s-ზე" - "იხსნება დღეს %s-ზე" - "იკეტება %s-ზე" - "დაიკეტა დღეს %s-ზე" - "ახლა ღიაა" - "ახლა დაკეტილია" - "სავარაუდ.სპამ.აბონ." - "ზარი დასრულდა %1$s" - "ამ ნომრიდან პირველად დაგირეკეს." - "ეჭვი გვაქვს, რომ ეს ზარი სპამია." - "დაბლოკ./სპამ.შეტყობ." - "კონტაქტის დამატება" - "არ არის სპამი" - diff --git a/InCallUI/res/values-kk/strings.xml b/InCallUI/res/values-kk/strings.xml deleted file mode 100644 index 07ea6fb7824f11f1675a76c08f4b7e2af4d7d12e..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-kk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "Күтуде" - "Белгісіз" - "Жеке нөмір" - "Автомат-телефон" - "Конференциялық қоңырау" - "Қоңырау үзілді" - "Динамик" - "Телефон құлаққабы" - "Сымды құлақаспап жинағы" - "Bluetooth" - "Келесі әуендер жіберілсін бе?\n" - "Жіберу әуендері\n" - "Жіберу" - "Иә" - "Жоқ" - "Қойылмалы таңбаны келесі таңбамен алмастыру" - "%s конференциялық қоңырауы" - "Дауыстық пошта нөмірі" - "Терілуде" - "Қайта терілуде" - "Конференциялық қоңырау" - "Кіріс қоңырау" - "Кіріс жұмыс қоңырауы" - "Қоңырау аяқталды" - "Күтуде" - "Қоңырау аяқталуда" - "Қоңырауда" - "Mенің нөмірім — %s" - "Бейне қосылуда" - "Бейне қоңырау" - "Бейне сұралуда" - "Бейне қоңырауға қосылу мүмкін емес" - "Бейне сұрауы қабылданбады" - "Кері қоңырау шалу нөміріңіз\n %1$s" - "Төтенше кері қоңырау шалу нөміріңіз\n %1$s" - "Терілуде" - "Өткізіп алған қоңырау" - "Өткізіп алған қоңыраулар" - "%s өткізіп алған қоңырау" - "%s қоңырауы өткізіп алынған" - "Ағымдағы қоңырау" - "Ағымдағы жұмыс қоңырауы" - "Ағымдағы Wi-Fi қоңырауы" - "Ағымдағы Wi-Fi жұмыс қоңырауы" - "Күтуде" - "Кіріс қоңырау" - "Кіріс жұмыс қоңырауы" - "Кіріс Wi-Fi қоңырауы" - "Кіріс Wi-Fi жұмыс қоңырауы" - "Кіріс бейне қоңырау" - "Кіріс қоңырауы спам болуы мүмкін" - "Кіріс бейне сұрау" - "Жаңа дауыстық хабар" - "Жаңа дауыстық хабар (%d)" - "%s нөмірін теру" - "Дауыстық пошта нөмірі белгісіз" - "Қызмет жоқ" - "Таңдалған (%s) желісі қол жетімді емес" - "Жауап" - "Қоңырауды аяқтау" - "Бейне" - "Дауыс" - "Қабылдау" - "Қабылдамау" - "Кері қоңырау шалу" - "Хабар" - "Қоңырау басқа құрылғыдан шалынуда" - "Қоңырауды басқа құрылғыға бағыттау" - "Қоңырау шалу үшін алдымен ұшақ режимін өшіріңіз." - "Желіде тіркелмеген." - "Ұялы желі қол жетімді емес." - "Қоңырау шалу үшін жарамды нөмірді енгізіңіз." - "Қоңырау шалу мүмкін емес." - "MMI қатарын бастау…" - "Қызметке қолдау көрсетілмейді." - "Қоңырауларды ауыстыру мүмкін емес." - "Қоңырауды бөлу мүмкін емес." - "Тасымалдау мүмкін емес." - "Конференция мүмкін емес." - "Қоңырауды қабылдамау мүмкін емес." - "Қоңырау(лар)ды босату мүмкін емес." - "SIP қоңырауы" - "Төтенше қоңырау" - "Радио қосылуда…" - "Қызмет жоқ. Әрекет қайталануда…" - "Қоңырау шалу мүмкін емес. %s төтенше нөмір емес." - "Қоңырау шалу мүмкін емес. Төтенше нөмірді теріңіз." - "Теру үшін пернетақтаны пайдалану" - "Қоңырауды ұстап тұру" - "Қоңырауды жалғастыру" - "Қоңырауды аяқтау" - "Теру тақтасын көрсету" - "Теру тақтасын жасыру" - "Дыбысты өшіру" - "Дыбысын қосу" - "Қоңырау қосу" - "Қоңырауларды біріктіру" - "Алмастыру" - "Қоңырауларды басқару" - "Конференциялық қоңырауды басқару" - "Конференциялық қоңырау" - "Басқару" - "Aудио" - "Бейне қоңырау" - "Дауыстық қоңырауға өзгерту" - "Камераны ауыстыру" - "Камераны қосу" - "Камераны өшіру" - "Қосымша опциялар" - "Ойнатқыш іске қосылды" - "Ойнатқыш тоқтатылды" - "Камера дайын емес" - "Камера дайын" - "Белгісіз қоңырау сеансы оқиғасы" - "Қызмет" - "Реттеу" - "<Орнатылмаған>" - "Басқа қоңырау параметрлері" - "%s арқылы қоңырау шалу" - "%s арқылы кіріс" - "контакт фотосуреті" - "жеке қоңырауға ауысу" - "контакт таңдау" - "Өзіңіздікін жазыңыз..." - "Бас тарту" - "Жіберу" - "Жауап" - "SMS жіберу" - "Қабылдамау" - "Бейне қоңырауға жауап беру" - "Аудио қоңырауға жауап беру" - "Бейне сұрауды қабылдау" - "Бейне сұрауды қабылдамау" - "Бейне тасымалдау сұрауын қабылдау" - "Бейне тасымалдау сұрауын қабылдамау" - "Бейне алу сұрауын қабылдау" - "Бейне алу сұрауын қабылдамау" - "%s үшін жоғары сырғытыңыз." - "%s үшін сол жаққа сырғытыңыз." - "%s үшін оң жаққа сырғытыңыз." - "%s үшін төмен сырғытыңыз." - "Діріл" - "Діріл" - "Дыбыс" - "Әдепкі дыбыс (%1$s)" - "Телефонның қоңырау әуені" - "Шылдырлағанда дірілдеу" - "Қоңырау әуені және діріл" - "Конференциялық қоңырауды басқару" - "Төтенше нөмір" - "Профиль фотосуреті" - "Камераны өшіру" - "%s арқылы" - "Ескертпе жіберілді" - "Жақындағы хабарлар" - "Іскери ақпарат" - "%.1f миля қашықтықта" - "%.1f км қашықтықта" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Ертең %s уақытында ашылады" - "Бүгін %s уақытында ашылады" - "%s уақытында жабылады" - "Бүгін %s уақытында жабық" - "Қазір ашық" - "Қазір жабық" - "Спам қоңырау шалушы болуы мүмкін" - "Қоңырау аяқталды (%1$s)" - "Бұл нөмірдің сізге алғашқы қоңырау шалуы." - "Бұл қоңырау спамер деп күдіктенеміз." - "Бөгеу/спамға жіберу" - "Контакт қосу" - "Спам емес" - diff --git a/InCallUI/res/values-km/strings.xml b/InCallUI/res/values-km/strings.xml deleted file mode 100644 index 7a31d8933967b4ef4811ce21f2f2098172e7cb4f..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-km/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ទូរស័ព្ទ" - "រង់ចាំ" - "មិនស្គាល់" - "លេខ​ឯកជន" - "ទូរស័ព្ទសាធារណៈ" - "ការហៅជាក្រុម" - "ការហៅទូរស័ព្ទបានដាក់ចុះ" - "ឧបករណ៍បំពងសម្លេង" - "អូប៉ាល័រសំឡេងទូរស័ព្ទ" - "កាសមានខ្សែ" - "ប៊្លូធូស" - "ផ្ញើសំឡេងដូចខាងក្រោមឬ?\n" - "ផ្ញើ​សំឡេង \n" - "ផ្ញើ" - "បាទ/ចាស" - "ទេ" - "ជំនួស​តួ​អក្សរ​ជំនួស​ដោយ" - "ការហៅជាក្រុម %s" - "លេខ​សារ​ជា​សំឡេង" - "កំពុងហៅ" - "ការចុចហៅឡើងវិញ" - "ការហៅជាក្រុម" - "ការហៅចូល" - "កំពុងហៅចូលពីកន្លែងការងារ" - "បាន​បញ្ចប់​ការ​ហៅ" - "រង់ចាំ" - "បញ្ចប់​ការ​សន្ទនា" - "កំពុង​ហៅ" - "លេខ​របស់​ខ្ញុំ​គឺ %s" - "ភ្ជាប់​វីដេអូ" - "ហៅជាវីដេអូ" - "ស្នើ​វីដេអូ" - "មិនអាចភ្ជាប់ការហៅជាវីដេអូបានទេ" - "បានបដិសេធសំណើហៅជាវីដេអូ" - "លេខហៅទៅវិញរបស់អ្នក\n%1$s" - "លេខហៅទៅវិញពេលអាសន្នរបស់អ្នក\n %1$s" - "កំពុង​ហៅ" - "ខកខាន​ទទួល" - "ខកខាន​ទទួល" - "ខកខានទទួល %s ដង" - "ខកខាន​ទទួល​ពី %s" - "កំពុង​បន្ត​ការ​ហៅ" - "ការហៅពីកន្លែងការងារកំពុងដំណើរការ" - "ការហៅតាម Wi-Fi កំពុងបន្ត" - "ការហៅតាម Wi-Fi ពីកន្លែងការងារកំពុងដំណើរការ" - "រង់ចាំ" - "ការហៅចូល" - "កំពុងហៅចូលពីកន្លែងការងារ" - "មានការហៅចូលតាម Wi-Fi" - "កំពុងហៅចូលពីកន្លែងការងារតាម Wi-Fi" - "ការ​ហៅចូលជា​វីដេអូ​" - "ការ​ហៅ​បន្លំ​​ចូល​​​ដែល​សង្ស័យ" - "សំណើ​ការ​ហៅ​ជា​វីដេអូ​ចូល" - "សារ​ជា​សំឡេង​ថ្មី" - "សារ​ជា​សំឡេង​ថ្មី (%d)" - "ហៅ %s" - "លេខសារជាសំឡេងមិនស្គាល់" - "គ្មានសេវាទេ" - "បណ្ដាញ​ដែល​បាន​ជ្រើស ( %s ) មិន​អាច​ប្រើ​បាន​ទេ" - "ឆ្លើយតប" - "បញ្ចប់​ការ​សន្ទនា" - "វីដេអូ" - "សំឡេង" - "ព្រម​ទទួល" - "បដិសេធ" - "ហៅ​ទៅ​វិញ" - "សារ" - "ការ​ហៅ​កំពុង​ដំណើរការ​លើ​ឧបករណ៍​ផ្សេង" - "ផ្ទេរ​ការហៅ" - "ដើម្បីកំណត់ការហៅ សូមបិទរបៀបពេលជិះយន្តហោះជាមុនសិន" - "មិនបានចុះឈ្មោះនៅលើបណ្ដាញទេ" - "បណ្ដាញចល័តមិនអាចប្រើបានទេ" - "ដើម្បីធ្វើការហៅ សូមបញ្ចូលលេខដែលត្រឹមត្រូវ" - "មិនអាចហៅបានទេ" - "កំពុងចាប់ផ្តើមលំដាប់ MMI…" - "សេវាកម្មមិនត្រូវបានគាំទ្រទេ" - "មិនអាចប្តូរការហៅបានទេ" - "មិនអាចបំបែកការហៅបានទេ" - "មិនអាចផ្ទេរបានទេ" - "មិនអាចធ្វើការហៅជាក្រុមបានទេ" - "មិនអាចបដិសេធការហៅបានទេ" - "មិនអាចធ្វើការហៅបានទេ" - "ការ​ហៅ SIP" - "ការ​ហៅ​ពេល​អាសន្ន" - "កំពុងបើកវិទ្យុ…" - "គ្មានសេវាទេ សូមព្យាយាមម្តង…" - "មិនអាចហៅបានទេ។ %s មិនមែនជាលេខអាសន្នទេ" - "មិនអាចហៅបានទេ សូមចុចហៅលេខអាសន្ន" - "ប្រើ​ក្ដារ​ចុច ​ដើម្បី​ចុច​លេខ" - "រង់ចាំការហៅ" - "បន្តការហៅ" - "បញ្ចប់ការហៅ" - "បង្ហាញ​បន្ទះ​លេខ" - "លាក់​បន្ទះ​លេខ" - "បិទ" - "បើក​សំឡេង" - "បន្ថែម​ការ​ហៅ" - "បញ្ចូល​ការ​ហៅ​ចូល​គ្នា" - "ប្ដូរ" - "គ្រប់គ្រង​ការ​ហៅ" - "គ្រប់គ្រងការហៅជាក្រុម" - "ការហៅជា​សន្និសិទ" - "គ្រប់គ្រង" - "សំឡេង" - "ហៅជាវីដេអូ" - "ប្ដូរ​ទៅ​ការ​ហៅ​ជា​សំឡេង" - "ប្ដូរកាមេរ៉ា" - "បើកកាមេរ៉ា" - "បិទកាមេរ៉ា" - "ជម្រើសច្រើនទៀត" - "អ្នកលេងបានចាប់ផ្តើម" - "អ្នកលេងបានឈប់" - "កាមេរ៉ាមិនទាន់ត្រៀមរួចរាល់ទេ" - "កាមេរ៉ាត្រៀមរួចរាល់ហើយ" - "ព្រឹត្តិការណ៍វេននៃការហៅមិនស្គាល់" - "សេវាកម្ម" - "ដំឡើង" - "<មិន​បាន​កំណត់>" - "​កំណត់​ការ​​ហៅ​ផ្សេងទៀត" - "ហៅតាមរយៈ %s" - "ចូល​តាមរយៈ %s" - "រូបថត​ទំនាក់ទំនង" - "ចូលជាលក្ខណៈឯកជន" - "ជ្រើសទំនាក់ទំនង" - "សរសេរដោយខ្លួនអ្នកផ្ទាល់..." - "បោះបង់" - "ផ្ញើ" - "ឆ្លើយតប" - "ផ្ញើសារ SMS" - "បដិសេធ" - "ឆ្លើយតប​ជា​ការ​ហៅ​ជា​​វីដេអូ" - "ឆ្លើយតប​ជា​ការ​ហៅ​ជា​សំឡេង" - "ទទួលយក​សំណើ​វីដេអូ" - "ទទួលយក​សំណើ​វីដេអូ" - "ទទួលយកសំណើបញ្ជូនជាវីដេអូ" - "បដិសេធសំណើបញ្ជូនជាវីដេអូ" - "ទទួលយកសំណើទទួលជាវីដេអូ" - "បដិសេធសំណើទទួលជាវីដេអូ" - "រុញឡើងលើដើម្បី %s" - "រុញទៅឆ្វេងដើម្បី %s" - "រុញទៅស្ដាំដើម្បី %s" - "រុញចុះក្រោមដើម្បី %s" - "ញ័រ" - "ញ័រ" - "សំឡេង" - "សំឡេង​លំនាំដើម (%1$s)" - "សំឡេងរោទ៍ទូរស័ព្ទ" - "ញ័រពេលរោទ៍" - "សំឡេងរោទ៍ និងញ័រ" - "គ្រប់គ្រងការហៅជាក្រុម" - "លេខអាសន្ន" - "រូបថត​ប្រវត្តិរូប" - "បិទកាមេរ៉ា" - "តាមរយៈ %s" - "បានផ្ញើចំណាំ" - "សារថ្មីៗ" - "ព័ត៌មានធុរកិច្ច" - "ចម្ងាយ %.1f ម៉ាយល៍" - "ចម្ងាយ %.1f គម" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "បើកថ្ងៃស្អែកនៅម៉ោង %s" - "បើកថ្ងៃនេះនៅម៉ោង %s" - "បិទនៅម៉ោង %s" - "បានបិទថ្ងៃនេះនៅម៉ោង %s" - "បើកឥឡូវនេះ" - "បិទឥឡូវនេះ" - "អ្នក​ហៅ​​បន្លំ​ដែល​សង្ស័យ" - "ការ​ហៅ​បាន​បញ្ចប់ %1$s" - "នេះ​គឺ​ជា​លើក​ដំបូង​ដែល​លេខ​នេះ​បាន​ហៅ​មក​អ្នក។" - "យើង​បាន​សង្ស័យ​ថា​​ការ​ហៅ​នេះ​ជា​សារ​ឥត​បាន​ការ។" - "រារាំង/រាយការណ៍សារឥតបានការ" - "បញ្ចូល​​ទំនាក់ទំនង" - "មិនមែន​សារ​ឥតបានការ" - diff --git a/InCallUI/res/values-kn/strings.xml b/InCallUI/res/values-kn/strings.xml deleted file mode 100644 index 441f6e189274eac322a3c59ea100c1c5c667d64d..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-kn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ಫೋನ್" - "ತಡೆಹಿಡಿಯಲಾಗಿದೆ" - "ಅಪರಿಚಿತ" - "ಖಾಸಗಿ ಸಂಖ್ಯೆ" - "ಪೇಫೋನ್" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ" - "ಕರೆಯನ್ನು ಬಿಡಲಾಗಿದೆ" - "ಸ್ಪೀಕರ್‌" - "ಹ್ಯಾಂಡ್‌ಸೆಟ್ ಇಯರ್‌ಪೀಸ್" - "ವೈರ್ಡ್ ಹೆಡ್‌ಸೆಟ್‌" - "ಬ್ಲೂಟೂತ್" - "ಕೆಳಗಿನ ಟೋನ್‌ಗಳನ್ನು ಕಳುಹಿಸುವುದೇ?\n" - "ಟೋನ್‌ಗಳನ್ನು ಕಳುಹಿಸಲಾಗುತ್ತಿದೆ\n" - "ಕಳುಹಿಸು" - "ಹೌದು" - "ಇಲ್ಲ" - "ಇದರೊಂದಿಗೆ ವಿಶೇಷ ಅಕ್ಷರಗಳನ್ನು ಸ್ಥಳಾಂತರಿಸು" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ %s" - "ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆ" - "ಡಯಲ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಮರು ಡಯಲ್ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ" - "ಒಳಬರುವ ಕರೆ" - "ಒಳಬರುವ ಕೆಲಸದ ಕರೆ" - "ಕರೆ ಅಂತ್ಯಗೊಂಡಿದೆ" - "ತಡೆಹಿಡಿಯಲಾಗಿದೆ" - "ಹ್ಯಾಂಗ್ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಕರೆಯಲ್ಲಿ" - "ನನ್ನ ಸಂಖ್ಯೆ %s" - "ವೀಡಿಯೊ ಸಂಪರ್ಕಪಡಿಸಲಾಗುತ್ತಿದೆ" - "ವೀಡಿಯೊ ಕರೆ" - "ವೀಡಿಯೊ ವಿನಂತಿಸಲಾಗುತ್ತಿದೆ" - "ವೀಡಿಯೊ ಕರೆಯನ್ನು ಸಂಪರ್ಕಪಡಿಸಲಾಗುವುದಿಲ್ಲ" - "ವೀಡಿಯೊ ವಿನಂತಿಯನ್ನು ತಿರಸ್ಕರಿಸಲಾಗಿದೆ" - "ನಿಮ್ಮ ಮರಳಿಕರೆ ಮಾಡುವ ಸಂಖ್ಯೆ\n %1$s" - "ನಿಮ್ಮ ತುರ್ತು ಮರಳಿಕರೆ ಮಾಡುವ ಸಂಖ್ಯೆ\n %1$s" - "ಡಯಲ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಮಿಸ್ಡ್‌ ಕಾಲ್‌" - "ಮಿಸ್ಡ್ ಕಾಲ್‌ಗಳು" - "%s ಮಿಸ್ಡ್ ಕಾಲ್‌ಗಳು" - "%s ಅವರಿಂದ ಮಿಸ್ಡ್ ಕಾಲ್" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಕರೆ" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಕೆಲಸದ ಕರೆ" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ವೈ-ಫೈ ಕರೆ" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ವೈ-ಫೈ ಕೆಲಸದ ಕರೆ" - "ತಡೆಹಿಡಿಯಲಾಗಿದೆ" - "ಒಳಬರುವ ಕರೆ" - "ಒಳಬರುವ ಕೆಲಸದ ಕರೆ" - "ಒಳಬರುವ ವೈ-ಫೈ ಕರೆ" - "ಒಳಬರುವ ವೈ-ಫೈ ಕೆಲಸದ ಕರೆ" - "ಒಳಬರುವ ವೀಡಿಯೊ ಕರೆ" - "ಒಳಬರುವ ಶಂಕಿತ ಸ್ಪ್ಯಾಮ್ ಕರೆ" - "ಒಳಬರುವ ವೀಡಿಯೊ ವಿನಂತಿ" - "ಹೊಸ ಧ್ವನಿಮೇಲ್‌" - "ಹೊಸ ಧ್ವನಿಮೇಲ್‌‌ (%d)" - "%s ಗೆ ಡಯಲ್‌‌ ಮಾಡು" - "ಅಪರಿಚಿತ ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆ" - "ಸೇವೆ ಇಲ್ಲ" - "ಆಯ್ಕೆಮಾಡಿದ (%s) ನೆಟ್‌ವರ್ಕ್‌ ಲಭ್ಯವಿಲ್ಲ" - "ಉತ್ತರ" - "ಹ್ಯಾಂಗ್ ಅಪ್" - "ವೀಡಿಯೊ" - "ಧ್ವನಿ" - "ಸಮ್ಮತಿಸು" - "ವಜಾಗೊಳಿಸಿ" - "ಮರಳಿ ಕರೆ" - "ಸಂದೇಶ" - "ಮತ್ತೊಂದು ಸಾಧನದಲ್ಲಿ ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಕರೆ" - "ಕರೆ ವರ್ಗಾಯಿಸಿ" - "ಕರೆ ಮಾಡಲು, ಮೊದಲು ಏರ್‌ಪ್ಲೇನ್‌‌ ಮೋಡ್‌‌ ಆಫ್‌ ಮಾಡಿ." - "ನೆಟ್‌ವರ್ಕ್‌ನಲ್ಲಿ ಇನ್ನೂ ನೋಂದಣಿಯಾಗಿಲ್ಲ." - "ಸೆಲ್ಯುಲಾರ್ ನೆಟ್‌ವರ್ಕ್‌ ಲಭ್ಯವಿಲ್ಲ." - "ಕರೆಯನ್ನು ಮಾಡಲು, ಮಾನ್ಯವಾದ ಸಂಖ್ಯೆಯನ್ನು ನಮೂದಿಸಿ." - "ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ." - "MMI ಅನುಕ್ರಮ ಪ್ರಾರಂಭವಾಗುತ್ತಿದೆ…" - "ಸೇವೆ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ." - "ಕರೆಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ಕರೆಯನ್ನು ಪ್ರತ್ಯೇಕಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ವರ್ಗಾಯಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ಕಾನ್ಫರೆನ್ಸ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ಕರೆ ತಿರಸ್ಕರಿಸಲಾಗುವುದಿಲ್ಲ." - "ಕರೆ(ಗಳು) ಬಿಡುಗಡೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ." - "SIP ಕರೆ" - "ತುರ್ತು ಕರೆ" - "ರೇಡಿಯೋ ಆನ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ…" - "ಯಾವುದೇ ಸೇವೆ ಇಲ್ಲ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಲಾಗುತ್ತಿದೆ..." - "ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. %s ತುರ್ತು ಸಂಖ್ಯೆಯಲ್ಲ." - "ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ತುರ್ತು ಸಂಖ್ಯೆಯನ್ನು ಡಯಲ್ ಮಾಡಿ." - "ಡಯಲ್‌ ಮಾಡಲು ಕೀಬೋರ್ಡ್‌ ಬಳಸಿ" - "ಕರೆಯನ್ನು ಹೋಲ್ಡ್‌‌ ಮಾಡು" - "ಕರೆಯನ್ನು ಮುಂದುವರಿಸಿ" - "ಕರೆ ಅಂತ್ಯಗೊಳಿಸಿ" - "ಡಯಲ್‌ಪ್ಯಾಡ್ ತೋರಿಸು" - "ಡಯಲ್‌ಪ್ಯಾಡ್ ಮರೆಮಾಡು" - "ಮ್ಯೂಟ್" - "ಅನ್‌ಮ್ಯೂಟ್" - "ಕರೆಯನ್ನು ಸೇರಿಸು" - "ಕರೆಗಳನ್ನು ವಿಲೀನಗೊಳಿಸು" - "ಸ್ವ್ಯಾಪ್‌ ಮಾಡು" - "ಕರೆಗಳನ್ನು ನಿರ್ವಹಿಸಿ" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆಯನ್ನು ನಿರ್ವಹಿಸಿ" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ" - "ನಿರ್ವಹಿಸು" - "ಆಡಿಯೊ" - "ವೀಡಿಯೊ ಕರೆ" - "ಧ್ವನಿ ಕರೆಗೆ ಬದಲಾಯಿಸಿ" - "ಕ್ಯಾಮರಾ ಬದಲಿಸಿ" - "ಕ್ಯಾಮರಾ ಆನ್ ಮಾಡಿ" - "ಕ್ಯಾಮರಾ ಆಫ್ ಮಾಡಿ" - "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" - "ಪ್ಲೇಯರ್‌ ಪ್ರಾರಂಭವಾಗಿದೆ" - "ಪ್ಲೇಯರ್‌ ನಿಲ್ಲಿಸಲಾಗಿದೆ" - "ಕ್ಯಾಮರಾ ಸಿದ್ಧವಾಗಿಲ್ಲ" - "ಕ್ಯಾಮರಾ ಸಿದ್ಧವಾಗಿದೆ" - "ಅಪರಿಚಿತ ಕರೆಯ ಸೆಶನ್‌ ಈವೆಂಟ್‌" - "ಸೇವೆ" - "ಸೆಟಪ್" - "<ಹೊಂದಿಸಿಲ್ಲ>" - "ಇತರ ಕರೆ ಸೆಟ್ಟಿಂಗ್‌ಗಳು" - "%s ಮೂಲಕ ಕರೆ ಮಾಡಲಾಗುತ್ತಿದೆ" - "%s ಮೂಲಕ ಒಳಬರುತ್ತಿರುವ ಕರೆ" - "ಸಂಪರ್ಕ ಫೋಟೋ" - "ಖಾಸಗಿಯಾಗಿ ಹೋಗಿ" - "ಸಂಪರ್ಕವನ್ನು ಆಯ್ಕೆಮಾಡಿ" - "ನಿಮ್ಮ ಸ್ವಂತದ್ದನ್ನು ಬರೆಯಿರಿ..." - "ರದ್ದುಮಾಡಿ" - "ಕಳುಹಿಸು" - "ಉತ್ತರ" - "SMS ಕಳುಹಿಸಿ" - "ನಿರಾಕರಿಸು" - "ವೀಡಿಯೊ ಕರೆ ರೂಪದಲ್ಲಿ ಉತ್ತರಿಸಿ" - "ಆಡಿಯೊ ಕರೆಯಂತೆ ಉತ್ತರಿಸಿ" - "ವೀಡಿಯೊ ವಿನಂತಿ ಒಪ್ಪಿಕೊಳ್ಳು" - "ವೀಡಿಯೊ ವಿನಂತಿ ತಿರಸ್ಕರಿಸು" - "ವೀಡಿಯೊ ಪ್ರಸಾರ ವಿನಂತಿ ಸಮ್ಮತಿಸಿ" - "ವೀಡಿಯೊ ಪ್ರಸಾರ ವಿನಂತಿ ತಿರಸ್ಕರಿಸಿ" - "ವೀಡಿಯೊ ಸ್ವೀಕರಿಸುವಿಕೆ ವಿನಂತಿ ಸಮ್ಮತಿಸಿ" - "ವೀಡಿಯೊ ಸ್ವೀಕರಿಸುವಿಕೆ ವಿನಂತಿ ತಿರಸ್ಕರಿಸಿ" - "%s ಗೆ ಮೇಲಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "%s ಗೆ ಎಡಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "%s ಗೆ ಬಲಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "%s ಗೆ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "ವೈಬ್ರೇಟ್‌" - "ವೈಬ್ರೇಟ್‌" - "ಶಬ್ದ" - "ಡಿಫಾಲ್ಟ್‌ ಧ್ವನಿ (%1$s)" - "ಫೋನ್ ರಿಂಗ್‌ಟೋನ್" - "ರಿಂಗ್ ಆಗುವಾಗ ವೈಬ್ರೇಟ್‌ ಆಗು" - "ರಿಂಗ್‌ಟೋನ್‌‌ ಮತ್ತು ವೈಬ್ರೇಟ್‌" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆಯನ್ನು ನಿರ್ವಹಿಸಿ" - "ತುರ್ತು ಸಂಖ್ಯೆ" - "ಪ್ರೊಫೈಲ್ ಫೋಟೋ" - "ಕ್ಯಾಮರಾ ಆಫ್‌" - "%s ಮೂಲಕ" - "ಟಿಪ್ಪಣಿ ಕಳುಹಿಸಲಾಗಿದೆ" - "ಇತ್ತೀಚಿನ ಸಂದೇಶಗಳು" - "ವ್ಯಾಪಾರ ಮಾಹಿತಿ" - "%.1f ಮೈಲು ದೂರ" - "%.1f ಕಿಮೀ ದೂರ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ನಾಳೆ %s ಗಂಟೆಗೆ ತೆರೆಯುತ್ತದೆ" - "ಇಂದು %s ಗಂಟೆಗೆ ತೆರೆಯುತ್ತದೆ" - "%s ಗಂಟೆಗೆ ಮುಚ್ಚಲಾಗಿದೆ" - "ಇಂದು %s ಗಂಟೆಗೆ ಮುಚ್ಚಲಾಗಿದೆ" - "ಇದೀಗ ತೆರೆಯಲಾಗಿದೆ" - "ಇದೀಗ ಮುಚ್ಚಲಾಗಿದೆ" - "ಶಂಕಿತ ಸ್ಪ್ಯಾಮ್ ಕರೆದಾರರು" - "ಕರೆ ಮುಕ್ತಾಯಗೊಂಡಿದೆ %1$s" - "ಇದೇ ಮೊದಲ ಬಾರಿಗೆ ಈ ಸಂಖ್ಯೆಯಿಂದ ನಿಮಗೆ ಕರೆ ಮಾಡಲಾಗಿದೆ." - "ನಾವು ಈ ಕರೆಯನ್ನು ಸ್ಪ್ಯಾಮರ್‌ ಎಂದು ಶಂಕಿಸಿದ್ದೇವೆ." - "ಸ್ಪ್ಯಾಮ್ ನಿರ್ಬಂಧಿಸು/ವರದಿ ಮಾಡು" - "ಸಂಪರ್ಕ ಸೇರಿಸಿ" - "ಸ್ಪ್ಯಾಮ್‌ ಅಲ್ಲ" - diff --git a/InCallUI/res/values-ko/strings.xml b/InCallUI/res/values-ko/strings.xml deleted file mode 100644 index 973dc46370fdbd4e7555164293f3efc633897ac6..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ko/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "전화" - "대기 중" - "알 수 없음" - "비공개 번호" - "공중전화" - "다자간 통화" - "연락되지 않음" - "스피커" - "핸드셋 수화부" - "유선 헤드셋" - "블루투스" - "다음 톤을 보내시겠습니까?\n" - "신호음 보내기\n" - "전송" - "예" - "아니요" - "와일드 문자를 다음으로 바꿈:" - "다자간 통화 %s" - "음성사서함 번호" - "전화 거는 중" - "재다이얼 중" - "다자간 통화" - "수신 전화" - "수신 업무 전화" - "통화 종료됨" - "대기 중" - "전화 끊는 중" - "통화 중" - "내 전화번호는 %s입니다." - "화상 통화 연결 중" - "화상 통화" - "화상 통화 요청 중" - "화상 통화를 연결할 수 없습니다." - "화상 통화 요청이 거부되었습니다." - "콜백 번호\n %1$s" - "긴급 콜백 번호\n%1$s" - "전화 거는 중" - "부재중 전화" - "부재중 전화" - "부재중 전화 %s통" - "%s의 부재중 전화" - "발신 전화" - "발신 업무 전화" - "발신 Wi-Fi 전화" - "발신 Wi-Fi 업무 전화" - "대기 중" - "수신 전화" - "수신 업무 전화" - "Wi-Fi 수신 전화" - "수신 Wi-Fi 업무 전화" - "수신 화상 통화" - "의심스러운 스팸 발신자로부터 온 전화" - "수신 화상 통화 요청" - "새로운 음성사서함" - "새 음성사서함(%d개)" - "%s(으)로 전화 걸기" - "알 수 없는 음성사서함 번호" - "서비스 불가" - "선택한 네트워크(%s)를 사용할 수 없음" - "전화 받기" - "전화 끊기" - "화상" - "음성" - "수락" - "해제" - "전화 걸기" - "메시지" - "다른 기기에서 진행 중인 통화" - "통화 전환" - "전화를 걸려면 먼저 비행기 모드를 해제하세요." - "네트워크에서 등록되지 않았습니다." - "사용 가능한 이동통신망이 없습니다." - "전화를 걸려면 올바른 번호를 입력하세요." - "전화를 걸 수 없습니다." - "MMI 시퀀스 시작 중..." - "서비스가 지원되지 않습니다." - "통화를 전환할 수 없습니다." - "통화를 분리할 수 없습니다." - "통화를 전환할 수 없습니다." - "다자간 통화를 이용할 수 없습니다." - "통화를 거부할 수 없습니다." - "통화를 끊을 수 없습니다." - "SIP 통화" - "긴급 전화" - "무선을 켜는 중..." - "서비스를 사용할 수 없습니다. 다시 시도 중..." - "전화를 걸 수 없습니다. %s은(는) 긴급 번호가 아닙니다." - "전화를 걸 수 없습니다. 긴급 번호를 사용하세요." - "키보드를 사용하여 전화 걸기" - "통화 대기" - "통화 재개" - "통화 종료" - "다이얼패드 표시" - "다이얼패드 숨기기" - "음소거" - "음소거 해제" - "통화 추가" - "통화 병합" - "전환" - "통화 관리" - "다자간 통화 관리" - "다자간 통화" - "관리" - "오디오" - "화상 통화" - "음성 통화로 변경" - "카메라 전환" - "카메라 켜기" - "카메라 끄기" - "옵션 더보기" - "플레이어가 시작되었습니다." - "플레이어가 중지되었습니다." - "카메라가 준비되지 않았습니다." - "카메라가 준비되었습니다." - "알 수 없는 통화 세션 이벤트" - "서비스" - "설정" - "<설정 안됨>" - "기타 통화 설정" - "%s을(를) 통해 걸려온 전화" - "%s을(를) 통해 걸려온 전화" - "연락처 사진" - "비공개로 실행" - "연락처 선택" - "나만의 응답 작성…" - "취소" - "전송" - "전화 받기" - "SMS 보내기" - "거부" - "화상 통화로 받기" - "음성 통화로 받기" - "화상 통화 요청 수락" - "화상 통화 요청 거부" - "화상 통화 전송 요청 허용" - "화상 통화 전송 요청 거부" - "화상 통화 수신 요청 허용" - "화상 통화 수신 요청 거부" - "%s하려면 위로 슬라이드합니다." - "%s하려면 왼쪽으로 슬라이드합니다." - "%s하려면 오른쪽으로 슬라이드합니다." - "%s하려면 아래로 슬라이드합니다." - "진동" - "진동" - "소리" - "기본 알림음(%1$s)" - "전화 벨소리" - "전화 수신 시 진동" - "벨소리 및 진동" - "다자간 통화 관리" - "비상 전화번호" - "프로필 사진" - "카메라 꺼짐" - "수신 번호: %s" - "메모가 전송되었습니다." - "최근 메시지" - "비즈니스 정보" - "%.1fmi 거리" - "%.1fkm 거리" - "%1$s, %2$s" - "%1$s~%2$s" - "%1$s, %2$s" - "내일 %s에 영업 시작" - "오늘 %s에 영업 시작" - "%s에 영업 종료" - "오늘 %s에 영업 종료됨" - "영업 중" - "영업 종료" - "의심스러운 스팸 발신자" - "%1$s번으로 끝나는 번호에서 걸려온 전화" - "이 번호에서 처음으로 걸려온 전화입니다." - "스팸 전화로 의심됩니다." - "스팸 차단/신고" - "연락처 추가" - "스팸 해제" - diff --git a/InCallUI/res/values-ky/strings.xml b/InCallUI/res/values-ky/strings.xml deleted file mode 100644 index bebdcfde88763ef69d78051697c0dce35ef5e95f..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ky/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "Күтүлүүдө" - "Белгисиз" - "Купуя номер" - "Таксофон" - "Конференц-чалуу" - "Чалуу үзүлдү" - "Катуу сүйлөткүч" - "Гарнитура" - "Зымдуу гарнитура" - "Bluetooth" - "Төмөнкү номер жөнөтүлсүнбү?\n" - "Обондор жөнөтүлүүдө\n" - "Жөнөтүү" - "Ооба" - "Жок" - "Атайын белгини төмөнкүгө алмаштыруу" - "Конференц-чалуу %s" - "Үн почтасынын номери" - "Терилүүдө" - "Кайра терилүүдө" - "Конференц-чалуу" - "Кирүүчү чалуу" - "Жумуш боюнча чалуу" - "Чалуу аяктады" - "Күтүлүүдө" - "Чалуу аяктоодо" - "Чалууда" - "Менин номерим %s" - "Видео туташтырылууда" - "Видео чалуу" - "Видео суралууда" - "Видео чалууга туташуу мүмкүн болбой жатат" - "Видео сурам четке кагылды" - "Кайра чалына турган номер\n %1$s" - "Өзгөчө кырдаалда кайра чалына турган номер\n %1$s" - "Терилүүдө" - "Кабыл алынбаган чалуу" - "Кабыл алынбаган чалуулар" - "%s кабыл алынбаган чалуу" - "%s дегенден кабыл алынбаган чалуу" - "Учурдагы чалуу" - "Учурдагы чалуу (жумуш боюнча)" - "Учурдагы Wi-Fi чалуу" - "Учурдагы Wi-Fi чалуу (жумуш боюнча)" - "Күтүлүүдө" - "Кирүүчү чалуу" - "Жумуш боюнча чалуу" - "Кирүүчү Wi-Fi чалуу" - "Жумуш боюнча келип жаткан Wi-Fi чалуу" - "Кирүүчү видео чалуу" - "Келип жаткан чалуу спам окшойт" - "Кирүүчү видео сурамы" - "Жаңы үн почтасы" - "Жаңы үн почтасы (%d)" - "%s номерин терүү" - "Үн почтасынын номери белгисиз" - "Байланыш жок" - "Тандалган тармак (%s) жеткиликсиз" - "Жооп берүү" - "Чалууну бүтүрүү" - "Видео" - "Үн" - "Кабыл алуу" - "Этибарга албоо" - "Кайра чалуу" - "Билдирүү" - "Башка түзмөктө сүйлөшүп жатасыз" - "Чалууну бул түзмөккө өткөрүү" - "Учак режимин өчүрүп туруп чалыңыз." - "Тармакта катталган эмес." - "Мобилдик тармак жеткиликтүү эмес." - "Чалуу үчүн, жарактуу номер киргизиңиз." - "Чалынбай жатат." - "MMI кезеги башталууда…" - "Кызмат колдоого алынбайт." - "Чалуулар которуштурулбай жатат." - "Чалуу бөлүнбөй жатат." - "Өткөрүлбөй жатат." - "Конференц-чалуу түзүлбөй жатат." - "Чалуу четке кагылбай жатат." - "Чалуу (-лар) ажыратылбай жатат." - "SIP чалуу" - "Өзгөчө кырдаалда чалуу" - "Радио күйгүзүлүүдө…" - "Кызмат жок. Кайра аракет кылууда…" - "Чалынбай жатат. %s өзгөчө кырдаал номери эмес." - "Чалынбай жатат. Өзгөчө кырдаал номерин териңиз." - "Баскычтоп менен териңиз" - "Чалууну кармап туруу" - "Чалууну улантуу" - "Чалууну бүтүрүү" - "Номер тергичти көрсөтүү" - "Номер тергичти жашыруу" - "Үнсүз" - "Үндү чыгаруу" - "Чалуу кошуу" - "Чалууларды бириктирүү" - "Алмаштыруу" - "Чалууларды башкаруу" - "Конференц-чалууну башкаруу" - "Конференц чалуу" - "Башкаруу" - "Аудио" - "Видео чалуу" - "Үн чалууга өзгөртүү" - "Камераны которуштуруу" - "Камераны күйгүзүү" - "Камераны өчүрүү" - "Дагы параметрлер" - "Ойноткуч башталды" - "Ойноткуч токтотулду" - "Камера даяр эмес" - "Камера даяр" - "Чалуу сеансынын окуясы белгисиз" - "Кызмат" - "Орнотуу" - "<Коюлган эмес>" - "Башка чалуу жөндөөлөрү" - "%s аркылуу чалуу" - "%s аркылуу келүүдө" - "байланыштын сүрөтү" - "купуя режимине өтүү" - "байланыш тандоо" - "Сиздин жообуңуз…" - "Жокко чыгаруу" - "Жөнөтүү" - "Жооп берүү" - "SMS жөнөтүү" - "Четке кагуу" - "Видео чалуу түрүндө жооп берүү" - "Аудио чалуу түрүндө жооп берүү" - "Видео сурамын кабыл алуу" - "Видео сурамын четке кагуу" - "Видео өткөрүү сурамын кабыл алуу" - "Видео өткөрүү сурамын четке кагуу" - "Видео алуу сурамын кабыл алуу" - "Видео алуу сурамын четке кагуу" - "%s үчүн жогору жылмыштырыңыз." - "%s үчүн солго жылмыштырыңыз." - "%s үчүн оңго жылмыштырыңыз." - "%s үчүн төмөн жылмыштырыңыз." - "Дирилдөө" - "Дирилдөө" - "Үн" - "Демейки үнү (%1$s)" - "Телефондун рингтону" - "Дирилдеп шыңгырасын" - "Шыңгыр жана дирилдөө" - "Конференц-чалууну башкаруу" - "Өзгөчө кырдаал номери" - "Профилдин сүрөтү" - "Камера өчүк" - "%s аркылуу" - "Билдирүү жөнөтүлдү" - "Акыркы билдирүүлөр" - "Компания тууралуу маалымат" - "%.1f миля алыста" - "%.1f км алыста" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Эртең саат %s ачылат" - "Бүгүн саат %s ачылат" - "Саат %s жабылат" - "Бүгүн саат %s жабылды" - "Азыр ачык" - "Эми жабылды" - "Спам окшойт" - "Чалуу %1$s бүттү" - "Бул номер сизге биринчи жолу чалып жатат." - "Бул чалуу спам окшойт." - "Бөгөттөө/спам катары кабарлоо" - "Байланыш кошуу" - "Спам эмес" - diff --git a/InCallUI/res/values-lo/strings.xml b/InCallUI/res/values-lo/strings.xml deleted file mode 100644 index 3e7e815209382246de89c825694b464a2b61b234..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-lo/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ໂທລະສັບ" - "ຖືສາຍລໍຖ້າ" - "ບໍ່ຮູ້ຈັກ" - "ເບີສ່ວນຕົວ" - "ຕູ້​ໂທ​ລະ​ສັບ​ສາ​ທາ​ລະ​ນະ" - "ການປະຊຸມທາງໂທລະສັບ" - "ສາຍ​ຫຼຸດ​ແລ້ວ" - "ລຳໂພງ" - "ຊຸດຫູຟັງ" - "ຊຸດຫູຟັງແບບມີສາຍ" - "Bluetooth" - "ສົ່ງໂທນສຽງຕໍ່ໄປນີ້ບໍ?\n" - "ກຳລັງສົ່ງໂທນສຽງ\n" - "ສົ່ງ" - "ແມ່ນ" - "ບໍ່" - "ປ່ຽນແທນ \"ອັກຂະລະຕົວແທນ\" ດ້ວຍ" - "ການປະຊຸມທາງໂທລະສັບ %s" - "ເບີຂໍ້ຄວາມສຽງ" - "ກຳລັງໂທ" - "ກຳ​ລັງ​ໂທ​ຄືນ" - "ການປະຊຸມທາງໂທລະສັບ" - "​ສາຍ​ໂທ​ເຂົ້າ" - "ສາຍໂທເຂົ້າຈາກບ່ອນເຮັດວຽກ" - "ວາງສາຍແລ້ວ" - "ຖືສາຍລໍຖ້າ" - "ກຳລັງວາງສາຍ" - "ຢູ່ໃນສາຍ" - "ເບີໂທຂອງຂ້ອຍແມ່ນ %s" - "​ກຳ​ລັງ​ເຊື່ອມ​ຕໍ່​ວິ​ດີ​ໂອ" - "​ການໂທ​​ວິ​ດີ​ໂອ" - "​ກຳ​ລັງ​ຮ້ອງ​ຂໍການໂທ​ວິ​ດີ​ໂອ" - "ບໍ່​ສາ​ມາດ​ເຊື່ອມ​ຕໍ່​ການ​ໂທວິດີໂອ​ໄດ້" - "ປະ​ຕິ​ເສດ​ການຮ້ອງ​ຂໍການ​ໂທວິ​ດີ​ໂອ​ແລ້ວ" - "ເບີໂທກັບຂອງທ່ານ\n %1$s" - "ເບີ​ໂທ​ກັບ​ສຸກ​ເສີນ​ຂອງ​ທ່ານ\n %1$s" - "ກຳລັງໂທ" - "ສາຍບໍ່ໄດ້ຮັບ" - "ສາຍບໍ່ໄດ້ຮັບ" - "%s ສາຍບໍ່ໄດ້ຮັບ" - "ສາຍບໍ່ໄດ້ຮັບຈາກ %s" - "ສາຍກຳລັງໂທ" - "ສາຍກຳລັງໂທຈາກບ່ອນເຮັດວຽກ" - "ສາຍກຳລັງໂທຜ່ານ Wi​-Fi" - "ສາຍກຳລັງໂທຜ່ານ Wi-Fi ຈາກບ່ອນເຮັດວຽກ" - "ຖືສາຍລໍຖ້າ" - "​ສາຍ​ໂທ​ເຂົ້າ" - "ສາຍໂທເຂົ້າຈາກບ່ອນເຮັດວຽກ" - "ສາຍໂທເຂົ້າຜ່ານ Wi-Fi" - "ສາຍໂທເຂົ້າຜ່ານ Wi-Fi ຈາກບ່ອນເຮັດວຽກ" - "ສາຍໂທ​ວິດີໂອ​ເຂົ້າ" - "ມີການໂທທີ່ຄາດວ່າເປັນສະແປມໂທເຂົ້າມາ" - "​ຄຳ​ຮ້ອງ​ຂໍ​ວິ​ດີ​ໂອທີ່​ເຂົ້າ​ມາ" - "ຂໍ້ຄວາມສຽງໃໝ່" - "ຂໍ້ຄວາມສຽງໃໝ່ (%d)" - "ໂທຫາ %s" - "ເບີຂໍ້ຄວາມສຽງບໍ່ຮູ້ຈັກ" - "ບໍ່ມີການບໍລິການ" - "ເຄືອຂ່າຍທີ່ເລືອກ (%s) ບໍ່ສາມາດໃຊ້ໄດ້" - "ຮັບສາຍ" - "ວາງສາຍ" - "ວິດີໂອ" - "ສຽງ" - "ຍອມຮັບ" - "ປິດໄວ້" - "ໂທກັບ" - "ຂໍ້ຄວາມ" - "ສາຍທີ່ກຳລັງໂທອອກໃນອຸປະກອນອື່ນ" - "ໂອນສາຍ" - "ເພື່ອເຮັດການໂທ, ໃຫ້ປິດໂໝດເຮືອບິນກ່ອນ" - "ບໍ່ໄດ້ລົງທະບຽນໃນເຄືອຂ່າຍ." - "ບໍ່​ມີ​ເຄືອ​ຂ່າຍ​ມື​ຖື​ທີ່​​ໃຊ້​ໄດ້." - "ເພື່ອເຮັດການ​ໂທ, ປ້ອນ​ເບີ​ໂທ​ທີ່​ໃຊ້​ໄດ້​." - "ບໍ່​ສາ​ມາດ​ໂທ​ໄດ້." - "ກຳລັງເລີ່ມຕົ້ນລຳດັບ MMI..." - "ບໍ່ຮອງຮັບການ​ບໍ​ລິ​ການ." - "ບໍ່​ສາ​ມາດ​ສະ​ຫຼັບ​ສາ​ຍ​ໂທ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ແຍກ​ສາຍ​ໂທ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ໂອນສາຍ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ປະ​ຊຸມ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ປະ​ຕິ​ເສດ​ສາຍ​ໂທ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ປ່ອຍ​ສາຍ​ໂທ​ໄດ້." - "ການໂທ SIP" - "ການໂທສຸກເສີນ" - "ກຳລັງເປີດວິທະຍຸ" - "ບໍ່​ມີ​ການ​ບໍ​ລິ​ການ. ກຳ​ລັງ​ລອງ​ໃໝ່​ອີກ…" - "ບໍ່ສາມາດໂທໄດ້. %s ບໍ່ແມ່ນເບີໂທສຸກເສີນ." - "ບໍ່​ສາ​ມາດ​ໂທ​ໄດ້. ກົດ​ເບີ​ໂທ​ສຸກ​ເສີນ." - "ໃຊ້ແປ້ນພິມເພື່ອກົດໂທ" - "ຖືສາຍ" - "​ສືບ​ຕໍ່​ສາຍ" - "ວາງສາຍ" - "ສະແດງປຸ່ມກົດ" - "ເຊື່ອງປຸ່ມກົດ" - "ປິດສຽງ" - "ເຊົາປິດສຽງ" - "ເພີ່ມການໂທ" - "ລວມສາຍ" - "ສະຫຼັບ" - "ຈັດການການໂທ" - "ຈັດ​ການ​ການ​ປະ​ຊຸມ​ທາງໂທລະສັບ" - "ການປະຊຸມທາງໂທລະສັບ" - "ຈັດການ" - "ສຽງ" - "​ການໂທ​​ວິ​ດີ​ໂອ" - "ປ່ຽນ​ເປັນ​ການ​ໂທ​ດ້ວຍ​ສຽງ" - "ສັບປ່ຽນກ້ອງ" - "ເປີດກ້ອງ" - "ປິດກ້ອງ" - "ຕົວເລືອກ​ເພີ່ມ​ເຕີມ" - "ເຄື່ອງ​ຫຼິ້ນ​ເລີ່ມ​ຕົ້ນ​ແລ້ວ" - "ເຄື່ອງ​ຫຼິ້ນ​ຢຸດ​ແລ້ວ" - "ກ້ອງ​ຖ່າຍ​ຮູບ​ບໍ່​ພ້ອມ" - "ກ້ອງ​ຖ່າຍ​ຮູບ​ພ້ອມ​ແລ້ວ" - "ເຫດ​ການ​ເຊ​ສ​ຊັນ​ການ​ໂທ​ບໍ່​ຮູ້​ຈັກ" - "ການບໍລິການ" - "ຕັ້ງຄ່າ" - "<ບໍ່ໄດ້ຕັ້ງ>" - "ການຕັ້ງຄ່າການໂທອື່ນ" - "ກຳລັງໂທຜ່ານ %s" - "ສາຍໂທເຂົ້າ​ຈາກ %s" - "ຮູບລາຍຊື່ຜູ້ຕິດຕໍ່" - "ໃຊ້ແບບສ່ວນຕົວ" - "ເລືອກລາຍຊື່ຜູ້ຕິດຕໍ່" - "ຂຽນ...ຂອງທ່ານເອງ" - "ຍົກເລີກ" - "ສົ່ງ" - "ຮັບສາຍ" - "ສົ່ງ SMS" - "ປະຕິເສດ" - "ຮັບສາຍໂທວິດີໂອ" - "ຮັບສາຍໂທແບບສຽງ" - "ຍອມຮັບການຂໍວິດີໂອ" - "ປະຕິເສດການຂໍວິດີໂອ" - "ຍອມ​ຮັບ​ການ​ຂໍ​ສົ່ງ​ວິ​ດີ​ໂອ" - "ປະ​ຕິ​ເສດ​ການ​ຂໍ​ສົ່ງ​ວິ​ດີ​ໂອ" - "ຍອມ​ຮັບ​ການ​ຂໍ​ຮັບ​ວິ​ດີ​ໂອ" - "ປະ​ຕິ​ເສດ​ການ​ຂໍ​ຮັບ​ວິ​ດີ​ໂອ" - "ເລື່ອນຂຶ້ນເພື່ອ %s." - "ເລື່ອນໄປຊ້າຍເພື່ອ %s." - "ເລື່ອນໄປຂວາເພື່ອ %s." - "ເລື່ອນລົງເພື່ອ %s." - "ສັ່ນເຕືອນ" - "ສັ່ນເຕືອນ" - "ສຽງ" - "ສຽງເລີ່ມຕົ້ນ (%1$s)" - "ຣິງໂທນໂທລະສັບ" - "ສັ່ນເຕືອນເມື່ອດັງ" - "ຣິງໂທນ ແລະ ການສັ່ນເຕືອນ" - "ຈັດ​ການ​ການ​ປະ​ຊຸມ​ທາງໂທລະສັບ" - "ເບີໂທສຸກເສີນ" - "ຮູບໂປຣໄຟລ໌" - "ກ້ອງ​ຖ່າຍ​ຮູບ​ປິດຢູ່" - "ຜ່ານ %s" - "ສົ່ງ​ບັນ​ທຶກ​ແລ້ວ" - "ຂໍ້​ຄວາມ​ບໍ່​ດົນ​ມາ​ນີ້" - "ຂໍ້​ມູນ​ທຸ​ລະ​ກິດ" - "ຫ່າງອອກໄປ %.1f ໄມ​ລ໌​" - "ຫ່າງອອກໄປ %.1f ກມ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ເປີດມື້ອື່ນເວລາ %s" - "ເປີດມື້ນີ້ເວລາ %s" - "ປິດເວລາ %s" - "ປິດແລ້ວມື້ນີ້ເວລາ %s" - "ດຽວ​ນີ້​ເປີດ" - "​ປິດ​ແລ້ວດຽວນີ້" - "ຄາດວ່າເປັນການໂທສະແປມ" - "ການໂທສິ້ນສຸດແລ້ວ %1$s" - "ນີ້ເປັນເທື່ອທຳອິດທີ່ເບີນີ້ໂທຫາທ່ານ." - "ພວກເຮົາສົງໄສວ່າເບີໂທນີ້ເປັນສະແປມ." - "ບລັອກ/ລາຍງານສະແປມ" - "ເພີ່ມລາຍຊື່ຜູ້ຕິດຕໍ່" - "ບໍ່ແມ່ນສະແປມ" - diff --git a/InCallUI/res/values-lt/strings.xml b/InCallUI/res/values-lt/strings.xml deleted file mode 100644 index 2d2a701469b71a441385d61c73f383e224246488..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-lt/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefonas" - "Sulaikyta" - "Nežinoma" - "Privatus numeris" - "Taksofonas" - "Konferencinis skambutis" - "Skambutis atmestas" - "Garsiakalbis" - "Tel. su gars. prie ausies" - "Laidinės ausinės" - "Bluetooth" - "Siųsti šiuo tonus?\n" - "Siunčiami tonai\n" - "Siųsti" - "Taip" - "Ne" - "Pakaitos simbolį pakeisti" - "Konferencinis skambutis %s" - "Balso pašto numeris" - "Renkamas numeris" - "Numeris renkamas pakartotinai" - "Konferencinis skambutis" - "Gaunamasis skambutis" - "Gaunamasis darbo skambutis" - "Skambutis baigtas" - "Sulaikyta" - "Baigiamas pokalbis" - "Dalyvauju skambutyje" - "Mano numeris: %s" - "Prisijungiama prie vaizdo skambučio" - "Vaizdo skambutis" - "Pateikiama vaizdo skambučio užklausa" - "Nepavyko prijungti vaizdo įrašo skambučio" - "Vaizdo įrašo užklausa atmesta" - "Atskambinimo numeris\n%1$s" - "Atskambinimo numeris, kuriuos skambina pagalbos tarnyba\n%1$s" - "Renkamas numeris" - "Praleistas skambutis" - "Praleisti skambučiai" - "Praleistų skambučių: %s" - "Praleistas skambutis nuo %s" - "Vykstantis pokalbis" - "Vykstantis darbo skambutis" - "Vykstantis „Wi-Fi“ skambutis" - "Vykstantis „Wi-Fi“ darbo skambutis" - "Sulaikyta" - "Gaunamasis skambutis" - "Gaunamasis darbo skambutis" - "Gaunamasis „Wi-Fi“ skambutis" - "Gaunamasis „Wi-Fi“ darbo skambutis" - "Gaunamas vaizdo skambutis" - "Gaunamasis įtartinas šlamšto skambutis" - "Gaunama vaizdo skambučio užklausa" - "Naujas balso pašto pranešimas" - "Naujas balso pašto pranešimas (%d)" - "Rinkti %s" - "Nežinomas balso pašto numeris" - "Nėra paslaugos" - "Pasirinktas tinklas (%s) negalimas" - "Atsiliepti" - "Padėti ragelį" - "Vaizdo įrašas" - "Balsas" - "Priimti" - "Atsisakyti" - "Perskambinti" - "Siųsti pranešimą" - "Kitame įrenginyje vykstantis skambutis" - "Perkelti skambutį" - "Jei norite skambinti, išjunkite lėktuvo režimą." - "Neregistruota tinkle." - "Korinis tinklas nepasiekiamas" - "Kad galėtumėte paskambinti, įveskite tinkamą numerį." - "Nepavyko paskambinti." - "Paleidžiama MMI seka..." - "Paslauga nepalaikoma." - "Nepavyko perjungti skambučių." - "Nepavyko atskirti skambučio." - "Nepavyko peradresuoti." - "Nepavyko sukurti konferencijos." - "Nepavyko atmesti skambučio." - "Nepavyko atjungti skamb." - "SIP skambutis" - "Skambutis pagalbos numeriu" - "Įjungiamas radijas…" - "Nėra ryšio. Bandoma dar kartą…" - "Nepavyko paskambinti. %s nėra pagalbos numeris." - "Nepavyko paskambinti. Surinkite pagalbos tarnybos numerį." - "Naudokite klaviatūrą ir rinkite numerius" - "Sulaikyti skambutį" - "Tęsti skambutį" - "Baigti skambutį" - "Rodyti skambinimo skydelį" - "Slėpti skambinimo skydelį" - "Nutildyti" - "Įjungti garsą" - "Pridėti skambutį" - "Sujungti skambučius" - "Apkeisti" - "Valdyti skambučius" - "Tvarkyti konferencinį skambutį" - "Konferencinis skambutis" - "Tvarkyti" - "Garsas" - "Vaizdo skambutis" - "Pakeisti į balso skambutį" - "Perjungti fotoaparatą" - "Įjungti fotoaparatą" - "Išjungti fotoaparatą" - "Daugiau parinkčių" - "Leistuvė paleista" - "Leistuvė sustabdyta" - "Fotoaparatas neparuoštas" - "Fotoaparatas paruoštas" - "Nežinomas skambučio sesijos įvykis" - "Paslaugos teikėjas" - "Sąranka" - "<Nenustatyta>" - "Kiti skambučio nustatymai" - "Skambinama naudojantis „%s“ paslaugomis" - "Gaunama per „%s“" - "kontakto nuotrauka" - "naudoti privatų režimą" - "pasirinkti kontaktą" - "Sukurkite patys..." - "Atšaukti" - "Siųsti" - "Atsiliepti" - "Siųsti SMS" - "Atmesti" - "Atsiliepti kaip į vaizdo skambutį" - "Atsiliepti kaip į garso skambutį" - "Priimti vaizdo įrašo užkl" - "Atmesti vaizdo įrašo užklausą" - "Priimti vaizdo įrašo perdavimo užklausą" - "Atmesti vaizdo įrašo perdavimo užklausą" - "Priimti vaizdo įrašo gavimo užklausą" - "Atmesti vaizdo įrašo gavimo užklausą" - "Slyskite aukštyn link parinkties „%s“." - "Slyskite į kairę link parinkties „%s“." - "Slyskite į dešinę link parinkties „%s“." - "Slyskite žemyn link %s." - "Vibruoti" - "Vibruoti" - "Garsas" - "Numatytasis garsas (%1$s)" - "Telefono skambėjimo tonas" - "Vibruoti, kai skambina" - "Skambėjimo tonas ir vibracija" - "Tvarkyti konferencinį skambutį" - "Pagalbos numeris" - "Profilio nuotrauka" - "Fotoaparatas išjungtas" - "naudojant %s" - "Užrašas išsiųstas" - "Naujausi pranešimai" - "Įmonės informacija" - "Už %.1f myl." - "Už %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Rytoj atidaroma %s" - "Šiandien atidaroma %s" - "Uždaroma %s" - "Šiandien uždaryta %s" - "Dabar atidaryta" - "Dabar uždaryta" - "Įt. skamb. dėl šl." - "Skambutis baigtas (%1$s)" - "Tai pirmas kartas, kai jums buvo skambinama iš šio numerio." - "Įtarėme, kad šis skambutis yra šlamštas." - "Bl. / pran. apie šl." - "Pridėti kontaktą" - "Ne šlamštas" - diff --git a/InCallUI/res/values-lv/strings.xml b/InCallUI/res/values-lv/strings.xml deleted file mode 100644 index 685ba8b0cfdacdfe1f4103e630183cda78091582..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-lv/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Tālrunis" - "Aizturēts" - "Nezināms" - "Privāts numurs" - "Maksas tālrunis" - "Konferences zvans" - "Zvans tika pārtraukts." - "Skaļrunis" - "Auss skaļrunis" - "Austiņas ar vadu" - "Bluetooth" - "Vai sūtīt tālāk norādītos signālus?\n" - "Sūtīšanas signāli\n" - "Sūtīt" - "Jā" - "Nē" - "Aizstāt aizstājējzīmi ar:" - "Konferences zvans: %s" - "Balss pasta numurs" - "Notiek numura sastādīšana" - "Notiek atkārtota zvanīšana" - "Konferences zvans" - "Ienākošs zvans" - "Ienākošs darba zvans" - "Zvans ir pabeigts" - "Aizturēts" - "Notiek klausules nolikšana" - "Notiek zvans" - "Mans tālruņa numurs: %s" - "Notiek video savienojuma izveide" - "Videozvans" - "Notiek video pieprasīšana" - "Nevar veikt videozvanu" - "Video pieprasījums noraidīts" - "Jūsu atzvana numurs\n %1$s" - "Jūsu ārkārtas atzvana numurs\n %1$s" - "Notiek numura sastādīšana" - "Neatbildēts zvans" - "Neatbildēti zvani" - "%s neatbildēti zvani" - "Neatbildēts zvans no: %s" - "Notiekošs zvans" - "Notiekošs darba zvans" - "Notiekošs Wi-Fi zvans" - "Notiekošs darba Wi-Fi zvans" - "Aizturēts" - "Ienākošs zvans" - "Ienākošs darba zvans" - "Ienākošs Wi-Fi zvans" - "Ienākošs darba Wi-Fi zvans" - "Ienākošs videozvans" - "Ienākošs, iespējams, nevēlams zvans" - "Ienākošs video pieprasījums" - "Jauns balss pasta ziņojums" - "Jauns balss pasts (%d)" - "Sastādiet šādu numuru: %s" - "Balss pasta numurs nav zināms." - "Nav pakalpojuma" - "Atlasītais tīkls (%s) nav pieejams." - "Atbildēt" - "Beigt zvanu" - "Video" - "Balss" - "Pieņemt" - "Noraidīt" - "Atzvanīt" - "Sūtīt īsziņu" - "Notiekošs zvans citā ierīcē" - "Pāradresēt zvanu" - "Lai veiktu zvanu, vispirms izslēdziet lidojuma režīmu." - "Nav reģistrēts tīklā." - "Mobilais tīkls nav pieejams." - "Lai veiktu zvanu, ievadiet derīgu numuru." - "Nevar veikt zvanu." - "Notiek MMI secības startēšana…" - "Pakalpojums netiek atbalstīts." - "Nevar pārslēgt zvanus." - "Nevar nošķirt zvanu." - "Nevar pārsūtīt." - "Nevar veikt konferences zvanu." - "Nevar noraidīt zvanu." - "Nevar pārtraukt zvanu(-us)." - "SIP zvans" - "Ārkārtas izsaukums" - "Notiek radio ieslēgšana…" - "Nav pakalpojuma. Notiek atkārtots mēģinājums…" - "Nevar veikt zvanu. %s nav ārkārtas numurs." - "Nevar veikt zvanu. Zvaniet ārkārtas numuram." - "Izmantojiet tastatūru, lai sastādītu numuru" - "Aizturēt zvanu" - "Atsākt zvanu" - "Beigt zvanu" - "Rādīt numura sastādīšanas tastatūru" - "Slēpt numura sastādīšanas tastatūru" - "Izslēgt skaņu" - "Ieslēgt skaņu" - "Pievienot zvanu" - "Apvienot zvanus" - "Mainīt" - "Pārvaldīt zvanus" - "Pārvaldīt konferences zvanu" - "Konferences zvans" - "Pārvaldīt" - "Audio" - "Videozvans" - "Mainīt uz balss zvanu" - "Pārslēgt kameru" - "Ieslēgt kameru" - "Izslēgt kameru" - "Citas iespējas" - "Atskaņošana sākta" - "Atskaņošana apturēta" - "Kamera nav gatava" - "Kamera ir gatava" - "Nezināms zvana sesijas notikums" - "Pakalpojums" - "Iestatīšana" - "<Nav iestatīts>" - "Citi zvanu iestatījumi" - "Zvans, ko nodrošina %s" - "Ienākošie zvani, ko nodrošina %s" - "kontaktpersonas fotoattēls" - "pārslēgt uz privāto režīmu" - "atlasīt kontaktpersonu" - "Rakstīt savu…" - "Atcelt" - "Sūtīt" - "Atbildēt" - "Sūtīt īsziņu" - "Noraidīt" - "Atbildēt videozvanā" - "Atbildēt audiozvanā" - "Apstiprināt video pieprasījumu" - "Noraidīt video pieprasījumu" - "Apstiprināt video pārsūtīšanas pieprasījumu" - "Noraidīt video pārsūtīšanas pieprasījumu" - "Apstiprināt video saņemšanas pieprasījumu" - "Noraidīt video saņemšanas pieprasījumu" - "Velciet uz augšu, lai veiktu šādu darbību: %s." - "Velciet pa kreisi, lai veiktu šādu darbību: %s." - "Velciet pa labi, lai veiktu šādu darbību: %s." - "Velciet uz leju, lai veiktu šādu darbību: %s." - "Vibrācija" - "Vibrācija" - "Signāls" - "Noklusējuma signāls (%1$s)" - "Tālruņa zvana signāls" - "Vibrācija zvana laikā" - "Zvana signāls un vibrācija" - "Konferences zvana pārvaldība" - "Ārkārtas numurs" - "Profila fotoattēls" - "Kamera ir izslēgta" - "no numura %s" - "Piezīme nosūtīta" - "Pēdējie ziņojumi" - "Informācija par uzņēmumu" - "%.1f jūdzes(-džu) attālumā" - "%.1f km attālumā" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Tiks atvērts rīt plkst. %s" - "Tiks atvērts šodien plkst. %s" - "Tiks slēgts plkst. %s" - "Tika slēgts šodien plkst. %s" - "Atvērts" - "Slēgts" - "Nevēlams zvanītājs" - "Zvans beidzās: %1$s" - "Šis jums ir pirmais zvans no šī numura." - "Iespējams, šis zvans bija no nevēlama zvanītāja." - "Bloķēt numuru/ziņot" - "Pievienot personu" - "Nav nevēlams numurs" - diff --git a/InCallUI/res/values-mk/strings.xml b/InCallUI/res/values-mk/strings.xml deleted file mode 100644 index 3161c5475bcfcea92cebf60f5977ddbc9c07de56..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-mk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "На чекање" - "Непознат" - "Приватен број" - "Говорница" - "Конференциски повик" - "Повикот е прекинат" - "Звучник" - "Слушалка" - "Жичени слушалки" - "Bluetooth" - "Испратете ги следниве тонови?\n" - "Се испраќаат тонови\n" - "Испрати" - "Да" - "Не" - "Заменете го резервниот знак со" - "Конференциски повик %s" - "Број на говорна пошта" - "Бирање" - "Повторно бирање" - "Конференциски повик" - "Дојдовен повик" - "Дојдовен работен повик" - "Повикот заврши" - "На чекање" - "Повикот се прекинува" - "Повик во тек" - "Мојот број е %s" - "Се поврзува видео" - "Видеоповик" - "Се бара видео" - "Не може да се поврзе видеоповик" - "Барањето за видео е одбиено" - "Вашиот број за повратен повик\n %1$s" - "Вашиот број за итен повик\n %1$s" - "Бирање" - "Пропуштен повик" - "Пропуштени повици" - "%s пропуштени повици" - "Пропуштен повик од %s" - "Тековен повик" - "Тековен работен повик" - "Појдовен повик преку Wi-Fi" - "Тековен работен повик преку Wi-Fi" - "На чекање" - "Дојдовен повик" - "Дојдовен работен повик" - "Дојдовен повик преку Wi-Fi" - "Дојдовен работен повик преку Wi-Fi" - "Дојдовен видеоповик" - "Дојдовниот повик може да е спам" - "Дојдовно барање за видео" - "Нова говорна пошта" - "Нова говорна пошта (%d)" - "Бирај %s" - "Непознат број на говорна пошта" - "Нема услуга" - "Избраната мрежа (%s) е недостапна" - "Одговори" - "Спушти" - "Видео" - "Гласовен" - "Прифати" - "Отфрли" - "Врати повик" - "Порака" - "Повик во тек на друг уред" - "Префрлање повик" - "За да остварите повик, прво исклучете го авионскиот режим." - "Не е регистриран на мрежа." - "Не е достапна мобилна мрежа." - "За да остварите повик, внесете важечки број." - "Не може да се повика." - "Започнува ММИ низа..." - "Услугата не е поддржана." - "Не може да се префрлат повици." - "Не може да се оддели повик." - "Не може да се пренесе." - "Не може да се оствари конференциски повик." - "Не може да се отфрли повик." - "Не може да се оствари повик." - "Повик преку SIP" - "Повик за итни случаи" - "Се вклучува радиото..." - "Нема услуга. Се обидува повторно…" - "Не може да се повика. %s не е број за итни повици." - "Не може да се повика. Бирајте го бројот за итни повици." - "Користете ја тастатурата за бирање" - "Стави на чекање" - "Продолжи го повикот" - "Заврши го повикот" - "Прикажи копчиња за бирање" - "Сокриј копчиња за бирање" - "Исклучи звук" - "Вклучи звук" - "Додај повик" - "Спои повици" - "Замени" - "Управувај со повици" - "Управувај со конференциски повик" - "Конференциски повик" - "Управувај" - "Аудио" - "Видеоповик" - "Промени во гласовен повик" - "Промени ја камерата" - "Вклучете ја камерата" - "Исклучете ја камерата" - "Повеќе опции" - "Плеерот се вклучи" - "Плеерот запре" - "Камерата не е подготвена" - "Камерата е подготвена" - "Непознат настан при сесија повици" - "Услуга" - "Поставување" - "<Нема поставка>" - "Други поставки за повик" - "Повикување преку %s" - "Дојдовни повици преку %s" - "фотографија на контакт" - "префли на приватно" - "избери контакт" - "Напиши сопствена..." - "Откажи" - "Испрати" - "Одговори" - "Испрати SMS" - "Одбиј" - "Одговори со видеоповик" - "Одговори со аудиоповик" - "Прифати барање за видео" - "Одбиј барање за видео" - "Прифати барање за пренос на видео" - "Одбиј барање за пренос на видео" - "Прифати барање за прием на видео" - "Одбиј барање за прием на видео" - "Лизгај нагоре за %s." - "Лизгај налево за %s." - "Лизгај надесно за %s." - "Лизгај надолу за %s." - "Вибрации" - "Вибрации" - "Звук" - "Стандарден звук (%1$s)" - "Мелодија на телефонот" - "Вибрации при ѕвонење" - "Мелодија и вибрации" - "Управувај со конференциски повик" - "Број за итни случаи" - "Фотографија на профилот" - "Камерата е исклучена" - "преку %s" - "Испратена е белешка" - "Неодамнешни пораки" - "Деловни информации" - "Оддалечено %.1f милји" - "Оддалечено %.1f км" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Отвора утре во %s" - "Отвора денес во %s" - "Затвора во %s" - "Денес затвори во %s" - "Сега е отворено" - "Сега е затворено" - "Повикот е можен спам" - "Повикот заврши %1$s" - "За првпат добивте повик од бројов." - "Постоеше сомнеж дека повиков е спам." - "Блок./пријави спам" - "Додајте го контактот" - "Не е спам" - diff --git a/InCallUI/res/values-ml/strings.xml b/InCallUI/res/values-ml/strings.xml deleted file mode 100644 index 5c313ad31b1f16f2875a4b22b0a78fb0bddbc3f3..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ml/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ഫോൺ" - "ഹോൾഡിലാണ്" - "അജ്ഞാതം" - "സ്വകാര്യ നമ്പർ" - "പണം നൽകി ഉപയോഗിക്കുന്ന ഫോൺ" - "കോൺഫറൻസ് കോൾ" - "കോൾ വിട്ടു" - "സ്പീക്കർ" - "ഹാൻഡ്‌സെറ്റ് ഇയർപീസ്" - "വയേർഡ് ഹെഡ്സെറ്റ്" - "Bluetooth" - "ഇനിപ്പറയുന്ന ടോണുകൾ അയയ്‌ക്കണോ?\n" - "ടോണുകൾ അയയ്‌ക്കുന്നു\n" - "അയയ്‌ക്കുക" - "ഉവ്വ്" - "ഇല്ല" - "വൈൽഡ് പ്രതീകം ഇതുപയോഗിച്ച് മാറ്റിസ്ഥാപിക്കുക" - "കോൺഫറൻസ് കോൾ %s" - "വോയ്‌സ്‌മെയിൽ നമ്പർ" - "ഡയൽ ചെയ്യുന്നു" - "വീണ്ടും ഡയൽചെയ്യുന്നു" - "കോൺഫറൻസ് കോൾ" - "ഇൻകമിംഗ് കോൾ" - "ഇൻകമിംഗ് ഔദ്യോഗിക കോൾ" - "കോൾ അവസാനിച്ചു" - "ഹോൾഡിലാണ്" - "ഹാംഗ് അപ്പ് ചെയ്യുന്നു" - "കോളിലാണ്" - "എന്റെ നമ്പർ %s ആണ്" - "വീഡിയോ കണക്‌റ്റുചെയ്യുന്നു" - "വീഡിയോ കോൾ" - "വീഡിയോ അഭ്യർത്ഥിക്കുന്നു" - "വീഡിയോ കോളുമായി കണക്‌റ്റുചെയ്യാനാവില്ല" - "വീഡിയോ അഭ്യർത്ഥന നിരസിച്ചു" - "നിങ്ങൾ തിരിച്ചുവിളിക്കേണ്ട നമ്പർ\n %1$s" - "അടിയന്തിരമായി നിങ്ങൾ തിരിച്ചുവിളിക്കേണ്ട നമ്പർ\n %1$s" - "ഡയൽ ചെയ്യുന്നു" - "മിസ്‌ഡ് കോൾ" - "മിസ്‌ഡ് കോളുകൾ" - "%s മിസ്‌ഡ് കോളുകൾ" - "%s എന്നതിൽ നിന്നുള്ള മിസ്‌ഡ് കോൾ" - "കോൾ സജീവമാണ്" - "ഓൺഗോയിംഗ് ഔദ്യോഗിക കോൾ" - "ഓൺഗോയിംഗ് വൈഫൈ കോൾ" - "ഓൺഗോയിംഗ് വൈഫൈ ഔദ്യോഗിക കോൾ" - "ഹോൾഡിലാണ്" - "ഇൻകമിംഗ് കോൾ" - "ഇൻകമിംഗ് ഔദ്യോഗിക കോൾ" - "ഇൻകമിംഗ് വൈഫൈ കോൾ" - "ഇൻകമിംഗ് വൈഫൈ ഔദ്യോഗിക കോൾ" - "ഇൻകമിംഗ് വീഡിയോ കോൾ" - "സംശയാസ്‌പദമായ ഇൻകമിംഗ് സ്‌പാം കോൾ" - "ഇൻകമിംഗ് വീഡിയോ അഭ്യർത്ഥന" - "പുതിയ വോയ്‌സ്‌മെയിൽ" - "പുതിയ വോയ്‌സ്‌മെയിൽ (%d)" - "%s ഡയൽ ചെയ്യുക" - "വോയ്‌സ്‌മെയിൽ നമ്പർ അജ്ഞാതമാണ്" - "സേവനമില്ല" - "തിരഞ്ഞെടുത്ത നെറ്റ്‌വർക്ക് (%s) ലഭ്യമല്ല" - "മറുപടി" - "ഹാംഗ് അപ്പുചെയ്യുക" - "വീഡിയോ" - "വോയ്‌സ്" - "അംഗീകരിക്കുക" - "ഡിസ്മിസ്" - "തിരിച്ചുവിളിക്കുക" - "സന്ദേശം" - "മറ്റൊരു ഉപകരണത്തിൽ നടന്നുകൊണ്ടിരിക്കുന്ന കോൾ" - "കോൾ കൈമാറുക" - "ഒരു കോൾ ചെയ്യാൻ, ആദ്യം ഫ്ലൈറ്റ് മോഡ് ഓഫുചെയ്യുക." - "നെറ്റ്‌വർക്കിൽ രജിസ്റ്റർ ചെയ്‌തിട്ടില്ല." - "സെല്ലുലാർ നെറ്റ്‌വർക്ക് ലഭ്യമല്ല." - "ഒരു കോൾ ചെയ്യുന്നതിന്, സാധുതയുള്ള നമ്പർ നൽകുക." - "കോൾ ചെയ്യാനായില്ല." - "MMI സീക്വൻസ് ആരംഭിക്കുന്നു…" - "സേവനം പിന്തുണയ്‌ക്കുന്നില്ല." - "കോളുകൾ മാറാനാവില്ല." - "കോൾ വേർതിരിക്കാനാവില്ല." - "കൈമാറ്റം ചെയ്യാനാവില്ല." - "കോൺഫറൻസ് കോൾ ചെയ്യാനാവില്ല." - "കോൾ നിരസിക്കാനാവില്ല." - "കോൾ (കോളുകൾ) വിളിക്കാനാവില്ല." - "SIP കോൾ" - "എമർജൻസി കോൾ" - "റേഡിയോ ഓൺ ചെയ്യുന്നു…" - "സേവനമൊന്നുമില്ല. വീണ്ടും ശ്രമിക്കുന്നു…" - "കോൾ ചെയ്യാനാവില്ല. %s എന്നത് ഒരു അടിയന്തിര നമ്പറല്ല." - "കോൾ ചെയ്യാനാവില്ല. ഒരു അടിയന്തിര കോൾ നമ്പർ ഡയൽചെയ്യുക." - "ഡയൽ ചെയ്യാൻ കീബോർഡ് ഉപയോഗിക്കുക" - "കോൾ ഹോൾഡുചെയ്യുക" - "കോൾ പുനരാരംഭിക്കുക" - "കോൾ അവസാനിപ്പിക്കുക" - "ഡയൽപാഡ് കാണിക്കുക" - "ഡയൽപാഡ് മറയ്‌ക്കുക" - "മ്യൂട്ടുചെയ്യുക" - "അൺമ്യൂട്ടുചെയ്യുക" - "കോൾ ചേർക്കുക" - "കോളുകൾ ലയിപ്പിക്കുക" - "സ്വാപ്പുചെയ്യുക" - "കോളുകൾ നിയന്ത്രിക്കുക" - "കോൺഫറൻസ് കോൾ നിയന്ത്രിക്കുക" - "കോൺഫറൻസ് കോൾ" - "മാനേജുചെയ്യുക" - "ഓഡിയോ" - "വീഡിയോ കോൾ" - "വോയ്‌സ്‌ കോളിലേക്ക് മാറ്റുക" - "ക്യാമറ സ്വിച്ചുചെയ്യുക" - "ക്യാമറ ഓണാക്കുക" - "ക്യാമറ ഓഫാക്കുക" - "കൂടുതൽ ഓ‌പ്‌ഷനുകൾ" - "പ്ലെയർ ആരംഭിച്ചു" - "പ്ലേയർ നിർത്തി" - "ക്യാമറ തയ്യാറായില്ല" - "ക്യാമറ തയ്യാറായി" - "അജ്ഞാത കോൾ സെഷൻ ഇവന്റ്" - "സേവനം" - "സജ്ജമാക്കുക" - "<ക്രമീകരിച്ചിട്ടില്ല>" - "മറ്റ് കോൾ ക്രമീകരണം" - "%s മുഖേന വിളിക്കുന്നു" - "%s മുഖേനയുള്ള ഇൻകമിംഗ്" - "കോൺടാക്റ്റ് ഫോട്ടോ" - "സ്വകാര്യം എന്നതിലേക്ക് പോകുക" - "കോൺടാക്റ്റ് തിരഞ്ഞെടുക്കുക" - "നിങ്ങളുടെ സ്വന്തം സന്ദേശമെഴുതുക..." - "റദ്ദാക്കുക" - "അയയ്‌ക്കുക" - "മറുപടി" - "SMS അയയ്ക്കുക" - "നിരസിക്കുക" - "വീഡിയോ കോളായി മറുപടി നൽകുക" - "ഓഡിയോ കോളായി മറുപടി നൽകുക" - "വീഡിയോ കോളിനുള്ള അഭ്യർത്ഥന അംഗീകരിക്കുക" - "വീഡിയോ കോൾ അഭ്യർത്ഥന നിരസിക്കുക" - "വീഡിയോ പ്രക്ഷേപണ അഭ്യർത്ഥന അംഗീകരിക്കുക" - "വീഡിയോ പ്രക്ഷേപണ അഭ്യർത്ഥന നിരസിക്കുക" - "വീഡിയോ കോൾ സ്വീകരിക്കാനുള്ള അഭ്യർത്ഥന അംഗീകരിക്കുക" - "വീഡിയോ കോൾ സ്വീകരിക്കാനുള്ള അഭ്യർത്ഥന നിരസിക്കുക" - "%s എന്നതിനായി മുകളിലേയ്‌ക്ക് സ്ലൈഡുചെയ്യുക." - "%s എന്നതിനായി ഇടത്തേയ്‌ക്ക് സ്ലൈഡുചെയ്യുക." - "%s എന്നതിനായി വലത്തേയ്‌ക്ക് സ്ലൈഡുചെയ്യുക." - "%s എന്നതിനായി താഴേക്ക് സ്ലൈഡുചെയ്യുക." - "വൈബ്രേറ്റുചെയ്യുക" - "വൈബ്രേറ്റുചെയ്യുക" - "ശബ്‌ദം" - "സ്ഥിര ശബ്‌ദം (%1$s)" - "ഫോൺ റിംഗ്ടോൺ" - "റിംഗുചെയ്യുമ്പോൾ വൈബ്രേറ്റുചെയ്യുക" - "റിംഗ്ടോണും വൈബ്രേറ്റുചെയ്യലും" - "കോൺഫറൻസ് കോൾ നിയന്ത്രിക്കുക" - "അടിയന്തര നമ്പർ" - "പ്രൊഫൈൽ ഫോട്ടോ" - "ക്യാമറ ഓഫാക്കുക" - "%s വഴി" - "കുറിപ്പ് അയച്ചു" - "ഏറ്റവും പുതിയ സന്ദേശങ്ങൾ" - "ബിസിനസ്സ് വിവരം" - "%.1f മൈൽ അകലെ" - "%.1f കിലോമീറ്റർ അകലെ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "നാളെ %s-ന് തുറക്കുന്നു" - "ഇന്ന് %s-ന് തുറക്കുന്നു" - "%s-ന് അടയ്ക്കുന്നു" - "ഇന്ന് %s-ന് അടച്ചു" - "ഇപ്പോൾ തുറന്നിരിക്കുന്നു" - "ഇപ്പോൾ അടച്ചിരിക്കുന്നു" - "സംശയാസ്‌പദമായ സ്‌പാം കോളർ" - "കോൾ അവസാനിച്ചു, %1$s" - "ഈ നമ്പറിൽ നിന്ന് ആദ്യമായാണ് നിങ്ങൾക്ക് കോൾ വരുന്നത്." - "ഈ കോൾ ഒരു സ്‌പാമർ ആണെന്ന് ഞങ്ങൾക്ക് സംശയമുണ്ടായിരുന്നു." - "ബ്ലോക്കുചെയ്യുക/സ്പാമാണെന്ന് റിപ്പോർട്ടുചെയ്യുക" - "കോൺടാക്റ്റ് ചേർക്കുക" - "സ്പാം അല്ല" - diff --git a/InCallUI/res/values-mn/strings.xml b/InCallUI/res/values-mn/strings.xml deleted file mode 100644 index 972f914a44ae78019c70f183be72e98dbe05a2ce..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-mn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Утас" - "Хүлээлгэнд байгаа" - "Тодорхойгүй" - "Нууцалсан дугаар" - "Төлбөртэй утас" - "Хурлын дуудлага" - "Дуудлага таслагдсан" - "Чанга яригч" - "Утасны чихэвч" - "Утастай чихэвч" - "Bluetooth" - "Дараах аяыг илгээх үү?\n" - "Ая илгээж байна\n" - "Илгээх" - "Тийм" - "Үгүй" - "Тэмдэгтийг дараахаар солих" - "Хурлын дуудлага %s" - "Дуут шуудангийн дугаар" - "Залгаж байна" - "Дахин залгаж байна" - "Хурлын дуудлага" - "Орох дуудлага" - "Орох ажлын дуудлага" - "Дуудлага дууссан" - "Хүлээлгэнд" - "Тасалж байна" - "Дуудлагатай" - "Миний дугаар %s" - "Видеог холбож байна" - "Видео дуудлага" - "Видео хүлээж байна" - "Видео дуудлагад холбогдож чадсангүй" - "Бичлэг хийх хүсэлтийг зөвшөөрсөнгүй" - "Таны буцаан залгах дугаар\n %1$s" - "Таны яаралтай хулээн авах дугаар\n %1$s" - "Залгаж байна" - "Аваагүй дуудлага" - "Аваагүй дуудлага" - "%s аваагүй дуудлага" - "%s-н аваагүй дуудлага" - "Залгаж буй дуудлага" - "Холбогдсон албаны дуудлага" - "Холбогдсон Wi-Fi дуудлага" - "Залгаж буй Wi-Fi албаны дуудлага" - "Хүлээгдэж байна" - "Орох дуудлага" - "Орох ажлын дуудлага" - "Орох Wi-Fi дуудлага" - "Орох Wi-Fi албаны дуудлага" - "Орох видео дуудлага" - "Орж ирсэн сэжигтэй спам дуудлага" - "Орох видео хүсэлт" - "Шинэ дуут шуудан" - "Шинэ дуут шуудан (%d)" - "%s руу залгах" - "Дуут шуудангийн дугаар тодорхойгүй" - "Үйлчилгээ байхгүй" - "Сонгосон сүлжээг (%s) ашиглах боломжгүй" - "Хариулт" - "Таслах" - "Видео" - "Дуу хоолой" - "Зөвшөөрөх" - "Алгасах" - "Буцааж залгах" - "Зурвас" - "Өөр төхөөрөмж дээр хийгдэж буй дуудлага" - "Дуудлага шилжүүлэх" - "Залгахын тулд эхлээд Нислэгийн горимоос гарна уу." - "Сүлжээнд бүртгэгдээгүй байна." - "Үүрэн сүлжээ байхгүй." - "Залгахын тулд хүчин төгөлдөр дугаар оруулна уу." - "Залгах боломжгүй байна." - "MMI дарааллыг эхлүүлж байна…" - "Дэмжигдээгүй үйлчилгээ байна." - "Дуудлагыг солих боломжгүй байна." - "Дуудлагыг салгаж чадахгүй байна." - "Шилжүүлэх боломжгүй байна." - "Хурлын дуудлага хийх боломжгүй байна." - "Дуудлагыг цуцлах боломжгүй байна." - "Дуудлага чөлөөлөх боломжгүй байна." - "SIP дуудлага" - "яаралтай" - "Радиог асааж байна..." - "Ажиллагаагүй байна. Дахин оролдоно уу..." - "Залгах боломжгүй. %s нь яаралтай дугаар биш байна." - "Залгах боломжгүй. Яаралтай дугаар луу залгана уу." - "Залгахдаа гар ашиглана уу" - "Дуудлага хүлээлгэх" - "Дуудлагыг үргэлжлүүлэх" - "Дуудлагыг дуусгах" - "Залгах товчлуурыг харуулах" - "Залгах товчлуурыг нуух" - "Дуу хаах" - "Дууг нээх" - "Дуудлага нэмэх" - "Дуудлага нэгтгэх" - "Солих" - "Дуудлага удирдах" - "Хурлын дуудлага удирдах" - "Хурлын дуудлага" - "Удирдах" - "Аудио" - "Видео дуудлага" - "Дуут дуудлага руу өөрчлөх" - "Камер солих" - "Камераа асаана уу" - "Камер унтраах" - "Нэмэлт сонголт" - "Тоглуулагчийг эхлүүлсэн" - "Тоглуулагчийг зогсоосон" - "Камер бэлэн бус байна" - "Камер бэлэн байна" - "Үл мэдэгдэх дуудлагын үе" - "Үйлчилгээ" - "Тохируулга" - "Тохируулаагүй" - "Бусад дуудлагын тохиргоо" - "%s-р залгаж байна" - "%s-р ирж байна" - "харилцагчийн зураг" - "хувийн яриа" - "харилцагч сонгох" - "Өөрийн ...-г бичээрэй" - "Цуцлах" - "Илгээх" - "Хариулт" - "SMS илгээх" - "Татгалзах" - "Видео дуудлагаар хариулах" - "Аудио дуудлагаар хариулах" - "Видео хүсэлтийг хүлээн зөвшөөрөх" - "Видео хүсэлтээс татгалзах" - "Видео дамжуулах хүсэлтийг хүлээн зөвшөөрөх" - "Видео дамжуулах хүсэлтээс татгалзах" - "Видео хүлээж авах хүсэлтийг зөвшөөрөх" - "Видео хүлээн авах хүсэлтээс татгалзах" - "%s хийх бол дээш гулсуулна уу." - "%s-г харахын тулд зүүн тийш гулсуулна уу." - "%s харахын тулд баруун тийш гулсуулна уу." - "%s-г харахын тулд доош гулсуулна уу." - "Чичиргээ" - "Чичиргээ" - "Дуу" - "Үндсэн дуу (%1$s)" - "Утасны хонхны ая" - "Хонх дуугарах үед чичрэх" - "Хонхны ая, Чичиргээ" - "Хурлын дуудлагыг удирдах" - "Яаралтай дугаар" - "Профайл зураг" - "Камер унтраалттай байна" - "%s-р" - "Тэмдэглэлийг илгээсэн" - "Саяхны зурвас" - "Бизнес мэдээлэл" - "%.1f милийн зайтай" - "%.1f км-н зайтай" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Маргааш %s-с нээгдэнэ" - "Өнөөдөр %s-с нээгдэнэ" - "%s-с хаадаг" - "Өнөөдөр %s-с хаасан" - "Одоо нээлттэй" - "Одоо хаалттай" - "Сэжигтэй спам дуудлага хийгч" - "Дуудлага дууссан %1$s" - "Энэ дугаараас танд анх удаа дуудлага ирсэн." - "Бид үүнийг спам дуудлага гэж үзсэн." - "Спам гэж мэдээлэх/хориглох" - "Харилцагч нэмэх" - "Спам биш" - diff --git a/InCallUI/res/values-mr/strings.xml b/InCallUI/res/values-mr/strings.xml deleted file mode 100644 index 15b3dfc32bf52e86694f6906cdcfc8c6b9201fb0..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-mr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "फोन" - "होल्ड वर" - "अज्ञात" - "खाजगी नंबर" - "सार्वजनिक फोन" - "परिषद कॉल" - "कॉल सोडला" - "स्पीकर" - "हँडसेट इअरपीस" - "वायर्ड हेडसेट" - "ब्लूटुथ" - "खालील टोन पाठवायचे?\n" - "टोन पाठवित आहे\n" - "पाठवा" - "होय" - "नाही" - "खराब वर्णास यासह पुनर्स्थित करा" - "परिषद कॉल %s" - "व्हॉइसमेल नंबर" - "डायल करीत आहे" - "रीडायल करत आहे" - "परिषद कॉल" - "येणारा कॉल" - "येणारा कार्य कॉल" - "कॉल संपला" - "होल्ड वर" - "हँग अप करणेे" - "कॉल मधील" - "माझा नंबर %s आहे" - "व्हिडिओ कनेक्ट करत आहे" - "व्हिडिओ कॉल" - "व्हिडिओ विनंती करत आहे" - "व्हिडिओ कॉल कनेक्ट करू शकत नाही" - "व्हिडिओ विनंती नाकारली" - "आपला कॉलबॅक नंबर\n %1$s" - "आपला आणीबाणी कॉलबॅक नंबर\n %1$s" - "डायल करीत आहे" - "सुटलेला कॉल" - "सुटलेले कॉल" - "%s सुटलेले कॉल" - "%s कडील सुटलेला कॉल" - "सुरू असलेला कॉल" - "सुरु असलेला कार्य कॉल" - "सुरु असलेला वाय-फाय कॉल" - "सुरु असलेला वाय-फाय कार्य कॉल" - "होल्ड वर" - "येणारा कॉल" - "येणारा कार्य कॉल" - "येणारा वाय-फाय कॉल" - "येणारा वाय-फाय कार्य कॉल" - "येणारा व्हिडिओ कॉल" - "येणारा संशयित स्पॅम कॉल" - "येणारी व्हिडिओ विनंती" - "नवीन व्हॉइसमेल" - "नवीन व्हॉइसमेल (%d)" - "%s डायल करा" - "व्हॉइसमेल नंबर अज्ञात" - "सेवा नाही" - "निवडलेले नेटवर्क (%s) अनुपलब्ध" - "उत्तर" - "हँग अप" - "व्हिडिओ" - "व्हॉइस" - "स्वीकार करा" - "डिसमिस करा" - "पुन्हा कॉल करा" - "संदेश" - "दुसऱ्या डिव्हाइसवर सुरु असलेला कॉल" - "कॉल स्थानांतरित करा" - "कॉल करण्यासाठी, प्रथम विमान मोड बंद करा." - "नेटवर्कवर नोंदणीकृत नाही." - "मोबाईल नेटवर्क उपलब्ध नाही." - "कॉल करण्यासाठी, एक वैध नंबर प्रविष्ट करा." - "कॉल करू शकत नाही." - "MMI क्रम प्रारंभ करीत आहे..." - "सेवा समर्थित नाही." - "कॉल स्विच करू शकत नाही." - "कॉल विभक्त करू शकत नाही." - "हस्तांतर करू शकत नाही." - "परिषद घेऊ शकत नाही." - "कॉल नाकारू शकत नाही." - "कॉल रिलीज करू शकत नाही." - "SIP कॉल" - "आणीबाणी कॉल" - "रेडिओ चालू करीत आहे..." - "सेवा नाही. पुन्हा प्रयत्न करत आहे…" - "कॉल करू शकत नाही. %s हा आणीबाणी नंबर नाही." - "कॉल करू शकत नाही. आणीबाणी नंबर डायल करा." - "डायल करण्यासाठी कीबोर्डचा वापर करा" - "कॉल होल्ड करा" - "कॉल पुनः सुरु करा" - "कॉल समाप्त करा" - "डायलपॅड दर्शवा" - "डायलपॅड लपवा" - "नि:शब्द करा" - "सशब्द करा" - "कॉल जोडा" - "कॉल विलीन करा" - "अदलाबदल करा" - "कॉल व्यवस्थापित करा" - "परिषद कॉल व्यवस्थापित करा" - "परिषद कॉल" - "व्यवस्थापित करा" - "ऑडिओ" - "व्हिडिओ कॉल" - "व्हॉइस कॉल वर बदला" - "कॅमेरा स्विच करा" - "कॅमेरा चालू करा" - "कॅमेरा बंद करा" - "अधिक पर्याय" - "प्लेअर सुरु झाले" - "प्लेअर थांबले" - "कॅमेरा तयार नाही" - "कॅमेरा तयार" - "अज्ञात कॉल सत्र इव्हेंट" - "सेवा" - "सेटअप" - "<सेट नाही>" - "इतर कॉल सेटिंग्ज" - "%s द्वारे कॉल करीत आहे" - "%s द्वारे येणारे" - "संपर्क फोटो" - "खाजगी व्हा" - "संपर्क निवडा" - "आपण स्वतः लिहा…" - "रद्द करा" - "पाठवा" - "उत्तर" - "SMS पाठवा" - "नकार द्या" - "व्हिडिओ कॉल म्हणून उत्तर द्या" - "ऑडिओ कॉल म्हणून उत्तर द्या" - "व्हिडिओ विनंती स्वीकारा" - "व्हिडिओ विनंतीस नकार द्या" - "व्हिडिओ प्रसारण विनंती स्वीकार करा" - "व्हिडिओ प्रसारण विनंतीस नकार द्या" - "व्हिडिओ प्राप्त करा विनंती स्वीकार करा" - "व्हिडिओ प्राप्त करा विनंतीस नकार द्या" - "%s साठी वर स्लाइड करा." - "%s साठी डावीकडे स्लाइड करा." - "%s साठी उजवीकडे स्लाइड करा." - "%s साठी खाली स्लाइड करा." - "कंपन करा" - "कंपन करा" - "ध्वनी" - "डीफॉल्ट आवाज (%1$s)" - "फोन रिंगटोन" - "रिंग करताना कंपन करा" - "रिंगटोन आणि कंपन" - "परिषद कॉल व्यवस्थापित करा" - "आणीबाणी नंबर" - "प्रोफाइल फोटो" - "कॅमेरा बंद" - "%s द्वारा" - "टीप पाठविली" - "अलीकडील संदेश" - "व्यवसाय माहिती" - "%.1f मैल दूर" - "%.1f किमी दूर" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "उद्या %s वाजता उघडेल" - "आज %s उघडेल" - "आज %s वाजता बंद होईल" - "आज %s वाजता बंद केले" - "आता उघडा" - "आता बंद केले आहे" - "संशयित स्पॅम कॉलर" - "कॉल समाप्त झाला %1$s" - "या नंबरने अापल्याला कॉल केल्याची ही पहिलीच वेळ आहे." - "अाम्हाला संशय अाहे की हा कॉल एक स्पॅमर असू शकतो." - "अवरोधित करा/स्पॅमचा अहवाल द्या" - "संपर्क जोडा" - "स्पॅम नाही" - diff --git a/InCallUI/res/values-ms/strings.xml b/InCallUI/res/values-ms/strings.xml deleted file mode 100644 index 4c7be38054e84a489d1cdf50b5e5ee3e88778678..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ms/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Ditunda" - "Tidak diketahui" - "Nombor peribadi" - "Telefon Awam" - "Panggilan sidang" - "Panggilan diputuskan" - "Pembesar suara" - "Alat dengar tel bimbit" - "Set kepala berwayar" - "Bluetooth" - "Hantar nada berikut?\n" - "Menghantar nada\n" - "Hantar" - "Ya" - "Tidak" - "Gantikan aksara bebas dengan" - "Panggilan sidang %s" - "Nombor mel suara" - "Mendail" - "Mendail semula" - "Panggilan sidang" - "Panggilan masuk" - "Pgln masuk tempat kerja" - "Panggilan tamat" - "Ditunda" - "Menamatkan panggilan" - "Dalam panggilan" - "Nombor saya ialah %s" - "Menyambungkan video" - "Panggilan video" - "Meminta video" - "Tidak dapat menyambungkan panggilan video" - "Permintaan video ditolak" - "Nombor panggilan balik anda%1$s\n" - "Nombor panggilan balik kecemasan anda\n%1$s" - "Mendail" - "Panggilan terlepas" - "Panggilan terlepas" - "%s panggilan terlepas" - "Panggilan tidak dijawab daripada %s" - "Panggilan sedang berlangsung" - "Panggilan sedang berlangsung daripada tempat kerja" - "Panggilan Wi-Fi sedang berlangsung" - "Panggian Wi-Fi sedang berlangsung daripada tempat kerja" - "Ditunda" - "Panggilan masuk" - "Panggilan masuk daripada tempat kerja" - "Panggilan masuk melalui Wi-Fi" - "Panggilan masuk melalui Wi-Fi daripada tempat kerja" - "Panggilan video masuk" - "Disyaki panggilan spam masuk" - "Permintaan video masuk" - "Mel suara baharu" - "Mel suara baharu (%d)" - "Dail %s" - "Nombor mel suara tidak dikenali" - "Tiada perkhidmatan" - "Rangkaian pilihan (%s) tidak tersedia" - "Jawab" - "Letakkan gagang" - "Video" - "Suara" - "Terima" - "Ketepikan" - "Panggil balik" - "Mesej" - "Panggilan sedang berlangsung pada peranti lain" - "Pindahkan Panggilan" - "Untuk membuat panggilan, matikan mod Pesawat terlebih dahulu." - "Tidak didaftarkan pada rangkaian." - "Rangkaian selular tidak tersedia." - "Untuk membuat panggilan, masukkan nombor yang sah." - "Tidak dapat memanggil." - "Memulakan jujukan MMI..." - "Perkhidmatan tidak disokong." - "Tidak dapat menukar panggilan." - "Tidak dapat mengasingkan panggilan." - "Tidak dapat memindahkan." - "Tidak dapat membuat panggilan persidangan." - "Tidak dapat menolak panggilan." - "Tidak dapat melepaskan panggilan." - "Panggilan SIP" - "Panggilan kecemasan" - "Menghidupkan radio..." - "Tiada perkhidmatan. Mencuba lagi..." - "Tidak dapat memanggil. %s bukan nombor kecemasan." - "Tidak dapat memanggil. Dail nombor kecemasan." - "Gunakan papan kekunci untuk mendail" - "Tahan Panggilan" - "Sambung Semula Panggilan" - "Tamatkan Panggilan" - "Tunjukkan Pad Pendail" - "Sembunyikan Pad Pendail" - "Redam" - "Nyahredam" - "Tambah panggilan" - "Gabung panggilan" - "Silih" - "Urus panggilan" - "Urus panggilan sidang" - "Panggilan sidang" - "Urus" - "Audio" - "Panggilan video" - "Tukar ke panggilan suara" - "Tukar kamera" - "Hidupkan kamera" - "Matikan kamera" - "Lagi pilihan" - "Pemain Dimulakan" - "Pemain Dihentikan" - "Kamera tidak bersedia" - "Kamera bersedia" - "Acara sesi panggilan tidak diketahui" - "Perkhidmatan" - "Persediaan" - "<Tidak ditetapkan>" - "Tetapan panggilan lain" - "Memanggil melalui %s" - "Panggilan masuk melalui %s" - "foto kenalan" - "jadi peribadi" - "pilih kenalan" - "Tulis sendiri…" - "Batal" - "Hantar" - "Jawab" - "Hantar SMS" - "Tolak" - "Jawab sebagai panggilan video" - "Jawab sebagai panggilan audio" - "Terima permintaan video" - "Tolak permintaan video" - "Terima permintaan hantar video" - "Tolak permintaan hantar video" - "Terima permintaan terima video" - "Tolak permintaan terima video" - "Luncurkan ke atas untuk %s." - "Luncurkan ke kiri untuk %s." - "Luncurkan ke kanan untuk %s." - "Luncurkan ke bawah untuk %s." - "Bergetar" - "Bergetar" - "Bunyi" - "Bunyi lalai (%1$s)" - "Nada dering telefon" - "Bergetar apabila berdering" - "Nada dering & Bergetar" - "Urus panggilan sidang" - "Nombor kecemasan" - "Foto profil" - "Kamera dimatikan" - "melalui %s" - "Nota dihantar" - "Mesej terbaharu" - "Maklumat perniagaan" - "%.1f batu dari sini" - "%.1f km dari sini" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Dibuka esok pada pukul %s" - "Dibuka hari ini pada pukul %s" - "Tutup pada pukul %s" - "Ditutup hari ini pada pukul %s" - "Dibuka sekarang" - "Ditutup sekarang" - "Disyaki pmggil spam" - "Panggilan tamat %1$s" - "Ini kali pertama nombor ini memanggil anda." - "Kami mengesyaki panggilan ini adalah spam." - "Sekat/laporkan spam" - "Tambahkan kenalan" - "Bukan spam" - diff --git a/InCallUI/res/values-my/strings.xml b/InCallUI/res/values-my/strings.xml deleted file mode 100644 index efe91436f57bfbe43c7066c48af0f3e960ca5fe9..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-my/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ဖုန်း" - "ခဏ ကိုင်ထားစဉ်" - "အမျိုးအမည်မသိ" - "ကိုယ်ပိုင်ဖုန်းနံပါတ်" - "အများသုံးဖုန်း" - "အစည်းအဝေးခေါ်ဆိုမှု" - "ဖုန်းလိုင်းကျသွားခဲ့သည်" - "စပီကာ" - "လက်ကိုင်တယ်လီဖုန်းနားခွက်" - "ကြိုးတပ် မိုက်ခွက်ပါနားကြပ်" - "ဘလူးတုသ်" - "အောက်ပါ တီးလုံးများကို ပို့မလား။\n" - "အသံများ ပို့နေသည်\n" - "ပို့ပါ" - "Yes" - "No" - "အစားထိုး အထူးအက္ခရာတွင် အစားထိုးရန်" - "အစည်းအဝေးခေါ်ဆိုမှု %s" - "အသံစာနံပါတ်" - "ခေါ်ဆိုနေသည်" - "ပြန်ခေါ်နေသည်" - "အစည်းအဝေးခေါ်ဆိုမှု" - "အဝင် ခေါ်ဆိုမှု" - "အလုပ်ဆိုင်ရာ အဝင် ခေါ်ဆိုမှု" - "ဖုန်းခေါ်ဆိုမှု ပြီးဆုံးပါပြီ" - "ခဏ ကိုင်ထားစဉ်" - "ဖုန်းချနေပါသည်" - "ဖုန်းခေါ်ဆိုနေဆဲ" - "ကျွန်ုပ်၏ နံပါတ်မှာ %s ဖြစ်ပါသည်" - "ဗီဒီယို ချိတ်ဆက်နေသည်" - "ဗီဒီယို ခေါ်ဆိုမှု" - "ဗီဒီယိုခေါ်ဆိုနေသည်" - "ဗွီဒီယို ခေါ်ဆိုမှု ချိတ်ဆက်၍မရပါ။" - "ဗီဒီယို ခေါ်ဆိုမှုကို ပယ်ချလိုက်ပါပြီ" - "သင့်ကိုပြန်လည်ခေါ်ဆိုရန် နံပါတ်\n %1$s" - "သင့်ကိုအရေးပေါ် ပြန်လည်ခေါ်ဆိုရန် နံပါတ်\n %1$s" - "ဖုန်းခေါ်နေသည်" - "လွတ်သွားသော ခေါ်ဆိုမှု" - "လွတ်သွားသော ခေါ်ဆိုမှုများ" - "လွတ်သွားသော ခေါ်ဆိုမှု %s" - "%s မှလွတ်သွားသော ခေါ်ဆိုမှု" - "လက်ရှိခေါ်ဆိုမှု" - "လက်ရှိအလုပ်ခေါ်ဆိုမှု" - "လက်ရှိ Wi-Fi ခေါ်ဆိုမှု" - "လက်ရှိအလုပ် Wi-Fi ခေါ်ဆိုမှု" - "ခဏ ကိုင်ထားစဉ်" - "အဝင် ခေါ်ဆိုမှု" - "အလုပ်ဆိုင်ရာ အဝင်ခေါ်ဆိုမှု" - "အဝင် Wi-Fi ခေါ်ဆိုမှု" - "အလုပ်ဆိုင်ရာ အဝင် Wi-Fi ခေါ်ဆိုမှု" - "အဝင်ဗီဒီယိုခေါ်ဆိုမှု" - "ခေါ်နေသော မသင်္ကာဖွယ်ရာ စပမ်းခေါ်ဆိုမှု" - "ဗီဒီယိုအဝင် ခေါ်ဆိုမှု" - "အသံစာအသစ်" - "အသံစာ အသစ် (%d) ခု" - "%s ကိုခေါ်ပါ" - "အသံစာ၏နံပါတ်ကို မသိပါ" - "ဆက်သွယ်မှု ဧရိယာပြင်ပသို့ ရောက်ရှိနေသည်" - "ရွေးချယ်ထားသော ကွန်ရက် (%s) မရရှိနိုင်ပါ" - "ဖြေကြားပါ" - "ဖုန်းချပါ" - "ဗီဒီယို" - "အသံ" - "လက်ခံပါ" - "ပယ်ပါ" - "ပြန်ခေါ်ပါ" - "မက်ဆေ့ဂျ်" - "အခြားကိရိယာတွင် လက်ရှိခေါ်ဆိုနေမှု" - "ခေါ်ဆိုမှုကို လွှဲပြောင်းပါ" - "ခေါ်ဆိုမှု ပြုလုပ်ရန်အတွက် လေယာဉ်ပျံမုဒ်ကို ဦးစွာပိတ်ပါ။" - "ကွန်ယက်ပေါ်တွင် မှတ်ပုံတင်ထားခြင်း မရှိပါ။" - "ဆဲလ်လူလာ ကွန်ရက် မရှိပါ။" - "ခေါ်ဆိုမှု ပြုလုပ်ရန်အတွက် မှန်ကန်သည့်နံပါတ်တစ်ခုကို ထည့်ပါ။" - "ခေါ်ဆို၍မရပါ။" - "MMI အစီအစဉ် စတင်နေသည်..." - "ဝန်ဆောင်မှုအား ပံ့ပိုးမထားပါ။" - "ခေါ်ဆိုမှုများကို လှည့်ပြောင်း၍မရပါ။" - "ခေါ်ဆိုမှုကို ခွဲခြား၍မရပါ။" - "မလွှဲပြောင်းနိုင်ပါ။" - "အစည်းအဝေးခေါ်ဆိုမှု ပြုလုပ်၍မရပါ။" - "ခေါ်ဆိုမှုကို ငြင်းဆို၍မရပါ။" - "ခေါ်ဆိုမှု(များ) ကို လွှတ်၍မရပါ။" - "SIP ခေါ်ဆိုမှု" - "အရေးပေါ် ခေါ်ဆိုမှု" - "ရေဒီယို ဖွင့်နေသည်…" - "ချိတ်ဆက်မှု ဧရိယာပြင်ပရောက်နေပါသည်။ ထပ်စမ်းကြည့်ပါ..." - "ခေါ်ဆို၍မရနိုင်ပါ။ %s သည်အရေးပေါ်နံပါတ်တစ်ခု မဟုတ်ပါ။" - "ခေါ်ဆို၍မရနိုင်ပါ။ အရေးပေါ်နံပါတ်တစ်ခုကို ခေါ်ဆိုပါ။" - "ခေါ်ဆိုရန် ကီးဘုတ်ကိုအသုံးပြုပါ" - "ခေါ်ဆိုမှု ခေတ္တရပ်ထားပါ" - "ခေါ်ဆိုမှုကို ဆက်လုပ်ပါ" - "ခေါ်ဆိုမှု အပြီးသတ်ပါ" - "နံပါတ်အကွက် ပြပါ" - "နံပါတ်အကွက် ဝှက်ထားပါ" - "အသံပိတ်ပါ" - "အသံပြန်ဖွင့်ပါ" - "ခေါ်ဆိုမှုထည့်ပါ" - "ခေါ်ဆိုမှုများကို ပေါင်းစည်းပါ" - "ဖလှယ်ပါ" - "ခေါ်ဆိုမှုများကို စီမံခန့်ခွဲပါ" - "အစည်းအဝေးခေါ်ဆိုမှုကို စီမံခန့်ခွဲပါ" - "မျက်နှာစုံညီစည်းဝေး ဖုန်းခေါ်ဆိုမှု" - "စီမံခန့်ခွဲပါ" - "အသံ" - "ဗီဒီယို ခေါ်ဆိုမှု" - "အသံခေါ်ဆိုမှုသို့ ပြောင်းပါ" - "ကင်မရာပြောင်းပါ" - "ကင်မရာဖွင့်ပါ" - "ကင်မရာပိတ်ပါ" - "နောက်ထပ် ရွေးစရာများ" - "ပလေယာ စပါပြီ" - "ပလေယာ ရပ်တန့်သွားပါပြီ" - "ကင်မရာအဆင်သင့် မဖြစ်သေးပါ" - "ကင်မရာအဆင်သင့်ဖြစ်ပါပြီ" - "အမျိုးအမည်မသိ ခေါ်ဆိုမှုအချိန်ကာလ" - "ဝန်ဆောင်မှု" - "စနစ်ထည့်သွင်းမှုပြုလုပ်ပါ" - "<မသတ်မှတ်ထားပါ>" - "အခြားခေါ်ဆိုမှုဆက်တင်များ" - "%s မှတစ်ဆင့် ခေါ်ဆိုခြင်း" - "%s မှတစ်ဆင့်အဝင်ခေါ်ဆိုမှု" - "အဆက်အသွယ်ဓာတ်ပုံ" - "တသီးတသန့်ချိတ်ဆက်ရန်" - "လိပ်စာရွေးပါ" - "သင့်ကိုယ်ပိုင် စာသား ရေးပါ..." - "မလုပ်တော့" - "ပို့ပါ" - "ဖြေကြားပါ" - "SMS ပို့ပါ" - "ငြင်းပယ်ပါ" - "ဗီဒီယိုခေါ်ဆိုမှုအဖြစ် ဖြေကြားပါ" - "အသံခေါ်ဆိုမှုအဖြစ် ဖြေကြားပါ" - "ဗီဒီယိုခေါ်ဆိုမှုကို လက်ခံပါ" - "ဗီဒီယိုခေါ်ဆိုမှုကို ငြင်းပယ်ပါ" - "ဗီဒီယိုထုတ်လွှင့်ခြင်းတောင်းဆိုမှုကို လက်ခံပါ" - "ဗီဒီယိုထုတ်လွှင့်ခြင်းတောင်းဆိုမှုကို ငြင်းပယ်ပါ" - "ဗီဒီယိုလက်ခံရရှိမှုတောင်းဆိုချက်ကို လက်ခံပါ" - "ဗီဒီယိုလက်ခံရရှိကြောင်းတောင်းဆိုမှုကို ငြင်းပယ်ပါ" - "%s အတွက် အပေါ်ကို ပွတ်ဆွဲပါ" - "%s အတွက် ဘယ်ဖက်ကို ပွတ်ဆွဲပါ" - "%s အတွက် ညာဖက်ကို ပွတ်ဆွဲပါ" - "%s အတွက် အောက်ကို ပွတ်ဆွဲပါ" - "တုန်ခါပါ" - "တုန်ခါပါ" - "အသံ" - "မူရင်း အသံ (%1$s)" - "ဖုန်းမြည်သံ" - "ဖုန်းမြည်စဉ် တုန်ခါပါ" - "ဖုန်းမြည်သံ & တုန်ခါသံ" - "အစည်းအဝေးခေါ်ဆိုမှုကို စီမံခန့်ခွဲပါ" - "အရေးပေါ်နံပါတ်" - "ပရိုဖိုင် ဓာတ်ပုံ" - "ကင်မရာ ပိတ်ပါ" - "%s မှတစ်ဆင့်" - "မှတ်ချက်ကို ပို့လိုက်ပါပြီ" - "မကြာသေးမီက မက်ဆေ့ဂျ်များ" - "စီးပွားရေး အချက်အလက်" - "%.1f မိုင်အကွာ" - "%.1f ကီလိုမီတာအကွာ" - "%1$s%2$s" - "%1$s - %2$s" - "%1$s%2$s" - "မနက်ဖြန် %s ၌ဖွင့်မည်" - "ယနေ့ %s ၌ဖွင့်မည်" - "%s ၌ပိတ်ပါမည်" - "ယနေ့ %s ၌ပိတ်ခဲ့သည်" - "ယခုဖွင့်ပါ" - "ယခုပိတ်ပါ" - "မသင်္ကာဖွယ်ရာ စပမ်းခေါ်ဆိုသူ" - "ဖုန်းခေါ်ဆိုမှု ပြီးဆုံးပါပြီ %1$s" - "ဤနံပါတ်သည် သင့်ထံ ပထမဆုံးခေါ်ဆိုသော နံပါတ်ဖြစ်သည်။" - "ယခုဖုန်းခေါ်ဆိုမှုသည် စပမ်းပို့သူဆီမှ ဖြစ်နိုင်သည်ဟု ထင်ပါသည်။" - "စပမ်းကို ပိတ်ဆို့ပါ/သတင်းပေးပို့ပါ" - "အဆက်အသွယ် ထည့်ပါ" - "စပမ်း မဟုတ်ပါ" - diff --git a/InCallUI/res/values-nb/strings.xml b/InCallUI/res/values-nb/strings.xml deleted file mode 100644 index d39e4d4416736ae85eb35f6f3d290b70a99a5d7d..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-nb/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "På vent" - "Ukjent" - "Skjult nummer" - "Telefonkiosk" - "Konferansesamtale" - "Anropet ble avbrutt" - "Høyttaler" - "Telefonhøyttaler" - "Hodetelefoner med kabel" - "Bluetooth" - "Vil du sende disse lydene?\n" - "Sender lydene\n" - "Send" - "Ja" - "Nei" - "Erstatt jokertegn med" - "Konferansesamtale %s" - "Nummeret til talepostkassen" - "Ringer" - "Ringer på nytt" - "Konferansesamtale" - "Innkommende anrop" - "Innkommende jobbanrop" - "Anropet er avsluttet" - "På vent" - "Legger på" - "Anrop pågår" - "Nummeret mitt er %s" - "Kobler til video" - "Videoanrop" - "Ber om video" - "Kan ikke koble til videoanropet" - "Videoforespørselen er avvist" - "Tilbakeringingsnummeret ditt\n %1$s" - "Tilbakeringingsnummeret ditt for nødstilfeller\n %1$s" - "Ringer" - "Tapt anrop" - "Tapte anrop" - "%s tapte anrop" - "Tapt anrop fra %s" - "Pågående anrop" - "Pågående jobbanrop" - "Pågående Wi-Fi-anrop" - "Pågående jobbanrop via Wi-Fi" - "På vent" - "Innkommende anrop" - "Innkommende jobbanrop" - "Innkommende Wi-Fi-anrop" - "Innkommende jobbanrop via Wi-Fi" - "Innkommende videoanrop" - "Innkommende anrop fra en mulig useriøs oppringer" - "Innkommende videoforespørsel" - "Ny talepost" - "Ny talepost (%d)" - "Ring %s" - "Nummeret til talepostkassen er ukjent" - "Ingen tjeneste" - "Det valgte nettverket (%s) er ikke tilgjengelig" - "Svar" - "Legg på" - "Video" - "Uten video" - "Godta" - "Avvis" - "Ring tilbake" - "Melding" - "Samtale pågår på en annen enhet" - "Overfør samtalen" - "For å ringe, slå av flymodus først." - "Ikke registrert på nettverket." - "Mobilnettverket er ikke tilgjengelig." - "For å ringe, skriv inn et gyldig nummer." - "Kan ikke ringe." - "Starter MMI-sekvens …" - "Tjenesten støttes ikke." - "Kan ikke bytte samtaler." - "Kan ikke splitte opp anropet." - "Kan ikke overføre." - "Kan ikke opprette konferanse." - "Kan ikke avvise anropet." - "Kan ikke frigjøre samtale(r)." - "SIP-anrop" - "Nødanrop" - "Slår på radioen …" - "Ingen tjeneste. Prøver på nytt …" - "Kan ikke ringe. %s er ikke et nødnummer." - "Kan ikke ringe. Ring et nødnummer." - "Bruk tastaturet for å ringe" - "Sett anropet på vent" - "Gjenoppta anropet" - "Avslutt anropet" - "Vis tastaturet" - "Skjul tastaturet" - "Slå av lyden" - "Slå på lyden" - "Legg til anrop" - "Slå sammen anrop" - "Bytt" - "Administrer anrop" - "Administrer konferansesamtale" - "Konferansesamtale" - "Administrer" - "Lyd" - "Videoanrop" - "Bytt til taleanrop" - "Bytt kamera" - "Slå på kameraet" - "Slå av kameraet" - "Flere alternativer" - "Avspilleren har startet" - "Avspilleren har stoppet" - "Kameraet er ikke klart" - "Kameraet er klart" - "Ukjent anrop" - "Tjeneste" - "Konfigurering" - "<Ikke angitt>" - "Andre anropsinnstillinger" - "Ringer via %s" - "Innkommende via %s" - "kontaktbilde" - "aktivér privat samtale" - "velg kontakt" - "Skriv ditt eget" - "Avbryt" - "Send" - "Svar" - "Send SMS" - "Avslå" - "Svar med video" - "Svar uten video" - "Godta videoforespørselen" - "Avslå videoforespørselen" - "Godta forespørselen om å sende video" - "Avslå forespørselen om å sende video" - "Godta forespørselen om å motta video" - "Avslå forespørselen om å motta video" - "Dra opp for %s." - "Dra til venstre for å %s." - "Dra til høyre for å %s." - "Dra ned for å %s." - "Vibrering" - "Vibrering" - "Lyd" - "Standardlyd (%1$s)" - "Telefonringelyd" - "Vibrer når telefonen ringer" - "Ringelyd og vibrering" - "Administrer konferansesamtale" - "Nødnummer" - "Profilbilde" - "Kameraet er slått av" - "via %s" - "Notatet er sendt" - "Nylige meldinger" - "Informasjon om bedriften" - "%.1f mile unna" - "%.1f km unna" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Åpner i morgen kl. %s" - "Åpner i dag kl. %s" - "Stenger kl. %s" - "Stengte i dag kl. %s" - "Åpen nå" - "Stengt nå" - "Mulig useriøst anrop" - "Samtalen ble avsluttet %1$s" - "Dette er første gang du blir oppringt fra dette nummeret." - "Vi har mistanke om at dette anropet kommer fra en useriøs oppringer." - "Blokkér/rapportér" - "Legg til som kontakt" - "Ikke useriøs" - diff --git a/InCallUI/res/values-ne/strings.xml b/InCallUI/res/values-ne/strings.xml deleted file mode 100644 index 71c1ccae962cd2da0feb569c1b99787bab577abb..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ne/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "फोन" - "होल्डमा" - "अज्ञात" - "निजी नम्बर" - "पेफोन" - "सम्मेलन कल" - "कल ड्रप भयो" - "स्पिकर" - "हेन्डसेट इयरपिस" - "तारसहितको हेडसेट" - "ब्लुटुथ" - "निम्न टोनहरू पठाउने हो?\n" - "टोनहरू\n पठाउँदै" - "पठाउनुहोस्" - "हो" - "होइन" - "यसलाई वाइल्ड क्यारेक्टर राखेर बदल्नुहोस्" - "सम्मेलन कल %s" - "भ्वाइस मेल नम्बर" - "डायल गर्दै" - "पुन: डायल गर्दै" - "सम्मेलन कल" - "आगमन कल" - "कार्यालयबाट आएको कल" - "कल अन्त्य भयो" - "होल्डमा छ" - "फोन काट्दै" - "कलमा" - "मेरो नम्बर %s हो" - "भिडियो जडान गरिँदै" - "भिडियो कल" - "भिडियोका लागि अनुरोध गर्दै" - "भिडियो कलमा जडान गर्न सक्दैन" - "भिडियो अनुरोध अस्वीकार गरियो" - "तपाईंको कलब्याक नम्बर\n %1$s" - "तपाईंको आपतकालीन कलब्याक नम्बर\n %1$s" - "डायल गर्दै" - "छुटेको कल" - "छुटेका कलहरू" - "%s छुटेका कलहरू" - "%s बाट आएको छुटेको कल" - "चलिरहेको कल" - "चालु रहेको कार्यालयको कल" - "चालु रहेको WI-Fi कल" - "Wi-Fi मार्फत चालु रहेको कार्यालयको कल" - "होल्डमा" - "आगमन कल" - "कार्यालयबाट आएको कल" - "आगमन Wi-Fi कल" - "Wi-Fi मार्फत कार्यालयबाट आएको कल" - "आगमन भिडियो कल" - "शंकास्पद आगमन स्प्याम कल" - "आगमन भिडियो अनुरोध" - "नयाँ भ्वाइस मेल" - "नयाँ भ्वाइसमेल (%d)" - "%s मा डायल गर्नुहोस्" - "भ्वाइस मेल नम्बर अज्ञात छ" - "कुनै सेवा छैन" - "चयन गरिएको नेटवर्क (%s) अनुपलब्ध छ" - "जवाफ दिनुहोस्" - "राख्नुहोस्" - "भिडियो" - "आवाज" - "स्वीकार गर्नुहोस्" - "खारेज गर्नुहोस्" - "कल फर्काउने" - "सन्देश" - "अर्को यन्त्रमा चलिरहेको कल" - "कल स्थानान्तरण गर्नुहोस्" - "कल गर्नका लागि, पहिले हवाइजहाज मोड बन्द गर्नुहोस्।" - "नेटवर्कमा दर्ता भएको छैन।" - "सेलुलर नेटवर्क उपलब्ध छैन।" - "एक कल गर्नको लागि, मान्य नम्बर प्रविष्ट गर्नुहोस्।" - "कल गर्न सकिंदैन।" - "MMI अनुक्रम सुरु गर्दै..." - "सेवा समर्थित छैन।" - "कल स्विच गर्न सक्दैन।" - "कल अलग गर्न सक्दैन।" - "ट्रान्सफर गर्न सक्दैन।" - "सम्मेलन गर्न सक्दैन।" - "कल अस्वीकार गर्न सक्दैन।" - "कल (हरू) जारी गर्न सकिंदैन।" - "SIP कल" - "आपतकालीन कल" - "रेडियो खोल्दै..." - "कुनै सेवा छैन। फेरि प्रयास गर्दै..." - "कल गर्न सकिंदैन। %s आपतकालीन नम्बर होइन।" - "कल गर्न सकिंदैन। आपतकालीन नम्बर डायल गर्नुहोस्।" - "डायल गर्न किबोर्ड प्रयोग गर्नुहोस्" - "कललाई होल्ड गर्नुहोस्" - "कललाई पुन: निरन्तरता दिनुहोस्" - "कल अन्त्य गर्नुहोस्" - "डायलप्याड देखाउनुहोस्" - "डायलप्याड लुकाउनुहोस्" - "मौन" - "अनम्यूट गर्नुहोस्" - "कल थप्नुहोस्" - "कलहरू मर्ज गर्नुहोस्" - "स्वाप" - "कलहरूको प्रबन्ध मिलाउनुहोस्" - "सम्मेलन कलको प्रबन्ध मिलाउनहोस्" - "सम्मेलन कल" - "व्यवस्थापन गर्नुहोस्" - "अडियो" - "भिडियो कल" - "आवाज कलमा परिवर्तन गर्नुहोस्" - "क्यामेरा स्विच गर्नुहोस्" - "क्यामेरालाई सक्रिय गर्नुहोस्" - "क्यामेरालाई निष्क्रिय पार्नुहोस्" - "थप विकल्पहरू" - "प्लेयर सुरु भयो" - "प्लेयर रोकियो" - "क्यामेरा तयार छैन" - "क्यामेरा तयार छ" - "अज्ञात कल सत्र घटना" - "सेवा" - "सेटअप" - "<सेट गरिएको छैन>" - "अन्य कल सेटिङहरू" - "%s मार्फत कल गर्दै" - "%s मार्फत आगमन" - "सम्पर्क तस्बिर" - "निजी कलमा जानुहोस्" - "सम्पर्क चयन गर्नुहोस्" - "तपाईंको आफ्नै लेख्नुहोस्..." - "रद्द गर्नुहोस्" - "पठाउनुहोस्" - "जवाफ दिनुहोस्" - "SMS पठाउनुहोस्" - "अस्वीकार गर्नुहोस्" - "भिडियो कलको रूपमा जवाफ दिनुहोस्" - "अडियो कलको रूपमा जवाफ दिनुहोस्" - "भिडियो अनुरोध स्वीकार गर्नुहोस्" - "भिडियो अनुरोध अस्वीकार गर्नुहोस्" - "भिडियो प्रसारण गर्ने अनुरोध स्वीकार गर्नुहोस्" - "भिडियो प्रसारण गर्ने अनुरोध अस्वीकार गर्नुहोस्" - "भिडियो प्राप्त गर्ने अनुरोधलाई स्वीकार गर्नुहोस्" - "भिडियो प्राप्त गर्ने अनुरोध अस्वीकार गर्नुहोस्" - "%s को लागि माथि स्लाइड गर्नुहोस्।" - "%s को लागि बायाँ स्लाइड गर्नुहोस्।" - "%s को लागि दायाँ स्लाइड गर्नुहोस्।" - "%s को लागि तल स्लाइड गर्नुहोस्।" - "कम्पन हुने" - "कम्पन हुने" - "आवाज" - "पूर्वनिर्धारित ध्वनि (%1$s)" - "फोनको रिङटोन" - "घन्टी बज्दा कम्पन गराउनुहोस्" - "रिङटोन & कम्पन" - "सम्मेलन कलको प्रबन्ध मिलाउनुहोस्" - "आपतकालीन नम्बर" - "प्रोफाइल तस्बिर" - "क्यामेरा बन्द छ" - "%s बाट" - "नोट पठाइयो" - "भर्खरैका सन्देशहरू" - "व्यवसाय बारे जानकारी" - "%.1f माइल टाढा" - "%.1f किलोमिटर टाढा" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "भोलि %s मा खुल्छ" - "आज %s मा खुल्छ" - "%s मा बन्द हुन्छ" - "आज %s मा बन्द भयो" - "अहिले खुला छ" - "अब बन्द भयो" - "शंकास्पद स्प्याम कलर" - "कल समाप्त भयो %1$s" - "यो नम्बरबाट तपाईँलाई फोन आएको यो पहिलो पटक हो।" - "हामीले यो कल स्प्यामर हुन सक्ने आशङ्का गर्‍यौँ।" - "स्प्याम रोक्नु्/रिपोर्ट गर्ने" - "सम्पर्क थप्नुहोस्" - "स्प्याम होइन" - diff --git a/InCallUI/res/values-nl/strings.xml b/InCallUI/res/values-nl/strings.xml deleted file mode 100644 index 9eaf556c0287c928f074a5f49408adf876a2edfe..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-nl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefoon" - "In de wacht" - "Onbekend" - "Privénummer" - "Betaaltelefoon" - "Telefonische vergadering" - "Oproep beëindigd" - "Luidspreker" - "Oortelefoon van handset" - "Bedrade headset" - "Bluetooth" - "De volgende tonen verzenden?\n" - "Nummers verzenden\n" - "Verzenden" - "Ja" - "Nee" - "Jokerteken vervangen door" - "Telefonische vergadering %s" - "Voicemailnummer" - "Kiezen" - "Opnieuw bellen" - "Telefonische vergadering" - "Inkomende oproep" - "Inkom. zakelijke oproep" - "Oproep beëindigd" - "In de wacht" - "Ophangen" - "In gesprek" - "Mijn nummer is %s" - "Verbinding maken met video" - "Videogesprek" - "Video aanvragen" - "Kan geen videogesprek starten" - "Videoverzoek geweigerd" - "Je terugbelnummer\n %1$s" - "Je terugbelnummer bij alarm\n %1$s" - "Kiezen" - "Gemiste oproep" - "Gemiste oproepen" - "%s gemiste oproepen" - "Gemiste oproep van %s" - "Actieve oproep" - "Actieve zakelijke oproep" - "Actieve wifi-oproep" - "Actieve zakelijke oproep via wifi" - "In de wacht" - "Inkomende oproep" - "Inkomende zakelijke oproep" - "Inkomende wifi-oproep" - "Inkomende zakelijke oproep via wifi" - "Inkomend videogesprek" - "Inkomende vermoedelijke spamoproep" - "Inkomend videoverzoek" - "Nieuwe voicemail" - "Nieuwe voicemail (%d)" - "%s bellen" - "Voicemailnummer onbekend" - "Geen service" - "Geselecteerd netwerk (%s) niet beschikbaar" - "Beantwoorden" - "Ophangen" - "Video" - "Spraak" - "Accepteren" - "Sluiten" - "Terugbellen" - "Bericht" - "Actief gesprek op een ander apparaat" - "Gesprek doorschakelen" - "Als je wilt bellen, moet je eerst de vliegtuigmodus uitschakelen." - "Niet geregistreerd op netwerk." - "Mobiel netwerk niet beschikbaar." - "Als je wilt bellen, moet je een geldig nummer invoeren." - "Kan niet bellen." - "MMI-reeks starten..." - "Service wordt niet ondersteund." - "Kan niet schakelen tussen oproepen." - "Kan oproep niet scheiden." - "Kan niet doorschakelen." - "Telefonische vergadering niet mogelijk." - "Kan oproep niet weigeren." - "Kan oproep(en) niet vrijgeven." - "SIP-oproep" - "Noodoproep" - "Radio inschakelen…" - "Geen bereik. Opnieuw proberen…" - "Kan niet bellen. %s is geen alarmnummer." - "Kan niet bellen. Bel een alarmnummer." - "Toetsen gebruiken om te bellen" - "Oproep in de wacht zetten" - "Oproep hervatten" - "Oproep beëindigen" - "Toetsenblok weergeven" - "Toetsenblok verbergen" - "Dempen" - "Dempen opheffen" - "Oproep toevoegen" - "Samenvoegen" - "Wisselen" - "Oproepen beheren" - "Telef. vergadering beheren" - "Telefonische vergadering" - "Beheren" - "Audio" - "Vid.gespr." - "Wijzigen in spraakoproep" - "Van camera wisselen" - "Camera inschakelen" - "Camera uitschakelen" - "Meer opties" - "Speler gestart" - "Speler gestopt" - "Camera niet gereed" - "Camera gereed" - "Onbekende oproepsessiegebeurtenis" - "Service" - "Configuratie" - "<Niet ingesteld>" - "Andere instellingen voor bellen" - "Bellen via %s" - "Inkomend via %s" - "contactfoto" - "privé" - "contact selecteren" - "Eigen reactie opstellen..." - "Annuleren" - "Verzenden" - "Beantwoorden" - "Sms verzenden" - "Weigeren" - "Beantwoorden als videogesprek" - "Beantwoorden als audiogesprek" - "Videoverzoek accepteren" - "Videoverzoek weigeren" - "Verzoek voor video-overdracht accepteren" - "Verzoek voor video-overdracht weigeren" - "Verzoek voor video-ontvangst accepteren" - "Verzoek voor video-ontvangst weigeren" - "Veeg omhoog voor %s." - "Veeg naar links voor %s." - "Veeg naar rechts voor %s." - "Veeg omlaag voor %s." - "Trillen" - "Trillen" - "Geluid" - "Standaardgeluid (%1$s)" - "Beltoon telefoon" - "Trillen bij bellen" - "Beltoon en trillen" - "Telefonische vergadering beheren" - "Alarmnummer" - "Profielfoto" - "Camera uit" - "via %s" - "Notitie verzonden" - "Recente berichten" - "Bedrijfsinformatie" - "%.1f mijl hiervandaan" - "%.1f km hiervandaan" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Gaat morgen open om %s" - "Gaat vandaag open om %s" - "Sluit om %s" - "Vandaag gesloten vanaf %s" - "Nu geopend" - "Nu gesloten" - "Vermoedelijke spambeller" - "Oproep beëindigd %1$s" - "Dit is de eerste keer dat je bent gebeld door dit nummer." - "We vermoedden dat deze oproep afkomstig was van een spammer." - "Blokk./spam melden" - "Contact toevoegen" - "Geen spam" - diff --git a/InCallUI/res/values-pa/strings.xml b/InCallUI/res/values-pa/strings.xml deleted file mode 100644 index 318fc4cdccd2328a3982ee5435a55ec287102f7d..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-pa/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ਫੋਨ" - "ਰੋਕੀ ਗਈ" - "ਅਗਿਆਤ" - "ਨਿੱਜੀ ਨੰਬਰ" - "ਪੇ-ਫੋਨ" - "ਕਾਨਫਰੰਸ ਕਾਲ" - "ਕਾਲ ਡ੍ਰੌਪ ਹੋਈ" - "ਸਪੀਕਰ" - "ਹੈੱਡਸੈੱਟ ਈਯਰਪੀਸ" - "ਵਾਇਰ ਵਾਲਾ ਹੈੱਡਸੈੱਟ" - "ਬਲੂਟੁੱਥ" - "ਕੀ ਅੱਗੇ ਦਿੱਤੀਆਂ ਗਈਆਂ ਧੁਨੀਆਂ ਭੇਜਣੀਆਂ ਹਨ?\n" - "ਧੁਨੀਆਂ ਭੇਜੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ\n" - "ਭੇਜੋ" - "ਹਾਂ" - "ਨਹੀਂ" - "ਵਾਈਲਡ ਅੱਖਰ ਨੂੰ ਇਸ ਨਾਲ ਬਦਲੋ" - "ਕਾਨਫਰੰਸ ਕਾਲ %s" - "ਵੌਇਸਮੇਲ ਨੰਬਰ" - "ਡਾਇਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਦੁਬਾਰਾ ਡਾਇਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਕਾਨਫਰੰਸ ਕਾਲ" - "ਆ ਰਹੀ ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਆ ਰਹੀ ਕਾਲ" - "ਕਾਲ ਸਮਾਪਤ ਹੋਈ" - "ਰੋਕੀ ਗਈ" - "ਰੋਕੀ ਜਾ ਰਹੀ ਹੈ" - "ਚਾਲੂ ਕਾਲ" - "ਮੇਰਾ ਨੰਬਰ %s ਹੈ" - "ਵੀਡੀਓ ਨੂੰ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਵੀਡੀਓ ਕਾਲ" - "ਵੀਡੀਓ ਲਈ ਬੇਨਤੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ" - "ਵੀਡੀਓ ਕਾਲ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ" - "ਵੀਡੀਓ ਬੇਨਤੀ ਅਸਵੀਕਾਰ ਕੀਤੀ ਗਈ" - "ਤੁਹਾਡਾ ਕਾਲਬੈਕ ਨੰਬਰ \n %1$s" - "ਤੁਹਾਡਾ ਐਮਰਜੈਂਸੀ ਕਾਲਬੈਕ ਨੰਬਰ\n %1$s" - "ਡਾਇਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਖੁੰਝੀ ਹੋਈ ਕਾਲ" - "ਖੁੰਝੀਆਂ ਹੋਈਆਂ ਕਾਲਾਂ" - "%s ਖੁੰਝੀਆਂ ਹੋਈਆਂ ਕਾਲਾਂ" - "%s ਤੋਂ ਖੁੰਝੀ ਹੋਈ ਕਾਲ" - "ਜਾਰੀ ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਜਾਰੀ ਕਾਲ" - "ਜਾਰੀ Wi-Fi ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਜਾਰੀ Wi-Fi ਕਾਲ" - "ਰੋਕੀ ਗਈ" - "ਆ ਰਹੀ ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਆ ਰਹੀ ਕਾਲ" - "ਆ ਰਹੀ Wi-Fi ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਆ ਰਹੀ Wi-Fi ਕਾਲ" - "ਆ ਰਹੀ ਵੀਡੀਓ ਕਾਲ" - "ਸ਼ੱਕੀ ਸਪੈਮ ਕਾਲ ਆ ਰਹੀ ਹੈ" - "ਆ ਰਹੀ ਵੀਡੀਓ ਬੇਨਤੀ" - "ਨਵੀਂ ਵੌਇਸਮੇਲ" - "ਨਵੀਂ ਵੌਇਸਮੇਲ (%d)" - "%s ਡਾਇਲ ਕਰੋ" - "ਵੌਇਸਮੇਲ ਨੰਬਰ ਅਗਿਆਤ" - "ਕੋਈ ਸੇਵਾ ਨਹੀਂ" - "ਚੁਣਿਆ ਗਿਆ ਨੈੱਟਵਰਕ (%s) ਉਪਲਬਧ ਨਹੀਂ ਹੈ" - "ਜਵਾਬ ਦਿਓ" - "ਰੋਕੋ" - "ਵੀਡੀਓ" - "ਵੌਇਸ" - "ਸਵੀਕਾਰ ਕਰੋ" - "ਰੱਦ ਕਰੋ" - "ਵਾਪਸ ਕਾਲ ਕਰੋ" - "ਸੁਨੇਹਾ" - "ਕਿਸੇ ਹੋਰ ਡੀਵਾਈਸ \'ਤੇ ਜਾਰੀ ਕਾਲ" - "ਕਾਲ ਟ੍ਰਾਂਸਫਰ ਕਰੋ" - "ਇੱਕ ਕਾਲ ਕਰਨ ਲਈ, ਪਹਿਲਾਂ ਜਹਾਜ਼ ਮੋਡ ਬੰਦ ਕਰੋ।" - "ਨੈੱਟਵਰਕ \'ਤੇ ਰਜਿਸਟਰ ਨਹੀਂ।" - "ਸੈਲਿਊਲਰ ਨੈੱਟਵਰਕ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।" - "ਇੱਕ ਕਾਲ ਕਰਨ ਲਈ, ਇੱਕ ਵੈਧ ਨੰਬਰ ਦਾਖਲ ਕਰੋ।" - "ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "MMI ਕੜੀ ਨੂੰ ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…" - "ਸੇਵਾ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।" - "ਕਾਲਾਂ ਸਵਿੱਚ ਨਹੀਂ ਕੀਤੀਆਂ ਜਾ ਸਕਦੀਆਂ।" - "ਵੱਖਰੇ ਤੌਰ \'ਤੇ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਲ ਟ੍ਰਾਂਸਫਰ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਨਫਰੰਸ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਲ ਰੱਦ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਲ(ਲਾਂ) ਰੀਲੀਜ਼ ਨਹੀਂ ਕੀਤੀਆਂ ਜਾ ਸਕਦੀਆਂ।" - "SIP ਕਾਲ" - "ਐਮਰਜੈਂਸੀ ਕਾਲ" - "ਰੇਡੀਓ ਚਾਲੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…" - "ਕੋਈ ਸੇਵਾ ਨਹੀਂ। ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ…" - "ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। %s ਇੱਕ ਐਮਰਜੈਂਸੀ ਨੰਬਰ ਨਹੀਂ ਹੈ।" - "ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। ਇੱਕ ਐਮਰਜੈਂਸੀ ਨੰਬਰ ਡਾਇਲ ਕਰੋ।" - "ਡਾਇਲ ਕਰਨ ਲਈ ਕੀ-ਬੋਰਡ ਵਰਤੋ" - "ਕਾਲ ਰੋਕੋ" - "ਕਾਲ ਮੁੜ-ਸ਼ੁਰੂ ਕਰੋ" - "ਕਾਲ ਸਮਾਪਤ ਕਰੋ" - "ਡਾਇਲਪੈਡ ਵਿਖਾਓ" - "ਡਾਇਲਪੈਡ ਲੁਕਾਓ" - "ਮਿਊਟ ਕਰੋ" - "ਅਣਮਿਊਟ ਕਰੋ" - "ਕਾਲ ਸ਼ਾਮਲ ਕਰੋ" - "ਕਾਲਾਂ ਸ਼ਾਮਲ ਕਰੋ" - "ਅਦਲਾ-ਬਦਲੀ ਕਰੋ" - "ਕਾਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਕਾਨਫਰੰਸ ਕਾਲ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਕਾਨਫਰੰਸ ਕਾਲ" - "ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਔਡੀਓ" - "ਵੀਡੀਓ ਕਾਲ" - "ਵੌਇਸ ਕਾਲ ਵਿੱਚ ਬਦਲੋ" - "ਕੈਮਰੇ \'ਤੇ ਬਦਲੋ" - "ਕੈਮਰਾ ਚਾਲੂ ਕਰੋ" - "ਕੈਮਰਾ ਬੰਦ ਕਰੋ" - "ਹੋਰ ਚੋਣਾਂ" - "ਪਲੇਅਰ ਸ਼ੁਰੂ ਹੋ ਗਿਆ" - "ਪਲੇਅਰ ਰੁਕ ਗਿਆ" - "ਕੈਮਰਾ ਤਿਆਰ ਨਹੀਂ ਹੈ" - "ਕੈਮਰਾ ਤਿਆਰ ਹੈ" - "ਅਗਿਆਤ ਕਾਲ ਸੈਸ਼ਨ ਵਰਤਾਰਾ" - "ਸੇਵਾ" - "ਸਥਾਪਨਾ" - "<ਸੈੱਟ ਨਹੀਂ>" - "ਹੋਰ ਕਾਲ ਸੈਟਿੰਗਾਂ" - "%s ਰਾਹੀਂ ਕਾਲ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ" - "%s ਰਾਹੀਂ ਆ ਰਹੀਆਂ ਕਾਲਾਂ" - "ਸੰਪਰਕ ਫ਼ੋਟੋ" - "ਨਿੱਜੀ ਵਿੱਚ ਬਦਲੋ" - "ਸੰਪਰਕ ਚੁਣੋ" - "ਆਪਣੇ ਆਪ ਲਿਖੋ..." - "ਰੱਦ ਕਰੋ" - "ਭੇਜੋ" - "ਜਵਾਬ ਦਿਓ" - "SMS ਭੇਜੋ" - "ਅਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਕਾਲ ਵਜੋਂ ਜਵਾਬ ਦਿਓ" - "ਔਡੀਓ ਕਾਲ ਵਜੋਂ ਜਵਾਬ ਦਿਓ" - "ਵੀਡੀਓ ਬੇਨਤੀ ਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਬੇਨਤੀ ਨੂੰ ਅਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਟ੍ਰਾਂਸਮਿਟ ਬੇਨਤੀ ਨੂੰ ਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਟ੍ਰਾਂਸਮਿਟ ਬੇਨਤੀ ਨੂੰ ਅਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਪ੍ਰਾਪਤ ਕਰਨ ਦੀ ਬੇਨਤੀ ਨੂੰ ਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਪ੍ਰਾਪਤ ਕਰਨ ਦੀ ਬੇਨਤੀ ਨੂੰ ਅਸਵੀਕਾਰ ਕਰੋ" - "%s ਲਈ ਉੱਤੇ ਸਲਾਈਡ ਕਰੋ।" - "%s ਲਈ ਖੱਬੇ ਪਾਸੇ ਸਲਾਈਡ ਕਰੋ।" - "%s ਲਈ ਸੱਜੇ ਪਾਸੇ ਸਲਾਈਡ ਕਰੋ।" - "%s ਲਈ ਹੇਠਾਂ ਸਲਾਈਡ ਕਰੋ।" - "ਥਰਥਰਾਹਟ ਕਰੋ" - "ਥਰਥਰਾਹਟ ਕਰੋ" - "ਧੁਨੀ" - "ਪੂਰਵ-ਨਿਰਧਾਰਤ ਧੁਨੀ (%1$s)" - "ਫੋਨ ਰਿੰਗਟੋਨ" - "ਘੰਟੀ ਵੱਜਣ \'ਤੇ ਥਰਥਰਾਹਟ ਕਰੋ" - "ਰਿੰਗਟੋਨ ਅਤੇ ਥਰਥਰਾਹਟ" - "ਕਾਨਫਰੰਸ ਕਾਲ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਐਮਰਜੈਂਸੀ ਨੰਬਰ" - "ਪ੍ਰੋਫਾਈਲ ਫ਼ੋਟੋ" - "ਕੈਮਰਾ ਬੰਦ ਹੈ" - "%s ਰਾਹੀਂ" - "ਨੋਟ-ਕਥਨ ਭੇਜਿਆ ਗਿਆ" - "ਹਾਲੀਆ ਸੁਨੇਹੇ" - "ਵਪਾਰ ਜਾਣਕਾਰੀ" - "%.1f ਮੀਲ ਦੂਰ" - "%.1f ਕਿ.ਮੀ. ਦੂਰ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ਕੱਲ੍ਹ %s ਵਜੇ ਖੁੱਲ੍ਹੇਗਾ" - "ਅੱਜ %s ਵਜੇ ਖੁੱਲ੍ਹੇਗਾ" - "%s ਵਜੇ ਬੰਦ ਹੋਵੇਗਾ" - "ਅੱਜ %s ਵਜੇ ਬੰਦ ਹੋਇਆ" - "ਹੁਣ ਖੁੱਲ੍ਹਾ ਹੈ" - "ਹੁਣ ਬੰਦ ਹੈ" - "ਸ਼ੱਕੀ ਸਪੈਮ ਕਾਲਰ" - "%1$s ਦੀ ਕਾਲ ਸਮਾਪਤ ਹੋਈ" - "ਇਸ ਨੰਬਰ ਤੋਂ ਤੁਹਾਨੂੰ ਪਹਿਲੀ ਵਾਰ ਕਾਲ ਪ੍ਰਾਪਤ ਹੋਈ ਹੈ।" - "ਸਾਨੂੰ ਇਹ ਕਾਲ ਇੱਕ ਸਪੈਮਰ ਜਾਪਦੀ ਸੀ।" - "ਸਪੈਮ ਨੂੰ ਬਲੌਕ ਕਰੋ/ਰਿਪੋਰਟ ਕਰੋ" - "ਸੰਪਰਕ ਸ਼ਾਮਲ ਕਰੋ" - "ਸਪੈਮ ਨਹੀਂ" - diff --git a/InCallUI/res/values-pl/strings.xml b/InCallUI/res/values-pl/strings.xml deleted file mode 100644 index f9f78ec79f083a1ae59ec39f559579228ae5219b..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-pl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Wstrzymane" - "Nieznany" - "Numer prywatny" - "Automat telefoniczny" - "Połączenie konferencyjne" - "Połączenie przerwane" - "Głośnik" - "Słuchawka telefonu" - "Przewodowy zestaw słuchawkowy" - "Bluetooth" - "Wysłać te tony?\n" - "Wysyłanie tonów\n" - "Wyślij" - "Tak" - "Nie" - "Zastąp symbol wieloznaczny" - "Połączenie konferencyjne: %s" - "Numer poczty głosowej" - "Wybieranie" - "Ponowne wybieranie numeru" - "Połączenie konferencyjne" - "Połączenie przychodzące" - "Przychodzące połączenie służbowe" - "Połączenie zakończone" - "Wstrzymane" - "Rozłączanie" - "Trwa połączenie" - "Mój numer to %s" - "Rozpoczynanie rozmowy wideo" - "Rozmowa wideo" - "Wysyłanie żądania wideo" - "Nie można nawiązać połączenia wideo" - "Żądanie wideo zostało odrzucone" - "Twój numer oddzwaniania\n %1$s" - "Twój numer oddzwaniania dla połączeń alarmowych\n %1$s" - "Wybieranie" - "Nieodebrane połączenie" - "Nieodebrane połączenia" - "Nieodebrane połączenia: %s" - "Nieodebrane połączenie od: %s" - "Trwa połączenie" - "Trwa połączenie służbowe" - "Trwa połączenie przez Wi-Fi" - "Trwa połączenie służbowe przez Wi-Fi" - "Wstrzymane" - "Połączenie przychodzące" - "Przychodzące połączenie służbowe" - "Przychodzące połączenie przez Wi-Fi" - "Przychodzące połączenie służbowe przez Wi-Fi" - "Przychodząca rozmowa wideo" - "Przychodzące połączenie podejrzanie o spam" - "Przychodzące żądanie wideo" - "Nowa poczta głosowa" - "Nowa poczta głosowa (%d)" - "Wybierz numer %s" - "Nieznany numer poczty głosowej" - "Brak usługi" - "Wybrana sieć (%s) jest niedostępna" - "Odbierz" - "Rozłącz" - "Wideo" - "Głos" - "Zaakceptuj" - "Odrzuć" - "Oddzwoń" - "Wyślij SMS-a" - "Trwająca rozmowa na innym urządzeniu" - "Przełącz rozmowę" - "Aby rozpocząć połączenie, wyłącz najpierw tryb samolotowy." - "Nie zarejestrowano w sieci." - "Sieć komórkowa jest niedostępna." - "Aby zadzwonić, wybierz prawidłowy numer." - "Nie można dzwonić." - "Rozpoczynam sekwencję MMI…" - "Usługa nie jest obsługiwana." - "Nie można przełączyć połączeń." - "Nie można rozdzielić połączenia." - "Nie można przekazać." - "Nie można nawiązać połączenia konferencyjnego." - "Nie można odrzucić połączenia." - "Nie można zwolnić połączeń." - "Połączenie SIP" - "Połączenie alarmowe" - "Włączam radio…" - "Brak sieci. Próbuję ponownie…" - "Nie można dzwonić. %s nie jest numerem alarmowym." - "Nie można dzwonić. Wybierz numer alarmowy." - "Aby zadzwonić, użyj klawiatury" - "Wstrzymaj połączenie" - "Wznów połączenie" - "Zakończ połączenie" - "Pokaż klawiaturę" - "Ukryj klawiaturę" - "Wycisz" - "Wyłącz wyciszenie" - "Dodaj połączenie" - "Scal połączenia" - "Przełącz" - "Zarządzaj połączeniami" - "Zarządzaj połączeniem konferencyjnym" - "Połączenie konferencyjne" - "Zarządzaj" - "Dźwięk" - "Rozmowa wideo" - "Zmień na połączenie głosowe" - "Przełącz kamerę" - "Włącz kamerę" - "Wyłącz kamerę" - "Więcej opcji" - "Odtwarzacz włączony" - "Odtwarzacz zatrzymany" - "Kamera niegotowa" - "Kamera gotowa" - "Nieznane zdarzenie sesji połączenia" - "Usługa" - "Konfiguracja" - "<Nie ustawiono>" - "Inne ustawienia połączeń" - "Nawiązywanie połączenia przez %s" - "Przychodzące z sieci %s" - "zdjęcie kontaktu" - "przejdź do rozmowy prywatnej" - "wybierz kontakt" - "Napisz własną..." - "Anuluj" - "Wyślij" - "Odbierz" - "Wyślij SMS-a" - "Odrzuć" - "Odbierz jako rozmowę wideo" - "Odbierz jako rozmowę audio" - "Zaakceptuj żądanie wideo" - "Odrzuć żądanie wideo" - "Zaakceptuj wysyłanie obrazu wideo" - "Odrzuć wysyłanie obrazu wideo" - "Zaakceptuj odbieranie obrazu wideo" - "Odrzuć odbieranie obrazu wideo" - "Przesuń w górę: %s." - "Przesuń w lewo: %s." - "Przesuń w prawo: %s." - "Przesuń w dół: %s." - "Wibracje" - "Wibracje" - "Dźwięk" - "Domyślny dźwięk (%1$s)" - "Dzwonek telefonu" - "Wibracje z dzwonkiem" - "Dzwonek i wibracje" - "Zarządzaj połączeniem konferencyjnym" - "Numer alarmowy" - "Zdjęcie profilowe" - "Kamera wyłączona" - "z %s" - "Notatka wysłana" - "Ostatnie wiadomości" - "Informacje o firmie" - "%.1f mil(e) stąd" - "%.1f km stąd" - "%1$s, %2$s" - "%1$s-%2$s" - "%1$s, %2$s" - "Otwarte jutro od %s" - "Otwarte dzisiaj od %s" - "Zamknięte od %s" - "Zamknięte dzisiaj od %s" - "Teraz otwarte" - "Teraz zamknięte" - "Podejrzenie spamu" - "Połączenie zakończone (%1$s)" - "To pierwsze połączenie z tego numeru." - "Podejrzewamy, że to połączenie to spam." - "Zablokuj/zgłoś spam" - "Dodaj kontakt" - "To nie spam" - diff --git a/InCallUI/res/values-pt-rBR/strings.xml b/InCallUI/res/values-pt-rBR/strings.xml deleted file mode 100644 index 5271f54ccdb625154812a03d19147c03bf779078..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-pt-rBR/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefone" - "Em espera" - "Desconhecido" - "Número privado" - "Chamada a cobrar" - "Teleconferência" - "A chamada caiu." - "Alto-falante" - "Minifone do aparelho" - "Fone de ouvido com fio" - "Bluetooth" - "Enviar os seguintes tons?\n" - "Enviando tons\n" - "Enviar" - "Sim" - "Não" - "Substituir caractere curinga por" - "Teleconferência %s" - "Número do correio de voz" - "Discando" - "Rediscando" - "Teleconferência" - "Chamada recebida" - "Chamada trabalho recebida" - "Chamada encerrada" - "Em espera" - "Desligando" - "Em chamada" - "Meu número é %s" - "Conectando vídeo" - "Videochamada" - "Solicitando vídeo" - "Não é possível conectar a videochamada" - "Solicitação de vídeo rejeitada" - "Seu número de retorno de chamada\n %1$s" - "Seu número de retorno de chamada de emergência\n %1$s" - "Discando" - "Chamada perdida" - "Chamadas perdidas" - "%s chamadas perdidas" - "Chamada perdida de %s" - "Chamanda em andamento" - "Chamada de trabalho em andamento" - "Chamada por Wi-Fi em andamento" - "Chamada de trabalho por Wi-Fi em andamento" - "Em espera" - "Chamada recebida" - "Chamada de trabalho recebida" - "Chamada por Wi-Fi recebida" - "Chamada de trabalho recebida por Wi-Fi" - "Videochamada recebida" - "Chamada recebida suspeita (spam)" - "Solicitação de vídeo recebida" - "Novo correio de voz" - "Novo correio de voz (%d)" - "Discar %s" - "Número correio de voz desconhecido" - "Sem serviço" - "A rede selecionada (%s) está indisponível" - "Atender" - "Desligar" - "Vídeo" - "Voz" - "Aceitar" - "Dispensar" - "Retor. cham." - "Mensagem" - "Chamada em andamento em outro dispositivo" - "Transferir chamada" - "Para fazer uma chamada, primeiro desative o modo avião." - "Não registrado na rede." - "Rede celular não disponível." - "Para realizar uma chamada, digite um número válido." - "Não é possível realizar chamadas." - "Iniciando sequência MMI…" - "Serviço não compatível." - "Não é possível alternar as chamadas." - "Não é possível separar a chamada." - "Não é possível transferir a chamada." - "Não é possível fazer uma conferência." - "Não é possível rejeitar a chamada." - "Não é possível liberar chamadas." - "Chamada SIP" - "Chamada de emergência" - "Ativando o rádio…" - "Sem serviço. Tentando novamente..." - "Não é possível realizar chamadas. %s não é um número de emergência." - "Não é possível realizar chamadas. Disque um número de emergência." - "Use o teclado para discar" - "Colocar chamada em espera" - "Retomar chamada" - "Encerrar chamada" - "Mostrar teclado" - "Ocultar teclado" - "Desativar som" - "Ativar som" - "Adicionar chamada" - "Juntar chamadas" - "Trocar" - "Gerenciar chamadas" - "Gerenciar teleconferência" - "Teleconferência" - "Gerenciar" - "Áudio" - "Videocham." - "Alterar para chamada de voz" - "Alternar câmera" - "Ativar câmera" - "Desativar câmera" - "Mais opções" - "Player iniciado" - "Player interrompido" - "A câmera não está pronta" - "Câmera pronta" - "Evento de sessão de chamada desconhecido" - "Serviço" - "Configuração" - "<Não definido>" - "Outras configurações de chamada" - "Chamando via %s" - "Chamada de %s" - "foto do contato" - "conversar em particular" - "selecionar contato" - "Escreva sua resposta..." - "Cancelar" - "Enviar" - "Atender" - "Enviar SMS" - "Recusar" - "Atender como videochamada" - "Atender como chamada de áudio" - "Aceitar solicitação de vídeo" - "Recusar solicitação de vídeo" - "Aceitar solicitação de transmissão de vídeo" - "Recusar solicitação de transmissão de vídeo" - "Aceitar solicitação de recebimento de vídeo" - "Recusar solicitação de recebimento de vídeo" - "Para %s, deslize para cima." - "Para %s, deslize para a esquerda." - "Para %s, deslize para a direita." - "Para %s, deslize para baixo." - "Vibrar" - "Vibrar" - "Som" - "Som padrão (%1$s)" - "Toque do telefone" - "Vibrar ao tocar" - "Toque e vibração" - "Gerenciar teleconferência" - "Número de emergência" - "Foto do perfil" - "Câmera desligada" - "via %s" - "Nota enviada" - "Mensagens recentes" - "Informações sobre a empresa" - "%.1f milhas de distância" - "%.1f km de distância" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre amanhã às %s" - "Abre hoje às %s" - "Fecha às %s" - "Fechou hoje às %s" - "Aberto agora" - "Fechado agora" - "Autor suspeito (spam)" - "Chamada encerra %1$s" - "Esta é a primeira vez que este número ligou para você." - "Suspeitamos que esta chamada seja de um criador de spams." - "Bloq./denunciar spam" - "Adicionar contato" - "Não é spam" - diff --git a/InCallUI/res/values-pt-rPT/strings.xml b/InCallUI/res/values-pt-rPT/strings.xml deleted file mode 100644 index 2a04556feeb4fba753b1100a0c15644969bf3002..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-pt-rPT/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefone" - "Em espera" - "Desconhecido" - "Número privado" - "Telefone público" - "Conferência" - "A chamada caiu" - "Altifalante" - "Auricular do telefone" - "Auscultadores com fios" - "Bluetooth" - "Pretende enviar os seguintes tons?\n" - "A enviar tons\n" - "Enviar" - "Sim" - "Não" - "Substituir o caráter universal por" - "Conferência %s" - "Número do correio de voz" - "A marcar" - "A remarcar" - "Conferência" - "Chamada recebida" - "Chamada de trab. recebida" - "Chamada terminada" - "Em espera" - "A desligar" - "Numa chamada" - "O meu número é %s" - "A ligar vídeo" - "Videochamada" - "A solicitar vídeo" - "Não é possível ligar a videochamada" - "Pedido de vídeo rejeitado" - "O seu número de retorno de chamadas\n %1$s" - "O seu número de retorno de chamadas de emergência\n %1$s" - "A marcar" - "Chamada não atendida" - "Chamadas não atendidas" - "%s chamadas não atendidas" - "Chamada não atendida de %s" - "Chamada em curso" - "Chamada de trabalho em curso" - "Chamada Wi-Fi em curso" - "Chamada de trabalho via Wi-Fi em curso" - "Em espera" - "Chamada recebida" - "Chamada de trab. recebida" - "Chamada Wi-Fi recebida" - "Chamada de trabalho recebida via Wi-Fi" - "Videochamada recebida" - "A receber chamada spam suspeita" - "Pedido de vídeo recebido" - "Nova mensagem de correio de voz" - "Nova mensagem de correio de voz (%d)" - "Marcar %s" - "Número do correio de voz desconhecido" - "Sem serviço" - "Rede selecionada (%s) indisponível" - "Atender" - "Desligar" - "Vídeo" - "Voz" - "Aceitar" - "Ignorar" - "Ligar de volta" - "Mensagem" - "Chamada em curso noutro dispositivo" - "Transferir chamada" - "Para efetuar uma chamada, desative primeiro o Modo de avião." - "Sem registo na rede." - "Rede móvel não disponível." - "Para efetuar uma chamada, introduza um número válido." - "Não é possível telefonar." - "A iniciar sequência de MMI..." - "Serviço não suportado." - "Não é possível alternar entre chamadas." - "Não é possível separar a chamada." - "Não é possível transferir." - "Não é possível efetuar uma conferência." - "Não é possível rejeitar a chamada." - "Não é possível libertar as chamadas." - "Chamada SIP" - "Chamada de emergência" - "A ligar o rádio..." - "Sem serviço. A tentar novamente…" - "Não é possível telefonar. %s não é um número de emergência." - "Não é possível telefonar. Marque um número de emergência." - "Utilizar o teclado para marcar" - "Colocar chamada em espera" - "Retomar chamada" - "Terminar chamada" - "Mostrar o teclado" - "Ocultar o teclado" - "Desativar som" - "Reativar o som" - "Adicionar chamada" - "Intercalar chamadas" - "Trocar" - "Gerir chamadas" - "Gerir conferência" - "Conferência" - "Gerir" - "Áudio" - "Videochamada" - "Mudar para chamada de voz" - "Trocar câmara" - "Ativar câmara" - "Desativar câmara" - "Mais opções" - "Leitor iniciado" - "Leitor interrompido" - "A câmara não está pronta" - "Câmara pronta" - "Evento de sessão de chamada desconhecido" - "Serviço" - "Configuração" - "<Não definido>" - "Outras definições de chamadas" - "A telefonar através de %s" - "Recebidas através de %s" - "foto do contacto" - "tornar privado" - "selecionar contacto" - "Escreva a sua própria..." - "Cancelar" - "Enviar" - "Atender" - "Enviar SMS" - "Recusar" - "Atender como videochamada" - "Atender como chamada de áudio" - "Aceitar pedido de vídeo" - "Recusar pedido de vídeo" - "Aceitar pedido para transmitir vídeo" - "Recusar pedido para transmitir vídeo" - "Aceitar pedido para receber vídeo" - "Recusar pedido para receber vídeo" - "Deslize lentamente para cima para %s." - "Deslize lentamente para a esquerda para %s." - "Deslize lentamente para a direita para %s." - "Deslize lentamente para baixo para %s." - "Vibrar" - "Vibrar" - "Som" - "Som predefinido (%1$s)" - "Toque do telemóvel" - "Vibrar ao tocar" - "Tocar e vibrar" - "Gerir conferência" - "Número de emergência" - "Foto do perfil" - "Câmara desligada" - "através de %s" - "Nota enviada" - "Mensagens recentes" - "Informações da empresa" - "A %.1f milhas de distância" - "A %.1f km de distância" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Abre amanhã às %s" - "Abre hoje às %s" - "Fecha às %s" - "Fechou hoje às %s" - "Aberto agora" - "Fechado agora" - "Chmd. spam suspeita" - "Chamada terminada: %1$s" - "É a primeira vez que este número lhe liga." - "Suspeitamos que a pessoa que fez esta chamada seja um spammer." - "Bloq./denunciar spam" - "Adicionar contacto" - "Não é spam" - diff --git a/InCallUI/res/values-pt/strings.xml b/InCallUI/res/values-pt/strings.xml deleted file mode 100644 index 5271f54ccdb625154812a03d19147c03bf779078..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-pt/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefone" - "Em espera" - "Desconhecido" - "Número privado" - "Chamada a cobrar" - "Teleconferência" - "A chamada caiu." - "Alto-falante" - "Minifone do aparelho" - "Fone de ouvido com fio" - "Bluetooth" - "Enviar os seguintes tons?\n" - "Enviando tons\n" - "Enviar" - "Sim" - "Não" - "Substituir caractere curinga por" - "Teleconferência %s" - "Número do correio de voz" - "Discando" - "Rediscando" - "Teleconferência" - "Chamada recebida" - "Chamada trabalho recebida" - "Chamada encerrada" - "Em espera" - "Desligando" - "Em chamada" - "Meu número é %s" - "Conectando vídeo" - "Videochamada" - "Solicitando vídeo" - "Não é possível conectar a videochamada" - "Solicitação de vídeo rejeitada" - "Seu número de retorno de chamada\n %1$s" - "Seu número de retorno de chamada de emergência\n %1$s" - "Discando" - "Chamada perdida" - "Chamadas perdidas" - "%s chamadas perdidas" - "Chamada perdida de %s" - "Chamanda em andamento" - "Chamada de trabalho em andamento" - "Chamada por Wi-Fi em andamento" - "Chamada de trabalho por Wi-Fi em andamento" - "Em espera" - "Chamada recebida" - "Chamada de trabalho recebida" - "Chamada por Wi-Fi recebida" - "Chamada de trabalho recebida por Wi-Fi" - "Videochamada recebida" - "Chamada recebida suspeita (spam)" - "Solicitação de vídeo recebida" - "Novo correio de voz" - "Novo correio de voz (%d)" - "Discar %s" - "Número correio de voz desconhecido" - "Sem serviço" - "A rede selecionada (%s) está indisponível" - "Atender" - "Desligar" - "Vídeo" - "Voz" - "Aceitar" - "Dispensar" - "Retor. cham." - "Mensagem" - "Chamada em andamento em outro dispositivo" - "Transferir chamada" - "Para fazer uma chamada, primeiro desative o modo avião." - "Não registrado na rede." - "Rede celular não disponível." - "Para realizar uma chamada, digite um número válido." - "Não é possível realizar chamadas." - "Iniciando sequência MMI…" - "Serviço não compatível." - "Não é possível alternar as chamadas." - "Não é possível separar a chamada." - "Não é possível transferir a chamada." - "Não é possível fazer uma conferência." - "Não é possível rejeitar a chamada." - "Não é possível liberar chamadas." - "Chamada SIP" - "Chamada de emergência" - "Ativando o rádio…" - "Sem serviço. Tentando novamente..." - "Não é possível realizar chamadas. %s não é um número de emergência." - "Não é possível realizar chamadas. Disque um número de emergência." - "Use o teclado para discar" - "Colocar chamada em espera" - "Retomar chamada" - "Encerrar chamada" - "Mostrar teclado" - "Ocultar teclado" - "Desativar som" - "Ativar som" - "Adicionar chamada" - "Juntar chamadas" - "Trocar" - "Gerenciar chamadas" - "Gerenciar teleconferência" - "Teleconferência" - "Gerenciar" - "Áudio" - "Videocham." - "Alterar para chamada de voz" - "Alternar câmera" - "Ativar câmera" - "Desativar câmera" - "Mais opções" - "Player iniciado" - "Player interrompido" - "A câmera não está pronta" - "Câmera pronta" - "Evento de sessão de chamada desconhecido" - "Serviço" - "Configuração" - "<Não definido>" - "Outras configurações de chamada" - "Chamando via %s" - "Chamada de %s" - "foto do contato" - "conversar em particular" - "selecionar contato" - "Escreva sua resposta..." - "Cancelar" - "Enviar" - "Atender" - "Enviar SMS" - "Recusar" - "Atender como videochamada" - "Atender como chamada de áudio" - "Aceitar solicitação de vídeo" - "Recusar solicitação de vídeo" - "Aceitar solicitação de transmissão de vídeo" - "Recusar solicitação de transmissão de vídeo" - "Aceitar solicitação de recebimento de vídeo" - "Recusar solicitação de recebimento de vídeo" - "Para %s, deslize para cima." - "Para %s, deslize para a esquerda." - "Para %s, deslize para a direita." - "Para %s, deslize para baixo." - "Vibrar" - "Vibrar" - "Som" - "Som padrão (%1$s)" - "Toque do telefone" - "Vibrar ao tocar" - "Toque e vibração" - "Gerenciar teleconferência" - "Número de emergência" - "Foto do perfil" - "Câmera desligada" - "via %s" - "Nota enviada" - "Mensagens recentes" - "Informações sobre a empresa" - "%.1f milhas de distância" - "%.1f km de distância" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre amanhã às %s" - "Abre hoje às %s" - "Fecha às %s" - "Fechou hoje às %s" - "Aberto agora" - "Fechado agora" - "Autor suspeito (spam)" - "Chamada encerra %1$s" - "Esta é a primeira vez que este número ligou para você." - "Suspeitamos que esta chamada seja de um criador de spams." - "Bloq./denunciar spam" - "Adicionar contato" - "Não é spam" - diff --git a/InCallUI/res/values-ro/strings.xml b/InCallUI/res/values-ro/strings.xml deleted file mode 100644 index ca0036d0d00f2595a01f3332aa7433b9e8eee498..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ro/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "În așteptare" - "Necunoscut" - "Număr privat" - "Telefon public" - "Conferință telefonică" - "Apelul s-a încheiat" - "Difuzor" - "Casca receptorului" - "Set căști-microfon cu fir" - "Bluetooth" - "Trimiteți următoarele tonuri?\n" - "Se trimit tonuri\n" - "Trimiteți" - "Da" - "Nu" - "Înlocuiți metacaracterul cu" - "Conferință telefonică %s" - "Numărul mesageriei vocale" - "Se apelează" - "Se reapelează" - "Conferință telefonică" - "Apel primit" - "Apel de serviciu primit" - "Apel încheiat" - "În așteptare" - "Se încheie apelul" - "Apel în desfășurare" - "Numărul meu este %s" - "Se conectează apelul video" - "Apel video" - "Se solicită apel video" - "Nu se poate conecta apelul video" - "Solicitarea pentru apel video a fost respinsă" - "Numărul de apelare inversă\n%1$s" - "Numărul de apelare inversă de urgență\n%1$s" - "Se apelează" - "Apel nepreluat" - "Apeluri nepreluate" - "%s (de) apeluri nepreluate" - "Apel nepreluat de la %s" - "Apel în desfășurare" - "Apel de serviciu în desfășurare" - "Apel prin Wi-Fi în desfășurare" - "Apel de serviciu prin Wi-Fi în desfășurare" - "În așteptare" - "Apel primit" - "Apel de serviciu primit" - "Apel prin Wi-Fi primit" - "Apel de serviciu prin Wi-Fi primit" - "Apel video primit" - "Un apel primit posibil spam" - "Solicitare de trecere la apel video" - "Mesaj vocal nou" - "Mesaj vocal nou (%d)" - "Apelați %s" - "Numărul mesageriei vocale este necunoscut" - "Fără semnal" - "Rețeaua selectată (%s) nu este disponibilă" - "Răspundeți" - "Încheiați apelul" - "Apel video" - "Apel vocal" - "Acceptați" - "Refuzați" - "Apelați înapoi" - "Trimiteți mesaj" - "Apel în curs pe alt dispozitiv" - "Transferați apelul" - "Pentru a apela, mai întâi dezactivați modul Avion." - "Neînregistrat în rețea." - "Rețeaua mobilă nu este disponibilă." - "Pentru a apela, introduceți un număr valid." - "Nu se poate apela." - "Se pornește secvența MMI…" - "Serviciul nu este acceptat." - "Apelurile nu pot fi comutate." - "Apelul nu poate fi separat." - "Nu se poate transfera." - "Conferința telefonică nu poate fi inițiată." - "Apelul nu poate fi respins." - "Apelurile nu pot fi eliberate." - "Apel SIP" - "Apel de urgență" - "Se activează radio…" - "Fără semnal. Se încearcă din nou…" - "Nu se poate apela. %s nu este un număr de urgență." - "Nu se poate apela. Formați un număr de urgență." - "Folosiți tastatura pentru a apela" - "Puneți apelul în așteptare" - "Reluați apelul" - "Încheiați apelul" - "Afișează tastatura numerică" - "Ascunde tastatura numerică" - "Dezactivează sunetul" - "Activează sunetul" - "Adăugați un apel" - "Îmbinați apelurile" - "Schimbați" - "Gestionați apelurile" - "Gestionați conferința telefonică" - "Conferință telefonică" - "Gestionați" - "Audio" - "Apel video" - "Treceți la apel vocal" - "Comutați camera foto" - "Activați camera" - "Dezactivați camera" - "Mai multe opțiuni" - "Playerul a pornit" - "Playerul s-a oprit" - "Camera foto nu este pregătită" - "Camera foto este pregătită" - "Eveniment necunoscut privind o sesiune de apeluri" - "Furnizor de servicii" - "Configurați" - "<Nesetat>" - "Alte setări de apel" - "Se apelează prin %s" - "Primite prin %s" - "fotografia persoanei de contact" - "treceți în modul privat" - "selectați o persoană de contact" - "Scrieți propriul răspuns…" - "Anulați" - "Trimiteți" - "Răspundeți" - "Trimiteți SMS" - "Refuzați" - "Răspundeți ca apel video" - "Răspundeți ca apel audio" - "Acceptați solicitarea de a trece la apel video" - "Refuzați solicitarea de a trece la apel video" - "Acceptați solicitarea de a transmite conținut video" - "Refuzați solicitarea de a transmite conținut video" - "Acceptați solicitarea de a primi conținut video" - "Refuzați solicitarea de a primi conținut video" - "Glisați în sus ca să %s." - "Glisați spre stânga ca să %s." - "Glisați spre dreapta ca să %s." - "Glisați în jos ca să %s." - "Vibrații" - "Vibrații" - "Sunet" - "Sunet prestabilit (%1$s)" - "Ton de sonerie pentru telefon" - "Vibrează când sună" - "Ton de sonerie și vibrații" - "Gestionați conferința telefonică" - "Număr de urgență" - "Fotografie de profil" - "Camera foto este oprită" - "pe %s" - "Nota a fost trimisă" - "Mesaje recente" - "Informații despre companie" - "%.1f mi distanță" - "%.1f km distanță" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Deschide mâine la %s" - "Deschide astăzi la %s" - "Închide la %s" - "A închis astăzi la %s" - "Acum este deschis" - "Acum este închis" - "Posibil apelant spam" - "Apelul s-a încheiat %1$s" - "Aceasta este prima dată când ați primit apel de la acest număr." - "Suspectăm că acesta este un apel spam." - "Blocați/raportați" - "Adăugați persoana" - "Nu este spam" - diff --git a/InCallUI/res/values-ru/strings.xml b/InCallUI/res/values-ru/strings.xml deleted file mode 100644 index 552ad4d0265d24529c366697f6df9f9862f5b9b9..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ru/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "На удержании" - "Неизвестный абонент" - "Скрытый номер" - "Телефон-автомат" - "Конференц-вызов" - "Звонок сброшен" - "Динамик" - "Динамик гарнитуры" - "Проводная гарнитура" - "Bluetooth" - "Отправить следующие тоны?\n" - "Отправка тональных сигналов\n" - "Отправить" - "Да" - "Нет" - "Заменить универсальный символ на" - "Конференц-вызов: %s" - "Номер голосовой почты" - "Набор номера…" - "Повторный вызов" - "Конференц-вызов" - "Входящий вызов" - "Входящий вызов (работа)" - "Вызов завершен" - "На удержании" - "Завершение разговора" - "Вызов" - "Мой номер: %s" - "Подключение видео" - "Видеовызов" - "Запрос видео" - "Не удалось совершить видеовызов" - "Видеовызов отклонен" - "Номер обратного вызова:\n%1$s" - "Номер обратного вызова для экстренных служб:\n%1$s" - "Набор номера…" - "Пропущенный вызов" - "Пропущенные вызовы" - "Пропущенных вызовов: %s" - "Пропущенные вызовы от абонента %s" - "Текущий вызов" - "Текущий звонок (работа)" - "Текущий Wi-Fi-звонок" - "Текущий Wi-Fi-звонок (работа)" - "На удержании" - "Входящий вызов" - "Входящий вызов (работа)" - "Входящий Wi-Fi-звонок" - "Входящий Wi-Fi-звонок (работа)" - "Входящий видеовызов" - "Входящий вызов: подозрение на спам" - "Входящий видеовызов" - "Новое сообщение голосовой почты" - "Новое сообщение голосовой почты (%d)" - "Набрать номер %s" - "Номер голосовой почты неизвестен" - "Нет сигнала" - "Выбранная сеть (%s) недоступна." - "Ответить" - "Завершить" - "Видео" - "Голос" - "Разрешить" - "Закрыть" - "Перезвонить" - "Написать SMS" - "Вы участвуете в разговоре на другом устройстве" - "Перевести на это устройство" - "Отключите режим полета." - "Нет регистрации в сети." - "Мобильная сеть недоступна." - "Недействительный номер." - "Не удалось позвонить." - "Запуск последовательности MMI..." - "Сервис не поддерживается." - "Не удалось переключить вызов." - "Не удалось разделить вызов." - "Не удалось перенести." - "Не удалось выполнить конференц-вызов." - "Не удалось отклонить вызов." - "Не удалось разъединить." - "Вызов SIP" - "Экстренный вызов" - "Включение радио…" - "Нет сигнала. Повторная попытка…" - "Не удалось позвонить. Номер %s не принадлежит экстренным службам." - "Не удалось позвонить. Наберите номер экстренных служб." - "Используйте клавиатуру для набора номера" - "Удерживать вызов" - "Возобновить вызов" - "Завершить вызов" - "Показать панель набора номера" - "Скрыть панель набора номера" - "Выключить звук" - "Включить звук" - "Добавить вызов" - "Объединить вызовы" - "Перевести звонок" - "Управление вызовами" - "Настройка конференц-связи" - "Конференц-вызов" - "Управление" - "Аудио" - "Видеовызов" - "Отключить видео" - "Сменить камеру" - "Включить камеру" - "Выключить камеру" - "Другие настройки" - "Видеоплеер включен" - "Видеоплеер отключен" - "Камера недоступна" - "Камера доступна" - "Неизвестное событие сеанса связи" - "Служба" - "Настройка" - "<Не задано>" - "Другие настройки вызовов" - "Звонок через %s" - "Входящий вызов (оператор: %s)" - "фотография контакта" - "приватная конференция" - "выбрать контакт" - "Ваш ответ…" - "Отмена" - "Отправить" - "Ответить" - "Отправить SMS" - "Отклонить" - "Ответить с видео" - "Ответить на голосовой вызов" - "Ответить на видеовызов" - "Отклонить видеовызов" - "Разрешить передачу видео" - "Отклонить передачу видео" - "Принять видео" - "Отклонить видео" - "Проведите вверх, чтобы %s." - "Проведите влево, чтобы %s." - "Проведите вправо, чтобы %s." - "Проведите вниз, чтобы %s." - "Вибросигнал" - "Вибросигнал" - "Звук" - "По умолчанию (%1$s)" - "Рингтон" - "Вибросигнал и рингтон" - "Мелодия звонка и вибросигнал" - "Настройка конференц-связи" - "Экстренная служба" - "Фото профиля" - "Камера отключена" - "через %s" - "Сообщение отправлено" - "Недавние сообщения" - "Информация о компании" - "%.1f мил." - "%.1f км" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Откроется завтра в %s" - "Откроется сегодня в %s" - "Работает до %s" - "Сегодня не работает с %s" - "Сейчас открыто" - "Сейчас закрыто" - "Подозрение на спам" - "Вызов завершен (%1$s)" - "Это первый вызов с этого номера." - "Похоже, этот вызов – спам." - "Заблокировать/в спам" - "Добавить контакт" - "Не спам" - diff --git a/InCallUI/res/values-si/strings.xml b/InCallUI/res/values-si/strings.xml deleted file mode 100644 index de0267a586dc560deb3a4b42d972414a169541b0..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-si/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "දුරකථනය" - "රඳවා ගනිමින්" - "නොදනී" - "රහසිගත අංකය" - "පේෆෝනය" - "සම්මන්ත්‍රණ ඇමතුම" - "ඇමතුම නැවතුණි" - "නාදකය" - "හෑන්ඩ්සෙටයේ සවන් කඬ" - "රැහැන් සහිත හෙඩ්සෙටය" - "බ්ලූටූත්" - "පහත නාද යවන්නද?\n" - "නාද යවමින්\n" - "යවන්න" - "ඔව්" - "නැත" - "අනුලකුණ ප්‍රතිස්ථාපනය කරන්නේ" - "සම්මන්ත්‍රණ ඇමතුම %s" - "හඬ තැපැල් අංකය" - "ඩයල් කරමින්" - "නැවත ඩයල් කරමින්" - "සම්මන්ත්‍රණ ඇමතුම" - "එන ඇමතුම" - "එන කාර්යාල ඇමතුම" - "ඇමතුම අවසන් විය" - "රඳවා ගනිමින්" - "විසන්ධි කරමින්" - "ඇමතුමක" - "මගේ අංකය %s" - "වීඩියෝවකට සම්බන්ධ කරමින්" - "වීඩියෝ ඇමතුම" - "වීඩියෝවක් ඉල්ලමින්" - "වීඩියෝ ඇමතුම සම්බන්ධ කළ නොහැක" - "වීඩියෝ ඉල්ලීම ප්‍රතික්ෂේප කරන ලදී" - "ඔබේ පසුඇමතුම් අංකය\n %1$s" - "ඔබගේ හදිසි පසුඇමතුම් අංකය\n %1$s" - "ඩයල් කරමින්" - "මඟ හැරුණු ඇමතුම" - "මඟ හැරුණු ඇමතුම්" - "මඟ හැරුණු ඇමතුම් %s" - "%s වෙතින් මඟ හැරුණු ඇමතුම" - "කරගෙන යන ඇමතුම" - "කරගෙන යන කාර්යාල ඇමතුම" - "දැනට කරගෙන යන Wi-Fi ඇමතුම" - "කරගෙන යන Wi-Fi කාර්යාල ඇමතුම" - "රඳවා ගනිමින්" - "එන ඇමතුම" - "එන කාර්යාල ඇමතුම" - "එන Wi-Fi ඇමතුම" - "එන Wi-Fi කාර්යාල ඇමතුම" - "එන වීඩියෝ ඇමතුම" - "එන සැකසහිත අයාචිත තැපැල් ඇමතුම" - "එන වීඩියෝ ඉල්ලීම" - "නව හඬ තැපෑල" - "නව හඬ තැපැල් (%d)" - "%s ඩයල් කරන්න" - "හඬ තැපැල් අංකය නොදනී" - "සේවාව නැත" - "තෝරා ඇති ජාලය (%s) නොමැත" - "පිළිතුරු දෙන්න" - "විසන්ධි කරන්න" - "වීඩියෝව" - "හඬ" - "පිළිගන්න" - "අස් කරන්න" - "පසුඇමතුම" - "පණිවිඩය" - "වෙනත් උපාංගයක සිදු වන ඇමතුම" - "ඇමතුම මාරු කරන්න" - "ඇමතුමක් ගැනීමට, මුලින්ම ගුවන් යානා ප්‍රකාරය ක්‍රියාවිරහිත කරන්න." - "ජාලය මත ලියාපදිංචි වී නැත." - "සෙලියුලර් ජාලය නොතිබේ." - "ඇමතුමක් ගැනීමට, වලංගු අංකයක් ඇතුළු කරන්න." - "ඇමතුම් ගැනීමට නොහැක." - "MMI අනුපිළිවෙළ ආරම්භ කරමින්…" - "සේවාවට සහාය දක්වන්නේ නැත." - "ඇමතුම් මාරු කිරීම කළ නොහැක." - "ඇමතුම වෙන් කිරීම කළ නොහැක." - "මාරු කිරීමට නොහැක." - "සම්මන්ත්‍රණය කළ නොහැක." - "ඇමතුම ප්‍රතික්ෂේප කළ නොහැක." - "ඇමතුම(ම්) මුදාහැරීම කළ නොහැක." - "SIP ඇමතුම" - "හදිසි ඇමතුම" - "රේඩියෝව ක්‍රියාත්මක කරමින්…" - "සේවාව නැත. නැවත උත්සාහ කරමින්…" - "ඇමතීමට නොහැකිය. %s මෙය හදිසි ඇමතුම් අංකයක් නොවේ." - "ඇමතිය නොහැක. හදිසි අංකයක් අමතන්න." - "ඩයල් කිරීමට යතුරු පුවරුව භාවිත කරන්න" - "ඇමතුම රඳවා ගන්න" - "ඇමතුම නැවත පටන් ගන්න" - "ඇමතුම අවසන් කරන්න" - "ඇමතුම් පෑඩය පෙන්වන්න" - "ඇමතුම් පෑඩය සඟවන්න" - "නිහඬ කරන්න" - "නිහඬ කිරීම ඉවත් කරන්න" - "ඇමතුමක් එක් කරන්න" - "ඇමතුම් ඒකාබද්ධ කරන්න" - "මාරු කරන්න" - "ඇමතුම් කළමනාකරණය කරන්න" - "සම්මන්ත්‍රණ ඇමතුම කළමනාකරණය කරන්න" - "සම්මන්ත්‍රණ ඇමතුම" - "කළමනාකරණය කරන්න" - "ශ්‍රව්‍යය" - "වීඩියෝ ඇමතුම" - "හඬ ඇමතුමක් වෙත මාරු කරන්න" - "කැමරාව මාරු කරන්න" - "කැමරාව ක්‍රියාත්මක කරන්න" - "කැමරාව ක්‍රියා විරහිත කරන්න" - "තවත් විකල්ප" - "ධාවකය ආරම්භ කරන ලදි" - "ධාවකය නැවතුණි" - "කැමරාව සූදානම් නැහැ" - "කැමරාව සූදානම්" - "නොදන්නා ඇමතුම් සැසි සිදුවීම" - "සේවාව" - "පිහිටුවීම" - "<පිහිටුවා නැත>" - "වෙනත් ඇමතුම් සැකසීම්" - "%s හරහා අමතමින්" - "%s හරහා එන" - "සම්බන්ධතා ඡායාරූපය" - "රහසිගත වන්න" - "සම්බන්ධතාවය තෝරාගන්න" - "ඔබේම එකක් ලියන්න..." - "අවලංගු කරන්න" - "යවන්න" - "පිළිතුරු දෙන්න" - "SMS යවන්න" - "ප්‍රතික්ෂේප කිරීම" - "වීඩියෝ ඇමතුමට පිළිතුරු දෙන්න" - "ශ්‍රව්‍ය ඇමතුමට පිළිතුරු දෙන්න" - "වීඩියෝ ඉල්ලීම පිළිගන්න" - "වීඩියෝ ඉල්ලීම ප්‍රතික්ෂේප කරන්න" - "වීඩියෝ සම්ප්‍ර්ෂණ ඉල්ලීම පිළිගන්න" - "වීඩියෝ සම්ප්‍ර්ෂණ ඉල්ලීම ප්‍රතික්ෂේප කරන්න" - "වීඩියෝ ලැබීමේ ඉල්ලීම පිළිගන්නා ලදි" - "වීඩියෝ ලැබීමේ ඉල්ලීම ප්‍රතික්ෂේප කරන්න" - "%s සඳහා උඩට සර්පණය කරන්න." - "%s සඳහා වමට සර්පණය කරන්න." - "%s සඳහා දකුණට සර්පණය කරන්න." - "%s සඳහා පහළට සර්පණය කරන්න." - "කම්පනය" - "කම්පනය" - "හඬ" - "පෙරනිමි ශබ්දය (%1$s)" - "දුරකථන රිගින්ටෝනය" - "රිගින් වන විට කම්පන වන්න" - "රිගින් ටෝන් සහ කම්පනය කරන්න" - "සම්මන්ත්‍රණ ඇමතුම කළමනාකරණය කරන්න" - "හදිසි ඇමතුම් අංකය" - "පැතිකඩ ඡායාරූපය" - "කැමරාව ක්‍රියාවිරහිතයි" - "%s හරහා" - "සටහන යවන ලදී" - "මෑත පණිවිඩ" - "ව්‍යාපාර තොරතුරු" - "සැතපුම් %.1fක් ඈතින්" - "කි.මි. %.1fක් ඈතින්" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "හෙට %sට විවෘත කෙරේ" - "අද %sට විවෘත කෙරේ" - "%sට වසයි" - "අද %sට වසන ලදී" - "දැන් විවෘතයි" - "දැන් වසා ඇත" - "සැකසහිත අයාචිත තැපැල් අමතන්නා" - "ඇමතුම අවසන් විය %1$s" - "මෙය ඔබට මෙම අංකයෙන් ඇමතුමක් ලැබුණ පළමු අවස්ථාව වේ." - "මෙම ඇමතුම අයාචිත එවන්නෙකු අපි සැක කළෙමු." - "අවහිර ක./අයාචිත වා." - "සම්බන්ධතාව එක් කරන්න" - "අයාචිත තැපෑලක් නොවේ" - diff --git a/InCallUI/res/values-sk/strings.xml b/InCallUI/res/values-sk/strings.xml deleted file mode 100644 index 07e8de67147eb59e075e7e704985f29056f868db..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefón" - "Podržaný hovor" - "Neznáme" - "Súkromné číslo" - "Telefónny automat" - "Konferenčný hovor" - "Hovor bol prerušený" - "Reproduktor" - "Slúchadlo" - "Náhlavná súprava s káblom" - "Bluetooth" - "Odoslať nasledujúce tóny?\n" - "Odosielanie tónov\n" - "Odoslať" - "Áno" - "Nie" - "Nahradiť zástupný znak znakom" - "Konferenčný hovor %s" - "Číslo hlasovej schránky" - "Vytáča sa" - "Znova sa vytáča" - "Konferenčný hovor" - "Prichádzajúci hovor" - "Prichádzajúci prac. hovor" - "Hovor bol ukončený" - "Podržaný hovor" - "Ukončovanie hovoru" - "Prebieha hovor" - "Moje číslo je %s" - "Pripája sa video" - "Videohovor" - "Žiada sa video" - "Videohovor nie je možné pripojiť" - "Žiadosť o video bola odmietnutá" - "Vaše číslo na spätné volanie\n %1$s" - "Vaše číslo na spätné tiesňové volanie\n %1$s" - "Vytáča sa" - "Zmeškaný hovor" - "Zmeškané hovory" - "Zmeškané hovory: %s" - "Zmeškaný hovor od volajúceho %s" - "Prebiehajúci hovor" - "Prebiehajúci pracovný hovor" - "Odchádzajúci hovor cez Wi-Fi" - "Prebiehajúci pracovný hovor cez Wi-Fi" - "Podržaný hovor" - "Prichádzajúci hovor" - "Prichádzajúci pracovný hovor" - "Prichádzajúci hovor cez Wi-Fi" - "Prichádzajúci pracovný hovor cez Wi-Fi" - "Prichádzajúci videohovor" - "Prichádzajúci hovor, pri ktorom je podozrenie, že ide o spam" - "Prichádzajúca žiadosť o video" - "Nová hlasová správa" - "Nová hlasová správa (%d)" - "Zavolať hlasovú schránku %s" - "Číslo hlasovej schránky je neznáme" - "Žiadny signál" - "Vybraná sieť (%s) nie je k dispozícii" - "Prijať" - "Položiť" - "Video" - "Hlas" - "Prijať" - "Odmietnuť" - "Zavolať späť" - "Správa" - "Prebiehajúci hovor v inom zariadení" - "Prepojiť hovor" - "Ak chcete volať, vypnite najprv režim v lietadle." - "Prihlásenie do siete nebolo úspešné." - "Mobilná sieť nie je k dispozícii." - "Ak chcete volať, zadajte platné číslo." - "Hovor sa nedá uskutočniť." - "Prebieha spúšťanie sekvencie MMI…" - "Služba nie je podporovaná." - "Nedajú sa prepínať hovory." - "Nedá sa rozdeliť hovor." - "Nedá sa preniesť." - "Konferenčný hovor sa nedá uskutočniť." - "Nedá sa odmietnuť hovor." - "Nedajú sa ukončiť hovory." - "Hovor SIP" - "Tiesňové volanie" - "Zapína sa rádio..." - "Žiadny signál. Prebieha ďalší pokus…" - "Hovor sa nedá uskutočniť. %s nie je číslo tiesňového volania." - "Hovor nie je možné uskutočniť. Vytočte číslo tiesňového volania." - "Číslo vytočíte pomocou klávesnice" - "Podržať hovor" - "Obnoviť hovor" - "Ukončiť hovor" - "Zobraziť číselnú klávesnicu" - "Skryť číselnú klávesnicu" - "Vypnúť zvuk" - "Zapnúť zvuk" - "Pridať hovor" - "Zlúčiť hovory" - "Zameniť" - "Spravovať hovory" - "Spravovať konferenčný hovor" - "Konferenčný hovor" - "Spravovať" - "Zvuk" - "Videohovor" - "Zmeniť na hlasový hovor" - "Zapnúť kameru" - "Zapnúť fotoaparát" - "Vypnúť fotoaparát" - "Ďalšie možnosti" - "Prehrávač bol spustený" - "Prehrávač bol zastavený" - "Kamera nie je pripravená" - "Kamera je pripravená" - "Neznáma udalosť relácie volania" - "Služba" - "Nastavenie" - "<Nenastavené>" - "Ďalšie nastavenia hovorov" - "Voláte prostredníctvom poskytovateľa %s" - "Prichádz. hovor prostred. poskytovateľa %s" - "fotka kontaktu" - "prepnúť na súkromné" - "vybrať kontakt" - "Napísať vlastnú..." - "Zrušiť" - "Odoslať" - "Prijať" - "Odoslať SMS" - "Odmietnuť" - "Prijať ako videohovor" - "Prijať ako zvukový hovor" - "Prijať žiadosť o videohovor" - "Odmietnuť žiadosť o videohovor" - "Prijať žiadosť o prenos videa" - "Odmietnuť žiadosť o prenos videa" - "Povoliť žiadosť o prijatie videa" - "Odmietnuť žiadosť o prijatie videa" - "Prejdite prstom nahor: %s." - "Prejdite prstom doľava: %s." - "Prejdite prstom doprava: %s." - "Prejdite prstom nadol: %s." - "Vibrovanie" - "Vibrovanie" - "Zvuk" - "Predvolený zvuk (%1$s)" - "Tón zvonenia telefónu" - "Vibrovať pri zvonení" - "Vyzváňací tón a vibrovanie" - "Správa konferenčného hovoru" - "Číslo tiesňového volania" - "Profilová fotka" - "Kamera je vypnutá" - "na čísle %s" - "Poznámka bola odoslaná" - "Nedávne správy" - "Informácie o firme" - "Vzdialené %.1f mi" - "Vzdialené %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Zajtra sa otvára o %s" - "Dnes sa otvára o %s" - "Zatvára sa o %s" - "Dnes bolo zatvorené o %s" - "Otvorené" - "Zatvorené" - "Podozrenie na spam" - "Hovor sa skončil %1$s" - "Toto bol prvý hovor z tohto čísla." - "Mali sme podozrenie, že tento hovor bol od šíriteľa spamu." - "Blok./nahlásiť spam" - "Pridať kontakt" - "Toto nie je spam" - diff --git a/InCallUI/res/values-sl/strings.xml b/InCallUI/res/values-sl/strings.xml deleted file mode 100644 index a2cf2102b18cb40d991374e791f39196b437ae2d..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Zadržan" - "Neznan" - "Zasebna številka" - "Telefonska govorilnica" - "Konferenčni klic" - "Klic je bil prekinjen" - "Zvočnik" - "Slušalka" - "Žične slušalke" - "Bluetooth" - "Ali želite poslati naslednje tone?\n" - "Pošiljanje tonov\n" - "Pošlji" - "Da" - "Ne" - "Zamenjaj nadomestni znak z" - "Konferenčni klic: %s" - "Številka odzivnika" - "Klicanje" - "Vnovično klicanje" - "Konferenčni klic" - "Dohodni klic" - "Dohodni delovni klic" - "Klic je končan" - "Zadržan" - "Prekinjanje" - "Klic poteka" - "Moja številka je %s" - "Povezovanje videa" - "Videoklic" - "Zahtevanje videa" - "Videoklica ni mogoče vzpostaviti" - "Zavrnjena zahteva za videoklic" - "Vaša številka za povratni klic:\n %1$s" - "Vaša številka za povratni klic v sili:\n %1$s" - "Klicanje" - "Neodgovorjeni klic" - "Neodgovorjeni klici" - "Št. neodgovorjenih klicev: %s" - "Neodgovorjeni klic od: %s" - "Aktivni klic" - "Aktivni delovni klic" - "Aktivni klic prek omrežja Wi-Fi" - "Aktivni delovni klic prek omrežja Wi-Fi" - "Zadržan" - "Dohodni klic" - "Dohodni delovni klic" - "Dohodni klic Wi-Fi" - "Dohodni delovni klic prek omrežja Wi-Fi" - "Dohodni videoklic" - "Domnevno neželeni dohodni klic" - "Zahteva za dohodni video" - "Novo sporočilo v odzivniku" - "Novo sporočilo v odzivniku (%d)" - "Klic: %s" - "Neznana številka odzivnika" - "Ni signala" - "Izbrano omrežje (%s) ni na voljo" - "Odgovor" - "Prekinitev" - "Video" - "Govor" - "Sprejmem" - "Opusti" - "Povrat. klic" - "SMS" - "Aktivni klic v drugi napravi" - "Prenos klica" - "Če želite poklicati, najprej izklopite način za letalo." - "Ni registrirano v omrežju." - "Mobilno omrežje ni na voljo." - "Če želite opraviti klic, vnesite veljavno številko." - "Klicanje ni mogoče." - "Začetek zaporedja MMI ..." - "Storitev ni podprta." - "Preklop med klici ni mogoč." - "Ločitev klica ni mogoča." - "Prenos ni mogoč." - "Konferenčni klic ni mogoč." - "Zavrnitev klica ni mogoča." - "Prekinitev klica ni mogoča." - "Klic SIP" - "Klic v sili" - "Vklop radia …" - "Ni signala. Vnovičen poskus …" - "Klicanje ni mogoče. %s ni številka za klic v sili." - "Klicanje ni mogoče. Opravite klic v sili." - "Za izbiranje številke uporabite tipkovnico" - "Zadrži klic" - "Nadaljuj klic" - "Končaj klic" - "Prikaži tipkovnico" - "Skrij tipkovnico" - "Izklopi zvok" - "Vklopi zvok" - "Dodaj klic" - "Združi klice" - "Zamenjaj" - "Upravljaj klice" - "Upravljaj konferenčne klice" - "Konferenčni klic" - "Upravljaj" - "Zvok" - "Videoklic" - "Preklopi na glasovni klic" - "Preklopi med fotoaparati" - "Vklopi kamero" - "Izklopi kamero" - "Več možnosti" - "Predvajanje začeto" - "Predvajanje ustavljeno" - "Fotoaparat ni pripravljen" - "Fotoaparat je pripravljen" - "Neznan dogodek seje klica" - "Storitev" - "Nastavitev" - "<Ni nastavljeno>" - "Druge klicne nastavitve" - "Klicanje prek ponudnika %s" - "Dohodni prek %s" - "fotografija stika" - "zasebno" - "izbira stika" - "Napišite lasten odgovor …" - "Prekliči" - "Pošlji" - "Odgovor" - "Pošiljanje SMS-ja" - "Zavrnitev" - "Odgovor z video povezavo" - "Odgovor z zvočno povezavo" - "Sprejemanje zahteve za video" - "Zavrnitev zahteve za video" - "Sprejemanje zahteve za pošiljanje videa" - "Zavrnitev zahteve za pošiljanje videa" - "Sprejemanje zahteve za prejem videa" - "Zavrnitev zahteve za prejem videa" - "Povlecite navzgor za %s." - "Povlecite v levo za %s." - "Povlecite v desno za %s." - "Povlecite navzdol za %s." - "Vibriranje" - "Vibriranje" - "Zvok" - "Privzeti zvok (%1$s)" - "Ton zvonjenja telefona" - "Vibriranje ob zvonjenju" - "Zvonjenje in vibriranje" - "Upravljanje konferenčnih klicev" - "Številka za klic v sili" - "Fotografija profila" - "Fotoaparat je izklopljen" - "prek %s" - "Opomba poslana" - "Nedavna sporočila" - "Podatki o podjetju" - "%.1f mi stran" - "%.1f km stran" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Odpre se jutri ob %s" - "Odpre se danes ob %s" - "Zapre se ob %s" - "Zaprto danes ob %s" - "Trenutno odprto" - "Trenutno zaprto" - "Neželeni klicatelj" - "Klic je bil končan %1$s" - "To je prvi klic, ki ste ga prejeli s te številke." - "Predvidevali smo, da je to neželeni klic." - "Blok./prij. než. kl." - "Dodaj stik" - "Ni neželeni klic" - diff --git a/InCallUI/res/values-sq/strings.xml b/InCallUI/res/values-sq/strings.xml deleted file mode 100644 index 43fd2263d17ec42f7f7d14d6a3597f58b567d226..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sq/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefoni" - "Në pritje" - "I panjohur" - "Numër privat" - "Telefon me pagesë" - "Telefonatë konferencë" - "Telefonata ra" - "Altoparlant" - "Kufje për vesh" - "Kufje me tel" - "Bluetooth" - "Dërgo tonet e mëposhtme?\n" - "Po dërgon tone\n" - "Dërgo" - "Po" - "Jo" - "Zëvendëso karakterin variabël me" - "Telefonatë konferencë %s" - "Numri i postës zanore" - "Po formon numrin" - "Po riformon numrin" - "Telefonatë konferencë" - "Telefonatë hyrëse" - "Telefonatë pune hyrëse" - "Telefonata përfundoi" - "Në pritje" - "Mbyllja" - "Në telefonatë" - "Numri im është %s" - "Po rilidh videon" - "Telefonatë me video" - "Po kërkon video" - "Nuk mund të lidhë telefonatën me video" - "Kërkesa me video u refuzua" - "Numri i kthimit të telefonatës\n %1$s" - "Numri i kthimit të telefonatës së urgjencës\n %1$s" - "Po formon numrin" - "Telefonatë e humbur" - "Telefonata të humbura" - "%s telefonata të humbura" - "Telefonatë e humbur nga %s" - "Telefonatë në vazhdim" - "Telefonatë pune dalëse" - "Telefonatë me Wi-Fi në vazhdim" - "Telefonatë pune dalëse me Wi-Fi" - "Në pritje" - "Telefonatë hyrëse" - "Telefonatë pune hyrëse" - "Telefonatë hyrëse me Wi-Fi" - "Telefonatë pune hyrëse me Wi-Fi" - "Telefonatë hyrëse me video" - "Telefonatë e dyshuar si e padëshiruar" - "Kërkesë për video hyrëse" - "Postë e re zanore" - "Postë e re zanore (%d)" - "Formo numrin %s" - "Numri i postës zanore është i panjohur" - "Nuk ka shërbim" - "Rrjeti i zgjedhur (%s) i padisponueshëm" - "Përgjigju" - "Mbyll" - "Video" - "Zanore" - "Prano" - "Largoje" - "Ri-telefono" - "Mesazh" - "Telefonatë në vazhdim në një pajisje tjetër" - "Transfero telefonatën" - "Për të kryer telefonatë, së pari çaktivizo modalitetin e aeroplanit." - "I paregjistruar në rrjet." - "Rrjeti celular nuk mundësohet." - "Për të kryer një telefonatë, fut një numër të vlefshëm." - "Nuk mund të telefonojë." - "Po fillon sekuencën MMI…" - "Shërbimi nuk mbështetet." - "Nuk mund të ndryshojë telefonatat." - "Nuk mund të ndajë telefonatën." - "Nuk mund të transferojë." - "Nuk mund të kryejë telefonatë konference." - "Nuk mund të refuzojë telefonatën." - "Nuk mund të lëshojë telefonatën(at)." - "Telefonatë SIP" - "Telefonata e urgjencës" - "Po aktivizon radion…" - "Nuk ka shërbim. Po provon sërish…" - "Nuk mund të telefonohet. %s nuk është një numër urgjence." - "Nuk mund të telefonohet. Formo një numër urgjence." - "Përdor tastierën për të formuar numrin" - "Vendose në pritje telefonatën" - "Rifillo telefonatën" - "Mbylle telefonatën" - "Shfaq bllokun e formimit të numrit" - "Fshih bllokun e formimit të numrit" - "Çaktivizo audion" - "Aktivizo audion" - "Shto telefonatë" - "Shkri telefonatat" - "Shkëmbe" - "Menaxho telefonatat" - "Menaxho telefonatën konferencë" - "Telefonatë konferencë" - "Menaxho" - "Audioja" - "Telefonatë me video" - "Ndërro në telefonatë me video" - "Ndërro kamerën" - "Aktivizo kamerën" - "Çaktivizo kamerën" - "Opsione të tjera" - "Luajtësi filloi" - "Luajtësi ndaloi" - "Kamera nuk është gati" - "Kamera është gati" - "Ngjarje e panjohur në sesionin e telefonatës" - "Shërbimi" - "Konfigurimi" - "<I pavendosur>" - "Cilësime të tjera të telefonatës" - "Telefonatë nëpërmjet %s" - "Hyrëse nëpërmjet %s" - "fotografia e kontaktit" - "bëje private" - "përzgjidh kontaktin" - "Shkruaj përgjigjen tënde..." - "Anulo" - "Dërgo" - "Përgjigju" - "Dërgo SMS" - "Refuzo" - "Përgjigju si telefonatë me video" - "Përgjigju si telefonatë me audio" - "Prano kërkesën për video" - "Refuzo kërkesën për video" - "Prano kërkesën për transmetimin e videos" - "Refuzo kërkesën për transmetimin e videos" - "Prano kërkesën për marrjen e videos" - "Refuzo kërkesën për marrjen e videos" - "Rrëshqit lart për %s." - "Rrëshqit majtas për %s" - "Rrëshqit djathtas për %s" - "Rrëshqit poshtë për %s." - "Dridhja" - "Dridhja" - "Tingulli" - "Tingulli i parazgjedhur (%1$s)" - "Toni i ziles i telefonit" - "Dridhje edhe kur bie zilja" - "Me zile dhe me dridhje" - "Menaxho telefonatën konferencë" - "Numri i urgjencës" - "Fotografia e profilit" - "Kamera joaktive" - "përmes %s" - "Shënimi u dërgua" - "Mesazhet e fundit" - "Informacioni i biznesit" - "%.1f milje larg" - "%.1f km larg" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Hapet nesër në %s" - "Hapet sot në %s" - "Mbyllet në %s" - "Mbyllur sot në %s" - "Tani është hapur" - "Tani është mbyllur" - "I padëshirueshëm" - "Telefonata përfundoi %1$s" - "Kjo është hera e parë që ky numër ka telefonuar." - "Ne dyshojmë që kjo telefonatë të jetë e padëshirueshme." - "Blloko/raporto të padëshiruar" - "Shto një kontakt" - "Jo i padëshiruar" - diff --git a/InCallUI/res/values-sr/strings.xml b/InCallUI/res/values-sr/strings.xml deleted file mode 100644 index 3a3820d8c4751722e0a08576c34233399aa2ebf0..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "На чекању" - "Непознат" - "Приватан број" - "Телефонска говорница" - "Конференцијски позив" - "Позив је прекинут" - "Звучник" - "Слушалица телефона" - "Жичане наглавне слушалице" - "Bluetooth" - "Желите ли да пошаљете следеће тонове?\n" - "Тонови се шаљу\n" - "Пошаљи" - "Да" - "Не" - "Замените џокер знак са" - "Конференцијски позив %s" - "Број говорне поште" - "Позива се" - "Поново се бира" - "Конференцијски позив" - "Долазни позив" - "Долазни позив за Work" - "Позив је завршен" - "На чекању" - "Веза се прекида" - "Позив је у току" - "Мој број је %s" - "Повезује се видео позив" - "Видео позив" - "Захтева се видео позив" - "Повезивање видео позива није успело" - "Захтев за видео позив је одбијен" - "Број за повратни позив\n %1$s" - "Број за хитан повратни позив\n %1$s" - "Позива се" - "Пропуштен позив" - "Пропуштени позиви" - "Број пропуштених позива: %s" - "Пропуштен позив од: %s" - "Текући позив" - "Текући позив за Work" - "Текући Wi-Fi позив" - "Текући позив за Work преко Wi-Fi-ја" - "На чекању" - "Долазни позив" - "Долазни позив за Work" - "Долазни Wi-Fi позив" - "Долазни позив за Work преко Wi-Fi-ја" - "Долазни видео позив" - "Сумња на непожељан долазни позив" - "Захтев за долазни видео позив" - "Нова порука говорне поште" - "Нова порука говорне поште (%d)" - "Позови %s" - "Непознат број говорне поште" - "Мобилна мрежа није доступна" - "Изабрана мрежа (%s) није доступна" - "Одговори" - "Прекини везу" - "Видео" - "Гласовни" - "Прихватам" - "Одбаци" - "Узврати позив" - "Пошаљи SMS" - "Позив је у току на другом уређају" - "Пребаци позив" - "Да бисте упутили позив, прво искључите режим рада у авиону." - "Није регистровано на мрежи." - "Мобилна мрежа није доступна." - "Да бисте упутили позив, унесите важећи број." - "Позив није успео." - "Покреће се MMI секвенца..." - "Услуга није подржана." - "Замена позива није успела." - "Раздвајање позива није успело." - "Пребацивање није успело." - "Конференцијски позив није успео." - "Одбијање позива није успело." - "Успостављање позива није успело." - "SIP позив" - "Хитни позив" - "Укључује се радио…" - "Мобилна мрежа није доступна. Покушавамо поново…" - "Позив није успео. %s није број за хитне случајеве." - "Позив није успео. Позовите број за хитне случајеве." - "Користите тастатуру за позивање" - "Стави позив на чекање" - "Настави позив" - "Заврши позив" - "Прикажи нумеричку тастатуру" - "Сакриј нумеричку тастатуру" - "Искључи звук" - "Укључи звук" - "Додај позив" - "Обједини позиве" - "Замени" - "Управљај позивима" - "Управљај конференцијским позивом" - "Конференцијски позив" - "Управљај" - "Аудио" - "Видео позив" - "Промени у гласовни позив" - "Промени камеру" - "Укључи камеру" - "Искључи камеру" - "Још опција" - "Плејер је покренут" - "Плејер је заустављен" - "Камера није спремна" - "Камера је спремна" - "Непознат догађај сесије позива" - "Услуга" - "Подешавање" - "<Није подешено>" - "Друга подешавања позива" - "Позива се преко добављача %s" - "Долазни позив преко %s" - "слика контакта" - "иди на приватно" - "изаберите контакт" - "Напишите сами…" - "Откажи" - "Пошаљи" - "Одговори" - "Пошаљи SMS" - "Одбиј" - "Одговори видео позивом" - "Одговори аудио-позивом" - "Прихвати захтев за видео" - "Одбиј захтев за видео" - "Прихвати захтев за одлазни видео позив" - "Одбиј захтев за одлазни видео позив" - "Прихвати захтев за долазни видео позив" - "Одбиј захтев за долазни видео позив" - "Превуците нагоре за %s." - "Превуците улево за %s." - "Превуците удесно за %s." - "Превуците надоле за %s." - "Вибрација" - "Вибрација" - "Звук" - "Подразумевани звук (%1$s)" - "Мелодија звона телефона" - "Вибрирај када звони" - "Мелодија звона и вибрација" - "Управљај конференцијским позивом" - "Број за хитне случајеве" - "Слика профила" - "Камера је искључена" - "на %s" - "Белешка је послата" - "Недавне поруке" - "Информације о предузећу" - "Удаљеност је %.1f mi" - "Удаљеност је %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Отвара се сутра у %s" - "Отвара се данас у %s" - "Затвара се у %s" - "Затворило се данас у %s" - "Тренутно отворено" - "Тренутно затворено" - "Непожељан позивалац" - "Позив се завршио у %1$s" - "Ово је био први позив са овог броја." - "Сумњамо да је овај позив непожељан." - "Блокирај/пријави" - "Додај контакт" - "Није непожељан" - diff --git a/InCallUI/res/values-sv/strings.xml b/InCallUI/res/values-sv/strings.xml deleted file mode 100644 index 980ecdb07e01878ef1bfa4c249274fe3c33f4201..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sv/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Parkerat" - "Okänd" - "Privat nummer" - "Telefonautomat" - "Konferenssamtal" - "Samtalet avbröts" - "Högtalare" - "Telefonlur" - "Trådanslutet headset" - "Bluetooth" - "Ska följande toner skickas?\nBREAK" - "Skickar signaler\n" - "Skicka" - "Ja" - "Nej" - "Ersätt jokertecknet med" - "Konferenssamtal %s" - "Nummer till röstbrevlåda" - "Ringer" - "Ringer upp igen" - "Konferenssamtal" - "Inkommande samtal" - "Inkommande jobbsamtal" - "Samtal avslutat" - "Parkerat" - "Lägger på" - "I samtal" - "Mitt telefonnummer är %s" - "Ansluter video" - "Videosamtal" - "Begär video" - "Det gick inte att ansluta till videosamtalet" - "Videobegäran avslogs" - "Ditt nummer för återuppringning\n %1$s" - "Ditt nummer för återuppringning vid nödsamtal\n %1$s" - "Ringer" - "Missat samtal" - "Missade samtal" - "%s missade samtal" - "Missat samtal från %s" - "Pågående samtal" - "Pågående jobbsamtal" - "Pågående Wi-Fi-samtal" - "Pågående jobbsamtal via Wi-Fi" - "Parkerat" - "Inkommande samtal" - "Inkommande jobbsamtal" - "Inkommande Wi-Fi-samtal" - "Inkommande jobbsamtal via Wi-Fi" - "Inkommande videosamtal" - "Inkommande misstänkt spamsamtal" - "Inkommande begäran om videosamtal" - "Nytt röstmeddelande" - "Nytt röstmeddelande (%d)" - "Ring %s" - "Nummer till röstbrevlåda okänt" - "Ingen tjänst" - "Det valda nätverket (%s) är inte tillgängligt" - "Svara" - "Lägg på" - "Video" - "Röstsamtal" - "Godkänn" - "Ignorera" - "Ring upp" - "Meddelande" - "Pågående samtal på en annan enhet" - "Överför samtal" - "Om du vill ringa måste du först inaktivera flygplansläge." - "Inte registrerat på nätverk." - "Det finns inget mobilnät tillgängligt." - "Ange ett giltigt nummer om du vill ringa ett samtal." - "Det gick inte att ringa." - "Startar sekvens för MMI-kod …" - "Tjänsten stöds inte." - "Det gick inte att växla mellan samtal." - "Det gick inte att koppla isär samtalen." - "Det gick inte att överföra." - "Det gick inte att starta ett konferenssamtal." - "Det gick inte att avvisa samtalet." - "Det gick inte att släppa samtal." - "SIP-samtal" - "Nödsamtal" - "Sätter på radion …" - "Ingen tjänst. Försöker igen …" - "Det gick inte att ringa. %s är inget nödnummer." - "Det gick inte att ringa. Slå ett nödnummer." - "Använd tangentbordet om du vill ringa" - "Parkera samtal" - "Återuppta samtal" - "Avsluta samtal" - "Visa knappsats" - "Dölj knappsats" - "Ljud av" - "Sluta ignorera" - "Lägg till samtal" - "Koppla ihop samtal" - "Byt" - "Hantera samtal" - "Hantera konferenssamtal" - "Konferenssamtal" - "Hantera" - "Ljud" - "Videosamt." - "Byt till röstsamtal" - "Byt kamera" - "Slå på kameran" - "Stäng av kameran" - "Fler alternativ" - "Videospelaren har startats" - "Videospelaren har stoppats" - "Kameran är inte klar" - "Kameran är klar" - "Okänd händelse vid samtalssession" - "Tjänst" - "Konfiguration" - "<Har inte angetts>" - "Övriga samtalsinställningar" - "Ringer via %s" - "Inkommande via %s" - "kontaktbild" - "gör privat" - "välj kontakt" - "Skriv ett eget svar …" - "Avbryt" - "Skicka" - "Svara" - "Skicka sms" - "Avvisa" - "Svara som videosamtal" - "Svara som röstsamtal" - "Godkänn videobegäran" - "Avvisa videobegäran" - "Godkänn begäran om att skicka video" - "Avvisa begäran om att skicka video" - "Godkänn begäran om att ta emot video" - "Avvisa begäran om att ta emot video" - "%s genom att dra uppåt." - "%s genom att dra åt vänster." - "%s genom att dra åt höger." - "%s genom att dra nedåt." - "Vibrera" - "Vibrera" - "Ljud" - "Standardsignal (%1$s)" - "Ringsignal" - "Enheten vibrerar vid samtal" - "Ringsignal och vibration" - "Hantera konferenssamtal" - "Nödsamtalsnummer" - "Profilbild" - "Kamera av" - "via %s" - "Anteckningen har skickats" - "Senaste meddelandena" - "Företagsuppgifter" - "%.1f miles bort" - "%.1f km bort" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Öppnar i morgon kl. %s" - "Öppnar i dag kl. %s" - "Stänger kl. %s" - "Stängde i dag kl. %s" - "Öppet" - "Stängt" - "Misstänkt spamsamtal" - "Samtalet avslutat %1$s" - "Det här är första gången det här numret ringde dig." - "Vi misstänkte att det här samtalet var en spammare." - "Blockera/rapp. spam" - "Lägg till kontakt" - "Inte spam" - diff --git a/InCallUI/res/values-sw/strings.xml b/InCallUI/res/values-sw/strings.xml deleted file mode 100644 index d60c4901a76e033fa382a7d52dddf1a38da48d07..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sw/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Simu" - "Inangoja" - "Isiyojulikana" - "Nambari ya faragha" - "Simu ya kulipia" - "Simu ya mkutano" - "Simu imekatwa" - "Spika" - "Kipaza sauti cha kichwani" - "Vifaa vya sauti visivyo na waya" - "Bluetooth" - "Ungependa kutuma milio ya sauti inayofuata? \n" - "Inatuma milio ya simu\n" - "Tuma" - "Ndiyo" - "Hapana" - "Badilisha herufi inayojitegemea kwa" - "Simu ya mkutano %s" - "Nambari ya ujumbe wa sauti" - "Inapiga" - "Inapiga simu tena" - "Simu ya mkutano" - "Unapigiwa simu" - "Simu ya kazi inayoingia" - "Simu imekamilika" - "Inangoja" - "Kukata simu" - "Katika simu" - "Nambari yangu ni %s" - "Inaunganisha video" - "Hangout ya Video" - "Inaomba video" - "Haiwezi kuunganisha Hangout ya video" - "Ombi la video limekataliwa" - "Nambari yako ya kupigiwa simu\n%1$s" - "Nambari yako ya dharura ya kupigiwa simu\n%1$s" - "Inapiga" - "Simu ambayo hukujibu" - "Simu ambazo hukujibu" - "Simu %s ambazo hukujibu" - "Simu ambayo hukujibu kutoka %s" - "Simu inayoendelea" - "Simu ya kazi inayoendelea" - "Simu ya Wi-Fi inayoendelea" - "Simu ya Wi-Fi ya kazi inayoendelea" - "Inangoja" - "Unapigiwa simu" - "Unapigiwa simu ya kazi" - "Unapigiwa simu kupitia Wi-Fi" - "Unapigiwa simu ya kazini kupitia Wi-Fi" - "Hangout ya Video inayoingia" - "Simu inayoingia inashukiwa kuwa taka" - "Ombi linaloingia la video" - "Ujumbe mpya wa sauti" - "Ujumbe (%d) mpya wa sauti" - "Piga %s" - "Nambari ya ujumbe wa sauti haijulikani." - "Hakuna huduma" - "Mtandao uliochaguliwa (%s) haupatikani" - "Jibu" - "Kata simu" - "Video" - "Sauti" - "Kubali" - "Ondoa" - "Mpigie" - "Ujumbe" - "Una Hangout inayoendelea kwenye kifaa kingine" - "Hamisha Hangout" - "Ili upige simu kwanza, zima Hali ya ndegeni." - "Haijasajiliwa kwenye mtandao." - "Mitandao ya simu za mkononi haipatikani." - "Ili upige simu, weka nambari sahihi." - "Haiwezi kupiga simu." - "Inaanzisha msururu wa MMI…" - "Huduma haitumiki." - "Haiwezi kubadili simu." - "Haiwezi kutenganisha simu." - "Haiwezi kuhamisha." - "Haiwezi kushiriki katika simu ya mkutano." - "Haiwezi kukataa simu." - "Haiwezi kutoa simu." - "Simu ya SIP" - "Simu ya dharura" - "Inawasha redio..." - "Hakuna huduma. Inajaribu tena..." - "Haiwezi kupiga simu. %s si nambari ya dharura." - "Haiwezi kupiga simu. Piga simu nambari ya dharura." - "Tumia kibodi kubonyeza" - "Shikilia Simu" - "Endelea na Simu" - "Kata Simu" - "Onyesha Vitufe vya Kupiga Simu" - "Ficha Vitufe vya Kupiga Simu" - "Zima Sauti" - "Rejesha sauti" - "Ongeza simu" - "Unganisha simu" - "Badili" - "Dhibiti simu" - "Dhibiti simu ya mkutano" - "Mkutano kwenye simu" - "Dhibiti" - "Sauti" - "Hangout ya Video" - "Badilisha iwe simu ya sauti" - "Badilisha kamera" - "Washa kamera" - "Zima kamera" - "Chaguo zaidi" - "Kichezaji Kimeanzishwa" - "Kichezaji Kimekomeshwa" - "Kamera haiko tayari" - "Kamera iko tayari" - "Tukio lisilojulikana la kipindi cha simu" - "Huduma" - "Weka mipangilio" - "<Haijawekwa>" - "Mipangilio mingine ya simu" - "Kupiga simu kupitia %s" - "Simu zinazoingia kupitia %s" - "picha ya anwani" - "tumia kwa faragha" - "chagua anwani" - "Andika yako binafsi..." - "Ghairi" - "Tuma" - "Jibu" - "Tuma SMS" - "Kataa" - "Pokea kama Hangout ya Video" - "Pokea kama simu ya sauti" - "Kubali ombi la video" - "Kataa ombi la video" - "Kubali ombi la kutuma kupitia hangout ya video" - "Kataa ombi la kutuma kupitia hangout ya video" - "Kubali ombi la kupokea kupitia hangout ya video" - "Kataa ombi la kupokea kupitia hangout ya video" - "Telezesha kidole juu ili %s ." - "Telezesha kidole kushoto ili %s." - "Telezesha kidole kulia ili %s." - "Telezesha kidole chini ili %s." - "Mtetemo" - "Mtetemo" - "Mlio" - "Sauti chaguo-msingi (%1$s)" - "Mlio wa simu" - "Tetema wakati wa kuita" - "Mlio wa simu na Mtetemo" - "Dhibiti simu ya mkutano" - "Nambari ya dharura" - "Picha ya wasifu" - "Kamera imezimwa" - "kupitia %s" - "Dokezo limetumwa" - "Ujumbe wa hivi majuzi" - "Maelezo ya biashara" - "Umbali wa maili %.1f" - "Umbali wa kilomita %.1f" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Itafunguliwa kesho saa %s" - "Itafunguliwa leo saa %s" - "Hufungwa saa %s" - "Imefungwa leo saa %s" - "Sasa imefunguliwa" - "Sasa imefungwa" - "Mpiga simu taka" - "Simu imekatwa %1$s" - "Hii ndiyo mara ya kwanza nambari hii imekupigia." - "Tunashuku kwamba simu hii ni taka." - "Zuia/ripoti taka" - "Ongeza anwani" - "Si barua taka" - diff --git a/InCallUI/res/values-sw360dp/dimens.xml b/InCallUI/res/values-sw360dp/dimens.xml deleted file mode 100644 index 9fbcd605b454e022885c518496a6da0f33c86e6f..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sw360dp/dimens.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - 22sp - 18sp - 45dp - 34sp - 18sp - - - @dimen/dialpad_key_number_default_margin_bottom - - @dimen/dialpad_zero_key_number_default_margin_bottom - @dimen/dialpad_digits_text_size - @dimen/dialpad_digits_height - @dimen/dialpad_key_numbers_default_size - diff --git a/InCallUI/res/values-sw410dp/config.xml b/InCallUI/res/values-sw410dp/config.xml deleted file mode 100644 index a57f867849f85dd4f35e5a6b22a603c56b1c6ff3..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-sw410dp/config.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - 6 - \ No newline at end of file diff --git a/InCallUI/res/values-ta/strings.xml b/InCallUI/res/values-ta/strings.xml deleted file mode 100644 index 1ee57b4bac88db7b9690c26fd7568003b768f6a3..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ta/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ஃபோன்" - "ஹோல்டில் உள்ளது" - "தெரியாத எண்" - "தனிப்பட்ட எண்" - "கட்டணத் தொலைபேசி" - "குழு அழைப்பு" - "அழைப்பு நிறுத்தப்பட்டது" - "ஸ்பீக்கர்" - "ஹேண்ட்செட் இயர்ஃபீஸ்" - "வயருள்ள ஹெட்செட்" - "புளூடூத்" - "பின்வரும் டோன்களை அனுப்பவா?\n" - "டோன்களை அனுப்புகிறது\n" - "அனுப்பு" - "ஆம்" - "வேண்டாம்" - "சிறப்புக்குறியை இதன் மூலம் மாற்றியமை" - "குழு அழைப்பு: %s" - "குரலஞ்சல் எண்" - "அழைக்கிறது" - "மீண்டும் டயல் செய்கிறது" - "குழு அழைப்பு" - "உள்வரும் அழைப்பு" - "உள்வரும் அழைப்பு (பணி)" - "அழைப்பு முடிந்தது" - "ஹோல்டில் உள்ளது" - "துண்டிக்கிறது" - "அழைப்பில்" - "எனது எண்: %s" - "வீடியோவை இணைக்கிறது" - "வீடியோ அழைப்பு" - "வீடியோவைக் கோருகிறது" - "வீடியோ அழைப்பை இணைக்க முடியவில்லை" - "வீடியோ கோரிக்கை நிராகரிக்கப்பட்டது" - "உங்களைத் திரும்ப அழைப்பதற்கான எண்\n %1$s" - "அவசர அழைப்பு எண்\n %1$s" - "அழைக்கிறது" - "தவறிய அழைப்பு" - "தவறிய அழைப்புகள்" - "%s தவறிய அழைப்புகள்" - "%s இடமிருந்து தவறிய அழைப்பு" - "செயலில் இருக்கும் அழைப்பு" - "செயலில் இருக்கும் அழைப்பு (பணி)" - "செயலில் இருக்கும் வைஃபை அழைப்பு" - "செயலில் இருக்கும் வைஃபை அழைப்பு" - "ஹோல்டில் உள்ளது" - "உள்வரும் அழைப்பு" - "உள்வரும் அழைப்பு (பணி)" - "உள்வரும் வைஃபை அழைப்பு" - "உள்வரும் வைஃபை அழைப்பு (பணி)" - "உள்வரும் வீடியோ அழைப்பு" - "உள்வரும் சந்தேகத்திற்குரிய ஸ்பேம் அழைப்பு" - "உள்வரும் வீடியோ கோரிக்கை" - "புதிய குரலஞ்சல்" - "புதிய குரலஞ்சல் (%d)" - "%sஐ அழை" - "குரலஞ்சல் எண் அறியப்படவில்லை" - "சேவை இல்லை" - "தேர்ந்தெடுத்த நெட்வொர்க் (%s) கிடைக்கவில்லை" - "பதிலளி" - "துண்டி" - "வீடியோ" - "குரல்" - "ஏற்கிறேன்" - "நிராகரி" - "திரும்ப அழை" - "செய்தி அனுப்பு" - "மற்றொரு சாதனத்தில் செயலில் இருக்கும் அழைப்பு" - "அழைப்பை இடமாற்று" - "அழைப்பதற்கு, முதலில் விமானப் பயன்முறையை முடக்கவும்." - "நெட்வொர்க்கில் பதிவுசெய்யப்படவில்லை." - "செல்லுலார் நெட்வொர்க் கிடைக்கவில்லை." - "அழைக்க, சரியான எண்ணை உள்ளிடவும்." - "அழைக்க முடியாது." - "MMI வரிசையைத் தொடங்குகிறது..." - "சேவை ஆதரிக்கப்படவில்லை." - "அழைப்புகளில் மாற முடியாது." - "அழைப்பைப் பிரிக்க முடியாது." - "மாற்ற முடியாது." - "குழு அழைப்பு செய்ய முடியாது." - "அழைப்பை நிராகரிக்க முடியாது." - "அழைப்பை(அழைப்புகளை) விடுவிக்க முடியாது." - "SIP அழைப்பு" - "அவசர அழைப்பு" - "ரேடியோவை இயக்குகிறது…" - "சேவை இல்லை. மீண்டும் முயல்கிறது…" - "%s என்பது அவசர அழைப்பு எண் இல்லை என்பதால் அழைக்க முடியாது." - "அழைக்க முடியாது. அவசர அழைப்பு எண்ணை அழைக்கவும்." - "டயல் செய்ய, விசைப்பலகையைப் பயன்படுத்தவும்" - "அழைப்பை ஹோல்டில் வை" - "அழைப்பை மீண்டும் தொடங்கு" - "அழைப்பை முடி" - "டயல்பேடைக் காட்டு" - "டயல்பேடை மறை" - "ஒலியடக்கு" - "ஒலி இயக்கு" - "அழைப்பைச் சேர்" - "அழைப்புகளை இணை" - "மாற்று" - "அழைப்புகளை நிர்வகி" - "குழு அழைப்பை நிர்வகி" - "குழு அழைப்பு" - "நிர்வகி" - "ஆடியோ" - "வீடியோ அழைப்பு" - "குரல் அழைப்பிற்கு மாறு" - "கேமராவை மாற்று" - "கேமராவை இயக்கு" - "கேமராவை முடக்கு" - "கூடுதல் விருப்பங்கள்" - "வீடியோ பிளேயர் துவங்கியது" - "வீடியோ பிளேயர் நிறுத்தப்பட்டது" - "கேமரா தயாராக இல்லை" - "கேமரா தயார்" - "தெரியாத அழைப்பு நேர நிகழ்வு" - "சேவை" - "அமை" - "<அமைக்கப்படவில்லை>" - "பிற அழைப்பு அமைப்புகள்" - "%s வழியாக அழைக்கிறது" - "%s மூலம் உள்வரும் அழைப்புகள்" - "தொடர்புப் படம்" - "தனிப்பட்டதிற்குச் செல்" - "தொடர்பைத் தேர்ந்தெடுக்கவும்" - "சொந்தமாக எழுதவும்..." - "ரத்துசெய்" - "அனுப்பு" - "பதிலளி" - "SMS அனுப்பு" - "நிராகரி" - "வீடியோ அழைப்பில் பதிலளி" - "ஆடியோ அழைப்பில் பதிலளி" - "வீடியோ கோரிக்கையை அனுமதி" - "வீடியோ கோரிக்கையை நிராகரி" - "வீடியோவைப் பரிமாற்றும் கோரிக்கையை அனுமதி" - "வீடியோவைப் பரிமாற்றும் கோரிக்கையை நிராகரி" - "வீடியோவைப் பெறும் கோரிக்கையை அனுமதி" - "வீடியோவைப் பெறும் கோரிக்கையை நிராகரி" - "%s, மேலே ஸ்லைடு செய்க." - "%s, இடப்புறம் ஸ்லைடு செய்க." - "%s, வலப்புறம் ஸ்லைடு செய்க." - "%s, கீழே ஸ்லைடு செய்க." - "அதிர்வுறு" - "அதிர்வுறு" - "ஒலி" - "இயல்பு ஒலி (%1$s)" - "ஃபோனின் ரிங்டோன்" - "அழைக்கும் போது அதிர்வுறு" - "ரிங்டோன் & அதிர்வு" - "குழு அழைப்பை நிர்வகி" - "அவசர அழைப்பு எண்" - "சுயவிவரப் படம்" - "கேமரா முடக்கப்பட்டது" - "%s வழியாக" - "குறிப்பு அனுப்பப்பட்டது" - "சமீபத்திய செய்திகள்" - "வணிகத் தகவல்" - "%.1f மைல் தொலைவில்" - "%.1f கிமீ தொலைவில்" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "நாளை %sக்குத் திறக்கப்படும்" - "இன்று %sக்குத் திறக்கப்படும்" - "%sக்கு மூடப்படும்" - "இன்று %sக்கு மூடப்பட்டது" - "இப்போது திறக்கப்பட்டுள்ளது" - "இப்போது மூடப்பட்டுள்ளது" - "சந்தேகத்திற்குரிய ஸ்பேம் அழைப்பாளர்" - "அழைப்பு துண்டிக்கப்பட்டது %1$s" - "இந்த எண்ணிலிருந்து உங்களுக்கு அழைப்பு வந்தது இதுவே முதல் முறை." - "இந்த அழைப்பு ஸ்பேமராக இருக்கக்கூடும் என சந்தேகிக்கிறோம்." - "தடு/ஸ்பேமென புகாரளி" - "தொடர்பைச் சேர்" - "ஸ்பேமில்லை" - diff --git a/InCallUI/res/values-te/strings.xml b/InCallUI/res/values-te/strings.xml deleted file mode 100644 index 936f1be7cc18761af5287b873c5c3a20d77bcd31..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-te/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ఫోన్" - "హోల్డ్‌లో ఉంది" - "తెలియదు" - "ప్రైవేట్ నంబర్" - "పే ఫోన్" - "కాన్ఫరెన్స్ కాల్" - "కాల్ కట్ అయింది" - "స్పీకర్" - "హ్యాండ్‌సెట్ ఇయర్‌పీస్" - "వైర్ గల హెడ్‌సెట్" - "బ్లూటూత్" - "కింది టోన్‌లను పంపాలా?\n" - "టోన్‌లు పంపుతోంది\n" - "పంపు" - "అవును" - "వద్దు" - "దీనితో వైల్డ అక్షరాన్ని భర్తీ చేయండి" - "కాన్ఫరెన్స్ కాల్ %s" - "వాయిస్ మెయిల్ నంబర్" - "డయల్ చేస్తోంది" - "మళ్లీ డయల్ చేస్తోంది" - "కాన్ఫరెన్స్ కాల్" - "ఇన్‌కమింగ్ కాల్" - "ఇన్‌కమింగ్ కార్యాలయ కాల్" - "కాల్ ముగిసింది" - "హోల్డ్‌లో ఉంది" - "ముగిస్తోంది" - "కాల్‌లో ఉంది" - "నా నంబర్ %s" - "వీడియోను కనెక్ట్ చేస్తోంది" - "వీడియో కాల్" - "వీడియో కోసం అభ్యర్థిస్తోంది" - "వీడియో కాల్‌ను కనెక్ట్ చేయలేరు" - "వీడియో అభ్యర్థన తిరస్కరించబడింది" - "మీ కాల్‌బ్యాక్ నంబర్\n %1$s" - "మీ అత్యవసర కాల్‌బ్యాక్ నంబర్\n %1$s" - "డయల్ చేస్తోంది" - "సమాధానం ఇవ్వని కాల్" - "సమాధానం ఇవ్వని కాల్‌లు" - "%s సమాధానం ఇవ్వని కాల్‌లు" - "%s నుండి సమాధానం ఇవ్వని కాల్" - "కాల్ కొనసాగుతోంది" - "కార్యాలయ కాల్ కొనసాగుతోంది" - "Wi-Fi కాల్ కొనసాగుతోంది" - "Wi-Fi కార్యాలయ కాల్ కొనసాగుతోంది" - "హోల్డ్‌లో ఉంది" - "ఇన్‌కమింగ్ కాల్" - "ఇన్‌కమింగ్ కార్యాలయ కాల్" - "ఇన్‌కమింగ్ Wi-Fi కాల్" - "ఇన్‌కమింగ్ Wi-Fi కార్యాలయ కాల్" - "ఇన్‌కమింగ్ వీడియో కాల్" - "అనుమానాస్పద స్పామ్ కాల్ వస్తోంది" - "ఇన్‌కమింగ్ వీడియో అభ్యర్థన" - "కొత్త వాయిస్ మెయిల్" - "కొత్త వాయిస్ మెయిల్ (%d)" - "%sకు డయల్ చేయండి" - "వాయిస్ మెయిల్ నంబర్ తెలియదు" - "సేవ లేదు" - "ఎంచుకున్న నెట్‌వర్క్ (%s) అందుబాటులో లేదు" - "సమాధానం" - "కాల్ ముగించు" - "వీడియో" - "వాయిస్" - "ఆమోదిస్తున్నాను" - "తీసివేయి" - "తిరిగి కాల్ చేయి" - "సందేశం పంపు" - "మరో పరికరంలో కాల్ జరుగుతోంది" - "కాల్‌ను బదిలీ చేయి" - "కాల్ చేయడానికి, ముందుగా ఎయిర్‌ప్లైన్ మోడ్‌ను ఆపివేయండి." - "నెట్‌వర్క్‌లో నమోదు కాలేదు." - "సెల్యులార్ నెట్‌వర్క్ అందుబాటులో లేదు." - "కాల్ చేయడానికి, చెల్లుబాటు అయ్యే నంబర్‌ను నమోదు చేయండి." - "కాల్ చేయలేరు." - "MMI శ్రేణిని ప్రారంభిస్తోంది…" - "సేవకు మద్దతు లేదు." - "కాల్‌లను మార్చలేరు." - "కాల్‌ను వేరు చేయలేరు." - "బదిలీ చేయలేరు." - "కాన్ఫరెన్స్ కాల్ కుదరదు." - "కాల్‌ను తిరస్కరించలేరు." - "కాల్(ల)ను విడిచిపెట్టలేరు." - "SIP కాల్" - "అత్యవసర కాల్" - "రేడియోను ఆన్ చేస్తోంది…" - "సేవ లేదు. మళ్లీ ప్రయత్నిస్తోంది…" - "కాల్ చేయలేరు. %s అత్యవసర నంబర్ కాదు." - "కాల్ చేయలేరు. అత్యవసర నంబర్‌కు డయల్ చేయండి." - "డయల్ చేయడానికి కీబోర్డ్‌ను ఉపయోగించండి" - "కాల్‌ను హోల్డ్‌లో ఉంచు" - "కాల్‌ను పునఃప్రారంభించు" - "కాల్‌ని ముగించు" - "డయల్‌ప్యాడ్‌ను చూపు" - "డయల్‌ప్యాడ్‌ను దాచు" - "మ్యూట్ చేయి" - "అన్‌మ్యూట్ చేయి" - "కాల్‌ను జోడించు" - "కాల్‌లను విలీనం చేయి" - "స్వాప్ చేయి" - "కాల్‌లను నిర్వహించు" - "కాన్ఫరెన్స్ కాల్‌ను నిర్వహించు" - "కాన్ఫరెన్స్ కాల్" - "నిర్వహించు" - "ఆడియో" - "వీడియో కాల్" - "వాయిస్ కాల్‌కి మార్చు" - "కెమెరాను మార్చు" - "కెమెరాను ఆన్ చేయి" - "కెమెరాను ఆఫ్ చేయి" - "మరిన్ని ఎంపికలు" - "ప్లేయర్ ప్రారంభమైంది" - "ప్లేయర్ ఆపివేయబడింది" - "కెమెరా సిద్ధంగా లేదు" - "కెమెరా సిద్ధంగా ఉంది" - "తెలియని కాల్ సెషన్ ఉదంతం" - "సేవ" - "సెటప్ చేయండి" - "<సెట్ చేయలేదు>" - "ఇతర కాల్ సెట్టింగ్‌లు" - "%s ద్వారా కాల్ చేయబడుతోంది" - "%s ద్వారా ఇన్‌కమింగ్" - "పరిచయం ఫోటో" - "ప్రైవేట్‌గా వెళ్లు" - "పరిచయాన్ని ఎంచుకోండి" - "మీ స్వంతంగా వ్రాయండి…" - "రద్దు చేయి" - "పంపు" - "సమాధానం" - "SMSని పంపుతుంది" - "తిరస్కరిస్తుంది" - "వీడియో కాల్ రూపంలో సమాధానం" - "ఆడియో కాల్ రూపంలో సమాధానం" - "వీడియో అభ్యర్థనను ఆమోదిస్తుంది" - "వీడియో అభ్యర్థనను తిరస్కరిస్తుంది" - "వీడియో ప్రసరణ అభ్యర్థనను ఆమోదిస్తుంది" - "వీడియో ప్రసరణ అభ్యర్థనను తిరస్కరిస్తుంది" - "వీడియో స్వీకరణ అభ్యర్థనను ఆమోదిస్తుంది" - "వీడియో స్వీకరణ అభ్యర్థనను తిరస్కరిస్తుంది" - "%s కోసం పైకి స్లైడ్ చేయండి." - "%s కోసం ఎడమవైపుకు స్లైడ్ చేయండి." - "%s కోసం కుడివైపుకు స్లైడ్ చేయండి." - "%s కోసం కిందికి స్లైడ్ చేయండి." - "వైబ్రేట్" - "వైబ్రేట్" - "ధ్వని" - "డిఫాల్ట్ ధ్వని (%1$s)" - "ఫోన్ రింగ్‌టోన్" - "రింగ్ అయ్యేప్పుడు వైబ్రేషన్" - "రింగ్‌టోన్ & వైబ్రేట్" - "కాన్ఫరెన్స్ కాల్‌ను నిర్వహించు" - "అత్యవసర నంబర్" - "ప్రొఫైల్ ఫోటో" - "కెమెరా ఆఫ్‌లో ఉంది" - "%s ద్వారా" - "గమనిక పంపబడింది" - "ఇటీవలి సందేశాలు" - "వ్యాపార సంస్థ సమాచారం" - "%.1f మై దూరంలో ఉంది" - "%.1f కి.మీ దూరంలో ఉంది" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "రేపు %sకి తెరవబడుతుంది" - "ఈరోజు %sకి తెరవబడుతుంది" - "%sకి మూసివేయబడుతుంది" - "ఈరోజు %sకి మూసివేయబడి ఉంటుంది" - "ఇప్పుడు తెరిచి ఉంది" - "ఇప్పుడు మూసివేయబడింది" - "అనుమానిత స్పామ్ కాలర్" - "కాల్ ముగిసింది %1$s" - "ఈ నంబర్ నుండి మీకు కాల్ రావడం ఇదే మొదటిసారి." - "ఈ కాల్ స్పామర్ కావచ్చని మేము అనుమానిస్తున్నాము." - "బ్లాక్/స్పామ్ నివే." - "పరిచయాన్ని జోడించు" - "స్పామ్ కాదు" - diff --git a/InCallUI/res/values-th/strings.xml b/InCallUI/res/values-th/strings.xml deleted file mode 100644 index 69ae44d07e69b673a69a661dd45af6fda70960e2..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-th/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "โทรศัพท์" - "พักสาย" - "ไม่ทราบ" - "หมายเลขส่วนตัว" - "โทรศัพท์สาธารณะ" - "การประชุมสาย" - "สายหลุด" - "ลำโพง" - "ชุดหูฟังโทรศัพท์" - "ชุดหูฟังแบบมีสาย" - "บลูทูธ" - "ส่งหมายเลขต่อไปไหม\n" - "กำลังส่งหมายเลข\n" - "ส่ง" - "ใช่" - "ไม่" - "แทนที่อักขระแทนด้วย" - "การประชุมสาย %s" - "หมายเลขข้อความเสียง" - "กำลังโทรออก" - "โทรใหม่" - "การประชุมสาย" - "สายเรียกเข้า" - "มีสายเรียกเข้าจากที่ทำงาน" - "วางสายแล้ว" - "พักสาย" - "กำลังวางสาย" - "กำลังใช้สาย" - "หมายเลขของฉันคือ %s" - "กำลังเชื่อมต่อวิดีโอ" - "แฮงเอาท์วิดีโอ" - "กำลังขอวิดีโอ" - "ไม่สามารถเชื่อมต่อแฮงเอาท์วิดีโอได้" - "คำขอวิดีโอถูกปฏิเสธ" - "หมายเลขโทรกลับของคุณ\n %1$s" - "หมายเลขโทรกลับฉุกเฉินของคุณ\n %1$s" - "กำลังโทรออก" - "สายที่ไม่ได้รับ" - "สายที่ไม่ได้รับ" - "ไม่ได้รับ %s สาย" - "สายที่ไม่ได้รับจาก %s" - "โทรต่อเนื่อง" - "กำลังอยู่ในสายจากที่ทำงาน" - "กำลังโทรผ่าน Wi-Fi" - "กำลังอยู่ในสายจากที่ทำงานผ่าน Wi-Fi" - "พักสาย" - "สายเรียกเข้า" - "มีสายเรียกเข้าจากที่ทำงาน" - "สายโทรเข้าผ่าน Wi-Fi" - "มีสายเรียกเข้าจากที่ทำงานผ่าน Wi-Fi" - "แฮงเอาท์วิดีโอเรียกเข้า" - "สายเรียกเข้าที่สงสัยว่าเป็นสแปม" - "คำขอโทรเข้าเป็นวิดีโอ" - "ข้อความเสียงใหม่" - "ข้อความเสียงใหม่ (%d)" - "หมุนหมายเลข %s" - "ไม่ทราบหมายเลขข้อความเสียง" - "ไม่มีบริการ" - "เครือข่ายที่เลือกไว้ (%s) ไม่พร้อมใช้งาน" - "รับสาย" - "วางสาย" - "วิดีโอ" - "เสียง" - "ยอมรับ" - "ปิด" - "โทรกลับ" - "ข้อความ" - "กำลังใช้สายบนอุปกรณ์อื่น" - "โอนสาย" - "หากต้องการโทรออก ให้ปิดโหมดบนเครื่องบินก่อน" - "ยังไม่ได้ลงทะเบียนบนเครือข่าย" - "เครือข่ายมือถือใช้งานไม่ได้" - "หากต้องการโทรออก โปรดป้อนหมายเลขที่ถูกต้อง" - "ไม่สามารถโทรได้" - "กำลังเริ่มต้นลำดับ MMI..." - "ไม่สนับสนุนบริการนี้" - "ไม่สามารถสลับสายได้" - "ไม่สามารถแยกสายได้" - "ไม่สามารถโอนได้" - "ไม่สามารถประชุมได้" - "ไม่สามารถปฏิเสธสายได้" - "ไม่สามารถเริ่มการโทรได้" - "โทรแบบ SIP" - "หมายเลขฉุกเฉิน" - "กำลังเปิดวิทยุ…" - "ไม่มีบริการ โปรดลองอีกครั้ง…" - "โทรออกไม่ได้ %s ไม่ใช่หมายเลขฉุกเฉิน" - "ไม่สามารถโทรออก โทรหมายเลขฉุกเฉิน" - "ใช้แป้นพิมพ์กดหมายเลขโทรศัพท์" - "พักสาย" - "โทรต่อ" - "วางสาย" - "แสดงแป้นหมายเลข" - "ซ่อนแป้นหมายเลข" - "ปิดเสียง" - "เปิดเสียง" - "เพิ่มการโทร" - "รวมสาย" - "สลับ" - "จัดการการโทร" - "จัดการการประชุมสาย" - "การประชุมสาย" - "จัดการ" - "เสียง" - "แฮงเอาท์วิดีโอ" - "เปลี่ยนเป็นการโทรด้วยเสียง" - "เปลี่ยนกล้อง" - "เปิดกล้อง" - "ปิดกล้อง" - "ตัวเลือกเพิ่มเติม" - "โปรแกรมเล่นเริ่มทำงานแล้ว" - "โปรแกรมเล่นหยุดแล้ว" - "กล้องไม่พร้อมทำงาน" - "กล้องพร้อมทำงาน" - "เหตุการณ์เซสชันการโทรที่ไม่รู้จัก" - "บริการ" - "ตั้งค่า" - "<ไม่ได้ตั้งค่า>" - "การตั้งค่าการโทรอื่นๆ" - "โทรผ่าน %s" - "สายเรียกเข้าผ่าน %s" - "ภาพของรายชื่อติดต่อ" - "เข้าสู่โหมดส่วนตัว" - "เลือกรายชื่อติดต่อ" - "เขียนคำตอบของคุณเอง..." - "ยกเลิก" - "ส่ง" - "รับสาย" - "ส่ง SMS" - "ปฏิเสธ" - "รับสายเป็นแฮงเอาท์วิดีโอ" - "รับสายเป็นการโทรด้วยเสียง" - "ยอมรับคำขอวิดีโอ" - "ปฏิเสธคำขอวิดีโอ" - "ยอมรับคำขอให้ส่งวิดีโอ" - "ปฏิเสธคำขอให้ส่งวิดีโอ" - "ยอมรับคำขอให้รับวิดีโอ" - "ปฏิเสธคำขอให้รับวิดีโอ" - "เลื่อนไปข้างบนเพื่อ %s" - "เลื่อนไปทางซ้ายเพื่อ %s" - "เลื่อนไปทางขวาเพื่อ %s" - "เลื่อนลงเพื่อ %s" - "สั่น" - "สั่น" - "เสียง" - "เสียงเริ่มต้น (%1$s)" - "เสียงเรียกเข้าโทรศัพท์" - "สั่นเมื่อมีสายเข้า" - "เสียงเรียกเข้าและสั่น" - "จัดการการประชุมสาย" - "หมายเลขฉุกเฉิน" - "รูปโปรไฟล์" - "ปิดกล้อง" - "ผ่านหมายเลข %s" - "ส่งโน้ตแล้ว" - "ข้อความล่าสุด" - "ข้อมูลธุรกิจ" - "อยู่ห่างออกไป %.1f ไมล์" - "อยู่ห่างออกไป %.1f กม." - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "เปิดให้บริการพรุ่งนี้เวลา %s" - "เปิดให้บริการวันนี้เวลา %s" - "ปิดให้บริการเวลา %s" - "ปิดให้บริการแล้ววันนี้เวลา %s" - "ขณะนี้เปิดทำการ" - "ขณะนี้ปิดทำการ" - "ผู้โทรที่สงสัยว่าเป็นสแปม" - "วางสายแล้ว %1$s" - "หมายเลขนี้โทรหาคุณเป็นครั้งแรก" - "เราสงสัยว่าสายนี้จะเป็นนักส่งสแปม" - "บล็อก/รายงานสแปม" - "เพิ่มผู้ติดต่อ" - "ไม่ใช่สแปม" - diff --git a/InCallUI/res/values-tl/strings.xml b/InCallUI/res/values-tl/strings.xml deleted file mode 100644 index b5658d705d43b9941e48061b27a8163c3f48ac58..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-tl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telepono" - "Naka-hold" - "Hindi alam" - "Pribadong numero" - "Payphone" - "Conference call" - "Naputol ang tawag" - "Speaker" - "Handset earpiece" - "Wired na headset" - "Bluetooth" - "Ipapadala ba ang mga sumusunod na tono?\n" - "Nagpapadala ng mga tono\n" - "Ipadala" - "Oo" - "Hindi" - "Palitan ang wild character ng" - "Conference call %s" - "Numero ng voicemail" - "Dina-dial" - "Muling dina-dial" - "Conference call" - "Papasok na tawag" - "Papasok na tawag sa trabaho" - "Ibinaba ang tawag" - "Naka-hold" - "Binababa" - "Nasa tawag" - "Ang aking numero ay %s" - "Ikinokonekta ang video" - "Mag-video call" - "Humihiling ng video" - "Hindi makakonekta sa video call" - "Tinanggihan ang kahilingan sa video" - "Ang iyong numero ng callback\n %1$s" - "Ang iyong emergency na numero ng callback\n %1$s" - "Dina-dial" - "Hindi nasagot na tawag" - "Mga hindi nasagot na tawag" - "%s (na) hindi nasagot na tawag" - "Hindi nasagot ang tawag mula kay %s" - "Kasalukuyang tawag" - "Kasalukuyang tawag sa trabaho" - "Kasalukuyang tawag sa Wi-Fi" - "Kasalukuyang tawag sa trabaho sa pamamagitan ng Wi-Fi" - "Naka-hold" - "Papasok na tawag" - "Papasok na tawag sa trabaho" - "Papasok na tawag sa Wi-Fi" - "Papasok na tawag sa trabaho sa pamamagitan ng Wi-Fi" - "Papasok na video call" - "Papasok na pinaghihinalaang spam na tawag" - "Papasok na kahilingan ng video" - "Bagong voicemail" - "Bagong voicemail (%d)" - "I-dial ang %s" - "Hindi kilala ang numero ng voicemail" - "Walang serbisyo" - "Hindi available ang piniling network (%s)" - "Sagutin" - "Ibaba" - "Video" - "Boses" - "Tanggapin" - "I-dismiss" - "Tawagan" - "Mensahe" - "Kasalukuyang tawag sa isa pang device" - "Ilipat ang Tawag" - "Upang tumawag, paki-off muna ang Airplane mode." - "Hindi nakarehistro sa network." - "Hindi available ang cellular network." - "Upang tumawag, maglagay ng wastong numero." - "Hindi makatawag." - "Sinisimulan ang pagkakasunud-sunod ng MMI…" - "Hindi sinusuportahan ang serbisyo." - "Hindi mailipat ang mga tawag." - "Hindi mapaghiwalay ang tawag." - "Hindi mailipat." - "Hindi makapag-conference." - "Hindi matanggihan ang tawag." - "Hindi mailabas ang (mga) tawag." - "Tawag sa SIP" - "Emergency na tawag" - "Ino-on ang radyo…" - "Walang serbisyo. Sinusubukang muli…" - "Hindi makatawag. Ang %s ay hindi isang pang-emergency na numero." - "Hindi makatawag. Mag-dial ng pang-emergency na numero." - "Gamitin ang keyboard upang mag-dial" - "I-hold ang Tawag" - "Ituloy ang Tawag" - "Ibaba ang Tawag" - "Ipakita ang Dialpad" - "Itago ang Dialpad" - "I-mute" - "Alisin sa pagkaka-mute" - "Magdagdag ng tawag" - "Pagsamahin ang mga tawag" - "Pagpalitin" - "Pamahalaan ang mga tawag" - "Pamahalaan ang conference call" - "Conference call" - "Pamahalaan" - "Audio" - "Mag-video call" - "Gawing voice call" - "Lumipat ng camera" - "I-on ang camera" - "I-off ang camera" - "Higit pang mga opsyon" - "Nagsimula na ang Player" - "Huminto ang Player" - "Hindi pa handa ang camera" - "Handa na ang camera" - "Hindi alam na kaganapan ng session ng tawag" - "Serbisyo" - "I-setup" - "<Hindi nakatakda>" - "Iba pang mga setting ng tawag" - "Tumatawag sa pamamagitan ng %s" - "Papasok sa pamamagitan ng %s" - "larawan ng contact" - "maging pribado" - "pumili ng contact" - "Sumulat ng sarili mong tugon…" - "Kanselahin" - "Ipadala" - "Sagutin" - "Magpadala ng SMS" - "Tanggihan" - "Sagutin bilang video call" - "Sagutin bilang audio call" - "Tanggapin ang kahilingan sa video" - "Tanggihan ang kahilingan sa video" - "Tanggapin ang kahilingan sa pagpapadala ng video" - "Tanggihan ang kahilingan sa pagpapadala ng video" - "Tanggapin ang kahilingan sa pagtanggap ng video" - "Tanggihan ang kahilingan sa pagtanggap ng video" - "Mag-slide pataas para sa %s." - "Mag-slide pakaliwa para sa %s." - "Mag-slide pakanan para sa %s." - "Mag-slide pababa para sa %s." - "Mag-vibrate" - "Mag-vibrate" - "Tunog" - "Default na tunog (%1$s)" - "Ringtone ng telepono" - "Mag-vibrate kapag nagri-ring" - "Ringtone at Pag-vibrate" - "Pamahalaan ang conference call" - "Pang-emergency na numero" - "Larawan sa profile" - "Naka-off ang camera" - "sa pamamagitan ng %s" - "Naipadala ang tala" - "Mga kamakailang mensahe" - "Impormasyon ng negosyo" - "%.1f (na) milya ang layo" - "%.1f (na) kilometro ang layo" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Magbubukas bukas nang %s" - "Magbubukas ngayon nang %s" - "Magsasara nang %s" - "Sarado ngayon nang %s" - "Bukas ngayon" - "Sarado ngayon" - "Hinihinalang spam na tumatawag" - "Natapos ang tawag %1$s" - "Ito ang unang beses na tinawagan ka ng numerong ito." - "Pinaghihinalaan naming isang spammer ang tawag na ito." - "I-block/iulat na spam" - "Magdagdag ng contact" - "Hindi spam" - diff --git a/InCallUI/res/values-tr/strings.xml b/InCallUI/res/values-tr/strings.xml deleted file mode 100644 index 405bab8db21cbc0a52dfb9f88a48463f89c18ff7..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-tr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Beklemede" - "Bilinmiyor" - "Gizli numara" - "Ankesörlü telefon" - "Konferans görüşmesi" - "Çağrı kesildi" - "Hoparlör" - "Mobil cihaz kulaklığı" - "Kablolu kulaklık" - "Bluetooth" - "Aşağıdaki zil sesleri gönderilsin mi?\n" - "Numara tonları gönderiliyor\n" - "Gönder" - "Evet" - "Hayır" - "Joker karakteri şununla değiştir:" - "Konferans görüşmesi %s" - "Sesli mesaj numarası" - "Numara çevriliyor" - "Yeniden çevriliyor" - "Konferans görüşmesi" - "Gelen çağrı" - "İşle ilgili gelen çağrı" - "Çağrı sonlandırıldı" - "Beklemede" - "Sonlandırılıyor" - "Görüşmede" - "Numaram: %s" - "Video bağlanıyor" - "Video görüşmesi" - "Video isteniyor" - "Video görüşmesi bağlantısı yapılamıyor" - "Video isteği reddedildi" - "Geri aranacağınız numara\n %1$s" - "Acil durumda geri aranacağınız numara\n %1$s" - "Numara çevriliyor" - "Cevapsız çağrı" - "Cevapsız çağrılar" - "%s cevapsız çağrı" - "Cevapsız çağrı: %s" - "Devam eden çağrı" - "İşle ilgili devam eden çağrı" - "Devam eden kablosuz çağrı" - "İşle ilgili devam eden kablosuz çağrı" - "Beklemede" - "Gelen çağrı" - "İşle ilgili gelen çağrı" - "Gelen kablosuz çağrı" - "İşle ilgili gelen kablosuz çağrı" - "Gelen video görüşmesi isteği" - "Spam olabilecek gelen arama" - "Gelen video isteği" - "Yeni sesli mesaj" - "Yeni sesli mesaj (%d)" - "Çevir: %s" - "Sesli mesaj numarası bilinmiyor" - "Servis yok" - "Seçili ağ (%s) kullanılamıyor" - "Yanıtla" - "Kapat" - "Video" - "Ses" - "Kabul et" - "Yok say" - "Geri ara" - "İleti" - "Başka bir cihazda devam eden çağrı" - "Çağrıyı Aktar" - "Çağrı yapmak için öncelikle Uçak modunu kapatın." - "Ağda kayıtlı değil." - "Hücresel ağ kullanılamıyor." - "Çağrı yapmak için geçerli bir numara girin." - "Çağrı yapılamıyor." - "MMI dizisi başlatılıyor…" - "Service desteklenmiyor" - "Çağrı geçişi yapılamıyor." - "Çağrı ayrılamıyor." - "Aktarılamıyor." - "Konferans çağrısı yapılamıyor." - "Çağrı reddedilemiyor." - "Çağrılar bırakılamıyor." - "SIP çağrısı" - "Acil durum çağrısı" - "Radyo açılıyor…" - "Servis yok. Tekrar deneniyor…" - "Çağrı yapılamıyor. %s bir acil durum numarası değil." - "Çağrı yapılamıyor. Acil durum numarasını çevirin." - "Çevirmek için klavyeyi kullan" - "Çağrıyı Beklet" - "Çağrıyı Devam Ettir" - "Çağrıyı Sonlandır" - "Tuş Takımını Göster" - "Tuş Takımını Gizle" - "Sesi kapat" - "Sesi aç" - "Çağrı ekle" - "Çağrıları birleştir" - "Değiştir" - "Çağrıları yönet" - "Konferans çağrısını yönet" - "Konferans çağrısı" - "Yönet" - "Ses" - "Vid. görşm" - "Sesli çağrıya geç" - "Kamerayı değiştir" - "Kamerayı aç" - "Kamerayı kapat" - "Diğer seçenekler" - "Oynatıcı Başlatıldı" - "Oynatıcı Durduruldu" - "Kamera hazır değil" - "Kamera hazır" - "Bilinmeyen çağrı oturumu etkinliği" - "Hizmet" - "Kurulum" - "<Ayarlanmadı>" - "Diğer çağrı ayarları" - "%s üzerinden çağrı yapılıyor" - "%s adlı sağlayıcı üzerinden gelen çağrı" - "kişi fotoğrafı" - "özel görüşmeye geç" - "kişi seçin" - "Kendi yanıtınızı oluşturun…" - "İptal" - "Gönder" - "Yanıtla" - "SMS gönder" - "Reddet" - "Video görüşmesi olarak yanıtla" - "Sesli görüşme olarak yanıtla" - "Video isteğini kabul et" - "Video isteğini reddet" - "Video aktarma isteğini kabul et" - "Video aktarma isteğini reddet" - "Video alma isteğini kabul et" - "Video alma isteğini reddet" - "%s için yukarı kaydırın." - "%s için sola kaydırın." - "%s için sağa kaydırın." - "%s için aşağı kaydırın." - "Titreşim" - "Titreşim" - "Ses" - "Varsayılan ses (%1$s)" - "Telefon zil sesi" - "Çalarken titret" - "Zil Sesi ve Titreşim" - "Konferans çağrısını yönetin" - "Acil durum numarası" - "Profil fotoğrafı" - "Kamera kapalı" - "%s üzerinden" - "Not gönderildi" - "Son iletiler" - "İşletme bilgileri" - "%.1f mil uzakta" - "%.1f km uzakta" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Yarın açılış saati: %s" - "Bugün açılış saati: %s" - "Kapanış saati: %s" - "Bugün kapanış saati: %s" - "Şu an açık" - "Şu an kapalı" - "Spam olabilck arayan" - "Arama sona erdi %1$s" - "Bu, bu numaradan ilk aranışınız." - "Bu aramanın spam olduğundan şüpheleniliyor." - "Engelle/spam bildir" - "Kişi ekle" - "Spam değil" - diff --git a/InCallUI/res/values-uk/strings.xml b/InCallUI/res/values-uk/strings.xml deleted file mode 100644 index b323acd0437098cea7a6d93605c7f111eb5ad56d..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-uk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Номер телефону" - "Очікування" - "Невідомо" - "Приватний номер" - "Таксофон" - "Конференц-зв’язок" - "Виклик перервано" - "Динамік" - "Динамік гарнітури" - "Дротова гарнітура" - "Bluetooth" - "Надіслати вказані нижче сигнали?\n" - "Надсилання сигналів\n" - "Надіслати" - "Так" - "Ні" - "Замінити довільний символ на" - "Конференц-зв’язок %s" - "Номер голосової пошти" - "Набір номера" - "Повторний набір" - "Конференц-зв’язок" - "Вхідний виклик" - "Вхідний робочий виклик" - "Виклик завершено" - "Очікування" - "Завершення виклику" - "Триває виклик" - "Мій номер: %s" - "Відеодзвінок: з’єднання" - "Відеодзвінок" - "Надсилання запиту на відеодзвінок" - "Не вдалося здійснити відеодзвінок" - "Запрошення на відеодзвінок відхилено" - "Номер для зв’язку:\n%1$s" - "Екстрений номер:\n%1$s" - "Набір номера" - "Пропущений виклик" - "Пропущені виклики" - "Пропущено викликів: %s" - "Пропущений виклик: %s" - "Поточний виклик" - "Поточний виклик на робочий телефон" - "Поточний виклик через Wi-Fi" - "Поточний виклик на робочий телефон через Wi-Fi" - "Очікування" - "Вхідний виклик" - "Вхідний виклик на робочий телефон" - "Вхідний виклик через Wi-Fi" - "Вхідний виклик на робочий телефон через Wi-Fi" - "Вхідний відеодзвінок" - "Цей дзвінок може бути спамом" - "Запит на вхідний відеодзвінок" - "Нові голосові повідомлення" - "Нові голосові повідомлення (%d)" - "Набрати %s" - "Невідомий номер голосової пошти" - "Немає зв’язку" - "Вибрана мережа (%s) недоступна" - "Відповісти" - "Завершити" - "Відео" - "Гол. виклик" - "Прийняти" - "Відхилити" - "Передзвонити" - "Написати SMS" - "Поточний виклик на іншому пристрої" - "Передати виклик" - "Щоб зателефонувати, вимкніть режим польоту." - "Не зареєстровано в мережі." - "Мобільна мережа недоступна." - "Щоб зателефонувати, введіть дійсний номер." - "Не вдається зателефонувати." - "Запуск ряду MMI…" - "Служба не підтримується." - "Неможливо переключитися між викликами." - "Неможливо розділити виклик." - "Неможливо перенести." - "Конференц-зв’язок недоступний." - "Неможливо відхилити виклик." - "Неможливо телефонувати." - "Виклик через протокол SIP" - "Екстрений виклик" - "Увімкнення радіо…" - "Немає зв’язку. Повторна спроба…" - "Не вдається зателефонувати. %s не є екстреним номером." - "Не вдається зателефонувати. Наберіть екстрений номер." - "Використовуйте для набору клавіатуру" - "Призупинити виклик" - "Відновити виклик" - "Завершити виклик" - "Показати цифрову клавіатуру" - "Сховати цифрову клавіатуру" - "Ігнорувати" - "Не ігнорувати" - "Додати виклик" - "Об’єднати виклики" - "Поміняти виклики" - "Керувати викликами" - "Керувати конференц-зв’язком" - "Конференц-зв’язок" - "Керувати" - "Аудіо" - "Відеодзвінок" - "Перейти в режим голосового виклику" - "Вибрати камеру" - "Увімкнути камеру" - "Вимкнути камеру" - "Інші опції" - "Програвач запущено" - "Програвач зупинено" - "Камера неготова" - "Камера готова" - "Невідомий сеанс виклику" - "Служба" - "Налаштування" - "<Не налаштовано>" - "Інші налаштування виклику" - "Виклик здійснюється через оператора %s" - "Вхідні виклики через оператора %s" - "фото контакта" - "приватна розмова" - "вибрати контакт" - "Напишіть власну відповідь…" - "Скасувати" - "Надіслати" - "Відповісти" - "Надіслати SMS" - "Відхилити" - "Відповісти в режимі відеодзвінка" - "Відповісти в режимі аудіодзвінка" - "Прийняти запит на відео" - "Відхилити запит на відео" - "Прийняти запит на передавання відео" - "Відхилити запит на передавання відео" - "Прийняти запит на отримання відео" - "Відхилити запит на отримання відео" - "Проведіть пальцем угору, щоб %s." - "Проведіть пальцем ліворуч, щоб %s." - "Проведіть пальцем праворуч, щоб %s." - "Проведіть пальцем донизу, щоб %s." - "Вібросигнал" - "Вібросигнал" - "Звук" - "Звук за умовчанням (%1$s)" - "Сигнал дзвінка телефона" - "Вібрувати під час виклику" - "Сигнал дзвінка та вібросигнал" - "Керування конференц-зв’язком" - "Екстрений номер" - "Фотографія профілю" - "Камеру вимкнено" - "на номер %s" - "Нотатку надіслано" - "Нещодавні повідомлення" - "Інформація про компанію" - "За %.1f мил." - "За %.1f км" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Відчиняється завтра о %s" - "Відчиняється сьогодні о %s" - "Зачиняється о %s" - "Зачинено сьогодні о %s" - "Відчинено" - "Зачинено" - "Може бути спамом" - "Дзвінок завершено %1$s" - "З цього номера вам телефонують уперше." - "Ми підозрюємо, що телефонував спамер." - "Заблокувати/це спам" - "Додати контакт" - "Не спам" - diff --git a/InCallUI/res/values-ur/strings.xml b/InCallUI/res/values-ur/strings.xml deleted file mode 100644 index 32e0d45aab631b209688bf8debf2002a95818383..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-ur/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "فون" - "ہولڈ پر ہے" - "نامعلوم" - "نجی نمبر" - "پے فون" - "کانفرنس کال" - "کال ختم ہو گئی" - "اسپیکر" - "ہینڈسیٹ ایئرپیس" - "تار والا ہیڈسیٹ" - "بلوٹوتھ" - "درج ذیل ٹونز بھیجیں؟\n" - "ٹونز بھیج رہا ہے\n" - "بھیجیں" - "ہاں" - "نہیں" - "وائلڈ کریکٹر کو اس کے ساتھ بدلیں" - "کانفرنس کال %s" - "صوتی میل نمبر" - "ڈائل ہو رہا ہے" - "دوبارہ ڈائل ہو رہا ہے" - "کانفرنس کال" - "آنے والی کال" - "کام سے متعلق آنے والی کال" - "کال ختم ہوگئی" - "ہولڈ پر ہے" - "کال منقطع ہو رہی ہے" - "کال میں" - "میرا نمبر ہے %s" - "ویڈیو منسلک ہو رہی ہے" - "ویڈیو کال" - "ویڈیو کی درخواست کی جا رہی ہے" - "ویڈیو کال منسلک نہیں ہو سکتی" - "ویڈیو کی درخواست مسترد ہو گئی" - "‏آپ کا کال بیک نمبر‎\n %1$s" - "‏آپ کا ہنگامی کال بیک نمبر‎\n %1$s" - "ڈائل ہو رہا ہے" - "چھوٹی ہوئی کال" - "چھوٹی ہوئی کالیں" - "%s چھوٹی ہوئی کالیں" - "%s کی جانب سے چھوٹی ہوئی کال" - "جاری کال" - "کام سے متعلق جاری کال" - "‏Wi-Fi کال جاری ہے" - "‏کام سے متعلق جاری Wi-Fi کال" - "ہولڈ پر ہے" - "آنے والی کال" - "کام سے متعلق آنے والی کال" - "‏آنے والی Wi-Fi کال" - "‏کام سے متعلق آنے والی Wi-Fi کال" - "آنے والی ویڈیو کال" - "آنے والی مشتبہ سپام کال" - "آنے والی ویڈیو کی درخواست" - "نیا صوتی میل" - "نیا صوتی میل (%d)" - "%s ڈائل کریں" - "صوتی میل نمبر نامعلوم ہے" - "کوئی سروس نہیں ہے" - "منتخب کردہ نیٹ ورک (%s) دستیاب نہیں ہے" - "جواب" - "کال منقطع کریں" - "ویڈیو" - "آواز" - "قبول کریں" - "برخاست کریں" - "واپس کال کریں" - "پیغام" - "ایک اور آلے پر جاری کال" - "کال منتقل کریں" - "کال کرنے کیلئے، پہلے ہوائی جہاز طرز کو آف کریں۔" - "نیٹ ورک پر رجسٹرڈ نہیں ہے۔" - "سیلولر نیٹ ورک دستیاب نہیں ہے۔" - "کال کرنے کیلئے، ایک درست نمبر درج کریں۔" - "کال نہیں ہو سکتی۔" - "‏MMI ترتیب شروع ہو رہی ہے…" - "سروس تعاون یافتہ نہیں ہے۔" - "کالز سوئچ نہیں ہو سکتیں۔" - "کال الگ نہیں ہو سکتی۔" - "منتقل نہیں ہو سکتی۔" - "کانفرنس نہیں ہو سکتی۔" - "کال مسترد نہیں ہو سکتی۔" - "کال(ز) ریلیز نہیں ہو سکتیں۔" - "‏SIP کال" - "ہنگامی کال" - "ریڈیو آن ہو رہا ہے…" - "کوئی سروس نہیں ہے۔ دوبارہ کوشش کی جا رہی ہے…" - "کال نہیں کی جا سکتی۔ %s ایک ہنگامی نمبر نہیں ہے۔" - "کال نہیں کی جا سکتی۔ ایک ہنگامی نمبر ڈائل کریں۔" - "ڈائل کرنے کیلئے کی بورڈ استعمال کریں" - "کال کو ہولڈ کریں" - "کال کو دوبارہ شروع کریں" - "کال ختم کریں" - "ڈائل پیڈ دکھائیں" - "ڈائل پیڈ چھپائیں" - "خاموش کریں" - "آواز چالو کریں" - "کال شامل کریں" - "کالز کو ضم کریں" - "تبادلہ کریں" - "کالز کا نظم کریں" - "کانفرنس کال کا نظم کریں" - "کانفرنس کال" - "نظم کریں" - "آڈیو" - "ویڈیو کال" - "صوتی کال میں تبدیل کریں" - "کیمرا سوئچ کریں" - "کیمرا آن کریں" - "کیمرا آف کریں" - "مزید اختیارات" - "پلیئر شروع ہوگیا" - "پلیئر بند ہوگیا" - "کیمرا تیار نہیں ہے" - "کیمرا تیار ہے" - "نامعلوم کال سیشن ایونٹ" - "سروس" - "ترتیب دیں" - "‏‎<سیٹ نہیں ہے>‎" - "کال کی دیگر ترتیبات" - "کالنگ بذریعہ %s" - "%s کے ذریعے آنے والی" - "رابطہ کی تصویر" - "نجی ہوجائیں" - "رابطہ منتخب کریں" - "اپنا ذاتی تحریر کریں…" - "منسوخ کریں" - "بھیجیں" - "جواب دیں" - "‏SMS بھیجیں" - "مسترد کریں" - "ویڈیو کال کے بطور جواب دیں" - "آڈیو کال کے بطور جواب دیں" - "ویڈیو کی درخواست قبول کریں" - "ویڈیو کی درخواست مسترد کریں" - "ویڈیو منتقل کرنے کی درخواست قبول کریں" - "ویڈیو منتقل کرنے کی درخواست مسترد کریں" - "ویڈیو موصول کرنے کی درخواست قبول کریں" - "ویڈیو موصول کرنے کی درخواست مسترد کریں" - "%s کیلئے اوپر سلائیڈ کریں۔" - "%s کیلئے بائیں سلائیڈ کریں۔" - "%s کیلئے دائیں سلائیڈ کریں۔" - "%s کیلئے نیچے سلائیڈ کریں۔" - "ارتعاش" - "ارتعاش" - "آواز" - "ڈیفالٹ آواز (%1$s)" - "فون رِنگ ٹون" - "رِنگ کے وقت مرتعش کریں" - "رنگ ٹون اور ارتعاش" - "کانفرنس کال کا نظم کریں" - "ہنگامی نمبر" - "پروفائل کی تصویر" - "کیمرا آف ہے" - "بذریعہ %s" - "نوٹ بھیج دیا گیا" - "حالیہ پیغامات" - "کاروباری معلومات" - "%.1f میل دور" - "%.1f کلومیٹر دور" - "%1$s، %2$s" - "%1$s - %2$s" - "%1$s، %2$s" - "کل %s بجے کھلے گا" - "آج %s بجے کھلے گا" - "%s بجے بند ہوگا" - "آج %s بجے بند ہوا" - "ابھی کھلا ہے" - "اب بند ہے" - "مشتبہ سپام کالر" - "‏کال ختم ہو گئی ‎%1$s" - "اس نمبر سے پہلی بار آپ کو کال آئی ہے۔" - "ہمیں شک ہے کہ یہ کال سپامر تھی۔" - "مسدود کریں/سپام کی اطلاع دیں" - "رابطہ شامل کریں" - "سپام نہیں ہے" - diff --git a/InCallUI/res/values-uz/strings.xml b/InCallUI/res/values-uz/strings.xml deleted file mode 100644 index 15d534d25373cbad40ce8c9a6870faeadb1e9f62..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-uz/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Kutilmoqda" - "Noma’lum" - "Maxfiy raqam" - "Taksofon" - "Konferens-aloqa" - "Chaqiruv uzilib qoldi" - "Karnay" - "Telefon quloqchini" - "Simli garnitura" - "Bluetooth" - "Quyidagi tovush signallari yuborilsinmi?\n" - "Tovush signallari yuborilmoqda\n" - "Yuborish" - "Ha" - "Yo‘q" - "Universal belgini bunga almashtirish" - "Konferens-aloqa: %s" - "Ovozli pochta raqami" - "Chaqiruv" - "Qayta terilmoqda" - "Konferens-aloqa" - "Kiruvchi qo‘ng‘iroq" - "Kiruvchi qo‘ng‘iroq (ish)" - "Chaqiruv tugadi" - "Kutilmoqda" - "Suhbat tugatilmoqda" - "Suhbat" - "Mening raqamim – %s" - "Videoga ulanmoqda" - "Video qo‘ng‘iroq" - "Video so‘ralmoqda" - "Video qo‘ng‘iroqqa ulanib bo‘lmadi" - "Video qo‘ng‘iroq so‘rovi rad etildi" - "Teskari qo‘ng‘iroq raqamingiz\n %1$s" - "Favqulodda holatlar uchun teskari qo‘ng‘iroq raqamingiz\n %1$s" - "Chaqiruv" - "Javobsiz qo‘ng‘iroq" - "Javobsiz qo‘ng‘iroqlar" - "%s ta javobsiz qo‘ng‘iroq" - "%s qo‘ng‘irog‘i javobsiz qoldirildi" - "Joriy qo‘ng‘iroq" - "Joriy qo‘ng‘iroq (ish)" - "Joriy Wi-Fi qo‘ng‘iroq" - "Joriy Wi-Fi qo‘ng‘iroq (ish)" - "Kutilmoqda" - "Kiruvchi qo‘ng‘iroq" - "Kiruvchi qo‘ng‘iroq (ish)" - "Kiruvchi Wi-Fi qo‘ng‘iroq" - "Kiruvchi Wi-Fi qo‘ng‘iroq (ish)" - "Kiruvchi video qo‘ng‘iroq" - "Shubhali kiruvchi qo‘ng‘iroq" - "Kiruvchi video qo‘ng‘iroq" - "Yangi ovozli xabar" - "Yangi ovozli xabar (%d)" - "%s raqamini terish" - "Ovozli pochta raqami noma’lum" - "Xizmat mavjud emas" - "Tanlangan tarmoq (%s) mavjud emas" - "Javob berish" - "Tugatish" - "Video aloqa" - "Ovozli aloqa" - "Qabul qilish" - "Rad etish" - "Telefon qilish" - "SMS yuborish" - "Boshqa qurilmada hozir qo‘ng‘iroq amalga oshirilmoqda." - "Qo‘ng‘iroqni o‘tkazish" - "Parvoz rejimini o‘chirib qo‘ying." - "Tarmoqda ro‘yxatdan o‘tmagan." - "Uyali tarmoq mavjud emas." - "Raqam noto‘g‘ri." - "Qo‘ng‘iroq qilib bo‘lmadi." - "MMI tartibi ishga tushmoqda…" - "Xizmat qo‘llab-quvvatlanmaydi." - "Qo‘ng‘iroqlarni almashtirib bo‘lmadi." - "Qo‘ng‘iroqni ajratib bo‘lmadi." - "O‘tkazib bo‘lmadi." - "Konferens-aloqa o‘rnatib bo‘lmadi." - "Qo‘ng‘iroqni rad qilib bo‘lmadi." - "Qo‘ng‘iroq(lar)ni chiqarib bo‘lmadi." - "SIP qo‘ng‘iroq" - "Favqulodda chaqiruv" - "Radio yoqilmoqda…" - "Aloqa yo‘q. Qayta urinilmoqda…" - "Qo‘ng‘iroq qilib bo‘lmadi. %s favqulodda raqam emas." - "Qo‘ng‘iroq qilib bo‘lmadi. Favqulodda raqamga tering." - "Terish uchun klaviaturadan foydalaning" - "Qo‘ng‘iroqni ushlab turish" - "Qo‘ng‘iroqni davom ettirish" - "Chaqiruvni tugatish" - "Raqam terish panelini ochish" - "Raqam terish panelini yopish" - "Ovozni o‘chirish" - "Ovozni yoqish" - "Chaqiruv qo‘shish" - "Qo‘ng‘iroqlarni birlashtirish" - "Almashtirish" - "Qo‘ng‘iroqlarni boshqarish" - "Konferens-aloqani sozlash" - "Konferens-aloqa" - "Boshqarish" - "Audio" - "Video qo‘ng‘iroq" - "Ovozli qo‘ng‘iroqqa o‘zgartirish" - "Kamerani almashtirish" - "Kamerani yoqish" - "Kamerani o‘chirish" - "Boshqa sozlamalar" - "Pleyer ishga tushirildi" - "Pleyer to‘xtatildi" - "Kamera tayyor emas" - "Kamera tayyor" - "Aloqa seansining noma’lum hodisasi" - "Xizmat" - "Sozlash" - "<Ko‘rsatilmagan>" - "Boshqa qo‘ng‘iroq sozlamalari" - "%s orqali qo‘ng‘rioq qilinmoqda" - "%s orqali kiruvchi qo‘ng‘iroqlar" - "kontakt rasmi" - "alohida suhbatga o‘tish" - "kontaktni tanlash" - "O‘z javobingizni yozing…" - "Bekor qilish" - "Yuborish" - "Javob berish" - "SMS yuborish" - "Rad etish" - "Video qo‘ng‘iroqqa javob berish" - "Ovozli qo‘ng‘iroqqa javob berish" - "Video qo‘ng‘iroq so‘rovini qabul qilish" - "Video qo‘ng‘iroq so‘rovini rad etish" - "Video uzatishga ruxsat berish" - "Video uzatishga ruxsat bermaslik" - "Kiruvchi video qo‘ng‘iroqni qabul qilish" - "Kiruvchi video qo‘ng‘iroqni rad etish" - "%s uchun tepaga suring." - "%s uchun chapga suring." - "%s uchun o‘ngga suring." - "%s uchun pastga suring." - "Tebranish" - "Tebranish" - "Ovoz" - "Standart ovoz (%1$s)" - "Telefon ringtoni" - "Jiringlash vaqtida tebranish" - "Qo‘ng‘iroq ohangi va tebranish" - "Konferens-aloqani sozlash" - "Favqulodda qo‘ng‘iroq raqami" - "Profil rasmi" - "Kamera o‘chiq" - "%s orqali" - "Xabar yuborildi" - "So‘nggi xabarlar" - "Kompaniya haqida ma’lumot" - "%.1f mil masofada" - "%.1f km masofada" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Ertaga %s da ochiladi" - "Bugun %s da ochiladi" - "%s da yopiladi" - "Bugun %s da yopiladi" - "Ochiq" - "Yopiq" - "Shubhali abonent" - "Qo‘ng‘iroq yakunlandi (%1$s)" - "Sizga bu raqamdan birinchi marta qo‘ng‘iroq qilishdi." - "Bu spam-qo‘ng‘iroqqa o‘xshayapti." - "Bloklash/spamga" - "Kontaktni qo‘shish" - "Spam emas" - diff --git a/InCallUI/res/values-vi/strings.xml b/InCallUI/res/values-vi/strings.xml deleted file mode 100644 index 58a50c278208b8914a6199d63281f970c8df2102..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-vi/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Điện thoại" - "Đang chờ" - "Không xác định" - "Số cá nhân" - "Điện thoại trả tiền" - "Cuộc gọi nhiều bên" - "Cuộc gọi bị gián đoạn" - "Loa" - "Tai nghe ĐTDĐ" - "Tai nghe có dây" - "Bluetooth" - "Gửi các âm sau?\n" - "Đang gửi âm\n" - "Gửi" - "Có" - "Không" - "Thay thế ký tự tự do bằng" - "Cuộc gọi nhiều bên %s" - "Số thư thoại" - "Đang gọi" - "Đang quay số lại" - "Cuộc gọi nhiều bên" - "Cuộc gọi đến" - "Cuộc gọi đến về công việc" - "Cuộc gọi đã kết thúc" - "Đang chờ" - "Kết thúc cuộc gọi" - "Trong cuộc gọi" - "Số điện thoại của tôi là %s" - "Đang kết nối video" - "Cuộc gọi điện video" - "Đang yêu cầu video" - "Không kết nối được cuộc gọi điện video" - "Đã từ chối yêu cầu video" - "Số gọi lại của bạn\n %1$s" - "Số gọi lại khẩn cấp của bạn\n %1$s" - "Đang gọi" - "Cuộc gọi nhỡ" - "Cuộc gọi nhỡ" - "%s cuộc gọi nhỡ" - "Cuộc gọi nhỡ từ %s" - "Cuộc gọi đang thực hiện" - "Cuộc gọi đang diễn ra về công việc" - "Cuộc gọi đang diễn ra qua Wi-Fi" - "Cuộc gọi đang diễn ra qua Wi-Fi về công việc" - "Đang chờ" - "Cuộc gọi đến" - "Cuộc gọi đến về công việc" - "Cuộc gọi đến qua Wi-Fi" - "Cuộc gọi đến qua Wi-Fi về công việc" - "Cuộc gọi điện video đến" - "Cuộc gọi spam đến bị nghi ngờ" - "Yêu cầu video đến" - "Thư thoại mới" - "Thư thoại mới (%d)" - "Quay số %s" - "Số thư thoại không xác định" - "Không có dịch vụ" - "Mạng được chọn (%s) không khả dụng" - "Trả lời" - "Gác máy" - "Video" - "Thoại" - "Chấp nhận" - "Loại bỏ" - "Gọi lại" - "Tin nhắn" - "Cuộc gọi đang diễn ra trên một thiết bị khác" - "Chuyển cuộc gọi" - "Để thực hiện cuộc gọi, trước tiên, hãy tắt chế độ trên Máy bay." - "Chưa được đăng ký trên mạng." - "Không có mạng di động." - "Để thực hiện cuộc gọi, hãy nhập một số hợp lệ." - "Không thực hiện được cuộc gọi." - "Đang khởi động chuỗi MMI…" - "Dịch vụ không được hỗ trợ." - "Không chuyển đổi được cuộc gọi." - "Không tách được cuộc gọi." - "Không chuyển được cuộc gọi." - "Không thực hiện được cuộc gọi nhiều bên." - "Không từ chối được cuộc gọi." - "Không thực hiện được cuộc gọi." - "Cuộc gọi qua SIP" - "Cuộc gọi khẩn cấp" - "Đang bật radio..." - "Không có dịch vụ nào. Đang thử lại…" - "Không thực hiện được cuộc gọi. %s không phải là số khẩn cấp." - "Không thực hiện được cuộc gọi. Hãy quay số khẩn cấp." - "Sử dụng bàn phím để quay số" - "Giữ cuộc gọi" - "Tiếp tục cuộc gọi" - "Kết thúc cuộc gọi" - "Hiển thị bàn phím số" - "Ẩn bàn phím số" - "Tắt tiếng" - "Bật tiếng" - "Thêm cuộc gọi" - "Hợp nhất cuộc gọi" - "Hoán đổi" - "Quản lý cuộc gọi" - "Quản lý cuộc gọi nhiều bên" - "Cuộc gọi nhiều bên" - "Quản lý" - "Âm thanh" - "Cuộc gọi điện video" - "Thay đổi thành cuộc gọi thoại" - "Chuyển máy ảnh" - "Bật máy ảnh" - "Tắt máy ảnh" - "Tùy chọn khác" - "Đã khởi động trình phát" - "Đã dừng trình phát" - "Máy ảnh chưa sẵn sàng" - "Máy ảnh đã sẵn sàng" - "Sự kiện phiên cuộc gọi không xác định" - "Dịch vụ" - "Thiết lập" - "<Chưa được đặt>" - "Cài đặt cuộc gọi khác" - "Gọi điện qua %s" - "Cuộc gọi đến qua %s" - "ảnh liên hệ" - "chuyển thành riêng tư" - "chọn địa chỉ liên hệ" - "Viết trả lời của riêng bạn..." - "Hủy" - "Gửi" - "Trả lời" - "Gửi SMS" - "Từ chối" - "Trả lời là cuộc gọi điện video" - "Trả lời là cuộc gọi âm thanh" - "Chấp nhận yêu cầu cuộc gọi video" - "Từ chối yêu cầu cuộc gọi video" - "Chấp nhận yêu cầu truyền video" - "Từ chối yêu cầu truyền video" - "Chấp nhận yêu cầu nhận video" - "Từ chối yêu cầu nhận video" - "Trượt lên để %s." - "Trượt sang trái để %s." - "Trượt sang phải để %s." - "Trượt xuống để %s." - "Rung" - "Rung" - "Âm thanh" - "Âm thanh mặc định (%1$s)" - "Nhạc chuông điện thoại" - "Rung khi đổ chuông" - "Nhạc chuông và rung" - "Quản lý cuộc gọi nhiều bên" - "Số khẩn cấp" - "Ảnh hồ sơ" - "Tắt máy ảnh" - "qua %s" - "Đã gửi ghi chú" - "Tin nhắn gần đây" - "Thông tin doanh nghiệp" - "Cách %.1f dặm" - "Cách %.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Mở cửa lúc %s ngày mai" - "Mở cửa lúc %s hôm nay" - "Đóng cửa lúc %s" - "Đã đóng cửa lúc %s hôm nay" - "Mở ngay bây giờ" - "Hiện đã đóng cửa" - "Người gọi spam bị nghi ngờ" - "Đã kết thúc cuộc gọi %1$s" - "Đây là lần đầu tiên số điện thoại này gọi điện cho bạn." - "Chúng tôi đã nghi ngờ cuộc gọi này là người gửi spam." - "Chặn/báo cáo spam" - "Thêm liên hệ" - "Không phải là spam" - diff --git a/InCallUI/res/values-w500dp-land/colors.xml b/InCallUI/res/values-w500dp-land/colors.xml deleted file mode 100644 index 77eea2e6804f342bf4923766947f26ba321d4cef..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-w500dp-land/colors.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - #000000 - diff --git a/InCallUI/res/values-w500dp-land/dimens.xml b/InCallUI/res/values-w500dp-land/dimens.xml deleted file mode 100644 index 112ec5f09b1fc8480a771095a9d4cfb2723054b9..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-w500dp-land/dimens.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - true - - - true - - - 40dp - - - 30dp - - 2dp - - 20dp - diff --git a/InCallUI/res/values-zh-rCN/strings.xml b/InCallUI/res/values-zh-rCN/strings.xml deleted file mode 100644 index f9c43764d78aa3c32929ca679601fb61fdf53aaf..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "电话" - "保持" - "未知" - "私密号码" - "公用电话" - "电话会议" - "通话中断" - "扬声器" - "手机听筒" - "有线耳机" - "蓝牙" - "发送以下信号音?\n" - "正在发送信号音\n" - "发送" - "是" - "否" - "将通配符替换为" - "电话会议(%s)" - "语音信箱号码" - "正在拨号" - "正在重拨" - "电话会议" - "来电" - "工作来电" - "通话已结束" - "保持" - "正在挂断" - "正在通话" - "我的电话号码:%s" - "正在接通视频" - "视频通话" - "正在发出视频请求" - "无法接通视频通话" - "视频请求遭拒" - "您的回拨号码如下:\n%1$s" - "您的紧急回拨号码如下:\n%1$s" - "正在拨号" - "未接电话" - "未接电话" - "%s 个未接电话" - "来自%s的未接电话" - "通话进行中" - "工作通话进行中" - "WLAN 通话进行中" - "WLAN 工作通话进行中" - "保持" - "来电" - "工作来电" - "WLAN 来电" - "WLAN 工作来电" - "视频通话来电" - "有疑似骚扰来电" - "收到视频通话请求" - "新语音邮件" - "新语音邮件 (%d)" - "拨打 %s" - "语音信箱号码未知" - "没有服务" - "所选网络(%s)不可用" - "接听" - "挂断" - "视频" - "语音" - "接受" - "拒绝" - "回电" - "发短信" - "其他设备上有正在进行的通话" - "转接通话" - "要拨打电话,请先关闭飞行模式。" - "尚未注册网络。" - "无法连接到移动网络。" - "要拨打电话,请输入有效的电话号码。" - "无法拨打该电话。" - "正在启动 MMI 序列…" - "服务不受支持。" - "无法切换通话。" - "无法单独通话。" - "无法转移呼叫。" - "无法进行电话会议。" - "无法拒接来电。" - "无法挂断。" - "SIP 通话" - "紧急呼救" - "正在开启无线装置…" - "找不到服务信号,正在重试…" - "无法拨打该电话。%s 不是紧急呼救号码。" - "无法拨打该电话。请拨打紧急呼救号码。" - "使用键盘拨号" - "保持通话" - "恢复通话" - "结束通话" - "显示拨号键盘" - "隐藏拨号键盘" - "静音" - "取消静音" - "添加通话" - "合并通话" - "切换" - "管理通话" - "管理电话会议" - "电话会议" - "管理" - "音频" - "视频通话" - "改为语音通话" - "切换摄像头" - "开启摄像头" - "关闭摄像头" - "更多选项" - "播放器已启动" - "播放器已停止" - "摄像头尚未准备就绪" - "摄像头已准备就绪" - "未知通话事件" - "服务" - "设置" - "<未设置>" - "其他通话设置" - "正在通过%s进行通话" - "有人通过%s来电" - "联系人照片" - "单独通话" - "选择联系人" - "自行撰写回复…" - "取消" - "发送" - "接听" - "发送短信" - "拒绝" - "以视频通话的形式接听" - "以音频通话的形式接听" - "接受视频请求" - "拒绝视频请求" - "接受视频传输请求" - "拒绝视频传输请求" - "接受视频接收请求" - "拒绝视频接收请求" - "向上滑动即可%s。" - "向左滑动即可%s。" - "向右滑动即可%s。" - "向下滑动即可%s。" - "振动" - "振动" - "提示音" - "默认提示音(%1$s)" - "手机铃声" - "响铃时振动" - "铃声和振动" - "管理电话会议" - "紧急呼救号码" - "个人资料照片" - "摄像头已关闭" - "通过 %s" - "已发送备注" - "最近的信息" - "商家信息" - "%.1f 英里远" - "%.1f 公里远" - "%2$s%1$s" - "%1$s - %2$s" - "%1$s%2$s" - "将于明天%s开始营业" - "将于今天%s开始营业" - "将于%s结束营业" - "已于今天%s结束营业" - "营业中" - "现已结束营业" - "疑似骚扰电话号码" - "通话已结束 %1$s" - "这是此号码的第一次来电。" - "我们怀疑这是骚扰电话。" - "屏蔽/举报骚扰电话号码" - "添加联系人" - "非骚扰电话号码" - diff --git a/InCallUI/res/values-zh-rHK/strings.xml b/InCallUI/res/values-zh-rHK/strings.xml deleted file mode 100644 index bf6f016cba9872e427589aba854a93c9b063fe14..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-zh-rHK/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "電話" - "保留通話" - "不明" - "私人號碼" - "公共電話" - "會議通話" - "通話已中斷" - "喇叭" - "免提聽筒" - "有線耳機" - "藍牙" - "要傳送以下訊號音嗎?\n" - "正在傳送訊號音\n" - "傳送" - "是" - "否" - "將萬用字元改為" - "會議通話:%s" - "留言號碼" - "正在撥號" - "正在重撥" - "會議通話" - "來電" - "工作來電" - "通話已結束" - "保留通話" - "正在掛斷電話" - "正在通話" - "我的電話號碼:%s" - "正在建立視像通話連線" - "視像通話" - "正在提出視像通話要求" - "無法建立視像通話連線" - "視像通話要求被拒" - "您的回撥號碼:\n%1$s" - "您的緊急回撥號碼:\n%1$s" - "正在撥號" - "未接來電" - "未接來電" - "%s 個未接來電" - "來自 %s 的未接來電" - "通話中" - "正在進行工作通話" - "正在進行 Wi-Fi 通話" - "正在進行 Wi-Fi 工作通話" - "保留通話" - "來電" - "工作來電" - "Wi-Fi 來電" - "Wi-Fi 工作來電" - "視像通話來電" - "疑似收到垃圾來電" - "收到視像通話要求" - "新留言" - "新留言 (%d 個)" - "撥打 %s" - "留言號碼不明" - "沒有服務" - "您選取的網絡 (%s) 無法使用" - "接聽" - "掛斷電話" - "視像通話" - "語音通話" - "接受" - "拒絕" - "回電" - "短訊" - "其他裝置上有正在進行的通話" - "轉接來電" - "如要撥打電話,請先關閉飛行模式。" - "未在網絡上註冊。" - "無法連線至流動網絡。" - "如要撥打電話,請輸入有效的號碼。" - "無法通話。" - "開始 MMI 序列…" - "不支援此服務。" - "無法切換通話。" - "無法分開通話。" - "無法轉接。" - "無法進行會議通話。" - "無法拒接來電。" - "無法結束通話。" - "SIP 通話" - "緊急電話" - "正在開啟無線電…" - "找不到服務,請再試一次…" - "無法通話。%s 不是緊急電話號碼。" - "無法通話。請撥打緊急電話號碼。" - "使用鍵盤撥號" - "保留通話" - "恢復通話" - "結束通話" - "顯示撥號鍵盤" - "隱藏撥號鍵盤" - "略過" - "取消靜音" - "新增通話" - "合併通話" - "切換" - "管理通話" - "管理會議通話" - "會議通話" - "管理" - "音訊" - "視像通話" - "變更為語音通話" - "切換鏡頭" - "開啟攝影機" - "關閉攝影機" - "更多選項" - "已啟動播放器" - "已停止播放器" - "相機未準備好" - "相機已準備就緒" - "不明的通話工作階段活動" - "服務" - "設定" - "<未設定>" - "其他通話設定" - "正在透過 %s 撥號" - "有人透過 %s 來電" - "聯絡人相片" - "私人通話" - "選取聯絡人" - "自行撰寫回覆..." - "取消" - "傳送" - "接聽" - "傳送短訊" - "拒絕" - "接聽視像通話" - "接聽語音通話" - "接受視像通話要求" - "拒絕視像通話要求" - "接受視像通話轉駁要求" - "拒絕視像通話轉駁要求" - "接受視像接收要求" - "拒絕視像接收要求" - "向上滑動即可%s。" - "向左滑動即可%s。" - "向右滑動即可%s。" - "向下滑動即可%s。" - "震動" - "震動" - "音效" - "預設音效 (%1$s)" - "手機鈴聲" - "響鈴時震動" - "鈴聲和震動" - "管理會議通話" - "緊急電話號碼" - "個人檔案相片" - "相機已關閉" - "透過 %s" - "已傳送筆記" - "最近的訊息" - "公司資料" - "%.1f 英里外" - "%.1f 公里外" - "%2$s%1$s" - "%1$s - %2$s" - "%1$s%2$s" - "將於明天%s開始營業" - "將於今天%s開始營業" - "將於%s關門" - "已於今天%s關門" - "營業中" - "目前休息" - "疑似垃圾來電者" - "通話結束 %1$s" - "這是此號碼的第一次來電。" - "我們懷疑此來電為垃圾來電。" - "封鎖/舉報為垃圾來電" - "新增聯絡人" - "非垃圾來電" - diff --git a/InCallUI/res/values-zh-rTW/strings.xml b/InCallUI/res/values-zh-rTW/strings.xml deleted file mode 100644 index e316c7d40c8ab81e92a7c17aa02a22536578c238..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "電話" - "保留" - "不明" - "私人號碼" - "公用電話" - "電話會議" - "通話已中斷" - "喇叭" - "手機聽筒" - "有線耳機" - "藍牙" - "要傳送以下的信號音嗎?\n" - "正在傳送信號音\n" - "傳送" - "是" - "否" - "將萬用字元替換為" - "電話會議 %s" - "語音留言號碼" - "撥號中" - "重撥中" - "電話會議" - "來電" - "公司來電" - "通話已結束" - "保留" - "掛斷中" - "通話中" - "我的電話號碼:%s" - "正在建立視訊通話連線" - "視訊通話" - "正在提出視訊通話要求" - "無法建立視訊通話連線" - "視訊通話要求遭拒" - "您的回撥號碼如下\n %1$s" - "您的緊急回撥號碼如下\n %1$s" - "撥號中" - "未接來電" - "未接來電" - "%s 通未接來電" - "來自 %s 的未接來電" - "進行中的通話" - "進行中的公司通話" - "進行中的通話 (透過 Wi-Fi)" - "進行中的公司通話 (透過 Wi-Fi)" - "保留" - "來電" - "公司來電" - "來電 (透過 Wi-Fi)" - "公司來電 (透過 Wi-Fi)" - "視訊通話來電" - "可疑的騷擾/廣告來電" - "收到視訊通話要求" - "新的語音留言" - "新的語音留言 (%d)" - "撥打 %s" - "語音留言號碼不明" - "沒有服務" - "您所選取的網路 (%s) 無法使用" - "接聽" - "掛斷" - "視訊通話" - "語音通話" - "接受" - "拒絕" - "回撥" - "傳送簡訊" - "其他裝置上有進行中的通話" - "轉接來電" - "撥號前,請先關閉飛航模式。" - "尚未註冊網路。" - "無法連線到行動網路。" - "如要撥打電話,請輸入有效的號碼。" - "無法通話。" - "開始 MMI 序列…" - "不支援的服務。" - "無法切換通話。" - "無法分割通話。" - "無法轉接。" - "無法進行電話會議。" - "無法拒接來電。" - "無法掛斷電話。" - "SIP 通話" - "緊急電話" - "正在開啟無線通訊…" - "找不到服務訊號,重試中…" - "無法通話。%s 不是緊急電話號碼。" - "無法通話。只能撥打緊急號碼。" - "使用鍵盤撥號" - "保留通話" - "恢復通話" - "結束通話" - "顯示撥號鍵盤" - "隱藏撥號鍵盤" - "忽略" - "取消忽略" - "新增通話" - "合併通話" - "切換" - "管理通話" - "管理電話會議" - "電話會議" - "管理" - "音訊" - "視訊通話" - "變更為語音通話" - "切換鏡頭" - "開啟攝影機" - "關閉攝影機" - "更多選項" - "已啟動播放器" - "已停止播放器" - "相機尚未就緒" - "相機已準備就緒" - "不明的通話工作階段事件" - "服務" - "設定" - "<未設定>" - "其他通話設定" - "正在透過 %s 撥號" - "有人透過 %s 來電" - "聯絡人相片" - "私人通話" - "選取聯絡人" - "自行撰寫回應…" - "取消" - "傳送" - "接聽" - "傳送簡訊" - "拒絕" - "接聽視訊通話" - "接聽語音通話" - "接受視訊通話要求" - "拒絕視訊通話要求" - "接受視訊傳送要求" - "拒絕視訊傳送要求" - "接受視訊接收要求" - "拒絕視訊接收要求" - "向上滑動即可%s。" - "向左滑動即可%s。" - "向右滑動即可%s。" - "向下滑動即可%s。" - "震動" - "震動" - "音效" - "預設音效 (%1$s)" - "手機鈴聲" - "鈴響時震動" - "鈴聲與震動" - "管理電話會議" - "緊急電話號碼" - "個人資料相片" - "相機已停用" - "透過 %s" - "備註已送出" - "最近的訊息" - "商家資訊" - "%.1f 英里遠" - "%.1f 公里遠" - "%2$s%1$s" - "%1$s - %2$s" - "%1$s%2$s" - "將於明日%s開始營業" - "將於本日%s開始營業" - "將於%s結束營業" - "已於本日%s結束營業" - "營業中" - "本日已結束營業" - "可疑的騷擾/廣告來電者" - "通話結束 %1$s" - "這組號碼首次致電給您。" - "我們懷疑這通來電是騷擾/廣告電話。" - "封鎖/回報為騷擾/廣告電話" - "新增聯絡人" - "非騷擾/廣告電話" - diff --git a/InCallUI/res/values-zu/strings.xml b/InCallUI/res/values-zu/strings.xml deleted file mode 100644 index 46bf5afbb38b6b382f89bc7dc54bdeb9a8412d49..0000000000000000000000000000000000000000 --- a/InCallUI/res/values-zu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Ifoni" - "Ibanjiwe" - "Akwaziwa" - "Inombolo eyimfihlo" - "Ucingo olufakwa imali" - "Ikholi yenkomfa" - "Ikholi ivaliwe" - "Isipikha" - "Isipikha sendlebe sama-ear phone" - "Ama-earphone anezintambo" - "I-Bluetooth" - "Thumela amathoni alandelayo?\n" - "Ithumela amathoni\n" - "Thumela" - "Yebo" - "Cha" - "Miselela uhlamvu lwasendle nge" - "Ikholi yenkomfa engu-%s" - "Inombolo yevoyisimeyili" - "Iyadayela" - "Iphinda iyadayela" - "Ikholi yenkomfa" - "Ikholi engenayo" - "Ikholi engenayo yomsebenzi" - "Ikholi iqediwe" - "Ibanjiwe" - "Iyavala" - "Ukukholi" - "Inombolo yami ngu-%s" - "Ixhuma ividiyo" - "Ikholi yevidiyo" - "Icela ividiyo" - "Ayikwazi ukuxhuma ikholi yevidiyo" - "Isicelo sevidiyo sinqatshelwe" - "Inombolo yakho yokuphinda ushaye\n%1$s" - "Inombolo yakho yokuphinda ushayelwe yesimo esiphuthumayo\n%1$s" - "Iyadayela" - "Ikholi ephuthelwe" - "Amakholi akuphuthele" - "%s amakholi akulahlekele" - "Uphuthelwe ikholi kusukela ku-%s" - "Ikholi eqhubekayo" - "Ikholi yomsebenzi eqhubekayo" - "Ikholi ye-Wi-Fi eqhubekayo" - "Ikholi yomsebenzi eqhubekayo ye-Wi-Fi" - "Ibanjiwe" - "Ikholi engenayo" - "Ikholi engenayo yomsebenzi" - "Ikholi ye-Wi-Fi engenayo" - "Ikholi engenayo yomsebenzi ye-Wi-Fi" - "Ikholi yevidiyo engenayo" - "Ikholi engenayo osolisayo kagaxekile" - "Isicelo sevidiyo engenayo" - "Ivoyisimeyili entsha" - "Ivoyisimeyili entsha (%d)" - "Dayela u-%s" - "Inombolo yevoyisimeyili ayaziwa" - "Ayikho isevisi" - "Inethiwekhi ekhethiwe (%s) ayitholakali" - "Phendula" - "Vala ikholi" - "Ividiyo" - "Izwi" - "Yamukela" - "Cashisa" - "Phinda ushayele" - "Umlayezo" - "Ikholi eqhubekayo kwenye idivayisi" - "Dlulisela ikholi" - "Ukwenza ikholi, vala kuqala imodi Yendiza." - "Ayibhalisiwe kwinethiwekhi." - "Inethiwekhi yeselula ayitholakali." - "Ukuze wenze ikholi, faka inombolo evumelekile." - "Ayikwazi ukushaya." - "Iqalisa ukulandelana kwe-MMI..." - "Isevisi ayisekelwe." - "Ayikwazi ukushintsha amakholi." - "Ayikwazi ukuhlukanisa ikholi." - "Ayikwazi ukudlulisela." - "Ayikwazi ukwenza inkomfa." - "Ayikwazi ukunqabela ikholi." - "Ayikwazi ukukhipha amakholi." - "Ikholi ye-SIP" - "Ikholi ephuthumayo" - "Ivula irediyo..." - "Ayikho isevisi. Iyazama futhi…" - "Ayikwazi ukushaya. U-%s akuyona inombolo yesimo esiphuthumayo." - "Ayikwazi ukushaya. Shayela inombolo yesimo esiphuthumayo." - "Sebenzisa ikhibhodi ukudayela" - "Bamba ikholi" - "Qalisa kabusha ikholi" - "Qeda ikholi" - "Bonisa iphedi yokudayela" - "Fihla iphedi yokudayela" - "Thulisa" - "Susa ukuthula" - "Engeza ikholi" - "Hlanganisa amakholi" - "Shintsha" - "Phatha amakholi" - "Phatha ucingo lwengqungquthela" - "Ikholi yenkomfa" - "Phatha" - "Umsindo" - "Ikholi yevidiyo" - "Shintshela kukholi yezwi" - "Shintsha Ikhamera" - "Vula ikhamera" - "Vala ikhamera" - "Izinketho eziningi" - "Umdlali uqalile" - "Umdlali umisiwe" - "Ikhamera ayilungile" - "Ikhamera ilungile" - "Umcimbi wesikhathi sekholi ongaziwa" - "Isevisi" - "Ukusetha" - "<Ayisethiwe>" - "Ezinye izilungiselelo zekholi" - "Ishaya nge-%s" - "Ingena nge-%s" - "isithombe soxhumana naye" - "yenza kube imfihlo" - "khetha othintana naye" - "Bhala okwakho…" - "Khansela" - "Thumela" - "Phendula" - "Thumela i-SMS" - "Yenqaba" - "Phendula njengekholi yevidiyo" - "Phendula njengekholi yomsindo" - "Yamukela isicelo sevidiyo" - "Yenqaba isicelo sevidiyo" - "Yamukela isicelo sokudlulisa ividiyo" - "Yenqaba isicelo sokudlulisa ividiyo" - "Yamukela isicelo sokwamukela ividiyo" - "Yenqaba isicelo sokwamukela ividiyo" - "Slayidela phezulu ku-%s." - "Slayida ngakwesokunxele ku-%s." - "Slayida ngakwesokudla ku-%s." - "Slayida ngezansi ku-%s." - "Dlidlizela" - "Dlidlizela" - "Umsindo" - "Umsindo ozenzakalelayo (%1$s)" - "Ithoni yokukhala yefoni" - "Dlidlizisa uma ikhala" - "Ithoni yokukhala nokudlidliza" - "Phatha ucingo lwengqungquthela" - "Inombolo ephuthumayo" - "Isithombe sephrofayela" - "Ikhamera ivaliwe" - "nge-%s" - "Inothi lithunyelwe" - "Imilayezo yakamuva" - "Ulwazi lwebhizinisi" - "%.1f amamitha kude" - "%.1f amakhilomitha kude" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Kuvulwa kusasa ngo-%s" - "Kuvulwa namuhla ngo-%s" - "Kuvalwa ngo-%s" - "Kuvalwe namuhla ngo-%s" - "Kuvuliwe manje" - "Kuvaliwe manje" - "Ofonayo osolisayo wogaxekile" - "Ikholi iphelile %1$s" - "Lesi isikhathi sokuqala le nombolo ikushayela." - "Sisolele le kholi ukuthi ugaxekile." - "Vimba/bika ugaxekile" - "Engeza oxhumana naye" - "Akusiko okugaxekile" - diff --git a/InCallUI/res/values/animation_constants.xml b/InCallUI/res/values/animation_constants.xml deleted file mode 100644 index 8df6a7281bc385f340a48dc12eb2cd51b890170c..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/animation_constants.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - 333 - 333 - 257 - diff --git a/InCallUI/res/values/array.xml b/InCallUI/res/values/array.xml deleted file mode 100644 index 7877ec8f3517fa03f6a0136e60249b9e6f4f15cd..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/array.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - @drawable/ic_lockscreen_answer - @null - @drawable/ic_lockscreen_decline - @null" - - - @string/description_target_answer - @null - @string/description_target_decline - @null" - - - @string/description_direction_right - @null - @string/description_direction_left - @null - - - - - @drawable/ic_lockscreen_answer - @drawable/ic_lockscreen_text - @drawable/ic_lockscreen_decline - @null" - - - @string/description_target_answer - @string/description_target_send_sms - @string/description_target_decline - @null" - - - @string/description_direction_right - @string/description_direction_up - @string/description_direction_left - @null - - - - - @drawable/ic_lockscreen_answer - @null - @drawable/ic_lockscreen_decline - @drawable/ic_lockscreen_answer_video - - - @string/description_target_answer_video_call - @null - @string/description_target_decline - @string/description_target_answer_audio_call - - - @string/description_direction_right - @null - @string/description_direction_left - @string/description_direction_down - - - - - @drawable/ic_lockscreen_answer_video - @drawable/ic_lockscreen_text - @drawable/ic_lockscreen_decline - @drawable/ic_lockscreen_answer - - - @string/description_target_answer_video_call - @string/description_target_send_sms - @string/description_target_decline - @string/description_target_answer_audio_call - - - @string/description_direction_right - @string/description_direction_up - @string/description_direction_left - @string/description_direction_down - - - - - @drawable/ic_lockscreen_answer_video - @drawable/ic_lockscreen_decline_video - - - - @string/description_target_accept_upgrade_to_video_request - @null - @string/description_target_decline_upgrade_to_video_request - @null" - - - @string/description_direction_right - @null - @string/description_direction_left - @null - - diff --git a/InCallUI/res/values/attrs.xml b/InCallUI/res/values/attrs.xml deleted file mode 100644 index e135fb72d02655b0bd1f5b0a77336f1a232f6395..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/attrs.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/values/colors.xml b/InCallUI/res/values/colors.xml deleted file mode 100644 index 238d360335c2d84f259c3dd99451e0808e37a79d..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/colors.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - @color/dialer_theme_color - #ffffff - - - @color/incall_background_color - #ffffff - - #ffffff - #f5f5f5 - #333333 - - @color/incall_background_color - @color/incall_call_banner_text_color - - #545454 - - - #cc000000 - - #f8f8f8 - #4d4d4d - #999999 - - #999999 - #ffffff - - #dddddd - - - #333 - - #ffffff - #ccaaaaaa - - @color/incall_background_color - @color/dialer_theme_color_dark - - #b3ffffff - - #33ffffff - - - @color/dialer_theme_color - - @color/dialer_theme_color - - #33999999 - - - #b3000000 - - #26ffffff - #ffffff - #ffffff - #cccccc - #00c853 - #ff1744 - #a3a3a3 - #ffffff - - - #B2FFFFFF - - - #330288d1 - - - - #00796B - #3367D6 - #303F9F - #7B1FA2 - #C2185B - #C53929 - #A52714 - - - - - #00695C - #2A56C6 - #283593 - #6A1B9A - #AD1457 - #B93221 - #841F10 - - - - #A52714 - - - #40000000 - - @color/incall_call_banner_subtext_color - @color/dialer_theme_color - @color/incall_call_banner_subtext_color - @color/incall_call_banner_subtext_color - @color/incall_call_banner_subtext_color - - - #ffffff - - - #919191 - diff --git a/InCallUI/res/values/config.xml b/InCallUI/res/values/config.xml deleted file mode 100644 index b81ba3ca03c0383947733579fc257d2890536e79..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/config.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - 5 - - - true - - 5000 - \ No newline at end of file diff --git a/InCallUI/res/values/dimens.xml b/InCallUI/res/values/dimens.xml deleted file mode 100644 index 59da7860a00803a7ffe2a5b6323766122c3406cd..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/dimens.xml +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - false - - - false - - - - 0dp - 20dp - - 3dp - - - 2dp - - - 24dp - - 16dp - 24dp - 32dp - 16sp - - - 8dp - - - 16sp - 12sp - 34dp - 28sp - 16sp - - 50sp - - - 48dp - - 0dp - 2dp - - - 1dp - - 0dp - 20sp - 50dp - 36dp - - -10dp - - 8dp - 6dp - - - 10dp - 5dp - 10dp - - - 64dp - - - 56dp - - - 250dp - - 125dp - - - 70dip - - - 40dip - - - 15dip - - -48dip - 0dip - - 2dp - - 2dp - - 50dp - - - 90dp - - 0dp - - 72dp - 56dp - - 64dp - 46dp - - 14sp - 19dp - 23dp - 13dp - 13dp - - 30dp - 16sp - 7dp - 12dp - 15dp - 2dp - 7dp - 14sp - - 10dp - 25dp - 20dp - 16sp - 12sp - - 40dp - diff --git a/InCallUI/res/values/ids.xml b/InCallUI/res/values/ids.xml deleted file mode 100644 index accb8fb73c1c431603f42218dadbd8c05cbe50cf..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/ids.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - diff --git a/InCallUI/res/values/strings.xml b/InCallUI/res/values/strings.xml deleted file mode 100644 index 92de14042aaf998751e1568928078f4df50c29f3..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/strings.xml +++ /dev/null @@ -1,540 +0,0 @@ - - - - - - - Phone - - - InCallUI - - - - - On hold - - - Unknown - - Private number - - Payphone - - - Conference call - - Call dropped - - - - - - Speaker - - Handset earpiece - - Wired headset - - Bluetooth - - - - Send the following tones?\n - - Sending tones\n - - Send - - Yes - - No - - Replace wild character with - - - Conference call %s - - - (650) 555-1234 - - Incoming phone number - - Fake Incoming Call - - - Voicemail number - - - - Dialing - - Redialing - - Conference call - - Incoming call - - Incoming work call - - Call ended - - On hold - - Hanging up - - In call - - My number is %s - - Connecting video - - Video call - - Requesting video - - Can\'t connect video call - - Video request rejected - - - Your callback number\n - %1$s - - - - Your emergency callback number\n - %1$s - - - - - Dialing - - Missed call - - Missed calls - - %s missed calls - - Missed call from %s - - Ongoing call - - Ongoing work call - - Ongoing Wi-Fi call - - Ongoing Wi-Fi work call - - On hold - - Incoming call - - Incoming work call - - Incoming Wi-Fi call - - Incoming Wi-Fi work call - - Incoming video call - - Incoming suspected spam call - - Incoming video request - - New voicemail - - New voicemail (%d) - - Dial %s - - Voicemail number unknown - - No service - - Selected network (%s) unavailable - - Answer - - Hang up - - Video - - Voice - - Accept - - Dismiss - - - Call back - - Message - - Ongoing call on another device - - Transfer Call - - - To place a call, first turn off Airplane mode. - - Not registered on network. - - Cellular network not available. - - To place a call, enter a valid number. - - Can\'t call. - - Starting MMI sequence\u2026 - - Service not supported. - - Can\'t switch calls. - - Can\'t separate call. - - Can\'t transfer. - - Can\'t conference. - - Can\'t reject call. - - Can\'t release call(s). - - - SIP call - - - Emergency call - - Turning on radio\u2026 - - No service. Trying again\u2026 - - - - Can\'t call. %s is not an emergency number. - - Can\'t call. Dial an emergency number. - - - Use keyboard to dial - - - Hold Call - - Resume Call - - End Call - - Show Dialpad - - Hide Dialpad - - Mute - - Unmute - - Add call - - Merge calls - - Swap - - Manage calls - - Manage conference call - - Conference call - - Manage - - Audio - - Video call - - Change to voice call - - Switch camera - - Turn on camera - - Turn off camera - - More options - - - Player Started - - Player Stopped - - Camera not ready - - Camera ready - - "Unkown call session event" - - - - ABSENT NUMBER - ABSENTNUMBER - - - - Service - - - Setup - - - <Not set> - - - Other call settings - - - Calling via %s - - Incoming via %s - - - contact photo - - go private - - select contact - - - Write your own... - - Cancel - - Send - - - Answer - - Send SMS - - Decline - - Answer as video call - - Answer as audio call - - Accept video request - - Decline video request - - Accept video transmit request - - Decline video transmit request - - Accept video receive request - - Decline video receive request - - - Slide up for %s. - - "Slide left for %s. - - Slide right for %s. - - Slide down for %s. - - - Vibrate - - Vibrate - - - Sound - - - Default sound (%1$s) - - - never - - - - always - silent - never - - - - Phone ringtone - - - Vibrate when ringing - - - Ringtone & Vibrate - - - Manage conference call - - - Emergency number - - - Profile photo - - - Camera off - - - via %s - - - Note sent - - - Recent messages - - - Business info - - - - - %.1f mi away - - %.1f km away - - %1$s, %2$s - - %1$s - %2$s - - %1$s, %2$s - - Opens tomorrow at %s - - Opens today at %s - - Closes at %s - - Closed today at %s - - Open now - - Closed now - - Suspected spam caller - - - Call ended %1$s - - This is the first time this number called you. - - We suspected this call to be a spammer. - - Block/report spam - - Add contact - - Not spam - diff --git a/InCallUI/res/values/styles.xml b/InCallUI/res/values/styles.xml deleted file mode 100644 index 11d636261dfe309695603d0c0b2dab3063c0e1ae..0000000000000000000000000000000000000000 --- a/InCallUI/res/values/styles.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - #FF333333 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/src/com/android/incallui/AccelerometerListener.java b/InCallUI/src/com/android/incallui/AccelerometerListener.java deleted file mode 100644 index b5ad29675f4c41afe0d5b487d8f027fe9f9c43d0..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/AccelerometerListener.java +++ /dev/null @@ -1,169 +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.incallui; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Handler; -import android.os.Message; -import android.util.Log; - -/** - * This class is used to listen to the accelerometer to monitor the - * orientation of the phone. The client of this class is notified when - * the orientation changes between horizontal and vertical. - */ -public class AccelerometerListener { - private static final String TAG = "AccelerometerListener"; - private static final boolean DEBUG = true; - private static final boolean VDEBUG = false; - - private SensorManager mSensorManager; - private Sensor mSensor; - - // mOrientation is the orientation value most recently reported to the client. - private int mOrientation; - - // mPendingOrientation is the latest orientation computed based on the sensor value. - // This is sent to the client after a rebounce delay, at which point it is copied to - // mOrientation. - private int mPendingOrientation; - - private OrientationListener mListener; - - // Device orientation - public static final int ORIENTATION_UNKNOWN = 0; - public static final int ORIENTATION_VERTICAL = 1; - public static final int ORIENTATION_HORIZONTAL = 2; - - private static final int ORIENTATION_CHANGED = 1234; - - private static final int VERTICAL_DEBOUNCE = 100; - private static final int HORIZONTAL_DEBOUNCE = 500; - private static final double VERTICAL_ANGLE = 50.0; - - public interface OrientationListener { - public void orientationChanged(int orientation); - } - - public AccelerometerListener(Context context) { - mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - } - - public void setListener(OrientationListener listener) { - mListener = listener; - } - - public void enable(boolean enable) { - if (DEBUG) Log.d(TAG, "enable(" + enable + ")"); - synchronized (this) { - if (enable) { - mOrientation = ORIENTATION_UNKNOWN; - mPendingOrientation = ORIENTATION_UNKNOWN; - mSensorManager.registerListener(mSensorListener, mSensor, - SensorManager.SENSOR_DELAY_NORMAL); - } else { - mSensorManager.unregisterListener(mSensorListener); - mHandler.removeMessages(ORIENTATION_CHANGED); - } - } - } - - private void setOrientation(int orientation) { - synchronized (this) { - if (mPendingOrientation == orientation) { - // Pending orientation has not changed, so do nothing. - return; - } - - // Cancel any pending messages. - // We will either start a new timer or cancel alltogether - // if the orientation has not changed. - mHandler.removeMessages(ORIENTATION_CHANGED); - - if (mOrientation != orientation) { - // Set timer to send an event if the orientation has changed since its - // previously reported value. - mPendingOrientation = orientation; - final Message m = mHandler.obtainMessage(ORIENTATION_CHANGED); - // set delay to our debounce timeout - int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE - : HORIZONTAL_DEBOUNCE); - mHandler.sendMessageDelayed(m, delay); - } else { - // no message is pending - mPendingOrientation = ORIENTATION_UNKNOWN; - } - } - } - - private void onSensorEvent(double x, double y, double z) { - if (VDEBUG) Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")"); - - // If some values are exactly zero, then likely the sensor is not powered up yet. - // ignore these events to avoid false horizontal positives. - if (x == 0.0 || y == 0.0 || z == 0.0) return; - - // magnitude of the acceleration vector projected onto XY plane - final double xy = Math.hypot(x, y); - // compute the vertical angle - double angle = Math.atan2(xy, z); - // convert to degrees - angle = angle * 180.0 / Math.PI; - final int orientation = (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL); - if (VDEBUG) Log.d(TAG, "angle: " + angle + " orientation: " + orientation); - setOrientation(orientation); - } - - SensorEventListener mSensorListener = new SensorEventListener() { - @Override - public void onSensorChanged(SensorEvent event) { - onSensorEvent(event.values[0], event.values[1], event.values[2]); - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // ignore - } - }; - - Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case ORIENTATION_CHANGED: - synchronized (this) { - mOrientation = mPendingOrientation; - if (DEBUG) { - Log.d(TAG, "orientation: " + - (mOrientation == ORIENTATION_HORIZONTAL ? "horizontal" - : (mOrientation == ORIENTATION_VERTICAL ? "vertical" - : "unknown"))); - } - if (mListener != null) { - mListener.orientationChanged(mOrientation); - } - } - break; - } - } - }; -} diff --git a/InCallUI/src/com/android/incallui/AccessibleAnswerFragment.java b/InCallUI/src/com/android/incallui/AccessibleAnswerFragment.java deleted file mode 100644 index 89c78ec6115493de113d77a17f3676a1b36f774d..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/AccessibleAnswerFragment.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.telecom.VideoProfile; -import android.view.GestureDetector; -import android.view.GestureDetector.SimpleOnGestureListener; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; - -import com.android.dialer.R; - -/** - * AnswerFragment to use when touch exploration is enabled in accessibility. - */ -public class AccessibleAnswerFragment extends AnswerFragment { - - private static final String TAG = AccessibleAnswerFragment.class.getSimpleName(); - private static final int SWIPE_THRESHOLD = 100; - - private View mAnswer; - private View mDecline; - private View mText; - - private TouchListener mTouchListener; - private GestureDetector mGestureDetector; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - ViewGroup group = (ViewGroup) inflater.inflate(R.layout.accessible_answer_fragment, - container, false); - - mTouchListener = new TouchListener(); - mGestureDetector = new GestureDetector(getContext(), new SimpleOnGestureListener() { - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { - return AccessibleAnswerFragment.this.onFling(e1, e2, velocityX, velocityX); - } - }); - - mAnswer = group.findViewById(R.id.accessible_answer_fragment_answer); - mAnswer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d(TAG, "Answer Button Clicked"); - onAnswer(VideoProfile.STATE_AUDIO_ONLY, getContext()); - } - }); - mDecline = group.findViewById(R.id.accessible_answer_fragment_decline); - mDecline.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d(TAG, "Decline Button Clicked"); - onDecline(getContext()); - } - }); - - mText = group.findViewById(R.id.accessible_answer_fragment_text); - mText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d(TAG, "Text Button Clicked"); - onText(); - } - }); - return group; - } - - @Override - public void onResume() { - super.onResume(); - // Intercept all touch events for full screen swiping gesture. - InCallActivity activity = (InCallActivity) getActivity(); - activity.setDispatchTouchEventListener(mTouchListener); - } - - @Override - public void onPause() { - super.onPause(); - InCallActivity activity = (InCallActivity) getActivity(); - activity.setDispatchTouchEventListener(null); - } - - private class TouchListener implements View.OnTouchListener { - @Override - public boolean onTouch(View v, MotionEvent event) { - return mGestureDetector.onTouchEvent(event); - } - } - - private boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { - if (hasPendingDialogs()) { - return false; - } - - float diffY = e2.getY() - e1.getY(); - float diffX = e2.getX() - e1.getX(); - if (Math.abs(diffX) > Math.abs(diffY)) { - if (Math.abs(diffX) > SWIPE_THRESHOLD) { - if (diffX > 0) { - onSwipeRight(); - } else { - onSwipeLeft(); - } - } - return true; - } else if (Math.abs(diffY) > SWIPE_THRESHOLD) { - if (diffY > 0) { - onSwipeDown(); - } else { - onSwipeUp(); - } - return true; - } - - return false; - } - - private void onSwipeUp() { - Log.d(TAG, "onSwipeUp"); - onText(); - } - - private void onSwipeDown() { - Log.d(TAG, "onSwipeDown"); - } - - private void onSwipeLeft() { - Log.d(TAG, "onSwipeLeft"); - onDecline(getContext()); - } - - private void onSwipeRight() { - Log.d(TAG, "onSwipeRight"); - onAnswer(VideoProfile.STATE_AUDIO_ONLY, getContext()); - } -} diff --git a/InCallUI/src/com/android/incallui/AnswerFragment.java b/InCallUI/src/com/android/incallui/AnswerFragment.java deleted file mode 100644 index 44ddfcd49c1dd1a5a6c95934f6fd7055161feba1..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/AnswerFragment.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ListView; - -import com.android.dialer.R; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Provides only common interface and functions. Should be derived to implement the actual UI. - */ -public abstract class AnswerFragment extends BaseFragment - implements AnswerPresenter.AnswerUi { - - public static final int TARGET_SET_FOR_AUDIO_WITHOUT_SMS = 0; - public static final int TARGET_SET_FOR_AUDIO_WITH_SMS = 1; - public static final int TARGET_SET_FOR_VIDEO_WITHOUT_SMS = 2; - public static final int TARGET_SET_FOR_VIDEO_WITH_SMS = 3; - public static final int TARGET_SET_FOR_VIDEO_ACCEPT_REJECT_REQUEST = 4; - - /** - * This fragment implement no UI at all. Derived class should do it. - */ - @Override - public abstract View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState); - - /** - * The popup showing the list of canned responses. - * - * This is an AlertDialog containing a ListView showing the possible choices. This may be null - * if the InCallScreen hasn't ever called showRespondViaSmsPopup() yet, or if the popup was - * visible once but then got dismissed. - */ - private Dialog mCannedResponsePopup = null; - - /** - * The popup showing a text field for users to type in their custom message. - */ - private AlertDialog mCustomMessagePopup = null; - - private ArrayAdapter mSmsResponsesAdapter; - - private final List mSmsResponses = new ArrayList<>(); - - @Override - public AnswerPresenter createPresenter() { - return InCallPresenter.getInstance().getAnswerPresenter(); - } - - @Override - public AnswerPresenter.AnswerUi getUi() { - return this; - } - - @Override - public void showMessageDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - - mSmsResponsesAdapter = new ArrayAdapter<>(builder.getContext(), - android.R.layout.simple_list_item_1, android.R.id.text1, mSmsResponses); - - final ListView lv = new ListView(getActivity()); - lv.setAdapter(mSmsResponsesAdapter); - lv.setOnItemClickListener(new RespondViaSmsItemClickListener()); - - builder.setCancelable(true).setView(lv).setOnCancelListener( - new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - onMessageDialogCancel(); - dismissCannedResponsePopup(); - getPresenter().onDismissDialog(); - } - }); - mCannedResponsePopup = builder.create(); - mCannedResponsePopup.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - mCannedResponsePopup.show(); - } - - private boolean isCannedResponsePopupShowing() { - if (mCannedResponsePopup != null) { - return mCannedResponsePopup.isShowing(); - } - return false; - } - - private boolean isCustomMessagePopupShowing() { - if (mCustomMessagePopup != null) { - return mCustomMessagePopup.isShowing(); - } - return false; - } - - /** - * Dismiss the canned response list popup. - * - * This is safe to call even if the popup is already dismissed, and even if you never called - * showRespondViaSmsPopup() in the first place. - */ - protected void dismissCannedResponsePopup() { - if (mCannedResponsePopup != null) { - mCannedResponsePopup.dismiss(); // safe even if already dismissed - mCannedResponsePopup = null; - } - } - - /** - * Dismiss the custom compose message popup. - */ - private void dismissCustomMessagePopup() { - if (mCustomMessagePopup != null) { - mCustomMessagePopup.dismiss(); - mCustomMessagePopup = null; - } - } - - public void dismissPendingDialogs() { - if (isCannedResponsePopupShowing()) { - dismissCannedResponsePopup(); - } - - if (isCustomMessagePopupShowing()) { - dismissCustomMessagePopup(); - } - } - - public boolean hasPendingDialogs() { - return !(mCannedResponsePopup == null && mCustomMessagePopup == null); - } - - /** - * Shows the custom message entry dialog. - */ - public void showCustomMessageDialog() { - // Create an alert dialog containing an EditText - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - final EditText et = new EditText(builder.getContext()); - builder.setCancelable(true).setView(et) - .setPositiveButton(R.string.custom_message_send, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // The order is arranged in a way that the popup will be destroyed - // when the InCallActivity is about to finish. - final String textMessage = et.getText().toString().trim(); - dismissCustomMessagePopup(); - getPresenter().rejectCallWithMessage(textMessage); - } - }) - .setNegativeButton(R.string.custom_message_cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dismissCustomMessagePopup(); - getPresenter().onDismissDialog(); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - dismissCustomMessagePopup(); - getPresenter().onDismissDialog(); - } - }) - .setTitle(R.string.respond_via_sms_custom_message); - mCustomMessagePopup = builder.create(); - - // Enable/disable the send button based on whether there is a message in the EditText - et.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - final Button sendButton = mCustomMessagePopup.getButton( - DialogInterface.BUTTON_POSITIVE); - sendButton.setEnabled(s != null && s.toString().trim().length() != 0); - } - }); - - // Keyboard up, show the dialog - mCustomMessagePopup.getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - mCustomMessagePopup.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - mCustomMessagePopup.show(); - - // Send button starts out disabled - final Button sendButton = mCustomMessagePopup.getButton(DialogInterface.BUTTON_POSITIVE); - sendButton.setEnabled(false); - } - - @Override - public void configureMessageDialog(List textResponses) { - mSmsResponses.clear(); - mSmsResponses.addAll(textResponses); - mSmsResponses.add(getResources().getString( - R.string.respond_via_sms_custom_message)); - if (mSmsResponsesAdapter != null) { - mSmsResponsesAdapter.notifyDataSetChanged(); - } - } - - @Override - public Context getContext() { - return getActivity(); - } - - public void onAnswer(int videoState, Context context) { - Log.d(this, "onAnswer videoState=" + videoState + " context=" + context); - getPresenter().onAnswer(videoState, context); - } - - public void onDecline(Context context) { - getPresenter().onDecline(context); - } - - public void onDeclineUpgradeRequest(Context context) { - InCallPresenter.getInstance().declineUpgradeRequest(context); - } - - public void onText() { - getPresenter().onText(); - } - - /** - * OnItemClickListener for the "Respond via SMS" popup. - */ - public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener { - - /** - * Handles the user selecting an item from the popup. - */ - @Override - public void onItemClick(AdapterView parent, // The ListView - View view, // The TextView that was clicked - int position, long id) { - Log.d(this, "RespondViaSmsItemClickListener.onItemClick(" + position + ")..."); - final String message = (String) parent.getItemAtPosition(position); - Log.v(this, "- message: '" + message + "'"); - dismissCannedResponsePopup(); - - // The "Custom" choice is a special case. - // (For now, it's guaranteed to be the last item.) - if (position == (parent.getCount() - 1)) { - // Show the custom message dialog - showCustomMessageDialog(); - } else { - getPresenter().rejectCallWithMessage(message); - } - } - } - - public void onShowAnswerUi(boolean shown) { - // Do Nothing - } - - public void showTargets(int targetSet) { - // Do Nothing - } - - public void showTargets(int targetSet, int videoState) { - // Do Nothing - } - - protected void onMessageDialogCancel() { - // Do nothing. - } -} diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java deleted file mode 100644 index 883b54fed7925fa27ace1040a44482b36e575547..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/AnswerPresenter.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; - -import com.android.dialer.compat.UserManagerCompat; -import com.android.dialer.util.TelecomUtil; -import com.android.incallui.InCallPresenter.InCallState; - -import java.util.List; - -/** - * Presenter for the Incoming call widget. The {@link AnswerPresenter} handles the logic during - * incoming calls. It is also in charge of responding to incoming calls, so there needs to be - * an instance alive so that it can receive onIncomingCall callbacks. - * - * An instance of {@link AnswerPresenter} is created by InCallPresenter at startup, registers - * for callbacks via InCallPresenter, and shows/hides the {@link AnswerFragment} via IncallActivity. - * - */ -public class AnswerPresenter extends Presenter - implements CallList.CallUpdateListener, InCallPresenter.InCallUiListener, - InCallPresenter.IncomingCallListener, - CallList.Listener { - - private static final String TAG = AnswerPresenter.class.getSimpleName(); - - private String mCallId; - private Call mCall = null; - private boolean mHasTextMessages = false; - - @Override - public void onUiShowing(boolean showing) { - if (showing) { - CallList.getInstance().addListener(this); - final CallList calls = CallList.getInstance(); - Call call; - call = calls.getIncomingCall(); - if (call != null) { - processIncomingCall(call); - } - call = calls.getVideoUpgradeRequestCall(); - Log.d(this, "getVideoUpgradeRequestCall call =" + call); - if (call != null) { - showAnswerUi(true); - processVideoUpgradeRequestCall(call); - } - } else { - CallList.getInstance().removeListener(this); - // This is necessary because the activity can be destroyed while an incoming call exists. - // This happens when back button is pressed while incoming call is still being shown. - if (mCallId != null) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - } - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - Log.d(this, "onIncomingCall: " + this); - Call modifyCall = CallList.getInstance().getVideoUpgradeRequestCall(); - if (modifyCall != null) { - showAnswerUi(false); - Log.d(this, "declining upgrade request id: "); - CallList.getInstance().removeCallUpdateListener(mCallId, this); - InCallPresenter.getInstance().declineUpgradeRequest(); - } - if (!call.getId().equals(mCallId)) { - // A new call is coming in. - processIncomingCall(call); - } - } - - @Override - public void onIncomingCall(Call call) { - } - - @Override - public void onCallListChange(CallList list) { - } - - @Override - public void onDisconnect(Call call) { - // no-op - } - - public void onSessionModificationStateChange(int sessionModificationState) { - boolean isUpgradePending = sessionModificationState == - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - - if (!isUpgradePending) { - // Stop listening for updates. - CallList.getInstance().removeCallUpdateListener(mCallId, this); - showAnswerUi(false); - } - } - - @Override - public void onLastForwardedNumberChange() { - // no-op - } - - @Override - public void onChildNumberChange() { - // no-op - } - - private boolean isVideoUpgradePending(Call call) { - return call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - } - - @Override - public void onUpgradeToVideo(Call call) { - Log.d(this, "onUpgradeToVideo: " + this + " call=" + call); - showAnswerUi(true); - boolean isUpgradePending = isVideoUpgradePending(call); - InCallPresenter inCallPresenter = InCallPresenter.getInstance(); - if (isUpgradePending - && inCallPresenter.getInCallState() == InCallPresenter.InCallState.INCOMING) { - Log.d(this, "declining upgrade request"); - //If there is incoming call reject upgrade request - inCallPresenter.declineUpgradeRequest(getUi().getContext()); - } else if (isUpgradePending) { - Log.d(this, "process upgrade request as no MT call"); - processVideoUpgradeRequestCall(call); - } - } - - private void processIncomingCall(Call call) { - mCallId = call.getId(); - mCall = call; - - // Listen for call updates for the current call. - CallList.getInstance().addCallUpdateListener(mCallId, this); - - Log.d(TAG, "Showing incoming for call id: " + mCallId + " " + this); - if (showAnswerUi(true)) { - final List textMsgs = CallList.getInstance().getTextResponses(call.getId()); - configureAnswerTargetsForSms(call, textMsgs); - } - } - - private boolean showAnswerUi(boolean show) { - final InCallActivity activity = InCallPresenter.getInstance().getActivity(); - if (activity != null) { - activity.showAnswerFragment(show); - if (getUi() != null) { - getUi().onShowAnswerUi(show); - } - return true; - } else { - return false; - } - } - - private void processVideoUpgradeRequestCall(Call call) { - Log.d(this, " processVideoUpgradeRequestCall call=" + call); - mCallId = call.getId(); - mCall = call; - - // Listen for call updates for the current call. - CallList.getInstance().addCallUpdateListener(mCallId, this); - - final int currentVideoState = call.getVideoState(); - final int modifyToVideoState = call.getRequestedVideoState(); - - if (currentVideoState == modifyToVideoState) { - Log.w(this, "processVideoUpgradeRequestCall: Video states are same. Return."); - return; - } - - AnswerUi ui = getUi(); - - if (ui == null) { - Log.e(this, "Ui is null. Can't process upgrade request"); - return; - } - showAnswerUi(true); - ui.showTargets(AnswerFragment.TARGET_SET_FOR_VIDEO_ACCEPT_REJECT_REQUEST, - modifyToVideoState); - } - - private boolean isEnabled(int videoState, int mask) { - return (videoState & mask) == mask; - } - - @Override - public void onCallChanged(Call call) { - Log.d(this, "onCallStateChange() " + call + " " + this); - if (call.getState() != Call.State.INCOMING) { - boolean isUpgradePending = isVideoUpgradePending(call); - if (!isUpgradePending) { - // Stop listening for updates. - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - - final Call incall = CallList.getInstance().getIncomingCall(); - if (incall != null || isUpgradePending) { - showAnswerUi(true); - } else { - showAnswerUi(false); - } - - mHasTextMessages = false; - } else if (!mHasTextMessages) { - final List textMsgs = CallList.getInstance().getTextResponses(call.getId()); - if (textMsgs != null) { - configureAnswerTargetsForSms(call, textMsgs); - } - } - } - - public void onAnswer(int videoState, Context context) { - if (mCallId == null) { - return; - } - - if (mCall.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - Log.d(this, "onAnswer (upgradeCall) mCallId=" + mCallId + " videoState=" + videoState); - InCallPresenter.getInstance().acceptUpgradeRequest(videoState, context); - } else { - Log.d(this, "onAnswer (answerCall) mCallId=" + mCallId + " videoState=" + videoState); - TelecomAdapter.getInstance().answerCall(mCall.getId(), videoState); - } - } - - /** - * TODO: We are using reject and decline interchangeably. We should settle on - * reject since it seems to be more prevalent. - */ - public void onDecline(Context context) { - Log.d(this, "onDecline " + mCallId); - if (mCall.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - InCallPresenter.getInstance().declineUpgradeRequest(context); - } else { - TelecomAdapter.getInstance().rejectCall(mCall.getId(), false, null); - } - } - - public void onText() { - if (getUi() != null) { - TelecomUtil.silenceRinger(getUi().getContext()); - getUi().showMessageDialog(); - } - } - - public void rejectCallWithMessage(String message) { - Log.d(this, "sendTextToDefaultActivity()..."); - TelecomAdapter.getInstance().rejectCall(mCall.getId(), true, message); - - onDismissDialog(); - } - - public void onDismissDialog() { - InCallPresenter.getInstance().onDismissDialog(); - } - - private void configureAnswerTargetsForSms(Call call, List textMsgs) { - if (getUi() == null) { - return; - } - mHasTextMessages = textMsgs != null; - boolean withSms = UserManagerCompat.isUserUnlocked(getUi().getContext()) - && call.can(android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT) - && mHasTextMessages; - - // Only present the user with the option to answer as a video call if the incoming call is - // a bi-directional video call. - if (VideoUtils.isBidirectionalVideoCall(call)) { - if (withSms) { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_VIDEO_WITH_SMS); - getUi().configureMessageDialog(textMsgs); - } else { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_VIDEO_WITHOUT_SMS); - } - } else { - if (withSms) { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_AUDIO_WITH_SMS); - getUi().configureMessageDialog(textMsgs); - } else { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_AUDIO_WITHOUT_SMS); - } - } - } - - interface AnswerUi extends Ui { - public void onShowAnswerUi(boolean shown); - public void showTargets(int targetSet); - public void showTargets(int targetSet, int videoState); - public void showMessageDialog(); - public void configureMessageDialog(List textResponses); - public Context getContext(); - } -} diff --git a/InCallUI/src/com/android/incallui/AudioModeProvider.java b/InCallUI/src/com/android/incallui/AudioModeProvider.java deleted file mode 100644 index ea56dd6249a97285a92d51bd69d3de9aafc9560b..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/AudioModeProvider.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telecom.CallAudioState; - -import com.google.common.collect.Lists; - -import java.util.List; - -/** - * Proxy class for getting and setting the audio mode. - */ -public class AudioModeProvider { - - static final int AUDIO_MODE_INVALID = 0; - - private static AudioModeProvider sAudioModeProvider = new AudioModeProvider(); - private int mAudioMode = CallAudioState.ROUTE_EARPIECE; - private boolean mMuted = false; - private int mSupportedModes = CallAudioState.ROUTE_EARPIECE - | CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET - | CallAudioState.ROUTE_SPEAKER; - private final List mListeners = Lists.newArrayList(); - - public static AudioModeProvider getInstance() { - return sAudioModeProvider; - } - - public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) { - onAudioModeChange(route, isMuted); - onSupportedAudioModeChange(supportedRouteMask); - } - - public void onAudioModeChange(int newMode, boolean muted) { - if (mAudioMode != newMode) { - mAudioMode = newMode; - for (AudioModeListener l : mListeners) { - l.onAudioMode(mAudioMode); - } - } - - if (mMuted != muted) { - mMuted = muted; - for (AudioModeListener l : mListeners) { - l.onMute(mMuted); - } - } - } - - public void onSupportedAudioModeChange(int newModeMask) { - mSupportedModes = newModeMask; - - for (AudioModeListener l : mListeners) { - l.onSupportedAudioMode(mSupportedModes); - } - } - - public void addListener(AudioModeListener listener) { - if (!mListeners.contains(listener)) { - mListeners.add(listener); - listener.onSupportedAudioMode(mSupportedModes); - listener.onAudioMode(mAudioMode); - listener.onMute(mMuted); - } - } - - public void removeListener(AudioModeListener listener) { - if (mListeners.contains(listener)) { - mListeners.remove(listener); - } - } - - public int getSupportedModes() { - return mSupportedModes; - } - - public int getAudioMode() { - return mAudioMode; - } - - public boolean getMute() { - return mMuted; - } - - /* package */ interface AudioModeListener { - void onAudioMode(int newMode); - void onMute(boolean muted); - void onSupportedAudioMode(int modeMask); - } -} diff --git a/InCallUI/src/com/android/incallui/BaseFragment.java b/InCallUI/src/com/android/incallui/BaseFragment.java deleted file mode 100644 index 58d991acd3dd4979d0bfcb5306c993396d27bd67..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/BaseFragment.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.app.Activity; -import android.app.Fragment; -import android.os.Bundle; - -/** - * Parent for all fragments that use Presenters and Ui design. - */ -public abstract class BaseFragment, U extends Ui> extends Fragment { - - private static final String KEY_FRAGMENT_HIDDEN = "key_fragment_hidden"; - - private T mPresenter; - - public abstract T createPresenter(); - - public abstract U getUi(); - - protected BaseFragment() { - mPresenter = createPresenter(); - } - - /** - * Presenter will be available after onActivityCreated(). - * - * @return The presenter associated with this fragment. - */ - public T getPresenter() { - return mPresenter; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mPresenter.onUiReady(getUi()); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mPresenter.onRestoreInstanceState(savedInstanceState); - if (savedInstanceState.getBoolean(KEY_FRAGMENT_HIDDEN)) { - getFragmentManager().beginTransaction().hide(this).commit(); - } - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mPresenter.onUiDestroy(getUi()); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mPresenter.onSaveInstanceState(outState); - outState.putBoolean(KEY_FRAGMENT_HIDDEN, isHidden()); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - ((FragmentDisplayManager) activity).onFragmentAttached(this); - } -} diff --git a/InCallUI/src/com/android/incallui/Call.java b/InCallUI/src/com/android/incallui/Call.java deleted file mode 100644 index 1ad37e01a8c21e5b92f97c9c3a7cd89b9b8bbc3b..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/Call.java +++ /dev/null @@ -1,1023 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.content.Context; -import android.hardware.camera2.CameraCharacteristics; -import android.net.Uri; -import android.os.Bundle; -import android.os.Trace; -import android.support.annotation.IntDef; -import android.telecom.Call.Details; -import android.telecom.Connection; -import android.telecom.DisconnectCause; -import android.telecom.GatewayInfo; -import android.telecom.InCallService.VideoCall; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; -import android.telecom.VideoProfile; -import android.text.TextUtils; - -import com.android.contacts.common.CallUtil; -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.compat.SdkVersionOverride; -import com.android.contacts.common.compat.telecom.TelecomManagerCompat; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.dialer.util.IntentUtil; -import com.android.incallui.util.TelecomCallUtil; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Objects; - -/** - * Describes a single call and its state. - */ -@NeededForTesting -public class Call { - - /** - * Specifies whether a number is in the call history or not. - * {@link #CALL_HISTORY_STATUS_UNKNOWN} means there is no result. - */ - @IntDef({CALL_HISTORY_STATUS_UNKNOWN, CALL_HISTORY_STATUS_PRESENT, - CALL_HISTORY_STATUS_NOT_PRESENT}) - @Retention(RetentionPolicy.SOURCE) - public @interface CallHistoryStatus {} - public static final int CALL_HISTORY_STATUS_UNKNOWN = 0; - public static final int CALL_HISTORY_STATUS_PRESENT = 1; - public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2; - - /* Defines different states of this call */ - public static class State { - public static final int INVALID = 0; - public static final int NEW = 1; /* The call is new. */ - public static final int IDLE = 2; /* The call is idle. Nothing active */ - public static final int ACTIVE = 3; /* There is an active call */ - public static final int INCOMING = 4; /* A normal incoming phone call */ - public static final int CALL_WAITING = 5; /* Incoming call while another is active */ - public static final int DIALING = 6; /* An outgoing call during dial phase */ - public static final int REDIALING = 7; /* Subsequent dialing attempt after a failure */ - public static final int ONHOLD = 8; /* An active phone call placed on hold */ - public static final int DISCONNECTING = 9; /* A call is being ended. */ - public static final int DISCONNECTED = 10; /* State after a call disconnects */ - public static final int CONFERENCED = 11; /* Call part of a conference call */ - public static final int SELECT_PHONE_ACCOUNT = 12; /* Waiting for account selection */ - public static final int CONNECTING = 13; /* Waiting for Telecom broadcast to finish */ - public static final int BLOCKED = 14; /* The number was found on the block list */ - - - public static boolean isConnectingOrConnected(int state) { - switch(state) { - case ACTIVE: - case INCOMING: - case CALL_WAITING: - case CONNECTING: - case DIALING: - case REDIALING: - case ONHOLD: - case CONFERENCED: - return true; - default: - } - return false; - } - - public static boolean isDialing(int state) { - return state == DIALING || state == REDIALING; - } - - public static String toString(int state) { - switch (state) { - case INVALID: - return "INVALID"; - case NEW: - return "NEW"; - case IDLE: - return "IDLE"; - case ACTIVE: - return "ACTIVE"; - case INCOMING: - return "INCOMING"; - case CALL_WAITING: - return "CALL_WAITING"; - case DIALING: - return "DIALING"; - case REDIALING: - return "REDIALING"; - case ONHOLD: - return "ONHOLD"; - case DISCONNECTING: - return "DISCONNECTING"; - case DISCONNECTED: - return "DISCONNECTED"; - case CONFERENCED: - return "CONFERENCED"; - case SELECT_PHONE_ACCOUNT: - return "SELECT_PHONE_ACCOUNT"; - case CONNECTING: - return "CONNECTING"; - case BLOCKED: - return "BLOCKED"; - default: - return "UNKNOWN"; - } - } - } - - /** - * Defines different states of session modify requests, which are used to upgrade to video, or - * downgrade to audio. - */ - public static class SessionModificationState { - public static final int NO_REQUEST = 0; - public static final int WAITING_FOR_RESPONSE = 1; - public static final int REQUEST_FAILED = 2; - public static final int RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3; - public static final int UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4; - public static final int REQUEST_REJECTED = 5; - } - - public static class VideoSettings { - public static final int CAMERA_DIRECTION_UNKNOWN = -1; - public static final int CAMERA_DIRECTION_FRONT_FACING = - CameraCharacteristics.LENS_FACING_FRONT; - public static final int CAMERA_DIRECTION_BACK_FACING = - CameraCharacteristics.LENS_FACING_BACK; - - private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN; - - /** - * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, - * the video state of the call should be used to infer the camera direction. - * - * @see {@link CameraCharacteristics#LENS_FACING_FRONT} - * @see {@link CameraCharacteristics#LENS_FACING_BACK} - */ - public void setCameraDir(int cameraDirection) { - if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING - || cameraDirection == CAMERA_DIRECTION_BACK_FACING) { - mCameraDirection = cameraDirection; - } else { - mCameraDirection = CAMERA_DIRECTION_UNKNOWN; - } - } - - /** - * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, - * the video state of the call should be used to infer the camera direction. - * - * @see {@link CameraCharacteristics#LENS_FACING_FRONT} - * @see {@link CameraCharacteristics#LENS_FACING_BACK} - */ - public int getCameraDir() { - return mCameraDirection; - } - - @Override - public String toString() { - return "(CameraDir:" + getCameraDir() + ")"; - } - } - - /** - * Tracks any state variables that is useful for logging. There is some amount of overlap with - * existing call member variables, but this duplication helps to ensure that none of these - * logging variables will interface with/and affect call logic. - */ - public static class LogState { - - // Contact lookup type constants - // Unknown lookup result (lookup not completed yet?) - public static final int LOOKUP_UNKNOWN = 0; - public static final int LOOKUP_NOT_FOUND = 1; - public static final int LOOKUP_LOCAL_CONTACT = 2; - public static final int LOOKUP_LOCAL_CACHE = 3; - public static final int LOOKUP_REMOTE_CONTACT = 4; - public static final int LOOKUP_EMERGENCY = 5; - public static final int LOOKUP_VOICEMAIL = 6; - - // Call initiation type constants - public static final int INITIATION_UNKNOWN = 0; - public static final int INITIATION_INCOMING = 1; - public static final int INITIATION_DIALPAD = 2; - public static final int INITIATION_SPEED_DIAL = 3; - public static final int INITIATION_REMOTE_DIRECTORY = 4; - public static final int INITIATION_SMART_DIAL = 5; - public static final int INITIATION_REGULAR_SEARCH = 6; - public static final int INITIATION_CALL_LOG = 7; - public static final int INITIATION_CALL_LOG_FILTER = 8; - public static final int INITIATION_VOICEMAIL_LOG = 9; - public static final int INITIATION_CALL_DETAILS = 10; - public static final int INITIATION_QUICK_CONTACTS = 11; - public static final int INITIATION_EXTERNAL = 12; - - public DisconnectCause disconnectCause; - public boolean isIncoming = false; - public int contactLookupResult = LOOKUP_UNKNOWN; - public int callInitiationMethod = INITIATION_EXTERNAL; - // If this was a conference call, the total number of calls involved in the conference. - public int conferencedCalls = 0; - public long duration = 0; - public boolean isLogged = false; - - @Override - public String toString() { - return String.format(Locale.US, "[" - + "%s, " // DisconnectCause toString already describes the object type - + "isIncoming: %s, " - + "contactLookup: %s, " - + "callInitiation: %s, " - + "duration: %s" - + "]", - disconnectCause, - isIncoming, - lookupToString(contactLookupResult), - initiationToString(callInitiationMethod), - duration); - } - - private static String lookupToString(int lookupType) { - switch (lookupType) { - case LOOKUP_LOCAL_CONTACT: - return "Local"; - case LOOKUP_LOCAL_CACHE: - return "Cache"; - case LOOKUP_REMOTE_CONTACT: - return "Remote"; - case LOOKUP_EMERGENCY: - return "Emergency"; - case LOOKUP_VOICEMAIL: - return "Voicemail"; - default: - return "Not found"; - } - } - - private static String initiationToString(int initiationType) { - switch (initiationType) { - case INITIATION_INCOMING: - return "Incoming"; - case INITIATION_DIALPAD: - return "Dialpad"; - case INITIATION_SPEED_DIAL: - return "Speed Dial"; - case INITIATION_REMOTE_DIRECTORY: - return "Remote Directory"; - case INITIATION_SMART_DIAL: - return "Smart Dial"; - case INITIATION_REGULAR_SEARCH: - return "Regular Search"; - case INITIATION_CALL_LOG: - return "Call Log"; - case INITIATION_CALL_LOG_FILTER: - return "Call Log Filter"; - case INITIATION_VOICEMAIL_LOG: - return "Voicemail Log"; - case INITIATION_CALL_DETAILS: - return "Call Details"; - case INITIATION_QUICK_CONTACTS: - return "Quick Contacts"; - default: - return "Unknown"; - } - } - } - - - private static final String ID_PREFIX = Call.class.getSimpleName() + "_"; - private static int sIdCounter = 0; - - private final android.telecom.Call.Callback mTelecomCallCallback = - new android.telecom.Call.Callback() { - @Override - public void onStateChanged(android.telecom.Call call, int newState) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " newState=" - + newState); - update(); - } - - @Override - public void onParentChanged(android.telecom.Call call, - android.telecom.Call newParent) { - Log.d(this, "TelecomCallCallback onParentChanged call=" + call + " newParent=" - + newParent); - update(); - } - - @Override - public void onChildrenChanged(android.telecom.Call call, - List children) { - update(); - } - - @Override - public void onDetailsChanged(android.telecom.Call call, - android.telecom.Call.Details details) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " details=" - + details); - update(); - } - - @Override - public void onCannedTextResponsesLoaded(android.telecom.Call call, - List cannedTextResponses) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call - + " cannedTextResponses=" + cannedTextResponses); - update(); - } - - @Override - public void onPostDialWait(android.telecom.Call call, - String remainingPostDialSequence) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call - + " remainingPostDialSequence=" + remainingPostDialSequence); - update(); - } - - @Override - public void onVideoCallChanged(android.telecom.Call call, - VideoCall videoCall) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " videoCall=" - + videoCall); - update(); - } - - @Override - public void onCallDestroyed(android.telecom.Call call) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call); - call.unregisterCallback(this); - } - - @Override - public void onConferenceableCallsChanged(android.telecom.Call call, - List conferenceableCalls) { - update(); - } - }; - - private final android.telecom.Call mTelecomCall; - private final LatencyReport mLatencyReport; - private boolean mIsEmergencyCall; - private Uri mHandle; - private final String mId; - private int mState = State.INVALID; - private DisconnectCause mDisconnectCause; - private int mSessionModificationState; - private final List mChildCallIds = new ArrayList<>(); - private final VideoSettings mVideoSettings = new VideoSettings(); - private int mVideoState; - - /** - * mRequestedVideoState is used to store requested upgrade / downgrade video state - */ - private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY; - - private InCallVideoCallCallback mVideoCallCallback; - private boolean mIsVideoCallCallbackRegistered; - private String mChildNumber; - private String mLastForwardedNumber; - private String mCallSubject; - private PhoneAccountHandle mPhoneAccountHandle; - @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN; - private boolean mIsSpam; - - /** - * Indicates whether the phone account associated with this call supports specifying a call - * subject. - */ - private boolean mIsCallSubjectSupported; - - private long mTimeAddedMs; - - private final LogState mLogState = new LogState(); - - /** - * Used only to create mock calls for testing - */ - @NeededForTesting - Call(int state) { - mTelecomCall = null; - mLatencyReport = new LatencyReport(); - mId = ID_PREFIX + Integer.toString(sIdCounter++); - setState(state); - } - - /** - * Creates a new instance of a {@link Call}. Registers a callback for - * {@link android.telecom.Call} events. - */ - public Call(android.telecom.Call telecomCall, LatencyReport latencyReport) { - this(telecomCall, latencyReport, true /* registerCallback */); - } - - /** - * Creates a new instance of a {@link Call}. Optionally registers a callback for - * {@link android.telecom.Call} events. - * - * Intended for use when creating a {@link Call} instance for use with the - * {@link ContactInfoCache}, where we do not want to register callbacks for the new call. - */ - public Call(android.telecom.Call telecomCall, LatencyReport latencyReport, - boolean registerCallback) { - mTelecomCall = telecomCall; - mLatencyReport = latencyReport; - mId = ID_PREFIX + Integer.toString(sIdCounter++); - - updateFromTelecomCall(registerCallback); - - if (registerCallback) { - mTelecomCall.registerCallback(mTelecomCallCallback); - } - - mTimeAddedMs = System.currentTimeMillis(); - } - - public android.telecom.Call getTelecomCall() { - return mTelecomCall; - } - - /** - * @return video settings of the call, null if the call is not a video call. - * @see VideoProfile - */ - public VideoSettings getVideoSettings() { - return mVideoSettings; - } - - private void update() { - Trace.beginSection("Update"); - int oldState = getState(); - // We want to potentially register a video call callback here. - updateFromTelecomCall(true /* registerCallback */); - if (oldState != getState() && getState() == Call.State.DISCONNECTED) { - CallList.getInstance().onDisconnect(this); - } else { - CallList.getInstance().onUpdate(this); - } - Trace.endSection(); - } - - private void updateFromTelecomCall(boolean registerCallback) { - Log.d(this, "updateFromTelecomCall: " + mTelecomCall.toString()); - final int translatedState = translateState(mTelecomCall.getState()); - if (mState != State.BLOCKED) { - setState(translatedState); - setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause()); - maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState()); - } - - if (registerCallback && mTelecomCall.getVideoCall() != null) { - if (mVideoCallCallback == null) { - mVideoCallCallback = new InCallVideoCallCallback(this); - } - mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback); - mIsVideoCallCallbackRegistered = true; - } - - mChildCallIds.clear(); - final int numChildCalls = mTelecomCall.getChildren().size(); - for (int i = 0; i < numChildCalls; i++) { - mChildCallIds.add( - CallList.getInstance().getCallByTelecomCall( - mTelecomCall.getChildren().get(i)).getId()); - } - - // The number of conferenced calls can change over the course of the call, so use the - // maximum number of conferenced child calls as the metric for conference call usage. - mLogState.conferencedCalls = Math.max(numChildCalls, mLogState.conferencedCalls); - - updateFromCallExtras(mTelecomCall.getDetails().getExtras()); - - // If the handle of the call has changed, update state for the call determining if it is an - // emergency call. - Uri newHandle = mTelecomCall.getDetails().getHandle(); - if (!Objects.equals(mHandle, newHandle)) { - mHandle = newHandle; - updateEmergencyCallState(); - } - - // If the phone account handle of the call is set, cache capability bit indicating whether - // the phone account supports call subjects. - PhoneAccountHandle newPhoneAccountHandle = mTelecomCall.getDetails().getAccountHandle(); - if (!Objects.equals(mPhoneAccountHandle, newPhoneAccountHandle)) { - mPhoneAccountHandle = newPhoneAccountHandle; - - if (mPhoneAccountHandle != null) { - TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); - PhoneAccount phoneAccount = - TelecomManagerCompat.getPhoneAccount(mgr, mPhoneAccountHandle); - if (phoneAccount != null) { - mIsCallSubjectSupported = phoneAccount.hasCapabilities( - PhoneAccount.CAPABILITY_CALL_SUBJECT); - } - } - } - } - - /** - * Tests corruption of the {@code callExtras} bundle by calling {@link - * Bundle#containsKey(String)}. If the bundle is corrupted a {@link IllegalArgumentException} - * will be thrown and caught by this function. - * - * @param callExtras the bundle to verify - * @returns {@code true} if the bundle is corrupted, {@code false} otherwise. - */ - protected boolean areCallExtrasCorrupted(Bundle callExtras) { - /** - * There's currently a bug in Telephony service (b/25613098) that could corrupt the - * extras bundle, resulting in a IllegalArgumentException while validating data under - * {@link Bundle#containsKey(String)}. - */ - try { - callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS); - return false; - } catch (IllegalArgumentException e) { - Log.e(this, "CallExtras is corrupted, ignoring exception", e); - return true; - } - } - - protected void updateFromCallExtras(Bundle callExtras) { - if (callExtras == null || areCallExtrasCorrupted(callExtras)) { - /** - * If the bundle is corrupted, abandon information update as a work around. These are - * not critical for the dialer to function. - */ - return; - } - // Check for a change in the child address and notify any listeners. - if (callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS)) { - String childNumber = callExtras.getString(Connection.EXTRA_CHILD_ADDRESS); - if (!Objects.equals(childNumber, mChildNumber)) { - mChildNumber = childNumber; - CallList.getInstance().onChildNumberChange(this); - } - } - - // Last forwarded number comes in as an array of strings. We want to choose the - // last item in the array. The forwarding numbers arrive independently of when the - // call is originally set up, so we need to notify the the UI of the change. - if (callExtras.containsKey(Connection.EXTRA_LAST_FORWARDED_NUMBER)) { - ArrayList lastForwardedNumbers = - callExtras.getStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER); - - if (lastForwardedNumbers != null) { - String lastForwardedNumber = null; - if (!lastForwardedNumbers.isEmpty()) { - lastForwardedNumber = lastForwardedNumbers.get( - lastForwardedNumbers.size() - 1); - } - - if (!Objects.equals(lastForwardedNumber, mLastForwardedNumber)) { - mLastForwardedNumber = lastForwardedNumber; - CallList.getInstance().onLastForwardedNumberChange(this); - } - } - } - - // Call subject is present in the extras at the start of call, so we do not need to - // notify any other listeners of this. - if (callExtras.containsKey(Connection.EXTRA_CALL_SUBJECT)) { - String callSubject = callExtras.getString(Connection.EXTRA_CALL_SUBJECT); - if (!Objects.equals(mCallSubject, callSubject)) { - mCallSubject = callSubject; - } - } - } - - /** - * Determines if a received upgrade to video request should be cancelled. This can happen if - * another InCall UI responds to the upgrade to video request. - * - * @param newVideoState The new video state. - */ - private void maybeCancelVideoUpgrade(int newVideoState) { - boolean isVideoStateChanged = mVideoState != newVideoState; - - if (mSessionModificationState == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST - && isVideoStateChanged) { - - Log.v(this, "maybeCancelVideoUpgrade : cancelling upgrade notification"); - setSessionModificationState(SessionModificationState.NO_REQUEST); - } - mVideoState = newVideoState; - } - private static int translateState(int state) { - switch (state) { - case android.telecom.Call.STATE_NEW: - case android.telecom.Call.STATE_CONNECTING: - return Call.State.CONNECTING; - case android.telecom.Call.STATE_SELECT_PHONE_ACCOUNT: - return Call.State.SELECT_PHONE_ACCOUNT; - case android.telecom.Call.STATE_DIALING: - return Call.State.DIALING; - case android.telecom.Call.STATE_RINGING: - return Call.State.INCOMING; - case android.telecom.Call.STATE_ACTIVE: - return Call.State.ACTIVE; - case android.telecom.Call.STATE_HOLDING: - return Call.State.ONHOLD; - case android.telecom.Call.STATE_DISCONNECTED: - return Call.State.DISCONNECTED; - case android.telecom.Call.STATE_DISCONNECTING: - return Call.State.DISCONNECTING; - default: - return Call.State.INVALID; - } - } - - public String getId() { - return mId; - } - - public long getTimeAddedMs() { - return mTimeAddedMs; - } - - public String getNumber() { - return TelecomCallUtil.getNumber(mTelecomCall); - } - - public void blockCall() { - mTelecomCall.reject(false, null); - setState(State.BLOCKED); - } - - public Uri getHandle() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandle(); - } - - public boolean isEmergencyCall() { - return mIsEmergencyCall; - } - - public int getState() { - if (mTelecomCall != null && mTelecomCall.getParent() != null) { - return State.CONFERENCED; - } else { - return mState; - } - } - - public void setState(int state) { - mState = state; - if (mState == State.INCOMING) { - mLogState.isIncoming = true; - } else if (mState == State.DISCONNECTED) { - mLogState.duration = getConnectTimeMillis() == 0 ? - 0: System.currentTimeMillis() - getConnectTimeMillis(); - } - } - - public int getNumberPresentation() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandlePresentation(); - } - - public int getCnapNamePresentation() { - return mTelecomCall == null ? null - : mTelecomCall.getDetails().getCallerDisplayNamePresentation(); - } - - public String getCnapName() { - return mTelecomCall == null ? null - : getTelecomCall().getDetails().getCallerDisplayName(); - } - - public Bundle getIntentExtras() { - return mTelecomCall.getDetails().getIntentExtras(); - } - - public Bundle getExtras() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getExtras(); - } - - /** - * @return The child number for the call, or {@code null} if none specified. - */ - public String getChildNumber() { - return mChildNumber; - } - - /** - * @return The last forwarded number for the call, or {@code null} if none specified. - */ - public String getLastForwardedNumber() { - return mLastForwardedNumber; - } - - /** - * @return The call subject, or {@code null} if none specified. - */ - public String getCallSubject() { - return mCallSubject; - } - - /** - * @return {@code true} if the call's phone account supports call subjects, {@code false} - * otherwise. - */ - public boolean isCallSubjectSupported() { - return mIsCallSubjectSupported; - } - - /** Returns call disconnect cause, defined by {@link DisconnectCause}. */ - public DisconnectCause getDisconnectCause() { - if (mState == State.DISCONNECTED || mState == State.IDLE) { - return mDisconnectCause; - } - - return new DisconnectCause(DisconnectCause.UNKNOWN); - } - - public void setDisconnectCause(DisconnectCause disconnectCause) { - mDisconnectCause = disconnectCause; - mLogState.disconnectCause = mDisconnectCause; - } - - /** Returns the possible text message responses. */ - public List getCannedSmsResponses() { - return mTelecomCall.getCannedTextResponses(); - } - - /** Checks if the call supports the given set of capabilities supplied as a bit mask. */ - public boolean can(int capabilities) { - int supportedCapabilities = mTelecomCall.getDetails().getCallCapabilities(); - - if ((capabilities & android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE) != 0) { - // We allow you to merge if the capabilities allow it or if it is a call with - // conferenceable calls. - if (mTelecomCall.getConferenceableCalls().isEmpty() && - ((android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE - & supportedCapabilities) == 0)) { - // Cannot merge calls if there are no calls to merge with. - return false; - } - capabilities &= ~android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE; - } - return (capabilities == (capabilities & mTelecomCall.getDetails().getCallCapabilities())); - } - - public boolean hasProperty(int property) { - return mTelecomCall.getDetails().hasProperty(property); - } - - /** Gets the time when the call first became active. */ - public long getConnectTimeMillis() { - return mTelecomCall.getDetails().getConnectTimeMillis(); - } - - public boolean isConferenceCall() { - return hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE); - } - - public GatewayInfo getGatewayInfo() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getGatewayInfo(); - } - - public PhoneAccountHandle getAccountHandle() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle(); - } - - /** - * @return The {@link VideoCall} instance associated with the {@link android.telecom.Call}. - * Will return {@code null} until {@link #updateFromTelecomCall()} has registered a valid - * callback on the {@link VideoCall}. - */ - public VideoCall getVideoCall() { - return mTelecomCall == null || !mIsVideoCallCallbackRegistered ? null - : mTelecomCall.getVideoCall(); - } - - public List getChildCallIds() { - return mChildCallIds; - } - - public String getParentId() { - android.telecom.Call parentCall = mTelecomCall.getParent(); - if (parentCall != null) { - return CallList.getInstance().getCallByTelecomCall(parentCall).getId(); - } - return null; - } - - public int getVideoState() { - return mTelecomCall.getDetails().getVideoState(); - } - - public boolean isVideoCall(Context context) { - return CallUtil.isVideoEnabled(context) && - VideoUtils.isVideoCall(getVideoState()); - } - - /** - * Handles incoming session modification requests. Stores the pending video request and sets - * the session modification state to - * {@link SessionModificationState#RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep track - * of the fact the request was received. Only upgrade requests require user confirmation and - * will be handled by this method. The remote user can turn off their own camera without - * confirmation. - * - * @param videoState The requested video state. - */ - public void setRequestedVideoState(int videoState) { - Log.d(this, "setRequestedVideoState - video state= " + videoState); - if (videoState == getVideoState()) { - mSessionModificationState = Call.SessionModificationState.NO_REQUEST; - Log.w(this,"setRequestedVideoState - Clearing session modification state"); - return; - } - - mSessionModificationState = Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - mRequestedVideoState = videoState; - CallList.getInstance().onUpgradeToVideo(this); - - Log.d(this, "setRequestedVideoState - mSessionModificationState=" - + mSessionModificationState + " video state= " + videoState); - update(); - } - - /** - * Set the session modification state. Used to keep track of pending video session modification - * operations and to inform listeners of these changes. - * - * @param state the new session modification state. - */ - public void setSessionModificationState(int state) { - boolean hasChanged = mSessionModificationState != state; - mSessionModificationState = state; - Log.d(this, "setSessionModificationState " + state + " mSessionModificationState=" - + mSessionModificationState); - if (hasChanged) { - CallList.getInstance().onSessionModificationStateChange(this, state); - } - } - - /** - * Determines if the call handle is an emergency number or not and caches the result to avoid - * repeated calls to isEmergencyNumber. - */ - private void updateEmergencyCallState() { - mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall); - } - - /** - * Gets the video state which was requested via a session modification request. - * - * @return The video state. - */ - public int getRequestedVideoState() { - return mRequestedVideoState; - } - - public static boolean areSame(Call call1, Call call2) { - if (call1 == null && call2 == null) { - return true; - } else if (call1 == null || call2 == null) { - return false; - } - - // otherwise compare call Ids - return call1.getId().equals(call2.getId()); - } - - public static boolean areSameNumber(Call call1, Call call2) { - if (call1 == null && call2 == null) { - return true; - } else if (call1 == null || call2 == null) { - return false; - } - - // otherwise compare call Numbers - return TextUtils.equals(call1.getNumber(), call2.getNumber()); - } - - /** - * Gets the current video session modification state. - * - * @return The session modification state. - */ - public int getSessionModificationState() { - return mSessionModificationState; - } - - public LogState getLogState() { - return mLogState; - } - - /** - * Determines if the call is an external call. - * - * An external call is one which does not exist locally for the - * {@link android.telecom.ConnectionService} it is associated with. - * - * External calls are only supported in N and higher. - * - * @return {@code true} if the call is an external call, {@code false} otherwise. - */ - public boolean isExternalCall() { - return CompatUtils.isNCompatible() && - hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - } - - /** - * Determines if the external call is pullable. - * - * An external call is one which does not exist locally for the - * {@link android.telecom.ConnectionService} it is associated with. An external call may be - * "pullable", which means that the user can request it be transferred to the current device. - * - * External calls are only supported in N and higher. - * - * @return {@code true} if the call is an external call, {@code false} otherwise. - */ - public boolean isPullableExternalCall() { - return CompatUtils.isNCompatible() && - (mTelecomCall.getDetails().getCallCapabilities() - & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) - == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL; - } - - /** - * Logging utility methods - */ - public void logCallInitiationType() { - if (isExternalCall()) { - return; - } - - if (getState() == State.INCOMING) { - getLogState().callInitiationMethod = LogState.INITIATION_INCOMING; - } else if (getIntentExtras() != null) { - getLogState().callInitiationMethod = - getIntentExtras().getInt(IntentUtil.EXTRA_CALL_INITIATION_TYPE, - LogState.INITIATION_EXTERNAL); - } - } - - @Override - public String toString() { - if (mTelecomCall == null) { - // This should happen only in testing since otherwise we would never have a null - // Telecom call. - return String.valueOf(mId); - } - - return String.format(Locale.US, "[%s, %s, %s, %s, children:%s, parent:%s, " + - "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]", - mId, - State.toString(getState()), - Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()), - Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()), - mChildCallIds, - getParentId(), - this.mTelecomCall.getConferenceableCalls(), - VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()), - mSessionModificationState, - getVideoSettings()); - } - - public String toSimpleString() { - return super.toString(); - } - - public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) { - mCallHistoryStatus = callHistoryStatus; - } - - @CallHistoryStatus - public int getCallHistoryStatus() { - return mCallHistoryStatus; - } - - public void setSpam(boolean isSpam) { - mIsSpam = isSpam; - } - - public boolean isSpam() { - return mIsSpam; - } - - public LatencyReport getLatencyReport() { - return mLatencyReport; - } -} diff --git a/InCallUI/src/com/android/incallui/CallButtonFragment.java b/InCallUI/src/com/android/incallui/CallButtonFragment.java deleted file mode 100644 index 6b633eaf3892b4ab25a557354bf0628b5db7ee7b..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallButtonFragment.java +++ /dev/null @@ -1,819 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_CALL; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_COUNT; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DIALPAD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DOWNGRADE_TO_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HOLD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MANAGE_VIDEO_CONFERENCE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MERGE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MUTE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_PAUSE_VIDEO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWAP; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWITCH_CAMERA; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_UPGRADE_TO_VIDEO; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.RippleDrawable; -import android.graphics.drawable.StateListDrawable; -import android.os.Bundle; -import android.telecom.CallAudioState; -import android.util.SparseIntArray; -import android.view.ContextThemeWrapper; -import android.view.HapticFeedbackConstants; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.ImageButton; -import android.widget.PopupMenu; -import android.widget.PopupMenu.OnDismissListener; -import android.widget.PopupMenu.OnMenuItemClickListener; - -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; - -/** - * Fragment for call control buttons - */ -public class CallButtonFragment - extends BaseFragment - implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener, - View.OnClickListener { - - private int mButtonMaxVisible; - // The button is currently visible in the UI - private static final int BUTTON_VISIBLE = 1; - // The button is hidden in the UI - private static final int BUTTON_HIDDEN = 2; - // The button has been collapsed into the overflow menu - private static final int BUTTON_MENU = 3; - - public interface Buttons { - - public static final int BUTTON_AUDIO = 0; - public static final int BUTTON_MUTE = 1; - public static final int BUTTON_DIALPAD = 2; - public static final int BUTTON_HOLD = 3; - public static final int BUTTON_SWAP = 4; - public static final int BUTTON_UPGRADE_TO_VIDEO = 5; - public static final int BUTTON_SWITCH_CAMERA = 6; - public static final int BUTTON_DOWNGRADE_TO_AUDIO = 7; - public static final int BUTTON_ADD_CALL = 8; - public static final int BUTTON_MERGE = 9; - public static final int BUTTON_PAUSE_VIDEO = 10; - public static final int BUTTON_MANAGE_VIDEO_CONFERENCE = 11; - public static final int BUTTON_COUNT = 12; - } - - private SparseIntArray mButtonVisibilityMap = new SparseIntArray(BUTTON_COUNT); - - private CompoundButton mAudioButton; - private CompoundButton mMuteButton; - private CompoundButton mShowDialpadButton; - private CompoundButton mHoldButton; - private ImageButton mSwapButton; - private ImageButton mChangeToVideoButton; - private ImageButton mChangeToVoiceButton; - private CompoundButton mSwitchCameraButton; - private ImageButton mAddCallButton; - private ImageButton mMergeButton; - private CompoundButton mPauseVideoButton; - private ImageButton mOverflowButton; - private ImageButton mManageVideoCallConferenceButton; - - private PopupMenu mAudioModePopup; - private boolean mAudioModePopupVisible; - private PopupMenu mOverflowPopup; - - private int mPrevAudioMode = 0; - - // Constants for Drawable.setAlpha() - private static final int HIDDEN = 0; - private static final int VISIBLE = 255; - - private boolean mIsEnabled; - private MaterialPalette mCurrentThemeColors; - - @Override - public CallButtonPresenter createPresenter() { - // TODO: find a cleaner way to include audio mode provider than having a singleton instance. - return new CallButtonPresenter(); - } - - @Override - public CallButtonPresenter.CallButtonUi getUi() { - return this; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - for (int i = 0; i < BUTTON_COUNT; i++) { - mButtonVisibilityMap.put(i, BUTTON_HIDDEN); - } - - mButtonMaxVisible = getResources().getInteger(R.integer.call_card_max_buttons); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View parent = inflater.inflate(R.layout.call_button_fragment, container, false); - - mAudioButton = (CompoundButton) parent.findViewById(R.id.audioButton); - mAudioButton.setOnClickListener(this); - mMuteButton = (CompoundButton) parent.findViewById(R.id.muteButton); - mMuteButton.setOnClickListener(this); - mShowDialpadButton = (CompoundButton) parent.findViewById(R.id.dialpadButton); - mShowDialpadButton.setOnClickListener(this); - mHoldButton = (CompoundButton) parent.findViewById(R.id.holdButton); - mHoldButton.setOnClickListener(this); - mSwapButton = (ImageButton) parent.findViewById(R.id.swapButton); - mSwapButton.setOnClickListener(this); - mChangeToVideoButton = (ImageButton) parent.findViewById(R.id.changeToVideoButton); - mChangeToVideoButton.setOnClickListener(this); - mChangeToVoiceButton = (ImageButton) parent.findViewById(R.id.changeToVoiceButton); - mChangeToVoiceButton.setOnClickListener(this); - mSwitchCameraButton = (CompoundButton) parent.findViewById(R.id.switchCameraButton); - mSwitchCameraButton.setOnClickListener(this); - mAddCallButton = (ImageButton) parent.findViewById(R.id.addButton); - mAddCallButton.setOnClickListener(this); - mMergeButton = (ImageButton) parent.findViewById(R.id.mergeButton); - mMergeButton.setOnClickListener(this); - mPauseVideoButton = (CompoundButton) parent.findViewById(R.id.pauseVideoButton); - mPauseVideoButton.setOnClickListener(this); - mOverflowButton = (ImageButton) parent.findViewById(R.id.overflowButton); - mOverflowButton.setOnClickListener(this); - mManageVideoCallConferenceButton = (ImageButton) parent.findViewById( - R.id.manageVideoCallConferenceButton); - mManageVideoCallConferenceButton.setOnClickListener(this); - return parent; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // set the buttons - updateAudioButtons(); - } - - @Override - public void onResume() { - if (getPresenter() != null) { - getPresenter().refreshMuteState(); - } - super.onResume(); - - updateColors(); - } - - @Override - public void onClick(View view) { - int id = view.getId(); - Log.d(this, "onClick(View " + view + ", id " + id + ")..."); - - if (id == R.id.audioButton) { - onAudioButtonClicked(); - } else if (id == R.id.addButton) { - getPresenter().addCallClicked(); - } else if (id == R.id.muteButton) { - getPresenter().muteClicked(!mMuteButton.isSelected()); - } else if (id == R.id.mergeButton) { - getPresenter().mergeClicked(); - mMergeButton.setEnabled(false); - } else if (id == R.id.holdButton) { - getPresenter().holdClicked(!mHoldButton.isSelected()); - } else if (id == R.id.swapButton) { - getPresenter().swapClicked(); - } else if (id == R.id.dialpadButton) { - getPresenter().showDialpadClicked(!mShowDialpadButton.isSelected()); - } else if (id == R.id.changeToVideoButton) { - getPresenter().changeToVideoClicked(); - } else if (id == R.id.changeToVoiceButton) { - getPresenter().changeToVoiceClicked(); - } else if (id == R.id.switchCameraButton) { - getPresenter().switchCameraClicked( - mSwitchCameraButton.isSelected() /* useFrontFacingCamera */); - } else if (id == R.id.pauseVideoButton) { - getPresenter().pauseVideoClicked( - !mPauseVideoButton.isSelected() /* pause */); - } else if (id == R.id.overflowButton) { - if (mOverflowPopup != null) { - mOverflowPopup.show(); - } - } else if (id == R.id.manageVideoCallConferenceButton) { - onManageVideoCallConferenceClicked(); - } else { - Log.wtf(this, "onClick: unexpected"); - return; - } - - view.performHapticFeedback( - HapticFeedbackConstants.VIRTUAL_KEY, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); - } - - public void updateColors() { - MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); - - if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { - return; - } - - View[] compoundButtons = { - mAudioButton, - mMuteButton, - mShowDialpadButton, - mHoldButton, - mSwitchCameraButton, - mPauseVideoButton - }; - - for (View button : compoundButtons) { - final LayerDrawable layers = (LayerDrawable) button.getBackground(); - final RippleDrawable btnCompoundDrawable = compoundBackgroundDrawable(themeColors); - layers.setDrawableByLayerId(R.id.compoundBackgroundItem, btnCompoundDrawable); - } - - ImageButton[] normalButtons = { - mSwapButton, - mChangeToVideoButton, - mChangeToVoiceButton, - mAddCallButton, - mMergeButton, - mOverflowButton - }; - - for (ImageButton button : normalButtons) { - final LayerDrawable layers = (LayerDrawable) button.getBackground(); - final RippleDrawable btnDrawable = backgroundDrawable(themeColors); - layers.setDrawableByLayerId(R.id.backgroundItem, btnDrawable); - } - - mCurrentThemeColors = themeColors; - } - - /** - * Generate a RippleDrawable which will be the background for a compound button, i.e. - * a button with pressed and unpressed states. The unpressed state will be the same color - * as the rest of the call card, the pressed state will be the dark version of that color. - */ - private RippleDrawable compoundBackgroundDrawable(MaterialPalette palette) { - Resources res = getResources(); - ColorStateList rippleColor = - ColorStateList.valueOf(res.getColor(R.color.incall_accent_color)); - - StateListDrawable stateListDrawable = new StateListDrawable(); - addSelectedAndFocused(res, stateListDrawable); - addFocused(res, stateListDrawable); - addSelected(res, stateListDrawable, palette); - addUnselected(res, stateListDrawable, palette); - - return new RippleDrawable(rippleColor, stateListDrawable, null); - } - - /** - * Generate a RippleDrawable which will be the background of a button to ensure it - * is the same color as the rest of the call card. - */ - private RippleDrawable backgroundDrawable(MaterialPalette palette) { - Resources res = getResources(); - ColorStateList rippleColor = - ColorStateList.valueOf(res.getColor(R.color.incall_accent_color)); - - StateListDrawable stateListDrawable = new StateListDrawable(); - addFocused(res, stateListDrawable); - addUnselected(res, stateListDrawable, palette); - - return new RippleDrawable(rippleColor, stateListDrawable, null); - } - - // state_selected and state_focused - private void addSelectedAndFocused(Resources res, StateListDrawable drawable) { - int[] selectedAndFocused = {android.R.attr.state_selected, android.R.attr.state_focused}; - Drawable selectedAndFocusedDrawable = res.getDrawable(R.drawable.btn_selected_focused); - drawable.addState(selectedAndFocused, selectedAndFocusedDrawable); - } - - // state_focused - private void addFocused(Resources res, StateListDrawable drawable) { - int[] focused = {android.R.attr.state_focused}; - Drawable focusedDrawable = res.getDrawable(R.drawable.btn_unselected_focused); - drawable.addState(focused, focusedDrawable); - } - - // state_selected - private void addSelected(Resources res, StateListDrawable drawable, MaterialPalette palette) { - int[] selected = {android.R.attr.state_selected}; - LayerDrawable selectedDrawable = (LayerDrawable) res.getDrawable(R.drawable.btn_selected); - ((GradientDrawable) selectedDrawable.getDrawable(0)).setColor(palette.mSecondaryColor); - drawable.addState(selected, selectedDrawable); - } - - // default - private void addUnselected(Resources res, StateListDrawable drawable, MaterialPalette palette) { - LayerDrawable unselectedDrawable = - (LayerDrawable) res.getDrawable(R.drawable.btn_unselected); - ((GradientDrawable) unselectedDrawable.getDrawable(0)).setColor(palette.mPrimaryColor); - drawable.addState(new int[0], unselectedDrawable); - } - - @Override - public void setEnabled(boolean isEnabled) { - mIsEnabled = isEnabled; - - mAudioButton.setEnabled(isEnabled); - mMuteButton.setEnabled(isEnabled); - mShowDialpadButton.setEnabled(isEnabled); - mHoldButton.setEnabled(isEnabled); - mSwapButton.setEnabled(isEnabled); - mChangeToVideoButton.setEnabled(isEnabled); - mChangeToVoiceButton.setEnabled(isEnabled); - mSwitchCameraButton.setEnabled(isEnabled); - mAddCallButton.setEnabled(isEnabled); - mMergeButton.setEnabled(isEnabled); - mPauseVideoButton.setEnabled(isEnabled); - mOverflowButton.setEnabled(isEnabled); - mManageVideoCallConferenceButton.setEnabled(isEnabled); - } - - @Override - public void showButton(int buttonId, boolean show) { - mButtonVisibilityMap.put(buttonId, show ? BUTTON_VISIBLE : BUTTON_HIDDEN); - } - - @Override - public void enableButton(int buttonId, boolean enable) { - final View button = getButtonById(buttonId); - if (button != null) { - button.setEnabled(enable); - } - } - - private View getButtonById(int id) { - if (id == BUTTON_AUDIO) { - return mAudioButton; - } else if (id == BUTTON_MUTE) { - return mMuteButton; - } else if (id == BUTTON_DIALPAD) { - return mShowDialpadButton; - } else if (id == BUTTON_HOLD) { - return mHoldButton; - } else if (id == BUTTON_SWAP) { - return mSwapButton; - } else if (id == BUTTON_UPGRADE_TO_VIDEO) { - return mChangeToVideoButton; - } else if (id == BUTTON_DOWNGRADE_TO_AUDIO) { - return mChangeToVoiceButton; - } else if (id == BUTTON_SWITCH_CAMERA) { - return mSwitchCameraButton; - } else if (id == BUTTON_ADD_CALL) { - return mAddCallButton; - } else if (id == BUTTON_MERGE) { - return mMergeButton; - } else if (id == BUTTON_PAUSE_VIDEO) { - return mPauseVideoButton; - } else if (id == BUTTON_MANAGE_VIDEO_CONFERENCE) { - return mManageVideoCallConferenceButton; - } else { - Log.w(this, "Invalid button id"); - return null; - } - } - - @Override - public void setHold(boolean value) { - if (mHoldButton.isSelected() != value) { - mHoldButton.setSelected(value); - mHoldButton.setContentDescription(getContext().getString( - value ? R.string.onscreenHoldText_selected - : R.string.onscreenHoldText_unselected)); - } - } - - @Override - public void setCameraSwitched(boolean isBackFacingCamera) { - mSwitchCameraButton.setSelected(isBackFacingCamera); - } - - @Override - public void setVideoPaused(boolean isVideoPaused) { - mPauseVideoButton.setSelected(isVideoPaused); - - if (isVideoPaused) { - mPauseVideoButton.setContentDescription(getText(R.string.onscreenTurnOnCameraText)); - } else { - mPauseVideoButton.setContentDescription(getText(R.string.onscreenTurnOffCameraText)); - } - } - - @Override - public void setMute(boolean value) { - if (mMuteButton.isSelected() != value) { - mMuteButton.setSelected(value); - mMuteButton.setContentDescription(getContext().getString( - value ? R.string.onscreenMuteText_selected - : R.string.onscreenMuteText_unselected)); - } - } - - private void addToOverflowMenu(int id, View button, PopupMenu menu) { - button.setVisibility(View.GONE); - menu.getMenu().add(Menu.NONE, id, Menu.NONE, button.getContentDescription()); - mButtonVisibilityMap.put(id, BUTTON_MENU); - } - - private PopupMenu getPopupMenu() { - return new PopupMenu(new ContextThemeWrapper(getActivity(), R.style.InCallPopupMenuStyle), - mOverflowButton); - } - - /** - * Iterates through the list of buttons and toggles their visibility depending on the - * setting configured by the CallButtonPresenter. If there are more visible buttons than - * the allowed maximum, the excess buttons are collapsed into a single overflow menu. - */ - @Override - public void updateButtonStates() { - View prevVisibleButton = null; - int prevVisibleId = -1; - PopupMenu menu = null; - int visibleCount = 0; - for (int i = 0; i < BUTTON_COUNT; i++) { - final int visibility = mButtonVisibilityMap.get(i); - final View button = getButtonById(i); - if (visibility == BUTTON_VISIBLE) { - visibleCount++; - if (visibleCount <= mButtonMaxVisible) { - button.setVisibility(View.VISIBLE); - prevVisibleButton = button; - prevVisibleId = i; - } else { - if (menu == null) { - menu = getPopupMenu(); - } - // Collapse the current button into the overflow menu. If is the first visible - // button that exceeds the threshold, also collapse the previous visible button - // so that the total number of visible buttons will never exceed the threshold. - if (prevVisibleButton != null) { - addToOverflowMenu(prevVisibleId, prevVisibleButton, menu); - prevVisibleButton = null; - prevVisibleId = -1; - } - addToOverflowMenu(i, button, menu); - } - } else if (visibility == BUTTON_HIDDEN) { - button.setVisibility(View.GONE); - } - } - - mOverflowButton.setVisibility(menu != null ? View.VISIBLE : View.GONE); - if (menu != null) { - mOverflowPopup = menu; - mOverflowPopup.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - final int id = item.getItemId(); - getButtonById(id).performClick(); - return true; - } - }); - } - } - - @Override - public void setAudio(int mode) { - updateAudioButtons(); - refreshAudioModePopup(); - - if (mPrevAudioMode != mode) { - updateAudioButtonContentDescription(mode); - mPrevAudioMode = mode; - } - } - - @Override - public void setSupportedAudio(int modeMask) { - updateAudioButtons(); - refreshAudioModePopup(); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - Log.d(this, "- onMenuItemClick: " + item); - Log.d(this, " id: " + item.getItemId()); - Log.d(this, " title: '" + item.getTitle() + "'"); - - int mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; - int resId = item.getItemId(); - - if (resId == R.id.audio_mode_speaker) { - mode = CallAudioState.ROUTE_SPEAKER; - } else if (resId == R.id.audio_mode_earpiece || resId == R.id.audio_mode_wired_headset) { - // InCallCallAudioState.ROUTE_EARPIECE means either the handset earpiece, - // or the wired headset (if connected.) - mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; - } else if (resId == R.id.audio_mode_bluetooth) { - mode = CallAudioState.ROUTE_BLUETOOTH; - } else { - Log.e(this, "onMenuItemClick: unexpected View ID " + item.getItemId() - + " (MenuItem = '" + item + "')"); - } - - getPresenter().setAudioMode(mode); - - return true; - } - - // PopupMenu.OnDismissListener implementation; see showAudioModePopup(). - // This gets called when the PopupMenu gets dismissed for *any* reason, like - // the user tapping outside its bounds, or pressing Back, or selecting one - // of the menu items. - @Override - public void onDismiss(PopupMenu menu) { - Log.d(this, "- onDismiss: " + menu); - mAudioModePopupVisible = false; - updateAudioButtons(); - } - - /** - * Checks for supporting modes. If bluetooth is supported, it uses the audio - * pop up menu. Otherwise, it toggles the speakerphone. - */ - private void onAudioButtonClicked() { - Log.d(this, "onAudioButtonClicked: " + - CallAudioState.audioRouteToString(getPresenter().getSupportedAudio())); - - if (isSupported(CallAudioState.ROUTE_BLUETOOTH)) { - showAudioModePopup(); - } else { - getPresenter().toggleSpeakerphone(); - } - } - - private void onManageVideoCallConferenceClicked() { - Log.d(this, "onManageVideoCallConferenceClicked"); - InCallPresenter.getInstance().showConferenceCallManager(true); - } - - /** - * Refreshes the "Audio mode" popup if it's visible. This is useful - * (for example) when a wired headset is plugged or unplugged, - * since we need to switch back and forth between the "earpiece" - * and "wired headset" items. - * - * This is safe to call even if the popup is already dismissed, or even if - * you never called showAudioModePopup() in the first place. - */ - public void refreshAudioModePopup() { - if (mAudioModePopup != null && mAudioModePopupVisible) { - // Dismiss the previous one - mAudioModePopup.dismiss(); // safe even if already dismissed - // And bring up a fresh PopupMenu - showAudioModePopup(); - } - } - - /** - * Updates the audio button so that the appriopriate visual layers - * are visible based on the supported audio formats. - */ - private void updateAudioButtons() { - final boolean bluetoothSupported = isSupported(CallAudioState.ROUTE_BLUETOOTH); - final boolean speakerSupported = isSupported(CallAudioState.ROUTE_SPEAKER); - - boolean audioButtonEnabled = false; - boolean audioButtonChecked = false; - boolean showMoreIndicator = false; - - boolean showBluetoothIcon = false; - boolean showSpeakerphoneIcon = false; - boolean showHandsetIcon = false; - - boolean showToggleIndicator = false; - - if (bluetoothSupported) { - Log.d(this, "updateAudioButtons - popup menu mode"); - - audioButtonEnabled = true; - audioButtonChecked = true; - showMoreIndicator = true; - - // Update desired layers: - if (isAudio(CallAudioState.ROUTE_BLUETOOTH)) { - showBluetoothIcon = true; - } else if (isAudio(CallAudioState.ROUTE_SPEAKER)) { - showSpeakerphoneIcon = true; - } else { - showHandsetIcon = true; - // TODO: if a wired headset is plugged in, that takes precedence - // over the handset earpiece. If so, maybe we should show some - // sort of "wired headset" icon here instead of the "handset - // earpiece" icon. (Still need an asset for that, though.) - } - - // The audio button is NOT a toggle in this state, so set selected to false. - mAudioButton.setSelected(false); - } else if (speakerSupported) { - Log.d(this, "updateAudioButtons - speaker toggle mode"); - - audioButtonEnabled = true; - - // The audio button *is* a toggle in this state, and indicated the - // current state of the speakerphone. - audioButtonChecked = isAudio(CallAudioState.ROUTE_SPEAKER); - mAudioButton.setSelected(audioButtonChecked); - - // update desired layers: - showToggleIndicator = true; - showSpeakerphoneIcon = true; - } else { - Log.d(this, "updateAudioButtons - disabled..."); - - // The audio button is a toggle in this state, but that's mostly - // irrelevant since it's always disabled and unchecked. - audioButtonEnabled = false; - audioButtonChecked = false; - mAudioButton.setSelected(false); - - // update desired layers: - showToggleIndicator = true; - showSpeakerphoneIcon = true; - } - - // Finally, update it all! - - Log.v(this, "audioButtonEnabled: " + audioButtonEnabled); - Log.v(this, "audioButtonChecked: " + audioButtonChecked); - Log.v(this, "showMoreIndicator: " + showMoreIndicator); - Log.v(this, "showBluetoothIcon: " + showBluetoothIcon); - Log.v(this, "showSpeakerphoneIcon: " + showSpeakerphoneIcon); - Log.v(this, "showHandsetIcon: " + showHandsetIcon); - - // Only enable the audio button if the fragment is enabled. - mAudioButton.setEnabled(audioButtonEnabled && mIsEnabled); - mAudioButton.setChecked(audioButtonChecked); - - final LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground(); - Log.d(this, "'layers' drawable: " + layers); - - layers.findDrawableByLayerId(R.id.compoundBackgroundItem) - .setAlpha(showToggleIndicator ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.moreIndicatorItem) - .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.bluetoothItem) - .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.handsetItem) - .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.speakerphoneItem) - .setAlpha(showSpeakerphoneIcon ? VISIBLE : HIDDEN); - - } - - /** - * Update the content description of the audio button. - */ - private void updateAudioButtonContentDescription(int mode) { - int stringId = 0; - - // If bluetooth is not supported, the audio buttion will toggle, so use the label "speaker". - // Otherwise, use the label of the currently selected audio mode. - if (!isSupported(CallAudioState.ROUTE_BLUETOOTH)) { - stringId = R.string.audio_mode_speaker; - } else { - switch (mode) { - case CallAudioState.ROUTE_EARPIECE: - stringId = R.string.audio_mode_earpiece; - break; - case CallAudioState.ROUTE_BLUETOOTH: - stringId = R.string.audio_mode_bluetooth; - break; - case CallAudioState.ROUTE_WIRED_HEADSET: - stringId = R.string.audio_mode_wired_headset; - break; - case CallAudioState.ROUTE_SPEAKER: - stringId = R.string.audio_mode_speaker; - break; - } - } - - if (stringId != 0) { - mAudioButton.setContentDescription(getResources().getString(stringId)); - } - } - - private void showAudioModePopup() { - Log.d(this, "showAudioPopup()..."); - - final ContextThemeWrapper contextWrapper = new ContextThemeWrapper(getActivity(), - R.style.InCallPopupMenuStyle); - mAudioModePopup = new PopupMenu(contextWrapper, mAudioButton /* anchorView */); - mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu, - mAudioModePopup.getMenu()); - mAudioModePopup.setOnMenuItemClickListener(this); - mAudioModePopup.setOnDismissListener(this); - - final Menu menu = mAudioModePopup.getMenu(); - - // TODO: Still need to have the "currently active" audio mode come - // up pre-selected (or focused?) with a blue highlight. Still - // need exact visual design, and possibly framework support for this. - // See comments below for the exact logic. - - final MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker); - speakerItem.setEnabled(isSupported(CallAudioState.ROUTE_SPEAKER)); - // TODO: Show speakerItem as initially "selected" if - // speaker is on. - - // We display *either* "earpiece" or "wired headset", never both, - // depending on whether a wired headset is physically plugged in. - final MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece); - final MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset); - - final boolean usingHeadset = isSupported(CallAudioState.ROUTE_WIRED_HEADSET); - earpieceItem.setVisible(!usingHeadset); - earpieceItem.setEnabled(!usingHeadset); - wiredHeadsetItem.setVisible(usingHeadset); - wiredHeadsetItem.setEnabled(usingHeadset); - // TODO: Show the above item (either earpieceItem or wiredHeadsetItem) - // as initially "selected" if speakerOn and - // bluetoothIndicatorOn are both false. - - final MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth); - bluetoothItem.setEnabled(isSupported(CallAudioState.ROUTE_BLUETOOTH)); - // TODO: Show bluetoothItem as initially "selected" if - // bluetoothIndicatorOn is true. - - mAudioModePopup.show(); - - // Unfortunately we need to manually keep track of the popup menu's - // visiblity, since PopupMenu doesn't have an isShowing() method like - // Dialogs do. - mAudioModePopupVisible = true; - } - - private boolean isSupported(int mode) { - return (mode == (getPresenter().getSupportedAudio() & mode)); - } - - private boolean isAudio(int mode) { - return (mode == getPresenter().getAudioMode()); - } - - @Override - public void displayDialpad(boolean value, boolean animate) { - if (getActivity() != null && getActivity() instanceof InCallActivity) { - boolean changed = ((InCallActivity) getActivity()).showDialpadFragment(value, animate); - if (changed) { - mShowDialpadButton.setSelected(value); - mShowDialpadButton.setContentDescription(getContext().getString( - value /* show */ ? R.string.onscreenShowDialpadText_unselected - : R.string.onscreenShowDialpadText_selected)); - } - } - } - - @Override - public boolean isDialpadVisible() { - if (getActivity() != null && getActivity() instanceof InCallActivity) { - return ((InCallActivity) getActivity()).isDialpadVisible(); - } - return false; - } - - @Override - public Context getContext() { - return getActivity(); - } -} diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java deleted file mode 100644 index defafda99a2ad1c1fc876ec4c1e69bc97792d705..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java +++ /dev/null @@ -1,486 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_CALL; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DIALPAD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DOWNGRADE_TO_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HOLD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MERGE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MUTE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_PAUSE_VIDEO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWAP; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWITCH_CAMERA; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_UPGRADE_TO_VIDEO; - -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.telecom.CallAudioState; -import android.telecom.InCallService.VideoCall; -import android.telecom.VideoProfile; - -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.compat.SdkVersionOverride; -import com.android.dialer.compat.UserManagerCompat; -import com.android.incallui.AudioModeProvider.AudioModeListener; -import com.android.incallui.InCallCameraManager.Listener; -import com.android.incallui.InCallPresenter.CanAddCallListener; -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; - -/** - * Logic for call buttons. - */ -public class CallButtonPresenter extends Presenter - implements InCallStateListener, AudioModeListener, IncomingCallListener, - InCallDetailsListener, CanAddCallListener, Listener { - - private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted"; - private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state"; - - private Call mCall; - private boolean mAutomaticallyMuted = false; - private boolean mPreviousMuteState = false; - - public CallButtonPresenter() { - } - - @Override - public void onUiReady(CallButtonUi ui) { - super.onUiReady(ui); - - AudioModeProvider.getInstance().addListener(this); - - // register for call state changes last - final InCallPresenter inCallPresenter = InCallPresenter.getInstance(); - inCallPresenter.addListener(this); - inCallPresenter.addIncomingCallListener(this); - inCallPresenter.addDetailsListener(this); - inCallPresenter.addCanAddCallListener(this); - inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this); - - // Update the buttons state immediately for the current call - onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), - CallList.getInstance()); - } - - @Override - public void onUiUnready(CallButtonUi ui) { - super.onUiUnready(ui); - - InCallPresenter.getInstance().removeListener(this); - AudioModeProvider.getInstance().removeListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - InCallPresenter.getInstance().removeDetailsListener(this); - InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this); - InCallPresenter.getInstance().removeCanAddCallListener(this); - } - - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - CallButtonUi ui = getUi(); - - if (newState == InCallState.OUTGOING) { - mCall = callList.getOutgoingCall(); - } else if (newState == InCallState.INCALL) { - mCall = callList.getActiveOrBackgroundCall(); - - // When connected to voice mail, automatically shows the dialpad. - // (On previous releases we showed it when in-call shows up, before waiting for - // OUTGOING. We may want to do that once we start showing "Voice mail" label on - // the dialpad too.) - if (ui != null) { - if (oldState == InCallState.OUTGOING && mCall != null) { - if (CallerInfoUtils.isVoiceMailNumber(ui.getContext(), mCall)) { - ui.displayDialpad(true /* show */, true /* animate */); - } - } - } - } else if (newState == InCallState.INCOMING) { - if (ui != null) { - ui.displayDialpad(false /* show */, true /* animate */); - } - mCall = callList.getIncomingCall(); - } else { - mCall = null; - } - updateUi(newState, mCall); - } - - /** - * Updates the user interface in response to a change in the details of a call. - * Currently handles changes to the call buttons in response to a change in the details for a - * call. This is important to ensure changes to the active call are reflected in the available - * buttons. - * - * @param call The active call. - * @param details The call details. - */ - @Override - public void onDetailsChanged(Call call, android.telecom.Call.Details details) { - // Only update if the changes are for the currently active call - if (getUi() != null && call != null && call.equals(mCall)) { - updateButtonsState(call); - } - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - onStateChange(oldState, newState, CallList.getInstance()); - } - - @Override - public void onCanAddCallChanged(boolean canAddCall) { - if (getUi() != null && mCall != null) { - updateButtonsState(mCall); - } - } - - @Override - public void onAudioMode(int mode) { - if (getUi() != null) { - getUi().setAudio(mode); - } - } - - @Override - public void onSupportedAudioMode(int mask) { - if (getUi() != null) { - getUi().setSupportedAudio(mask); - } - } - - @Override - public void onMute(boolean muted) { - if (getUi() != null && !mAutomaticallyMuted) { - getUi().setMute(muted); - } - } - - public int getAudioMode() { - return AudioModeProvider.getInstance().getAudioMode(); - } - - public int getSupportedAudio() { - return AudioModeProvider.getInstance().getSupportedModes(); - } - - public void setAudioMode(int mode) { - - // TODO: Set a intermediate state in this presenter until we get - // an update for onAudioMode(). This will make UI response immediate - // if it turns out to be slow - - Log.d(this, "Sending new Audio Mode: " + CallAudioState.audioRouteToString(mode)); - TelecomAdapter.getInstance().setAudioRoute(mode); - } - - /** - * Function assumes that bluetooth is not supported. - */ - public void toggleSpeakerphone() { - // this function should not be called if bluetooth is available - if (0 != (CallAudioState.ROUTE_BLUETOOTH & getSupportedAudio())) { - - // It's clear the UI is wrong, so update the supported mode once again. - Log.e(this, "toggling speakerphone not allowed when bluetooth supported."); - getUi().setSupportedAudio(getSupportedAudio()); - return; - } - - int newMode = CallAudioState.ROUTE_SPEAKER; - - // if speakerphone is already on, change to wired/earpiece - if (getAudioMode() == CallAudioState.ROUTE_SPEAKER) { - newMode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; - } - - setAudioMode(newMode); - } - - public void muteClicked(boolean checked) { - Log.d(this, "turning on mute: " + checked); - TelecomAdapter.getInstance().mute(checked); - } - - public void holdClicked(boolean checked) { - if (mCall == null) { - return; - } - if (checked) { - Log.i(this, "Putting the call on hold: " + mCall); - TelecomAdapter.getInstance().holdCall(mCall.getId()); - } else { - Log.i(this, "Removing the call from hold: " + mCall); - TelecomAdapter.getInstance().unholdCall(mCall.getId()); - } - } - - public void swapClicked() { - if (mCall == null) { - return; - } - - Log.i(this, "Swapping the call: " + mCall); - TelecomAdapter.getInstance().swap(mCall.getId()); - } - - public void mergeClicked() { - TelecomAdapter.getInstance().merge(mCall.getId()); - } - - public void addCallClicked() { - // Automatically mute the current call - mAutomaticallyMuted = true; - mPreviousMuteState = AudioModeProvider.getInstance().getMute(); - // Simulate a click on the mute button - muteClicked(true); - TelecomAdapter.getInstance().addCall(); - } - - public void changeToVoiceClicked() { - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - - VideoProfile videoProfile = new VideoProfile(VideoProfile.STATE_AUDIO_ONLY); - videoCall.sendSessionModifyRequest(videoProfile); - } - - public void showDialpadClicked(boolean checked) { - Log.v(this, "Show dialpad " + String.valueOf(checked)); - getUi().displayDialpad(checked /* show */, true /* animate */); - } - - public void changeToVideoClicked() { - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - int currVideoState = mCall.getVideoState(); - int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState); - currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL; - - VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState); - videoCall.sendSessionModifyRequest(videoProfile); - mCall.setSessionModificationState(Call.SessionModificationState.WAITING_FOR_RESPONSE); - } - - /** - * Switches the camera between the front-facing and back-facing camera. - * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or - * false if we should switch to using the back-facing camera. - */ - public void switchCameraClicked(boolean useFrontFacingCamera) { - InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); - cameraManager.setUseFrontFacingCamera(useFrontFacingCamera); - - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - - String cameraId = cameraManager.getActiveCameraId(); - if (cameraId != null) { - final int cameraDir = cameraManager.isUsingFrontFacingCamera() - ? Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING - : Call.VideoSettings.CAMERA_DIRECTION_BACK_FACING; - mCall.getVideoSettings().setCameraDir(cameraDir); - videoCall.setCamera(cameraId); - videoCall.requestCameraCapabilities(); - } - } - - - /** - * Stop or start client's video transmission. - * @param pause True if pausing the local user's video, or false if starting the local user's - * video. - */ - public void pauseVideoClicked(boolean pause) { - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - - final int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState()); - if (pause) { - videoCall.setCamera(null); - VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState - & ~VideoProfile.STATE_TX_ENABLED); - videoCall.sendSessionModifyRequest(videoProfile); - } else { - InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - videoCall.setCamera(cameraManager.getActiveCameraId()); - VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState - | VideoProfile.STATE_TX_ENABLED); - videoCall.sendSessionModifyRequest(videoProfile); - mCall.setSessionModificationState(Call.SessionModificationState.WAITING_FOR_RESPONSE); - } - getUi().setVideoPaused(pause); - } - - private void updateUi(InCallState state, Call call) { - Log.d(this, "Updating call UI for call: ", call); - - final CallButtonUi ui = getUi(); - if (ui == null) { - return; - } - - final boolean isEnabled = - state.isConnectingOrConnected() &&!state.isIncoming() && call != null; - ui.setEnabled(isEnabled); - - if (call == null) { - return; - } - - updateButtonsState(call); - } - - /** - * Updates the buttons applicable for the UI. - * - * @param call The active call. - */ - private void updateButtonsState(Call call) { - Log.v(this, "updateButtonsState"); - final CallButtonUi ui = getUi(); - final boolean isVideo = VideoUtils.isVideoCall(call); - - // Common functionality (audio, hold, etc). - // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available: - // (1) If the device normally can hold, show HOLD in a disabled state. - // (2) If the device doesn't have the concept of hold/swap, remove the button. - final boolean showSwap = call.can( - android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); - final boolean showHold = !showSwap - && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD) - && call.can(android.telecom.Call.Details.CAPABILITY_HOLD); - final boolean isCallOnHold = call.getState() == Call.State.ONHOLD; - - final boolean showAddCall = TelecomAdapter.getInstance().canAddCall() - && UserManagerCompat.isUserUnlocked(ui.getContext()); - final boolean showMerge = call.can( - android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); - final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call); - final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call); - final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE); - - ui.showButton(BUTTON_AUDIO, true); - ui.showButton(BUTTON_SWAP, showSwap); - ui.showButton(BUTTON_HOLD, showHold); - ui.setHold(isCallOnHold); - ui.showButton(BUTTON_MUTE, showMute); - ui.showButton(BUTTON_ADD_CALL, showAddCall); - ui.showButton(BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo); - ui.showButton(BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio); - ui.showButton(BUTTON_SWITCH_CAMERA, isVideo); - ui.showButton(BUTTON_PAUSE_VIDEO, isVideo); - if (isVideo) { - getUi().setVideoPaused(!VideoUtils.isTransmissionEnabled(call)); - } - ui.showButton(BUTTON_DIALPAD, true); - ui.showButton(BUTTON_MERGE, showMerge); - - ui.updateButtonStates(); - } - - private boolean hasVideoCallCapabilities(Call call) { - if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) { - return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX) - && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX); - } - // In L, this single flag represents both video transmitting and receiving capabilities - return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX); - } - - /** - * Determines if downgrading from a video call to an audio-only call is supported. In order to - * support downgrade to audio, the SDK version must be >= N and the call should NOT have the - * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}. - * @param call The call. - * @return {@code true} if downgrading to an audio-only call from a video call is supported. - */ - private boolean isDowngradeToAudioSupported(Call call) { - return !call.can(CallSdkCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO); - } - - public void refreshMuteState() { - // Restore the previous mute state - if (mAutomaticallyMuted && - AudioModeProvider.getInstance().getMute() != mPreviousMuteState) { - if (getUi() == null) { - return; - } - muteClicked(mPreviousMuteState); - } - mAutomaticallyMuted = false; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); - outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); - } - - @Override - public void onRestoreInstanceState(Bundle savedInstanceState) { - mAutomaticallyMuted = - savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); - mPreviousMuteState = - savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); - super.onRestoreInstanceState(savedInstanceState); - } - - public interface CallButtonUi extends Ui { - void showButton(int buttonId, boolean show); - void enableButton(int buttonId, boolean enable); - void setEnabled(boolean on); - void setMute(boolean on); - void setHold(boolean on); - void setCameraSwitched(boolean isBackFacingCamera); - void setVideoPaused(boolean isPaused); - void setAudio(int mode); - void setSupportedAudio(int mask); - void displayDialpad(boolean on, boolean animate); - boolean isDialpadVisible(); - - /** - * Once showButton() has been called on each of the individual buttons in the UI, call - * this to configure the overflow menu appropriately. - */ - void updateButtonStates(); - Context getContext(); - } - - @Override - public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) { - if (getUi() == null) { - return; - } - getUi().setCameraSwitched(!isUsingFrontFacingCamera); - } -} diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java deleted file mode 100644 index c2022d18c3751451b28f2c1e64cd4d7c613b19d7..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallCardFragment.java +++ /dev/null @@ -1,1510 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.AnimationDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Trace; -import android.support.v4.graphics.drawable.RoundedBitmapDrawable; -import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; -import android.telecom.DisconnectCause; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.view.ViewGroup; -import android.view.ViewPropertyAnimator; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.contacts.common.compat.PhoneNumberUtilsCompat; -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.contacts.common.widget.FloatingActionButtonController; -import com.android.dialer.R; -import com.android.phone.common.animation.AnimUtils; - -import java.util.List; - -/** - * Fragment for call card. - */ -public class CallCardFragment extends BaseFragment - implements CallCardPresenter.CallCardUi { - private static final String TAG = "CallCardFragment"; - - /** - * Internal class which represents the call state label which is to be applied. - */ - private class CallStateLabel { - private CharSequence mCallStateLabel; - private boolean mIsAutoDismissing; - - public CallStateLabel(CharSequence callStateLabel, boolean isAutoDismissing) { - mCallStateLabel = callStateLabel; - mIsAutoDismissing = isAutoDismissing; - } - - public CharSequence getCallStateLabel() { - return mCallStateLabel; - } - - /** - * Determines if the call state label should auto-dismiss. - * - * @return {@code true} if the call state label should auto-dismiss. - */ - public boolean isAutoDismissing() { - return mIsAutoDismissing; - } - }; - - private static final String IS_DIALPAD_SHOWING_KEY = "is_dialpad_showing"; - - /** - * The duration of time (in milliseconds) a call state label should remain visible before - * resetting to its previous value. - */ - private static final long CALL_STATE_LABEL_RESET_DELAY_MS = 3000; - /** - * Amount of time to wait before sending an announcement via the accessibility manager. - * When the call state changes to an outgoing or incoming state for the first time, the - * UI can often be changing due to call updates or contact lookup. This allows the UI - * to settle to a stable state to ensure that the correct information is announced. - */ - private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS = 500; - - private AnimatorSet mAnimatorSet; - private int mShrinkAnimationDuration; - private int mFabNormalDiameter; - private int mFabSmallDiameter; - private boolean mIsLandscape; - private boolean mHasLargePhoto; - private boolean mIsDialpadShowing; - - // Primary caller info - private TextView mPhoneNumber; - private TextView mNumberLabel; - private TextView mPrimaryName; - private View mCallStateButton; - private ImageView mCallStateIcon; - private ImageView mCallStateVideoCallIcon; - private TextView mCallStateLabel; - private TextView mCallTypeLabel; - private ImageView mHdAudioIcon; - private ImageView mForwardIcon; - private ImageView mSpamIcon; - private View mCallNumberAndLabel; - private TextView mElapsedTime; - private Drawable mPrimaryPhotoDrawable; - private TextView mCallSubject; - private ImageView mWorkProfileIcon; - - // Container view that houses the entire primary call card, including the call buttons - private View mPrimaryCallCardContainer; - // Container view that houses the primary call information - private ViewGroup mPrimaryCallInfo; - private View mCallButtonsContainer; - private ImageView mPhotoSmall; - - // Secondary caller info - private View mSecondaryCallInfo; - private TextView mSecondaryCallName; - private View mSecondaryCallProviderInfo; - private TextView mSecondaryCallProviderLabel; - private View mSecondaryCallConferenceCallIcon; - private View mSecondaryCallVideoCallIcon; - private View mProgressSpinner; - - // Call card content - private View mCallCardContent; - private ImageView mPhotoLarge; - private View mContactContext; - private TextView mContactContextTitle; - private ListView mContactContextListView; - private LinearLayout mContactContextListHeaders; - - private View mManageConferenceCallButton; - - // Dark number info bar - private TextView mInCallMessageLabel; - - private FloatingActionButtonController mFloatingActionButtonController; - private View mFloatingActionButtonContainer; - private ImageButton mFloatingActionButton; - private int mFloatingActionButtonVerticalOffset; - - private float mTranslationOffset; - private Animation mPulseAnimation; - - private int mVideoAnimationDuration; - // Whether or not the call card is currently in the process of an animation - private boolean mIsAnimating; - - private MaterialPalette mCurrentThemeColors; - - /** - * Call state label to set when an auto-dismissing call state label is dismissed. - */ - private CharSequence mPostResetCallStateLabel; - private boolean mCallStateLabelResetPending = false; - private Handler mHandler; - - /** - * Determines if secondary call info is populated in the secondary call info UI. - */ - private boolean mHasSecondaryCallInfo = false; - - @Override - public CallCardPresenter.CallCardUi getUi() { - return this; - } - - @Override - public CallCardPresenter createPresenter() { - return new CallCardPresenter(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mHandler = new Handler(Looper.getMainLooper()); - mShrinkAnimationDuration = getResources().getInteger(R.integer.shrink_animation_duration); - mVideoAnimationDuration = getResources().getInteger(R.integer.video_animation_duration); - mFloatingActionButtonVerticalOffset = getResources().getDimensionPixelOffset( - R.dimen.floating_action_button_vertical_offset); - mFabNormalDiameter = getResources().getDimensionPixelOffset( - R.dimen.end_call_floating_action_button_diameter); - mFabSmallDiameter = getResources().getDimensionPixelOffset( - R.dimen.end_call_floating_action_button_small_diameter); - - if (savedInstanceState != null) { - mIsDialpadShowing = savedInstanceState.getBoolean(IS_DIALPAD_SHOWING_KEY, false); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final CallList calls = CallList.getInstance(); - final Call call = calls.getFirstCall(); - getPresenter().init(getActivity(), call); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putBoolean(IS_DIALPAD_SHOWING_KEY, mIsDialpadShowing); - super.onSaveInstanceState(outState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - Trace.beginSection(TAG + " onCreate"); - mTranslationOffset = - getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset); - final View view = inflater.inflate(R.layout.call_card_fragment, container, false); - Trace.endSection(); - return view; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - mPulseAnimation = - AnimationUtils.loadAnimation(view.getContext(), R.anim.call_status_pulse); - - mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber); - mPrimaryName = (TextView) view.findViewById(R.id.name); - mNumberLabel = (TextView) view.findViewById(R.id.label); - mSecondaryCallInfo = view.findViewById(R.id.secondary_call_info); - mSecondaryCallProviderInfo = view.findViewById(R.id.secondary_call_provider_info); - mCallCardContent = view.findViewById(R.id.call_card_content); - mPhotoLarge = (ImageView) view.findViewById(R.id.photoLarge); - mPhotoLarge.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getPresenter().onContactPhotoClick(); - } - }); - - mContactContext = view.findViewById(R.id.contact_context); - mContactContextTitle = (TextView) view.findViewById(R.id.contactContextTitle); - mContactContextListView = (ListView) view.findViewById(R.id.contactContextInfo); - // This layout stores all the list header layouts so they can be easily removed. - mContactContextListHeaders = new LinearLayout(getView().getContext()); - mContactContextListView.addHeaderView(mContactContextListHeaders); - - mCallStateIcon = (ImageView) view.findViewById(R.id.callStateIcon); - mCallStateVideoCallIcon = (ImageView) view.findViewById(R.id.videoCallIcon); - mWorkProfileIcon = (ImageView) view.findViewById(R.id.workProfileIcon); - mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel); - mHdAudioIcon = (ImageView) view.findViewById(R.id.hdAudioIcon); - mForwardIcon = (ImageView) view.findViewById(R.id.forwardIcon); - mSpamIcon = (ImageView) view.findViewById(R.id.spamIcon); - mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber); - mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel); - mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime); - mPrimaryCallCardContainer = view.findViewById(R.id.primary_call_info_container); - mPrimaryCallInfo = (ViewGroup) view.findViewById(R.id.primary_call_banner); - mCallButtonsContainer = view.findViewById(R.id.callButtonFragment); - mPhotoSmall = (ImageView) view.findViewById(R.id.photoSmall); - mPhotoSmall.setVisibility(View.GONE); - mInCallMessageLabel = (TextView) view.findViewById(R.id.connectionServiceMessage); - mProgressSpinner = view.findViewById(R.id.progressSpinner); - - mFloatingActionButtonContainer = view.findViewById( - R.id.floating_end_call_action_button_container); - mFloatingActionButton = (ImageButton) view.findViewById( - R.id.floating_end_call_action_button); - mFloatingActionButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getPresenter().endCallClicked(); - } - }); - mFloatingActionButtonController = new FloatingActionButtonController(getActivity(), - mFloatingActionButtonContainer, mFloatingActionButton); - - mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getPresenter().secondaryInfoClicked(); - updateFabPositionForSecondaryCallInfo(); - } - }); - - mCallStateButton = view.findViewById(R.id.callStateButton); - mCallStateButton.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - getPresenter().onCallStateButtonTouched(); - return false; - } - }); - - mManageConferenceCallButton = view.findViewById(R.id.manage_conference_call_button); - mManageConferenceCallButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - InCallActivity activity = (InCallActivity) getActivity(); - activity.showConferenceFragment(true); - } - }); - - mPrimaryName.setElegantTextHeight(false); - mCallStateLabel.setElegantTextHeight(false); - mCallSubject = (TextView) view.findViewById(R.id.callSubject); - } - - @Override - public void setVisible(boolean on) { - if (on) { - getView().setVisibility(View.VISIBLE); - } else { - getView().setVisibility(View.INVISIBLE); - } - } - - /** - * Hides or shows the progress spinner. - * - * @param visible {@code True} if the progress spinner should be visible. - */ - @Override - public void setProgressSpinnerVisible(boolean visible) { - mProgressSpinner.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - @Override - public void setContactContextTitle(View headerView) { - mContactContextListHeaders.removeAllViews(); - mContactContextListHeaders.addView(headerView); - } - - @Override - public void setContactContextContent(ListAdapter listAdapter) { - mContactContextListView.setAdapter(listAdapter); - } - - @Override - public void showContactContext(boolean show) { - showImageView(mPhotoLarge, !show); - showImageView(mPhotoSmall, show); - mPrimaryCallCardContainer.setElevation( - show ? 0 : getResources().getDimension(R.dimen.primary_call_elevation)); - mContactContext.setVisibility(show ? View.VISIBLE : View.GONE); - } - - /** - * Sets the visibility of the primary call card. - * Ensures that when the primary call card is hidden, the video surface slides over to fill the - * entire screen. - * - * @param visible {@code True} if the primary call card should be visible. - */ - @Override - public void setCallCardVisible(final boolean visible) { - Log.v(this, "setCallCardVisible : isVisible = " + visible); - // When animating the hide/show of the views in a landscape layout, we need to take into - // account whether we are in a left-to-right locale or a right-to-left locale and adjust - // the animations accordingly. - final boolean isLayoutRtl = InCallPresenter.isRtl(); - - // Retrieve here since at fragment creation time the incoming video view is not inflated. - final View videoView = getView().findViewById(R.id.incomingVideo); - if (videoView == null) { - return; - } - - // Determine how much space there is below or to the side of the call card. - final float spaceBesideCallCard = getSpaceBesideCallCard(); - - // We need to translate the video surface, but we need to know its position after the layout - // has occurred so use a {@code ViewTreeObserver}. - final ViewTreeObserver observer = getView().getViewTreeObserver(); - observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // We don't want to continue getting called. - getView().getViewTreeObserver().removeOnPreDrawListener(this); - - float videoViewTranslation = 0f; - - // Translate the call card to its pre-animation state. - if (!mIsLandscape) { - mPrimaryCallCardContainer.setTranslationY(visible ? - -mPrimaryCallCardContainer.getHeight() : 0); - - ViewGroup.LayoutParams p = videoView.getLayoutParams(); - videoViewTranslation = p.height / 2 - spaceBesideCallCard / 2; - } - - // Perform animation of video view. - ViewPropertyAnimator videoViewAnimator = videoView.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mVideoAnimationDuration); - if (mIsLandscape) { - videoViewAnimator - .translationX(visible ? videoViewTranslation : 0); - } else { - videoViewAnimator - .translationY(visible ? videoViewTranslation : 0); - } - videoViewAnimator.start(); - - // Animate the call card sliding. - ViewPropertyAnimator callCardAnimator = mPrimaryCallCardContainer.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mVideoAnimationDuration) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - if (!visible) { - mPrimaryCallCardContainer.setVisibility(View.GONE); - } - } - - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - if (visible) { - mPrimaryCallCardContainer.setVisibility(View.VISIBLE); - } - } - }); - - if (mIsLandscape) { - float translationX = mPrimaryCallCardContainer.getWidth(); - translationX *= isLayoutRtl ? 1 : -1; - callCardAnimator - .translationX(visible ? 0 : translationX) - .start(); - } else { - callCardAnimator - .translationY(visible ? 0 : -mPrimaryCallCardContainer.getHeight()) - .start(); - } - - return true; - } - }); - } - - /** - * Determines the amount of space below the call card for portrait layouts), or beside the - * call card for landscape layouts. - * - * @return The amount of space below or beside the call card. - */ - public float getSpaceBesideCallCard() { - if (mIsLandscape) { - return getView().getWidth() - mPrimaryCallCardContainer.getWidth(); - } else { - final int callCardHeight; - // Retrieve the actual height of the call card, independent of whether or not the - // outgoing call animation is in progress. The animation does not run in landscape mode - // so this only needs to be done for portrait. - if (mPrimaryCallCardContainer.getTag(R.id.view_tag_callcard_actual_height) != null) { - callCardHeight = (int) mPrimaryCallCardContainer.getTag( - R.id.view_tag_callcard_actual_height); - } else { - callCardHeight = mPrimaryCallCardContainer.getHeight(); - } - return getView().getHeight() - callCardHeight; - } - } - - @Override - public void setPrimaryName(String name, boolean nameIsNumber) { - if (TextUtils.isEmpty(name)) { - mPrimaryName.setText(null); - } else { - mPrimaryName.setText(nameIsNumber - ? PhoneNumberUtilsCompat.createTtsSpannable(name) - : name); - - // Set direction of the name field - int nameDirection = View.TEXT_DIRECTION_INHERIT; - if (nameIsNumber) { - nameDirection = View.TEXT_DIRECTION_LTR; - } - mPrimaryName.setTextDirection(nameDirection); - } - } - - /** - * Sets the primary image for the contact photo. - * - * @param image The drawable to set. - * @param isVisible Whether the contact photo should be visible after being set. - */ - @Override - public void setPrimaryImage(Drawable image, boolean isVisible) { - if (image != null) { - setDrawableToImageViews(image); - showImageView(mPhotoLarge, isVisible); - } - } - - @Override - public void setPrimaryPhoneNumber(String number) { - // Set the number - if (TextUtils.isEmpty(number)) { - mPhoneNumber.setText(null); - mPhoneNumber.setVisibility(View.GONE); - } else { - mPhoneNumber.setText(PhoneNumberUtilsCompat.createTtsSpannable(number)); - mPhoneNumber.setVisibility(View.VISIBLE); - mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR); - } - } - - @Override - public void setPrimaryLabel(String label) { - if (!TextUtils.isEmpty(label)) { - mNumberLabel.setText(label); - mNumberLabel.setVisibility(View.VISIBLE); - } else { - mNumberLabel.setVisibility(View.GONE); - } - - } - - /** - * Sets the primary caller information. - * - * @param number The caller phone number. - * @param name The caller name. - * @param nameIsNumber {@code true} if the name should be shown in place of the phone number. - * @param label The label. - * @param photo The contact photo drawable. - * @param isSipCall {@code true} if this is a SIP call. - * @param isContactPhotoShown {@code true} if the contact photo should be shown (it will be - * updated even if it is not shown). - * @param isWorkCall Whether the call is placed through a work phone account or caller is a work - contact. - */ - @Override - public void setPrimary(String number, String name, boolean nameIsNumber, String label, - Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall) { - Log.d(this, "Setting primary call"); - // set the name field. - setPrimaryName(name, nameIsNumber); - - if (TextUtils.isEmpty(number) && TextUtils.isEmpty(label)) { - mCallNumberAndLabel.setVisibility(View.GONE); - mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); - } else { - mCallNumberAndLabel.setVisibility(View.VISIBLE); - mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); - } - - setPrimaryPhoneNumber(number); - - // Set the label (Mobile, Work, etc) - setPrimaryLabel(label); - - showInternetCallLabel(isSipCall); - - setDrawableToImageViews(photo); - showImageView(mPhotoLarge, isContactPhotoShown); - showImageView(mWorkProfileIcon, isWorkCall); - } - - @Override - public void setSecondary(boolean show, String name, boolean nameIsNumber, String label, - String providerLabel, boolean isConference, boolean isVideoCall, boolean isFullscreen) { - - if (show) { - mHasSecondaryCallInfo = true; - boolean hasProvider = !TextUtils.isEmpty(providerLabel); - initializeSecondaryCallInfo(hasProvider); - - // Do not show the secondary caller info in fullscreen mode, but ensure it is populated - // in case fullscreen mode is exited in the future. - setSecondaryInfoVisible(!isFullscreen); - - mSecondaryCallConferenceCallIcon.setVisibility(isConference ? View.VISIBLE : View.GONE); - mSecondaryCallVideoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); - - mSecondaryCallName.setText(nameIsNumber - ? PhoneNumberUtilsCompat.createTtsSpannable(name) - : name); - if (hasProvider) { - mSecondaryCallProviderLabel.setText(providerLabel); - } - - int nameDirection = View.TEXT_DIRECTION_INHERIT; - if (nameIsNumber) { - nameDirection = View.TEXT_DIRECTION_LTR; - } - mSecondaryCallName.setTextDirection(nameDirection); - } else { - mHasSecondaryCallInfo = false; - setSecondaryInfoVisible(false); - } - } - - /** - * Sets the visibility of the secondary caller info box. Note, if the {@code visible} parameter - * is passed in {@code true}, and there is no secondary caller info populated (as determined by - * {@code mHasSecondaryCallInfo}, the secondary caller info box will not be shown. - * - * @param visible {@code true} if the secondary caller info should be shown, {@code false} - * otherwise. - */ - @Override - public void setSecondaryInfoVisible(final boolean visible) { - boolean wasVisible = mSecondaryCallInfo.isShown(); - final boolean isVisible = visible && mHasSecondaryCallInfo; - Log.v(this, "setSecondaryInfoVisible: wasVisible = " + wasVisible + " isVisible = " - + isVisible); - - // If visibility didn't change, nothing to do. - if (wasVisible == isVisible) { - return; - } - - // If we are showing the secondary info, we need to show it before animating so that its - // height will be determined on layout. - if (isVisible) { - mSecondaryCallInfo.setVisibility(View.VISIBLE); - } else { - mSecondaryCallInfo.setVisibility(View.GONE); - } - - updateFabPositionForSecondaryCallInfo(); - // We need to translate the secondary caller info, but we need to know its position after - // the layout has occurred so use a {@code ViewTreeObserver}. - final ViewTreeObserver observer = getView().getViewTreeObserver(); - - observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // We don't want to continue getting called. - getView().getViewTreeObserver().removeOnPreDrawListener(this); - - // Get the height of the secondary call info now, and then re-hide the view prior - // to doing the actual animation. - int secondaryHeight = mSecondaryCallInfo.getHeight(); - if (isVisible) { - mSecondaryCallInfo.setVisibility(View.GONE); - } else { - mSecondaryCallInfo.setVisibility(View.VISIBLE); - } - Log.v(this, "setSecondaryInfoVisible: secondaryHeight = " + secondaryHeight); - - // Set the position of the secondary call info card to its starting location. - mSecondaryCallInfo.setTranslationY(visible ? secondaryHeight : 0); - - // Animate the secondary card info slide up/down as it appears and disappears. - ViewPropertyAnimator secondaryInfoAnimator = mSecondaryCallInfo.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mVideoAnimationDuration) - .translationY(isVisible ? 0 : secondaryHeight) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (!isVisible) { - mSecondaryCallInfo.setVisibility(View.GONE); - } - } - - @Override - public void onAnimationStart(Animator animation) { - if (isVisible) { - mSecondaryCallInfo.setVisibility(View.VISIBLE); - } - } - }); - secondaryInfoAnimator.start(); - - // Notify listeners of a change in the visibility of the secondary info. This is - // important when in a video call so that the video call presenter can shift the - // video preview up or down to accommodate the secondary caller info. - InCallPresenter.getInstance().notifySecondaryCallerInfoVisibilityChanged(visible, - secondaryHeight); - - return true; - } - }); - } - - @Override - public void setCallState( - int state, - int videoState, - int sessionModificationState, - DisconnectCause disconnectCause, - String connectionLabel, - Drawable callStateIcon, - String gatewayNumber, - boolean isWifi, - boolean isConference, - boolean isWorkCall) { - boolean isGatewayCall = !TextUtils.isEmpty(gatewayNumber); - CallStateLabel callStateLabel = getCallStateLabelFromState(state, videoState, - sessionModificationState, disconnectCause, connectionLabel, isGatewayCall, isWifi, - isConference, isWorkCall); - - Log.v(this, "setCallState " + callStateLabel.getCallStateLabel()); - Log.v(this, "AutoDismiss " + callStateLabel.isAutoDismissing()); - Log.v(this, "DisconnectCause " + disconnectCause.toString()); - Log.v(this, "gateway " + connectionLabel + gatewayNumber); - - // Check for video state change and update the visibility of the contact photo. The contact - // photo is hidden when the incoming video surface is shown. - // The contact photo visibility can also change in setPrimary(). - boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo(videoState, state); - mPhotoLarge.setVisibility(showContactPhoto ? View.VISIBLE : View.GONE); - - // Check if the call subject is showing -- if it is, we want to bypass showing the call - // state. - boolean isSubjectShowing = mCallSubject.getVisibility() == View.VISIBLE; - - if (TextUtils.equals(callStateLabel.getCallStateLabel(), mCallStateLabel.getText()) && - !isSubjectShowing) { - // Nothing to do if the labels are the same - if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { - mCallStateLabel.clearAnimation(); - mCallStateIcon.clearAnimation(); - } - return; - } - - if (isSubjectShowing) { - changeCallStateLabel(null); - callStateIcon = null; - } else { - // Update the call state label and icon. - setCallStateLabel(callStateLabel); - } - - if (!TextUtils.isEmpty(callStateLabel.getCallStateLabel())) { - if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { - mCallStateLabel.clearAnimation(); - } else { - mCallStateLabel.startAnimation(mPulseAnimation); - } - } else { - mCallStateLabel.clearAnimation(); - } - - if (callStateIcon != null) { - mCallStateIcon.setVisibility(View.VISIBLE); - // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is - // needed because the pulse animation operates on the view alpha. - mCallStateIcon.setAlpha(1.0f); - mCallStateIcon.setImageDrawable(callStateIcon); - - if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED - || TextUtils.isEmpty(callStateLabel.getCallStateLabel())) { - mCallStateIcon.clearAnimation(); - } else { - mCallStateIcon.startAnimation(mPulseAnimation); - } - - if (callStateIcon instanceof AnimationDrawable) { - ((AnimationDrawable) callStateIcon).start(); - } - } else { - mCallStateIcon.clearAnimation(); - - // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is - // needed because the pulse animation operates on the view alpha. - mCallStateIcon.setAlpha(0.0f); - mCallStateIcon.setVisibility(View.GONE); - } - - if (VideoUtils.isVideoCall(videoState) - || (state == Call.State.ACTIVE && sessionModificationState - == Call.SessionModificationState.WAITING_FOR_RESPONSE)) { - mCallStateVideoCallIcon.setVisibility(View.VISIBLE); - } else { - mCallStateVideoCallIcon.setVisibility(View.GONE); - } - } - - private void setCallStateLabel(CallStateLabel callStateLabel) { - Log.v(this, "setCallStateLabel : label = " + callStateLabel.getCallStateLabel()); - - if (callStateLabel.isAutoDismissing()) { - mCallStateLabelResetPending = true; - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - Log.v(this, "restoringCallStateLabel : label = " + - mPostResetCallStateLabel); - changeCallStateLabel(mPostResetCallStateLabel); - mCallStateLabelResetPending = false; - } - }, CALL_STATE_LABEL_RESET_DELAY_MS); - - changeCallStateLabel(callStateLabel.getCallStateLabel()); - } else { - // Keep track of the current call state label; used when resetting auto dismissing - // call state labels. - mPostResetCallStateLabel = callStateLabel.getCallStateLabel(); - - if (!mCallStateLabelResetPending) { - changeCallStateLabel(callStateLabel.getCallStateLabel()); - } - } - } - - private void changeCallStateLabel(CharSequence callStateLabel) { - Log.v(this, "changeCallStateLabel : label = " + callStateLabel); - if (!TextUtils.isEmpty(callStateLabel)) { - mCallStateLabel.setText(callStateLabel); - mCallStateLabel.setAlpha(1); - mCallStateLabel.setVisibility(View.VISIBLE); - } else { - Animation callStateLabelAnimation = mCallStateLabel.getAnimation(); - if (callStateLabelAnimation != null) { - callStateLabelAnimation.cancel(); - } - mCallStateLabel.setText(null); - mCallStateLabel.setAlpha(0); - mCallStateLabel.setVisibility(View.GONE); - } - } - - @Override - public void setCallbackNumber(String callbackNumber, boolean isEmergencyCall) { - if (mInCallMessageLabel == null) { - return; - } - - if (TextUtils.isEmpty(callbackNumber)) { - mInCallMessageLabel.setVisibility(View.GONE); - return; - } - - // TODO: The new Locale-specific methods don't seem to be working. Revisit this. - callbackNumber = PhoneNumberUtils.formatNumber(callbackNumber); - - int stringResourceId = isEmergencyCall ? R.string.card_title_callback_number_emergency - : R.string.card_title_callback_number; - - String text = getString(stringResourceId, callbackNumber); - mInCallMessageLabel.setText(text); - - mInCallMessageLabel.setVisibility(View.VISIBLE); - } - - /** - * Sets and shows the call subject if it is not empty. Hides the call subject otherwise. - * - * @param callSubject The call subject. - */ - @Override - public void setCallSubject(String callSubject) { - boolean showSubject = !TextUtils.isEmpty(callSubject); - - mCallSubject.setVisibility(showSubject ? View.VISIBLE : View.GONE); - if (showSubject) { - mCallSubject.setText(callSubject); - } else { - mCallSubject.setText(null); - } - } - - public boolean isAnimating() { - return mIsAnimating; - } - - private void showInternetCallLabel(boolean show) { - if (show) { - final String label = getView().getContext().getString( - R.string.incall_call_type_label_sip); - mCallTypeLabel.setVisibility(View.VISIBLE); - mCallTypeLabel.setText(label); - } else { - mCallTypeLabel.setVisibility(View.GONE); - } - } - - @Override - public void setPrimaryCallElapsedTime(boolean show, long duration) { - if (show) { - if (mElapsedTime.getVisibility() != View.VISIBLE) { - AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); - } - String callTimeElapsed = DateUtils.formatElapsedTime(duration / 1000); - mElapsedTime.setText(callTimeElapsed); - - String durationDescription = - InCallDateUtils.formatDuration(duration); - mElapsedTime.setContentDescription( - !TextUtils.isEmpty(durationDescription) ? durationDescription : null); - } else { - // hide() animation has no effect if it is already hidden. - AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION); - } - } - - /** - * Set all the ImageViews to the same photo. Currently there are 2 photo views: the large one - * (which fills about the bottom half of the screen) and the small one, which displays as a - * circle next to the primary contact info. This method does not handle whether the ImageView - * is shown or not. - * - * @param photo The photo to set for the image views. - */ - private void setDrawableToImageViews(Drawable photo) { - if (photo == null) { - photo = ContactInfoCache.getInstance(getView().getContext()) - .getDefaultContactPhotoDrawable(); - } - - if (mPrimaryPhotoDrawable == photo){ - return; - } - mPrimaryPhotoDrawable = photo; - - mPhotoLarge.setImageDrawable(photo); - - // Modify the drawable to be round for the smaller ImageView. - Bitmap bitmap = drawableToBitmap(photo); - if (bitmap != null) { - final RoundedBitmapDrawable drawable = - RoundedBitmapDrawableFactory.create(getResources(), bitmap); - drawable.setAntiAlias(true); - drawable.setCornerRadius(bitmap.getHeight() / 2); - photo = drawable; - } - mPhotoSmall.setImageDrawable(photo); - } - - /** - * Helper method for image view to handle animations. - * - * @param view The image view to show or hide. - * @param isVisible {@code true} if we want to show the image, {@code false} to hide it. - */ - private void showImageView(ImageView view, boolean isVisible) { - if (view.getDrawable() == null) { - if (isVisible) { - AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); - } - } else { - // Cross fading is buggy and not noticeable due to the multiple calls to this method - // that switch drawables in the middle of the cross-fade animations. Just show the - // photo directly instead. - view.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - } - - /** - * Converts a drawable into a bitmap. - * - * @param drawable the drawable to be converted. - */ - public static Bitmap drawableToBitmap(Drawable drawable) { - Bitmap bitmap; - if (drawable instanceof BitmapDrawable) { - bitmap = ((BitmapDrawable) drawable).getBitmap(); - } else { - if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { - // Needed for drawables that are just a colour. - bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); - } else { - bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - } - - Log.i(TAG, "Created bitmap with width " + bitmap.getWidth() + ", height " - + bitmap.getHeight()); - - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - } - return bitmap; - } - - /** - * Gets the call state label based on the state of the call or cause of disconnect. - * - * Additional labels are applied as follows: - * 1. All outgoing calls with display "Calling via [Provider]". - * 2. Ongoing calls will display the name of the provider. - * 3. Incoming calls will only display "Incoming via..." for accounts. - * 4. Video calls, and session modification states (eg. requesting video). - * 5. Incoming and active Wi-Fi calls will show label provided by hint. - * - * TODO: Move this to the CallCardPresenter. - */ - private CallStateLabel getCallStateLabelFromState(int state, int videoState, - int sessionModificationState, DisconnectCause disconnectCause, String label, - boolean isGatewayCall, boolean isWifi, boolean isConference, boolean isWorkCall) { - final Context context = getView().getContext(); - CharSequence callStateLabel = null; // Label to display as part of the call banner - - boolean hasSuggestedLabel = label != null; - boolean isAccount = hasSuggestedLabel && !isGatewayCall; - boolean isAutoDismissing = false; - - switch (state) { - case Call.State.IDLE: - // "Call state" is meaningless in this state. - break; - case Call.State.ACTIVE: - // We normally don't show a "call state label" at all in this state - // (but we can use the call state label to display the provider name). - if ((isAccount || isWifi || isConference) && hasSuggestedLabel) { - callStateLabel = label; - } else if (sessionModificationState - == Call.SessionModificationState.REQUEST_REJECTED) { - callStateLabel = context.getString(R.string.card_title_video_call_rejected); - isAutoDismissing = true; - } else if (sessionModificationState - == Call.SessionModificationState.REQUEST_FAILED) { - callStateLabel = context.getString(R.string.card_title_video_call_error); - isAutoDismissing = true; - } else if (sessionModificationState - == Call.SessionModificationState.WAITING_FOR_RESPONSE) { - callStateLabel = context.getString(R.string.card_title_video_call_requesting); - } else if (sessionModificationState - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - callStateLabel = context.getString(R.string.card_title_video_call_requesting); - } else if (VideoUtils.isVideoCall(videoState)) { - callStateLabel = context.getString(R.string.card_title_video_call); - } - break; - case Call.State.ONHOLD: - callStateLabel = context.getString(R.string.card_title_on_hold); - break; - case Call.State.CONNECTING: - case Call.State.DIALING: - if (hasSuggestedLabel && !isWifi) { - callStateLabel = context.getString(R.string.calling_via_template, label); - } else { - callStateLabel = context.getString(R.string.card_title_dialing); - } - break; - case Call.State.REDIALING: - callStateLabel = context.getString(R.string.card_title_redialing); - break; - case Call.State.INCOMING: - case Call.State.CALL_WAITING: - if (isWifi && hasSuggestedLabel) { - callStateLabel = label; - } else if (isAccount) { - callStateLabel = context.getString(R.string.incoming_via_template, label); - } else if (VideoUtils.isVideoCall(videoState)) { - callStateLabel = context.getString(R.string.notification_incoming_video_call); - } else { - callStateLabel = - context.getString(isWorkCall ? R.string.card_title_incoming_work_call - : R.string.card_title_incoming_call); - } - break; - case Call.State.DISCONNECTING: - // While in the DISCONNECTING state we display a "Hanging up" - // message in order to make the UI feel more responsive. (In - // GSM it's normal to see a delay of a couple of seconds while - // negotiating the disconnect with the network, so the "Hanging - // up" state at least lets the user know that we're doing - // something. This state is currently not used with CDMA.) - callStateLabel = context.getString(R.string.card_title_hanging_up); - break; - case Call.State.DISCONNECTED: - callStateLabel = disconnectCause.getLabel(); - if (TextUtils.isEmpty(callStateLabel)) { - callStateLabel = context.getString(R.string.card_title_call_ended); - } - break; - case Call.State.CONFERENCED: - callStateLabel = context.getString(R.string.card_title_conf_call); - break; - default: - Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state); - } - return new CallStateLabel(callStateLabel, isAutoDismissing); - } - - private void initializeSecondaryCallInfo(boolean hasProvider) { - // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccessible - // until mSecondaryCallInfo is inflated in the call above. - if (mSecondaryCallName == null) { - mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); - mSecondaryCallConferenceCallIcon = - getView().findViewById(R.id.secondaryCallConferenceCallIcon); - mSecondaryCallVideoCallIcon = - getView().findViewById(R.id.secondaryCallVideoCallIcon); - } - - if (mSecondaryCallProviderLabel == null && hasProvider) { - mSecondaryCallProviderInfo.setVisibility(View.VISIBLE); - mSecondaryCallProviderLabel = (TextView) getView() - .findViewById(R.id.secondaryCallProviderLabel); - } - } - - public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (event.getEventType() == AccessibilityEvent.TYPE_ANNOUNCEMENT) { - // Indicate this call is in active if no label is provided. The label is empty when - // the call is in active, not in other status such as onhold or dialing etc. - if (!mCallStateLabel.isShown() || TextUtils.isEmpty(mCallStateLabel.getText())) { - event.getText().add( - TextUtils.expandTemplate( - getResources().getText(R.string.accessibility_call_is_active), - mPrimaryName.getText())); - } else { - dispatchPopulateAccessibilityEvent(event, mCallStateLabel); - dispatchPopulateAccessibilityEvent(event, mPrimaryName); - dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); - dispatchPopulateAccessibilityEvent(event, mPhoneNumber); - } - return; - } - dispatchPopulateAccessibilityEvent(event, mCallStateLabel); - dispatchPopulateAccessibilityEvent(event, mPrimaryName); - dispatchPopulateAccessibilityEvent(event, mPhoneNumber); - dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); - dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); - dispatchPopulateAccessibilityEvent(event, mSecondaryCallProviderLabel); - - return; - } - - @Override - public void sendAccessibilityAnnouncement() { - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - if (getView() != null && getView().getParent() != null && - isAccessibilityEnabled(getContext())) { - AccessibilityEvent event = AccessibilityEvent.obtain( - AccessibilityEvent.TYPE_ANNOUNCEMENT); - dispatchPopulateAccessibilityEvent(event); - getView().getParent().requestSendAccessibilityEvent(getView(), event); - } - } - - private boolean isAccessibilityEnabled(Context context) { - AccessibilityManager accessibilityManager = - (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); - return accessibilityManager != null && accessibilityManager.isEnabled(); - - } - }, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS); - } - - @Override - public void setEndCallButtonEnabled(boolean enabled, boolean animate) { - if (enabled != mFloatingActionButton.isEnabled()) { - if (animate) { - if (enabled) { - mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); - } else { - mFloatingActionButtonController.scaleOut(); - } - } else { - if (enabled) { - mFloatingActionButtonContainer.setScaleX(1); - mFloatingActionButtonContainer.setScaleY(1); - mFloatingActionButtonContainer.setVisibility(View.VISIBLE); - } else { - mFloatingActionButtonContainer.setVisibility(View.GONE); - } - } - mFloatingActionButton.setEnabled(enabled); - updateFabPosition(); - } - } - - /** - * Changes the visibility of the HD audio icon. - * - * @param visible {@code true} if the UI should show the HD audio icon. - */ - @Override - public void showHdAudioIndicator(boolean visible) { - mHdAudioIcon.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - /** - * Changes the visibility of the forward icon. - * - * @param visible {@code true} if the UI should show the forward icon. - */ - @Override - public void showForwardIndicator(boolean visible) { - mForwardIcon.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - /** - * Changes the visibility of the spam icon. - * - * @param visible {@code true} if the UI should show the spam icon. - */ - @Override - public void showSpamIndicator(boolean visible) { - if (visible) { - mSpamIcon.setVisibility(View.VISIBLE); - mNumberLabel.setText(R.string.label_spam_caller); - mPhoneNumber.setVisibility(View.GONE); - } - } - - /** - * Changes the visibility of the "manage conference call" button. - * - * @param visible Whether to set the button to be visible or not. - */ - @Override - public void showManageConferenceCallButton(boolean visible) { - mManageConferenceCallButton.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - /** - * Determines the current visibility of the manage conference button. - * - * @return {@code true} if the button is visible. - */ - @Override - public boolean isManageConferenceVisible() { - return mManageConferenceCallButton.getVisibility() == View.VISIBLE; - } - - /** - * Determines the current visibility of the call subject. - * - * @return {@code true} if the subject is visible. - */ - @Override - public boolean isCallSubjectVisible() { - return mCallSubject.getVisibility() == View.VISIBLE; - } - - /** - * Get the overall InCallUI background colors and apply to call card. - */ - public void updateColors() { - MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); - - if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { - return; - } - - if (getResources().getBoolean(R.bool.is_layout_landscape)) { - final GradientDrawable drawable = - (GradientDrawable) mPrimaryCallCardContainer.getBackground(); - drawable.setColor(themeColors.mPrimaryColor); - } else { - mPrimaryCallCardContainer.setBackgroundColor(themeColors.mPrimaryColor); - } - mCallButtonsContainer.setBackgroundColor(themeColors.mPrimaryColor); - mCallSubject.setTextColor(themeColors.mPrimaryColor); - mContactContext.setBackgroundColor(themeColors.mPrimaryColor); - //TODO: set color of message text in call context "recent messages" to be the theme color. - - mCurrentThemeColors = themeColors; - } - - private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { - if (view == null) return; - final List eventText = event.getText(); - int size = eventText.size(); - view.dispatchPopulateAccessibilityEvent(event); - // if no text added write null to keep relative position - if (size == eventText.size()) { - eventText.add(null); - } - } - - @Override - public void animateForNewOutgoingCall() { - final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent(); - - final ViewTreeObserver observer = getView().getViewTreeObserver(); - - mIsAnimating = true; - - observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - final ViewTreeObserver observer = getView().getViewTreeObserver(); - if (!observer.isAlive()) { - return; - } - observer.removeOnGlobalLayoutListener(this); - - final LayoutIgnoringListener listener = new LayoutIgnoringListener(); - mPrimaryCallCardContainer.addOnLayoutChangeListener(listener); - - // Prepare the state of views before the slide animation - final int originalHeight = mPrimaryCallCardContainer.getHeight(); - mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, - originalHeight); - mPrimaryCallCardContainer.setBottom(parent.getHeight()); - - // Set up FAB. - mFloatingActionButtonContainer.setVisibility(View.GONE); - mFloatingActionButtonController.setScreenWidth(parent.getWidth()); - - mCallButtonsContainer.setAlpha(0); - mCallStateLabel.setAlpha(0); - mPrimaryName.setAlpha(0); - mCallTypeLabel.setAlpha(0); - mCallNumberAndLabel.setAlpha(0); - - assignTranslateAnimation(mCallStateLabel, 1); - assignTranslateAnimation(mCallStateIcon, 1); - assignTranslateAnimation(mPrimaryName, 2); - assignTranslateAnimation(mCallNumberAndLabel, 3); - assignTranslateAnimation(mCallTypeLabel, 4); - assignTranslateAnimation(mCallButtonsContainer, 5); - - final Animator animator = getShrinkAnimator(parent.getHeight(), originalHeight); - - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, - null); - setViewStatePostAnimation(listener); - mIsAnimating = false; - InCallPresenter.getInstance().onShrinkAnimationComplete(); - if (animator != null) { - animator.removeListener(this); - } - } - }); - animator.start(); - } - }); - } - - @Override - public void showNoteSentToast() { - Toast.makeText(getContext(), R.string.note_sent, Toast.LENGTH_LONG).show(); - } - - public void onDialpadVisibilityChange(boolean isShown) { - mIsDialpadShowing = isShown; - updateFabPosition(); - } - - private void updateFabPosition() { - int offsetY = 0; - if (!mIsDialpadShowing) { - offsetY = mFloatingActionButtonVerticalOffset; - if (mSecondaryCallInfo.isShown() && mHasLargePhoto) { - offsetY -= mSecondaryCallInfo.getHeight(); - } - } - - mFloatingActionButtonController.align( - FloatingActionButtonController.ALIGN_MIDDLE, - 0 /* offsetX */, - offsetY, - true); - - mFloatingActionButtonController.resize( - mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true); - } - - @Override - public Context getContext() { - return getActivity(); - } - - @Override - public void onResume() { - super.onResume(); - // If the previous launch animation is still running, cancel it so that we don't get - // stuck in an intermediate animation state. - if (mAnimatorSet != null && mAnimatorSet.isRunning()) { - mAnimatorSet.cancel(); - } - - mIsLandscape = getResources().getBoolean(R.bool.is_layout_landscape); - mHasLargePhoto = getResources().getBoolean(R.bool.has_large_photo); - - final ViewGroup parent = ((ViewGroup) mPrimaryCallCardContainer.getParent()); - final ViewTreeObserver observer = parent.getViewTreeObserver(); - parent.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - ViewTreeObserver viewTreeObserver = observer; - if (!viewTreeObserver.isAlive()) { - viewTreeObserver = parent.getViewTreeObserver(); - } - viewTreeObserver.removeOnGlobalLayoutListener(this); - mFloatingActionButtonController.setScreenWidth(parent.getWidth()); - updateFabPosition(); - } - }); - - updateColors(); - } - - /** - * Adds a global layout listener to update the FAB's positioning on the next layout. This allows - * us to position the FAB after the secondary call info's height has been calculated. - */ - private void updateFabPositionForSecondaryCallInfo() { - mSecondaryCallInfo.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - final ViewTreeObserver observer = mSecondaryCallInfo.getViewTreeObserver(); - if (!observer.isAlive()) { - return; - } - observer.removeOnGlobalLayoutListener(this); - - onDialpadVisibilityChange(mIsDialpadShowing); - } - }); - } - - /** - * Animator that performs the upwards shrinking animation of the blue call card scrim. - * At the start of the animation, each child view is moved downwards by a pre-specified amount - * and then translated upwards together with the scrim. - */ - private Animator getShrinkAnimator(int startHeight, int endHeight) { - final ObjectAnimator shrinkAnimator = - ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom", startHeight, endHeight); - shrinkAnimator.setDuration(mShrinkAnimationDuration); - shrinkAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mFloatingActionButton.setEnabled(true); - } - }); - shrinkAnimator.setInterpolator(AnimUtils.EASE_IN); - return shrinkAnimator; - } - - private void assignTranslateAnimation(View view, int offset) { - view.setLayerType(View.LAYER_TYPE_HARDWARE, null); - view.buildLayer(); - view.setTranslationY(mTranslationOffset * offset); - view.animate().translationY(0).alpha(1).withLayer() - .setDuration(mShrinkAnimationDuration).setInterpolator(AnimUtils.EASE_IN); - } - - private void setViewStatePostAnimation(View view) { - view.setTranslationY(0); - view.setAlpha(1); - } - - private void setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener) { - setViewStatePostAnimation(mCallButtonsContainer); - setViewStatePostAnimation(mCallStateLabel); - setViewStatePostAnimation(mPrimaryName); - setViewStatePostAnimation(mCallTypeLabel); - setViewStatePostAnimation(mCallNumberAndLabel); - setViewStatePostAnimation(mCallStateIcon); - - mPrimaryCallCardContainer.removeOnLayoutChangeListener(layoutChangeListener); - - mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); - } - - private final class LayoutIgnoringListener implements View.OnLayoutChangeListener { - @Override - public void onLayoutChange(View v, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - v.setLeft(oldLeft); - v.setRight(oldRight); - v.setTop(oldTop); - v.setBottom(oldBottom); - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java deleted file mode 100644 index 1ad0c11f17714723924a636c221af793ca5deea8..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallCardPresenter.java +++ /dev/null @@ -1,1181 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.telecom.Call.Details; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import android.telecom.StatusHints; -import android.telecom.TelecomManager; -import android.telecom.VideoProfile; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.view.View; -import android.view.accessibility.AccessibilityManager; -import android.widget.ListAdapter; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.compat.telecom.TelecomManagerCompat; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.incallui.Call.State; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; -import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallEventListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; -import com.android.incalluibind.ObjectFactory; - -import java.lang.ref.WeakReference; - -import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL; -/** - * Presenter for the Call Card Fragment. - *

- * This class listens for changes to InCallState and passes it along to the fragment. - */ -public class CallCardPresenter extends Presenter - implements InCallStateListener, IncomingCallListener, InCallDetailsListener, - InCallEventListener, CallList.CallUpdateListener, DistanceHelper.Listener { - - public interface EmergencyCallListener { - public void onCallUpdated(BaseFragment fragment, boolean isEmergency); - } - - private static final String TAG = CallCardPresenter.class.getSimpleName(); - private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000; - - private final EmergencyCallListener mEmergencyCallListener = - ObjectFactory.newEmergencyCallListener(); - private DistanceHelper mDistanceHelper; - - private Call mPrimary; - private Call mSecondary; - private ContactCacheEntry mPrimaryContactInfo; - private ContactCacheEntry mSecondaryContactInfo; - private CallTimer mCallTimer; - private Context mContext; - @Nullable private ContactsPreferences mContactsPreferences; - private boolean mSpinnerShowing = false; - private boolean mHasShownToast = false; - private InCallContactInteractions mInCallContactInteractions; - private boolean mIsFullscreen = false; - - public static class ContactLookupCallback implements ContactInfoCacheCallback { - private final WeakReference mCallCardPresenter; - private final boolean mIsPrimary; - - public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) { - mCallCardPresenter = new WeakReference(callCardPresenter); - mIsPrimary = isPrimary; - } - - @Override - public void onContactInfoComplete(String callId, ContactCacheEntry entry) { - CallCardPresenter presenter = mCallCardPresenter.get(); - if (presenter != null) { - presenter.onContactInfoComplete(callId, entry, mIsPrimary); - } - } - - @Override - public void onImageLoadComplete(String callId, ContactCacheEntry entry) { - CallCardPresenter presenter = mCallCardPresenter.get(); - if (presenter != null) { - presenter.onImageLoadComplete(callId, entry); - } - } - - @Override - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) { - CallCardPresenter presenter = mCallCardPresenter.get(); - if (presenter != null) { - presenter.onContactInteractionsInfoComplete(callId, entry); - } - } - } - - public CallCardPresenter() { - // create the call timer - mCallTimer = new CallTimer(new Runnable() { - @Override - public void run() { - updateCallTime(); - } - }); - } - - public void init(Context context, Call call) { - mContext = Preconditions.checkNotNull(context); - mDistanceHelper = ObjectFactory.newDistanceHelper(mContext, this); - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - - // Call may be null if disconnect happened already. - if (call != null) { - mPrimary = call; - if (shouldShowNoteSentToast(mPrimary)) { - final CallCardUi ui = getUi(); - if (ui != null) { - ui.showNoteSentToast(); - } - } - CallList.getInstance().addCallUpdateListener(call.getId(), this); - - // start processing lookups right away. - if (!call.isConferenceCall()) { - startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING); - } else { - updateContactEntry(null, true); - } - } - - onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance()); - } - - @Override - public void onUiReady(CallCardUi ui) { - super.onUiReady(ui); - - if (mContactsPreferences != null) { - mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); - } - - // Contact search may have completed before ui is ready. - if (mPrimaryContactInfo != null) { - updatePrimaryDisplayInfo(); - } - - // Register for call state changes last - InCallPresenter.getInstance().addListener(this); - InCallPresenter.getInstance().addIncomingCallListener(this); - InCallPresenter.getInstance().addDetailsListener(this); - InCallPresenter.getInstance().addInCallEventListener(this); - } - - @Override - public void onUiUnready(CallCardUi ui) { - super.onUiUnready(ui); - - // stop getting call state changes - InCallPresenter.getInstance().removeListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - InCallPresenter.getInstance().removeDetailsListener(this); - InCallPresenter.getInstance().removeInCallEventListener(this); - if (mPrimary != null) { - CallList.getInstance().removeCallUpdateListener(mPrimary.getId(), this); - } - - if (mDistanceHelper != null) { - mDistanceHelper.cleanUp(); - } - - mPrimary = null; - mPrimaryContactInfo = null; - mSecondaryContactInfo = null; - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - // same logic should happen as with onStateChange() - onStateChange(oldState, newState, CallList.getInstance()); - } - - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - Log.d(this, "onStateChange() " + newState); - final CallCardUi ui = getUi(); - if (ui == null) { - return; - } - - Call primary = null; - Call secondary = null; - - if (newState == InCallState.INCOMING) { - primary = callList.getIncomingCall(); - } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) { - primary = callList.getOutgoingCall(); - if (primary == null) { - primary = callList.getPendingOutgoingCall(); - } - - // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the - // highest priority call to display as the secondary call. - secondary = getCallToDisplay(callList, null, true); - } else if (newState == InCallState.INCALL) { - primary = getCallToDisplay(callList, null, false); - secondary = getCallToDisplay(callList, primary, true); - } - - if (mInCallContactInteractions != null && - (oldState == InCallState.INCOMING || newState == InCallState.INCOMING)) { - ui.showContactContext(newState != InCallState.INCOMING); - } - - Log.d(this, "Primary call: " + primary); - Log.d(this, "Secondary call: " + secondary); - - final boolean primaryChanged = !(Call.areSame(mPrimary, primary) && - Call.areSameNumber(mPrimary, primary)); - final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) && - Call.areSameNumber(mSecondary, secondary)); - - mSecondary = secondary; - Call previousPrimary = mPrimary; - mPrimary = primary; - - if (primaryChanged && shouldShowNoteSentToast(primary)) { - ui.showNoteSentToast(); - } - - // Refresh primary call information if either: - // 1. Primary call changed. - // 2. The call's ability to manage conference has changed. - // 3. The call subject should be shown or hidden. - if (shouldRefreshPrimaryInfo(primaryChanged, ui, shouldShowCallSubject(mPrimary))) { - // primary call has changed - if (previousPrimary != null) { - //clear progess spinner (if any) related to previous primary call - maybeShowProgressSpinner(previousPrimary.getState(), - Call.SessionModificationState.NO_REQUEST); - CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this); - } - CallList.getInstance().addCallUpdateListener(mPrimary.getId(), this); - - mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary, - mPrimary.getState() == Call.State.INCOMING); - updatePrimaryDisplayInfo(); - maybeStartSearch(mPrimary, true); - maybeClearSessionModificationState(mPrimary); - } - - if (previousPrimary != null && mPrimary == null) { - //clear progess spinner (if any) related to previous primary call - maybeShowProgressSpinner(previousPrimary.getState(), - Call.SessionModificationState.NO_REQUEST); - CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this); - } - - if (mSecondary == null) { - // Secondary call may have ended. Update the ui. - mSecondaryContactInfo = null; - updateSecondaryDisplayInfo(); - } else if (secondaryChanged) { - // secondary call has changed - mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary, - mSecondary.getState() == Call.State.INCOMING); - updateSecondaryDisplayInfo(); - maybeStartSearch(mSecondary, false); - maybeClearSessionModificationState(mSecondary); - } - - // Start/stop timers. - if (isPrimaryCallActive()) { - Log.d(this, "Starting the calltime timer"); - mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS); - } else { - Log.d(this, "Canceling the calltime timer"); - mCallTimer.cancel(); - ui.setPrimaryCallElapsedTime(false, 0); - } - - // Set the call state - int callState = Call.State.IDLE; - if (mPrimary != null) { - callState = mPrimary.getState(); - updatePrimaryCallState(); - } else { - getUi().setCallState( - callState, - VideoProfile.STATE_AUDIO_ONLY, - Call.SessionModificationState.NO_REQUEST, - new DisconnectCause(DisconnectCause.UNKNOWN), - null, - null, - null, - false /* isWifi */, - false /* isConference */, - false /* isWorkCall */); - getUi().showHdAudioIndicator(false); - } - - maybeShowManageConferenceCallButton(); - - // Hide the end call button instantly if we're receiving an incoming call. - getUi().setEndCallButtonEnabled(shouldShowEndCallButton(mPrimary, callState), - callState != Call.State.INCOMING /* animate */); - - maybeSendAccessibilityEvent(oldState, newState, primaryChanged); - } - - @Override - public void onDetailsChanged(Call call, Details details) { - updatePrimaryCallState(); - - if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) != - details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) { - maybeShowManageConferenceCallButton(); - } - } - - @Override - public void onCallChanged(Call call) { - // No-op; specific call updates handled elsewhere. - } - - /** - * Handles a change to the session modification state for a call. Triggers showing the progress - * spinner, as well as updating the call state label. - * - * @param sessionModificationState The new session modification state. - */ - @Override - public void onSessionModificationStateChange(int sessionModificationState) { - Log.d(this, "onSessionModificationStateChange : sessionModificationState = " + - sessionModificationState); - - if (mPrimary == null) { - return; - } - maybeShowProgressSpinner(mPrimary.getState(), sessionModificationState); - getUi().setEndCallButtonEnabled(sessionModificationState != - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST, - true /* shouldAnimate */); - updatePrimaryCallState(); - } - - /** - * Handles a change to the last forwarding number by refreshing the primary call info. - */ - @Override - public void onLastForwardedNumberChange() { - Log.v(this, "onLastForwardedNumberChange"); - - if (mPrimary == null) { - return; - } - updatePrimaryDisplayInfo(); - } - - /** - * Handles a change to the child number by refreshing the primary call info. - */ - @Override - public void onChildNumberChange() { - Log.v(this, "onChildNumberChange"); - - if (mPrimary == null) { - return; - } - updatePrimaryDisplayInfo(); - } - - private boolean shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui, - boolean shouldShowCallSubject) { - if (mPrimary == null) { - return false; - } - return primaryChanged || - ui.isManageConferenceVisible() != shouldShowManageConference() || - ui.isCallSubjectVisible() != shouldShowCallSubject; - } - - private String getSubscriptionNumber() { - // If it's an emergency call, and they're not populating the callback number, - // then try to fall back to the phone sub info (to hopefully get the SIM's - // number directly from the telephony layer). - PhoneAccountHandle accountHandle = mPrimary.getAccountHandle(); - if (accountHandle != null) { - TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); - PhoneAccount account = TelecomManagerCompat.getPhoneAccount(mgr, accountHandle); - if (account != null) { - return getNumberFromHandle(account.getSubscriptionAddress()); - } - } - return null; - } - - private void updatePrimaryCallState() { - if (getUi() != null && mPrimary != null) { - boolean isWorkCall = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL) - || (mPrimaryContactInfo == null ? false - : mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); - getUi().setCallState( - mPrimary.getState(), - mPrimary.getVideoState(), - mPrimary.getSessionModificationState(), - mPrimary.getDisconnectCause(), - getConnectionLabel(), - getCallStateIcon(), - getGatewayNumber(), - mPrimary.hasProperty(Details.PROPERTY_WIFI), - mPrimary.isConferenceCall(), - isWorkCall); - - maybeShowHdAudioIcon(); - setCallbackNumber(); - } - } - - /** - * Show the HD icon if the call is active and has {@link Details#PROPERTY_HIGH_DEF_AUDIO}, - * except if the call has a last forwarded number (we will show that icon instead). - */ - private void maybeShowHdAudioIcon() { - boolean showHdAudioIndicator = - isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO) && - TextUtils.isEmpty(mPrimary.getLastForwardedNumber()); - getUi().showHdAudioIndicator(showHdAudioIndicator); - } - - private void maybeShowSpamIconAndLabel() { - getUi().showSpamIndicator(mPrimary.isSpam()); - } - - /** - * Only show the conference call button if we can manage the conference. - */ - private void maybeShowManageConferenceCallButton() { - getUi().showManageConferenceCallButton(shouldShowManageConference()); - } - - /** - * Determines if a pending session modification exists for the current call. If so, the - * progress spinner is shown, and the call state is updated. - * - * @param callState The call state. - * @param sessionModificationState The session modification state. - */ - private void maybeShowProgressSpinner(int callState, int sessionModificationState) { - final boolean show = sessionModificationState == - Call.SessionModificationState.WAITING_FOR_RESPONSE - && callState == Call.State.ACTIVE; - if (show != mSpinnerShowing) { - getUi().setProgressSpinnerVisible(show); - mSpinnerShowing = show; - } - } - - /** - * Determines if the manage conference button should be visible, based on the current primary - * call. - * - * @return {@code True} if the manage conference button should be visible. - */ - private boolean shouldShowManageConference() { - if (mPrimary == null) { - return false; - } - - return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE) - && !mIsFullscreen; - } - - private void setCallbackNumber() { - String callbackNumber = null; - - // Show the emergency callback number if either: - // 1. This is an emergency call. - // 2. The phone is in Emergency Callback Mode, which means we should show the callback - // number. - boolean showCallbackNumber = mPrimary.hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE); - - if (mPrimary.isEmergencyCall() || showCallbackNumber) { - callbackNumber = getSubscriptionNumber(); - } else { - StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); - if (statusHints != null) { - Bundle extras = statusHints.getExtras(); - if (extras != null) { - callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER); - } - } - } - - final String simNumber = TelecomManagerCompat.getLine1Number( - InCallPresenter.getInstance().getTelecomManager(), - InCallPresenter.getInstance().getTelephonyManager(), - mPrimary.getAccountHandle()); - if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) { - Log.d(this, "Numbers are the same (and callback number is not being forced to show);" + - " not showing the callback number"); - callbackNumber = null; - } - - getUi().setCallbackNumber(callbackNumber, mPrimary.isEmergencyCall() || showCallbackNumber); - } - - public void updateCallTime() { - final CallCardUi ui = getUi(); - - if (ui == null) { - mCallTimer.cancel(); - } else if (!isPrimaryCallActive()) { - ui.setPrimaryCallElapsedTime(false, 0); - mCallTimer.cancel(); - } else { - final long callStart = mPrimary.getConnectTimeMillis(); - if (callStart > 0) { - final long duration = System.currentTimeMillis() - callStart; - ui.setPrimaryCallElapsedTime(true, duration); - } - } - } - - public void onCallStateButtonTouched() { - Intent broadcastIntent = ObjectFactory.getCallStateButtonBroadcastIntent(mContext); - if (broadcastIntent != null) { - Log.d(this, "Sending call state button broadcast: ", broadcastIntent); - mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE); - } - } - - /** - * Handles click on the contact photo by toggling fullscreen mode if the current call is a video - * call. - */ - public void onContactPhotoClick() { - if (mPrimary != null && mPrimary.isVideoCall(mContext)) { - InCallPresenter.getInstance().toggleFullscreenMode(); - } - } - - private void maybeStartSearch(Call call, boolean isPrimary) { - // no need to start search for conference calls which show generic info. - if (call != null && !call.isConferenceCall()) { - startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING); - } - } - - private void maybeClearSessionModificationState(Call call) { - if (call.getSessionModificationState() != - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - } - - /** - * Starts a query for more contact data for the save primary and secondary calls. - */ - private void startContactInfoSearch(final Call call, final boolean isPrimary, - boolean isIncoming) { - final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); - - cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary)); - } - - private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) { - final boolean entryMatchesExistingCall = - (isPrimary && mPrimary != null && TextUtils.equals(callId, mPrimary.getId())) || - (!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId())); - if (entryMatchesExistingCall) { - updateContactEntry(entry, isPrimary); - } else { - Log.w(this, "Dropping stale contact lookup info for " + callId); - } - - final Call call = CallList.getInstance().getCallById(callId); - if (call != null) { - call.getLogState().contactLookupResult = entry.contactLookupResult; - } - if (entry.contactUri != null) { - CallerInfoUtils.sendViewNotification(mContext, entry.contactUri); - } - } - - private void onImageLoadComplete(String callId, ContactCacheEntry entry) { - if (getUi() == null) { - return; - } - - if (entry.photo != null) { - if (mPrimary != null && callId.equals(mPrimary.getId())) { - boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo( - mPrimary.getVideoState(), mPrimary.getState()); - getUi().setPrimaryImage(entry.photo, showContactPhoto); - } - } - } - - private void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) { - if (getUi() == null) { - return; - } - - if (mPrimary != null && callId.equals(mPrimary.getId())) { - mPrimaryContactInfo.locationAddress = entry.locationAddress; - updateContactInteractions(); - } - } - - @Override - public void onLocationReady() { - // This will only update the contacts interactions data if the location returns after - // the contact information is found. - updateContactInteractions(); - } - - private void updateContactInteractions() { - if (mPrimary != null && mPrimaryContactInfo != null - && (mPrimaryContactInfo.locationAddress != null - || mPrimaryContactInfo.openingHours != null)) { - // TODO: This is hardcoded to "isBusiness" because functionality to differentiate - // between business and personal has not yet been added. - if (setInCallContactInteractionsType(true /* isBusiness */)) { - getUi().setContactContextTitle( - mInCallContactInteractions.getBusinessListHeaderView()); - } - - mInCallContactInteractions.setBusinessInfo( - mPrimaryContactInfo.locationAddress, - mDistanceHelper.calculateDistance(mPrimaryContactInfo.locationAddress), - mPrimaryContactInfo.openingHours); - getUi().setContactContextContent(mInCallContactInteractions.getListAdapter()); - getUi().showContactContext(mPrimary.getState() != State.INCOMING); - } else { - getUi().showContactContext(false); - } - } - - /** - * Update the contact interactions type so that the correct UI is shown. - * - * @param isBusiness {@code true} if the interaction is a business interaction, {@code false} if - * it is a personal contact. - * - * @return {@code true} if this is a new type of contact interaction (business or personal). - * {@code false} if it hasn't changed. - */ - private boolean setInCallContactInteractionsType(boolean isBusiness) { - if (mInCallContactInteractions == null) { - mInCallContactInteractions = - new InCallContactInteractions(mContext, isBusiness); - return true; - } - - return mInCallContactInteractions.switchContactType(isBusiness); - } - - private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) { - if (isPrimary) { - mPrimaryContactInfo = entry; - updatePrimaryDisplayInfo(); - } else { - mSecondaryContactInfo = entry; - updateSecondaryDisplayInfo(); - } - } - - /** - * Get the highest priority call to display. - * Goes through the calls and chooses which to return based on priority of which type of call - * to display to the user. Callers can use the "ignore" feature to get the second best call - * by passing a previously found primary call as ignore. - * - * @param ignore A call to ignore if found. - */ - private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) { - // Active calls come second. An active call always gets precedent. - Call retval = callList.getActiveCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Sometimes there is intemediate state that two calls are in active even one is about - // to be on hold. - retval = callList.getSecondActiveCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Disconnected calls get primary position if there are no active calls - // to let user know quickly what call has disconnected. Disconnected - // calls are very short lived. - if (!skipDisconnected) { - retval = callList.getDisconnectingCall(); - if (retval != null && retval != ignore) { - return retval; - } - retval = callList.getDisconnectedCall(); - if (retval != null && retval != ignore) { - return retval; - } - } - - // Then we go to background call (calls on hold) - retval = callList.getBackgroundCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Lastly, we go to a second background call. - retval = callList.getSecondBackgroundCall(); - - return retval; - } - - private void updatePrimaryDisplayInfo() { - final CallCardUi ui = getUi(); - if (ui == null) { - // TODO: May also occur if search result comes back after ui is destroyed. Look into - // removing that case completely. - Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!"); - return; - } - - if (mPrimary == null) { - // Clear the primary display info. - ui.setPrimary(null, null, false, null, null, false, false, false); - return; - } - - // Hide the contact photo if we are in a video call and the incoming video surface is - // showing. - boolean showContactPhoto = !VideoCallPresenter - .showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState()); - - // Call placed through a work phone account. - boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL); - - if (mPrimary.isConferenceCall()) { - Log.d(TAG, "Update primary display info for conference call."); - - ui.setPrimary( - null /* number */, - getConferenceString(mPrimary), - false /* nameIsNumber */, - null /* label */, - getConferencePhoto(mPrimary), - false /* isSipCall */, - showContactPhoto, - hasWorkCallProperty); - } else if (mPrimaryContactInfo != null) { - Log.d(TAG, "Update primary display info for " + mPrimaryContactInfo); - - String name = getNameForCall(mPrimaryContactInfo); - String number; - - boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber()); - boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()); - boolean isCallSubjectShown = shouldShowCallSubject(mPrimary); - - if (isCallSubjectShown) { - ui.setCallSubject(mPrimary.getCallSubject()); - } else { - ui.setCallSubject(null); - } - - if (isCallSubjectShown) { - number = null; - } else if (isChildNumberShown) { - number = mContext.getString(R.string.child_number, mPrimary.getChildNumber()); - } else if (isForwardedNumberShown) { - // Use last forwarded number instead of second line, if present. - number = mPrimary.getLastForwardedNumber(); - } else { - number = getNumberForCall(mPrimaryContactInfo); - } - - ui.showForwardIndicator(isForwardedNumberShown); - maybeShowHdAudioIcon(); - - boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number); - // Call with caller that is a work contact. - boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); - ui.setPrimary( - number, - name, - nameIsNumber, - isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label, - mPrimaryContactInfo.photo, - mPrimaryContactInfo.isSipCall, - showContactPhoto, - hasWorkCallProperty || isWorkContact); - - updateContactInteractions(); - } else { - // Clear the primary display info. - ui.setPrimary(null, null, false, null, null, false, false, false); - } - - if (mEmergencyCallListener != null) { - boolean isEmergencyCall = mPrimary.isEmergencyCall(); - mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall); - } - maybeShowSpamIconAndLabel(); - } - - private void updateSecondaryDisplayInfo() { - final CallCardUi ui = getUi(); - if (ui == null) { - return; - } - - if (mSecondary == null) { - // Clear the secondary display info. - ui.setSecondary(false, null, false, null, null, false /* isConference */, - false /* isVideoCall */, mIsFullscreen); - return; - } - - if (mSecondary.isConferenceCall()) { - ui.setSecondary( - true /* show */, - getConferenceString(mSecondary), - false /* nameIsNumber */, - null /* label */, - getCallProviderLabel(mSecondary), - true /* isConference */, - mSecondary.isVideoCall(mContext), - mIsFullscreen); - } else if (mSecondaryContactInfo != null) { - Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo); - String name = getNameForCall(mSecondaryContactInfo); - boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number); - ui.setSecondary( - true /* show */, - name, - nameIsNumber, - mSecondaryContactInfo.label, - getCallProviderLabel(mSecondary), - false /* isConference */, - mSecondary.isVideoCall(mContext), - mIsFullscreen); - } else { - // Clear the secondary display info. - ui.setSecondary(false, null, false, null, null, false /* isConference */, - false /* isVideoCall */, mIsFullscreen); - } - } - - - /** - * Gets the phone account to display for a call. - */ - private PhoneAccount getAccountForCall(Call call) { - PhoneAccountHandle accountHandle = call.getAccountHandle(); - if (accountHandle == null) { - return null; - } - return TelecomManagerCompat.getPhoneAccount( - InCallPresenter.getInstance().getTelecomManager(), - accountHandle); - } - - /** - * Returns the gateway number for any existing outgoing call. - */ - private String getGatewayNumber() { - if (hasOutgoingGatewayCall()) { - return getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress()); - } - return null; - } - - /** - * Return the string label to represent the call provider - */ - private String getCallProviderLabel(Call call) { - PhoneAccount account = getAccountForCall(call); - TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); - if (account != null && !TextUtils.isEmpty(account.getLabel()) - && TelecomManagerCompat.getCallCapablePhoneAccounts(mgr).size() > 1) { - return account.getLabel().toString(); - } - return null; - } - - /** - * Returns the label (line of text above the number/name) for any given call. - * For example, "calling via [Account/Google Voice]" for outgoing calls. - */ - private String getConnectionLabel() { - StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); - if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) { - return statusHints.getLabel().toString(); - } - - if (hasOutgoingGatewayCall() && getUi() != null) { - // Return the label for the gateway app on outgoing calls. - final PackageManager pm = mContext.getPackageManager(); - try { - ApplicationInfo info = pm.getApplicationInfo( - mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0); - return pm.getApplicationLabel(info).toString(); - } catch (PackageManager.NameNotFoundException e) { - Log.e(this, "Gateway Application Not Found.", e); - return null; - } - } - return getCallProviderLabel(mPrimary); - } - - private Drawable getCallStateIcon() { - // Return connection icon if one exists. - StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); - if (statusHints != null && statusHints.getIcon() != null) { - Drawable icon = statusHints.getIcon().loadDrawable(mContext); - if (icon != null) { - return icon; - } - } - - return null; - } - - private boolean hasOutgoingGatewayCall() { - // We only display the gateway information while STATE_DIALING so return false for any other - // call state. - // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which - // is also called after a contact search completes (call is not present yet). Split the - // UI update so it can receive independent updates. - if (mPrimary == null) { - return false; - } - return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null && - !mPrimary.getGatewayInfo().isEmpty(); - } - - /** - * Gets the name to display for the call. - */ - @NeededForTesting - String getNameForCall(ContactCacheEntry contactInfo) { - String preferredName = ContactDisplayUtils.getPreferredDisplayName( - contactInfo.namePrimary, - contactInfo.nameAlternative, - mContactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return contactInfo.number; - } - return preferredName; - } - - /** - * Gets the number to display for a call. - */ - @NeededForTesting - String getNumberForCall(ContactCacheEntry contactInfo) { - // If the name is empty, we use the number for the name...so don't show a second - // number in the number field - String preferredName = ContactDisplayUtils.getPreferredDisplayName( - contactInfo.namePrimary, - contactInfo.nameAlternative, - mContactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return contactInfo.location; - } - return contactInfo.number; - } - - public void secondaryInfoClicked() { - if (mSecondary == null) { - Log.w(this, "Secondary info clicked but no secondary call."); - return; - } - - Log.i(this, "Swapping call to foreground: " + mSecondary); - TelecomAdapter.getInstance().unholdCall(mSecondary.getId()); - } - - public void endCallClicked() { - if (mPrimary == null) { - return; - } - - Log.i(this, "Disconnecting call: " + mPrimary); - final String callId = mPrimary.getId(); - mPrimary.setState(Call.State.DISCONNECTING); - CallList.getInstance().onUpdate(mPrimary); - TelecomAdapter.getInstance().disconnectCall(callId); - } - - private String getNumberFromHandle(Uri handle) { - return handle == null ? "" : handle.getSchemeSpecificPart(); - } - - /** - * Handles a change to the fullscreen mode of the in-call UI. - * - * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode. - */ - @Override - public void onFullscreenModeChanged(boolean isFullscreenMode) { - mIsFullscreen = isFullscreenMode; - final CallCardUi ui = getUi(); - if (ui == null) { - return; - } - ui.setCallCardVisible(!isFullscreenMode); - ui.setSecondaryInfoVisible(!isFullscreenMode); - maybeShowManageConferenceCallButton(); - } - - @Override - public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { - // No-op - the Call Card is the origin of this event. - } - - private boolean isPrimaryCallActive() { - return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE; - } - - private String getConferenceString(Call call) { - boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE); - Log.v(this, "getConferenceString: " + isGenericConference); - - final int resId = isGenericConference - ? R.string.card_title_in_call : R.string.card_title_conf_call; - return mContext.getResources().getString(resId); - } - - private Drawable getConferencePhoto(Call call) { - boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE); - Log.v(this, "getConferencePhoto: " + isGenericConference); - - final int resId = isGenericConference - ? R.drawable.img_phone : R.drawable.img_conference; - Drawable photo = mContext.getResources().getDrawable(resId); - photo.setAutoMirrored(true); - return photo; - } - - private boolean shouldShowEndCallButton(Call primary, int callState) { - if (primary == null) { - return false; - } - if ((!Call.State.isConnectingOrConnected(callState) - && callState != Call.State.DISCONNECTING) || callState == Call.State.INCOMING) { - return false; - } - if (mPrimary.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - return false; - } - return true; - } - - private void maybeSendAccessibilityEvent(InCallState oldState, InCallState newState, - boolean primaryChanged) { - if (mContext == null) { - return; - } - final AccessibilityManager am = (AccessibilityManager) mContext.getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (!am.isEnabled()) { - return; - } - // Announce the current call if it's new incoming/outgoing call or primary call is changed - // due to switching calls between two ongoing calls (one is on hold). - if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING) - || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING) - || primaryChanged) { - if (getUi() != null) { - getUi().sendAccessibilityAnnouncement(); - } - } - } - - /** - * Determines whether the call subject should be visible on the UI. For the call subject to be - * visible, the call has to be in an incoming or waiting state, and the subject must not be - * empty. - * - * @param call The call. - * @return {@code true} if the subject should be shown, {@code false} otherwise. - */ - private boolean shouldShowCallSubject(Call call) { - if (call == null) { - return false; - } - - boolean isIncomingOrWaiting = mPrimary.getState() == Call.State.INCOMING || - mPrimary.getState() == Call.State.CALL_WAITING; - return isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject()) && - call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED && - call.isCallSubjectSupported(); - } - - /** - * Determines whether the "note sent" toast should be shown. It should be shown for a new - * outgoing call with a subject. - * - * @param call The call - * @return {@code true} if the toast should be shown, {@code false} otherwise. - */ - private boolean shouldShowNoteSentToast(Call call) { - return call != null && hasCallSubject(call) && (call.getState() == Call.State.DIALING - || call.getState() == Call.State.CONNECTING); - } - - private static boolean hasCallSubject(Call call) { - return !TextUtils.isEmpty(call.getTelecomCall().getDetails().getIntentExtras() - .getString(TelecomManager.EXTRA_CALL_SUBJECT)); - } - - public interface CallCardUi extends Ui { - void setVisible(boolean on); - void setContactContextTitle(View listHeaderView); - void setContactContextContent(ListAdapter listAdapter); - void showContactContext(boolean show); - void setCallCardVisible(boolean visible); - void setPrimary(String number, String name, boolean nameIsNumber, String label, - Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall); - void setSecondary(boolean show, String name, boolean nameIsNumber, String label, - String providerLabel, boolean isConference, boolean isVideoCall, - boolean isFullscreen); - void setSecondaryInfoVisible(boolean visible); - void setCallState(int state, int videoState, int sessionModificationState, - DisconnectCause disconnectCause, String connectionLabel, - Drawable connectionIcon, String gatewayNumber, boolean isWifi, - boolean isConference, boolean isWorkCall); - void setPrimaryCallElapsedTime(boolean show, long duration); - void setPrimaryName(String name, boolean nameIsNumber); - void setPrimaryImage(Drawable image, boolean isVisible); - void setPrimaryPhoneNumber(String phoneNumber); - void setPrimaryLabel(String label); - void setEndCallButtonEnabled(boolean enabled, boolean animate); - void setCallbackNumber(String number, boolean isEmergencyCalls); - void setCallSubject(String callSubject); - void setProgressSpinnerVisible(boolean visible); - void showHdAudioIndicator(boolean visible); - void showForwardIndicator(boolean visible); - void showSpamIndicator(boolean visible); - void showManageConferenceCallButton(boolean visible); - boolean isManageConferenceVisible(); - boolean isCallSubjectVisible(); - void animateForNewOutgoingCall(); - void sendAccessibilityAnnouncement(); - void showNoteSentToast(); - } -} diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java deleted file mode 100644 index 48870f68a311300fd8adec0736cc60976d4206f0..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallList.java +++ /dev/null @@ -1,695 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Handler; -import android.os.Message; -import android.os.Trace; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccount; - -import com.android.contacts.common.testing.NeededForTesting; -import com.android.dialer.logging.Logger; -import com.android.dialer.service.ExtendedCallInfoService; -import com.android.incallui.util.TelecomCallUtil; - -import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Maintains the list of active calls and notifies interested classes of changes to the call list - * as they are received from the telephony stack. Primary listener of changes to this class is - * InCallPresenter. - */ -public class CallList { - - private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200; - private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000; - private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000; - - private static final int EVENT_DISCONNECTED_TIMEOUT = 1; - private static final long BLOCK_QUERY_TIMEOUT_MS = 1000; - - private static CallList sInstance = new CallList(); - - private final HashMap mCallById = new HashMap<>(); - private final HashMap mCallByTelecomCall = new HashMap<>(); - private final HashMap> mCallTextReponsesMap = Maps.newHashMap(); - /** - * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is - * load factor before resizing, 1 means we only expect a single thread to - * access the map so make only a single shard - */ - private final Set mListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final HashMap> mCallUpdateListenerMap = Maps - .newHashMap(); - private final Set mPendingDisconnectCalls = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private ExtendedCallInfoService mExtendedCallInfoService; - - /** - * Static singleton accessor method. - */ - public static CallList getInstance() { - return sInstance; - } - - /** - * USED ONLY FOR TESTING - * Testing-only constructor. Instance should only be acquired through getInstance(). - */ - @NeededForTesting - CallList() { - } - - public void onCallAdded(final android.telecom.Call telecomCall, LatencyReport latencyReport) { - Trace.beginSection("onCallAdded"); - final Call call = new Call(telecomCall, latencyReport); - Log.d(this, "onCallAdded: callState=" + call.getState()); - - if (call.getState() == Call.State.INCOMING || - call.getState() == Call.State.CALL_WAITING) { - onIncoming(call, call.getCannedSmsResponses()); - if (mExtendedCallInfoService != null) { - String number = TelecomCallUtil.getNumber(telecomCall); - mExtendedCallInfoService.getExtendedCallInfo(number, null, - new ExtendedCallInfoService.Listener() { - @Override - public void onComplete(boolean isSpam) { - call.setSpam(isSpam); - onUpdate(call); - } - }); - } - } else { - onUpdate(call); - } - - call.logCallInitiationType(); - Trace.endSection(); - } - - public void onCallRemoved(android.telecom.Call telecomCall) { - if (mCallByTelecomCall.containsKey(telecomCall)) { - Call call = mCallByTelecomCall.get(telecomCall); - Logger.logCall(call); - if (updateCallInMap(call)) { - Log.w(this, "Removing call not previously disconnected " + call.getId()); - } - updateCallTextMap(call, null); - } - } - - /** - * Called when a single call disconnects. - */ - public void onDisconnect(Call call) { - if (updateCallInMap(call)) { - Log.i(this, "onDisconnect: " + call); - // notify those listening for changes on this specific change - notifyCallUpdateListeners(call); - // notify those listening for all disconnects - notifyListenersOfDisconnect(call); - } - } - - /** - * Called when a single call has changed. - */ - public void onIncoming(Call call, List textMessages) { - if (updateCallInMap(call)) { - Log.i(this, "onIncoming - " + call); - } - updateCallTextMap(call, textMessages); - - for (Listener listener : mListeners) { - listener.onIncomingCall(call); - } - } - - public void onUpgradeToVideo(Call call){ - Log.d(this, "onUpgradeToVideo call=" + call); - for (Listener listener : mListeners) { - listener.onUpgradeToVideo(call); - } - } - /** - * Called when a single call has changed. - */ - public void onUpdate(Call call) { - Trace.beginSection("onUpdate"); - onUpdateCall(call); - notifyGenericListeners(); - Trace.endSection(); - } - - /** - * Called when a single call has changed session modification state. - * - * @param call The call. - * @param sessionModificationState The new session modification state. - */ - public void onSessionModificationStateChange(Call call, int sessionModificationState) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onSessionModificationStateChange(sessionModificationState); - } - } - } - - /** - * Called when the last forwarded number changes for a call. With IMS, the last forwarded - * number changes due to a supplemental service notification, so it is not pressent at the - * start of the call. - * - * @param call The call. - */ - public void onLastForwardedNumberChange(Call call) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onLastForwardedNumberChange(); - } - } - } - - /** - * Called when the child number changes for a call. The child number can be received after a - * call is initially set up, so we need to be able to inform listeners of the change. - * - * @param call The call. - */ - public void onChildNumberChange(Call call) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onChildNumberChange(); - } - } - } - - public void notifyCallUpdateListeners(Call call) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onCallChanged(call); - } - } - } - - /** - * Add a call update listener for a call id. - * - * @param callId The call id to get updates for. - * @param listener The listener to add. - */ - public void addCallUpdateListener(String callId, CallUpdateListener listener) { - List listeners = mCallUpdateListenerMap.get(callId); - if (listeners == null) { - listeners = new CopyOnWriteArrayList(); - mCallUpdateListenerMap.put(callId, listeners); - } - listeners.add(listener); - } - - /** - * Remove a call update listener for a call id. - * - * @param callId The call id to remove the listener for. - * @param listener The listener to remove. - */ - public void removeCallUpdateListener(String callId, CallUpdateListener listener) { - List listeners = mCallUpdateListenerMap.get(callId); - if (listeners != null) { - listeners.remove(listener); - } - } - - public void addListener(Listener listener) { - Preconditions.checkNotNull(listener); - - mListeners.add(listener); - - // Let the listener know about the active calls immediately. - listener.onCallListChange(this); - } - - public void removeListener(Listener listener) { - if (listener != null) { - mListeners.remove(listener); - } - } - - /** - * TODO: Change so that this function is not needed. Instead of assuming there is an active - * call, the code should rely on the status of a specific Call and allow the presenters to - * update the Call object when the active call changes. - */ - public Call getIncomingOrActive() { - Call retval = getIncomingCall(); - if (retval == null) { - retval = getActiveCall(); - } - return retval; - } - - public Call getOutgoingOrActive() { - Call retval = getOutgoingCall(); - if (retval == null) { - retval = getActiveCall(); - } - return retval; - } - - /** - * A call that is waiting for {@link PhoneAccount} selection - */ - public Call getWaitingForAccountCall() { - return getFirstCallWithState(Call.State.SELECT_PHONE_ACCOUNT); - } - - public Call getPendingOutgoingCall() { - return getFirstCallWithState(Call.State.CONNECTING); - } - - public Call getOutgoingCall() { - Call call = getFirstCallWithState(Call.State.DIALING); - if (call == null) { - call = getFirstCallWithState(Call.State.REDIALING); - } - return call; - } - - public Call getActiveCall() { - return getFirstCallWithState(Call.State.ACTIVE); - } - - public Call getSecondActiveCall() { - return getCallWithState(Call.State.ACTIVE, 1); - } - - public Call getBackgroundCall() { - return getFirstCallWithState(Call.State.ONHOLD); - } - - public Call getDisconnectedCall() { - return getFirstCallWithState(Call.State.DISCONNECTED); - } - - public Call getDisconnectingCall() { - return getFirstCallWithState(Call.State.DISCONNECTING); - } - - public Call getSecondBackgroundCall() { - return getCallWithState(Call.State.ONHOLD, 1); - } - - public Call getActiveOrBackgroundCall() { - Call call = getActiveCall(); - if (call == null) { - call = getBackgroundCall(); - } - return call; - } - - public Call getIncomingCall() { - Call call = getFirstCallWithState(Call.State.INCOMING); - if (call == null) { - call = getFirstCallWithState(Call.State.CALL_WAITING); - } - - return call; - } - - public Call getFirstCall() { - Call result = getIncomingCall(); - if (result == null) { - result = getPendingOutgoingCall(); - } - if (result == null) { - result = getOutgoingCall(); - } - if (result == null) { - result = getFirstCallWithState(Call.State.ACTIVE); - } - if (result == null) { - result = getDisconnectingCall(); - } - if (result == null) { - result = getDisconnectedCall(); - } - return result; - } - - public boolean hasLiveCall() { - Call call = getFirstCall(); - if (call == null) { - return false; - } - return call != getDisconnectingCall() && call != getDisconnectedCall(); - } - - /** - * Returns the first call found in the call map with the specified call modification state. - * @param state The session modification state to search for. - * @return The first call with the specified state. - */ - public Call getVideoUpgradeRequestCall() { - for(Call call : mCallById.values()) { - if (call.getSessionModificationState() == - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - return call; - } - } - return null; - } - - public Call getCallById(String callId) { - return mCallById.get(callId); - } - - public Call getCallByTelecomCall(android.telecom.Call telecomCall) { - return mCallByTelecomCall.get(telecomCall); - } - - public List getTextResponses(String callId) { - return mCallTextReponsesMap.get(callId); - } - - /** - * Returns first call found in the call map with the specified state. - */ - public Call getFirstCallWithState(int state) { - return getCallWithState(state, 0); - } - - /** - * Returns the [position]th call found in the call map with the specified state. - * TODO: Improve this logic to sort by call time. - */ - public Call getCallWithState(int state, int positionToFind) { - Call retval = null; - int position = 0; - for (Call call : mCallById.values()) { - if (call.getState() == state) { - if (position >= positionToFind) { - retval = call; - break; - } else { - position++; - } - } - } - - return retval; - } - - /** - * This is called when the service disconnects, either expectedly or unexpectedly. - * For the expected case, it's because we have no calls left. For the unexpected case, - * it is likely a crash of phone and we need to clean up our calls manually. Without phone, - * there can be no active calls, so this is relatively safe thing to do. - */ - public void clearOnDisconnect() { - for (Call call : mCallById.values()) { - final int state = call.getState(); - if (state != Call.State.IDLE && - state != Call.State.INVALID && - state != Call.State.DISCONNECTED) { - - call.setState(Call.State.DISCONNECTED); - call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN)); - updateCallInMap(call); - } - } - notifyGenericListeners(); - } - - /** - * Called when the user has dismissed an error dialog. This indicates acknowledgement of - * the disconnect cause, and that any pending disconnects should immediately occur. - */ - public void onErrorDialogDismissed() { - final Iterator iterator = mPendingDisconnectCalls.iterator(); - while (iterator.hasNext()) { - Call call = iterator.next(); - iterator.remove(); - finishDisconnectedCall(call); - } - } - - /** - * Processes an update for a single call. - * - * @param call The call to update. - */ - private void onUpdateCall(Call call) { - Log.d(this, "\t" + call); - if (updateCallInMap(call)) { - Log.i(this, "onUpdate - " + call); - } - updateCallTextMap(call, call.getCannedSmsResponses()); - notifyCallUpdateListeners(call); - } - - /** - * Sends a generic notification to all listeners that something has changed. - * It is up to the listeners to call back to determine what changed. - */ - private void notifyGenericListeners() { - for (Listener listener : mListeners) { - listener.onCallListChange(this); - } - } - - private void notifyListenersOfDisconnect(Call call) { - for (Listener listener : mListeners) { - listener.onDisconnect(call); - } - } - - /** - * Updates the call entry in the local map. - * @return false if no call previously existed and no call was added, otherwise true. - */ - private boolean updateCallInMap(Call call) { - Preconditions.checkNotNull(call); - - boolean updated = false; - - if (call.getState() == Call.State.DISCONNECTED) { - // update existing (but do not add!!) disconnected calls - if (mCallById.containsKey(call.getId())) { - // For disconnected calls, we want to keep them alive for a few seconds so that the - // UI has a chance to display anything it needs when a call is disconnected. - - // Set up a timer to destroy the call after X seconds. - final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); - mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call)); - mPendingDisconnectCalls.add(call); - - mCallById.put(call.getId(), call); - mCallByTelecomCall.put(call.getTelecomCall(), call); - updated = true; - } - } else if (!isCallDead(call)) { - mCallById.put(call.getId(), call); - mCallByTelecomCall.put(call.getTelecomCall(), call); - updated = true; - } else if (mCallById.containsKey(call.getId())) { - mCallById.remove(call.getId()); - mCallByTelecomCall.remove(call.getTelecomCall()); - updated = true; - } - - return updated; - } - - private int getDelayForDisconnect(Call call) { - Preconditions.checkState(call.getState() == Call.State.DISCONNECTED); - - - final int cause = call.getDisconnectCause().getCode(); - final int delay; - switch (cause) { - case DisconnectCause.LOCAL: - delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS; - break; - case DisconnectCause.REMOTE: - case DisconnectCause.ERROR: - delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS; - break; - case DisconnectCause.REJECTED: - case DisconnectCause.MISSED: - case DisconnectCause.CANCELED: - // no delay for missed/rejected incoming calls and canceled outgoing calls. - delay = 0; - break; - default: - delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS; - break; - } - - return delay; - } - - private void updateCallTextMap(Call call, List textResponses) { - Preconditions.checkNotNull(call); - - if (!isCallDead(call)) { - if (textResponses != null) { - mCallTextReponsesMap.put(call.getId(), textResponses); - } - } else if (mCallById.containsKey(call.getId())) { - mCallTextReponsesMap.remove(call.getId()); - } - } - - private boolean isCallDead(Call call) { - final int state = call.getState(); - return Call.State.IDLE == state || Call.State.INVALID == state; - } - - /** - * Sets up a call for deletion and notifies listeners of change. - */ - private void finishDisconnectedCall(Call call) { - if (mPendingDisconnectCalls.contains(call)) { - mPendingDisconnectCalls.remove(call); - } - call.setState(Call.State.IDLE); - updateCallInMap(call); - notifyGenericListeners(); - } - - /** - * Notifies all video calls of a change in device orientation. - * - * @param rotation The new rotation angle (in degrees). - */ - public void notifyCallsOfDeviceRotation(int rotation) { - for (Call call : mCallById.values()) { - // First, ensure that the call videoState has video enabled (there is no need to set - // device orientation on a voice call which has not yet been upgraded to video). - // Second, ensure a VideoCall is set on the call so that the change can be sent to the - // provider (a VideoCall can be present for a call that does not currently have video, - // but can be upgraded to video). - - // NOTE: is it necessary to use this order because getVideoCall references the class - // VideoProfile which is not available on APIs <23 (M). - if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) { - call.getVideoCall().setDeviceOrientation(rotation); - } - } - } - - /** - * Handles the timeout for destroying disconnected calls. - */ - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case EVENT_DISCONNECTED_TIMEOUT: - Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); - finishDisconnectedCall((Call) msg.obj); - break; - default: - Log.wtf(this, "Message not expected: " + msg.what); - break; - } - } - }; - - public void setExtendedCallInfoService(ExtendedCallInfoService service) { - mExtendedCallInfoService = service; - } - - public void onInCallUiShown(boolean forFullScreenIntent) { - for (Call call : mCallById.values()) { - call.getLatencyReport().onInCallUiShown(forFullScreenIntent); - } - } - - /** - * Listener interface for any class that wants to be notified of changes - * to the call list. - */ - public interface Listener { - /** - * Called when a new incoming call comes in. - * This is the only method that gets called for incoming calls. Listeners - * that want to perform an action on incoming call should respond in this method - * because {@link #onCallListChange} does not automatically get called for - * incoming calls. - */ - public void onIncomingCall(Call call); - /** - * Called when a new modify call request comes in - * This is the only method that gets called for modify requests. - */ - public void onUpgradeToVideo(Call call); - /** - * Called anytime there are changes to the call list. The change can be switching call - * states, updating information, etc. This method will NOT be called for new incoming - * calls and for calls that switch to disconnected state. Listeners must add actions - * to those method implementations if they want to deal with those actions. - */ - public void onCallListChange(CallList callList); - - /** - * Called when a call switches to the disconnected state. This is the only method - * that will get called upon disconnection. - */ - public void onDisconnect(Call call); - - - } - - public interface CallUpdateListener { - // TODO: refactor and limit arg to be call state. Caller info is not needed. - public void onCallChanged(Call call); - - /** - * Notifies of a change to the session modification state for a call. - * - * @param sessionModificationState The new session modification state. - */ - public void onSessionModificationStateChange(int sessionModificationState); - - /** - * Notifies of a change to the last forwarded number for a call. - */ - public void onLastForwardedNumberChange(); - - /** - * Notifies of a change to the child number for a call. - */ - public void onChildNumberChange(); - } -} diff --git a/InCallUI/src/com/android/incallui/CallTimer.java b/InCallUI/src/com/android/incallui/CallTimer.java deleted file mode 100644 index d65e633731aafce057ca99f7137cdaf6e50de243..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallTimer.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.os.Handler; -import android.os.SystemClock; - -/** - * Helper class used to keep track of events requiring regular intervals. - */ -public class CallTimer extends Handler { - private Runnable mInternalCallback; - private Runnable mCallback; - private long mLastReportedTime; - private long mInterval; - private boolean mRunning; - - public CallTimer(Runnable callback) { - Preconditions.checkNotNull(callback); - - mInterval = 0; - mLastReportedTime = 0; - mRunning = false; - mCallback = callback; - mInternalCallback = new CallTimerCallback(); - } - - public boolean start(long interval) { - if (interval <= 0) { - return false; - } - - // cancel any previous timer - cancel(); - - mInterval = interval; - mLastReportedTime = SystemClock.uptimeMillis(); - - mRunning = true; - periodicUpdateTimer(); - - return true; - } - - public void cancel() { - removeCallbacks(mInternalCallback); - mRunning = false; - } - - private void periodicUpdateTimer() { - if (!mRunning) { - return; - } - - final long now = SystemClock.uptimeMillis(); - long nextReport = mLastReportedTime + mInterval; - while (now >= nextReport) { - nextReport += mInterval; - } - - postAtTime(mInternalCallback, nextReport); - mLastReportedTime = nextReport; - - // Run the callback - mCallback.run(); - } - - private class CallTimerCallback implements Runnable { - @Override - public void run() { - periodicUpdateTimer(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallerInfo.java b/InCallUI/src/com/android/incallui/CallerInfo.java deleted file mode 100644 index f3d0e0763db823a9f43fb9af60436cf6f47a74d1..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallerInfo.java +++ /dev/null @@ -1,585 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.android.dialer.util.PhoneLookupUtil; -import com.google.common.primitives.Longs; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.PhoneLookup; -import android.provider.ContactsContract.RawContacts; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; - -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.compat.PhoneLookupSdkCompat; -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.ContactsUtils.UserType; -import com.android.contacts.common.util.PhoneNumberHelper; -import com.android.contacts.common.util.TelephonyManagerUtils; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfoHelper; - -/** - * Looks up caller information for the given phone number. - */ -public class CallerInfo { - private static final String TAG = "CallerInfo"; - - // We should always use this projection starting from NYC onward. - private static final String[] DEFAULT_PHONELOOKUP_PROJECTION = new String[] { - PhoneLookupSdkCompat.CONTACT_ID, - PhoneLookup.DISPLAY_NAME, - PhoneLookup.LOOKUP_KEY, - PhoneLookup.NUMBER, - PhoneLookup.NORMALIZED_NUMBER, - PhoneLookup.LABEL, - PhoneLookup.TYPE, - PhoneLookup.PHOTO_URI, - PhoneLookup.CUSTOM_RINGTONE, - PhoneLookup.SEND_TO_VOICEMAIL - }; - - // In pre-N, contact id is stored in {@link PhoneLookup._ID} in non-sip query. - private static final String[] BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION = - new String[] { - PhoneLookup._ID, - PhoneLookup.DISPLAY_NAME, - PhoneLookup.LOOKUP_KEY, - PhoneLookup.NUMBER, - PhoneLookup.NORMALIZED_NUMBER, - PhoneLookup.LABEL, - PhoneLookup.TYPE, - PhoneLookup.PHOTO_URI, - PhoneLookup.CUSTOM_RINGTONE, - PhoneLookup.SEND_TO_VOICEMAIL - }; - - public static String[] getDefaultPhoneLookupProjection(Uri phoneLookupUri) { - if (CompatUtils.isNCompatible()) { - return DEFAULT_PHONELOOKUP_PROJECTION; - } - // Pre-N - boolean isSip = phoneLookupUri.getBooleanQueryParameter( - ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false); - return (isSip) ? DEFAULT_PHONELOOKUP_PROJECTION - : BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION; - } - - /** - * Please note that, any one of these member variables can be null, - * and any accesses to them should be prepared to handle such a case. - * - * Also, it is implied that phoneNumber is more often populated than - * name is, (think of calls being dialed/received using numbers where - * names are not known to the device), so phoneNumber should serve as - * a dependable fallback when name is unavailable. - * - * One other detail here is that this CallerInfo object reflects - * information found on a connection, it is an OUTPUT that serves - * mainly to display information to the user. In no way is this object - * used as input to make a connection, so we can choose to display - * whatever human-readable text makes sense to the user for a - * connection. This is especially relevant for the phone number field, - * since it is the one field that is most likely exposed to the user. - * - * As an example: - * 1. User dials "911" - * 2. Device recognizes that this is an emergency number - * 3. We use the "Emergency Number" string instead of "911" in the - * phoneNumber field. - * - * What we're really doing here is treating phoneNumber as an essential - * field here, NOT name. We're NOT always guaranteed to have a name - * for a connection, but the number should be displayable. - */ - public String name; - public String nameAlternative; - public String phoneNumber; - public String normalizedNumber; - public String forwardingNumber; - public String geoDescription; - - public String cnapName; - public int numberPresentation; - public int namePresentation; - public boolean contactExists; - - public String phoneLabel; - /* Split up the phoneLabel into number type and label name */ - public int numberType; - public String numberLabel; - - public int photoResource; - - // Contact ID, which will be 0 if a contact comes from the corp CP2. - public long contactIdOrZero; - public String lookupKeyOrNull; - public boolean needUpdate; - public Uri contactRefUri; - public @UserType long userType; - - /** - * Contact display photo URI. If a contact has no display photo but a thumbnail, it'll be - * the thumbnail URI instead. - */ - public Uri contactDisplayPhotoUri; - - // fields to hold individual contact preference data, - // including the send to voicemail flag and the ringtone - // uri reference. - public Uri contactRingtoneUri; - public boolean shouldSendToVoicemail; - - /** - * Drawable representing the caller image. This is essentially - * a cache for the image data tied into the connection / - * callerinfo object. - * - * This might be a high resolution picture which is more suitable - * for full-screen image view than for smaller icons used in some - * kinds of notifications. - * - * The {@link #isCachedPhotoCurrent} flag indicates if the image - * data needs to be reloaded. - */ - public Drawable cachedPhoto; - /** - * Bitmap representing the caller image which has possibly lower - * resolution than {@link #cachedPhoto} and thus more suitable for - * icons (like notification icons). - * - * In usual cases this is just down-scaled image of {@link #cachedPhoto}. - * If the down-scaling fails, this will just become null. - * - * The {@link #isCachedPhotoCurrent} flag indicates if the image - * data needs to be reloaded. - */ - public Bitmap cachedPhotoIcon; - /** - * Boolean which indicates if {@link #cachedPhoto} and - * {@link #cachedPhotoIcon} is fresh enough. If it is false, - * those images aren't pointing to valid objects. - */ - public boolean isCachedPhotoCurrent; - - /** - * String which holds the call subject sent as extra from the lower layers for this call. This - * is used to display the no-caller ID reason for restricted/unknown number presentation. - */ - public String callSubject; - - private boolean mIsEmergency; - private boolean mIsVoiceMail; - - public CallerInfo() { - // TODO: Move all the basic initialization here? - mIsEmergency = false; - mIsVoiceMail = false; - userType = ContactsUtils.USER_TYPE_CURRENT; - } - - /** - * getCallerInfo given a Cursor. - * @param context the context used to retrieve string constants - * @param contactRef the URI to attach to this CallerInfo object - * @param cursor the first object in the cursor is used to build the CallerInfo object. - * @return the CallerInfo which contains the caller id for the given - * number. The returned CallerInfo is null if no number is supplied. - */ - public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) { - CallerInfo info = new CallerInfo(); - info.photoResource = 0; - info.phoneLabel = null; - info.numberType = 0; - info.numberLabel = null; - info.cachedPhoto = null; - info.isCachedPhotoCurrent = false; - info.contactExists = false; - info.userType = ContactsUtils.USER_TYPE_CURRENT; - - Log.v(TAG, "getCallerInfo() based on cursor..."); - - if (cursor != null) { - if (cursor.moveToFirst()) { - // TODO: photo_id is always available but not taken - // care of here. Maybe we should store it in the - // CallerInfo object as well. - - long contactId = 0L; - int columnIndex; - - // Look for the name - columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); - if (columnIndex != -1) { - info.name = cursor.getString(columnIndex); - } - - // Look for the number - columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER); - if (columnIndex != -1) { - info.phoneNumber = cursor.getString(columnIndex); - } - - // Look for the normalized number - columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER); - if (columnIndex != -1) { - info.normalizedNumber = cursor.getString(columnIndex); - } - - // Look for the label/type combo - columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL); - if (columnIndex != -1) { - int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE); - if (typeColumnIndex != -1) { - info.numberType = cursor.getInt(typeColumnIndex); - info.numberLabel = cursor.getString(columnIndex); - info.phoneLabel = Phone.getTypeLabel(context.getResources(), - info.numberType, info.numberLabel) - .toString(); - } - } - - // Look for the person_id. - columnIndex = getColumnIndexForPersonId(contactRef, cursor); - if (columnIndex != -1) { - contactId = cursor.getLong(columnIndex); - // QuickContacts in M doesn't support enterprise contact id - if (contactId != 0 && (ContactsUtils.FLAG_N_FEATURE - || !Contacts.isEnterpriseContactId(contactId))) { - info.contactIdOrZero = contactId; - Log.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero); - - // cache the lookup key for later use with person_id to create lookup URIs - columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY); - if (columnIndex != -1) { - info.lookupKeyOrNull = cursor.getString(columnIndex); - } - } - } else { - // No valid columnIndex, so we can't look up person_id. - Log.v(TAG, "Couldn't find contactId column for " + contactRef); - // Watch out: this means that anything that depends on - // person_id will be broken (like contact photo lookups in - // the in-call UI, for example.) - } - - // Display photo URI. - columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); - if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) { - info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex)); - } else { - info.contactDisplayPhotoUri = null; - } - - // look for the custom ringtone, create from the string stored - // in the database. - columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE); - if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) { - if (TextUtils.isEmpty(cursor.getString(columnIndex))) { - // make it consistent with frameworks/base/.../CallerInfo.java - info.contactRingtoneUri = Uri.EMPTY; - } else { - info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex)); - } - } else { - info.contactRingtoneUri = null; - } - - // look for the send to voicemail flag, set it to true only - // under certain circumstances. - columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL); - info.shouldSendToVoicemail = (columnIndex != -1) && - ((cursor.getInt(columnIndex)) == 1); - info.contactExists = true; - - // Determine userType by directoryId and contactId - final String directory = contactRef == null ? null - : contactRef.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); - final Long directoryId = directory == null ? null : Longs.tryParse(directory); - info.userType = ContactsUtils.determineUserType(directoryId, contactId); - - info.nameAlternative = ContactInfoHelper.lookUpDisplayNameAlternative( - context, info.lookupKeyOrNull, info.userType, directoryId); - } - cursor.close(); - } - - info.needUpdate = false; - info.name = normalize(info.name); - info.contactRefUri = contactRef; - - return info; - } - - /** - * getCallerInfo given a URI, look up in the call-log database - * for the uri unique key. - * @param context the context used to get the ContentResolver - * @param contactRef the URI used to lookup caller id - * @return the CallerInfo which contains the caller id for the given - * number. The returned CallerInfo is null if no number is supplied. - */ - private static CallerInfo getCallerInfo(Context context, Uri contactRef) { - - return getCallerInfo(context, contactRef, - context.getContentResolver().query(contactRef, null, null, null, null)); - } - - /** - * Performs another lookup if previous lookup fails and it's a SIP call - * and the peer's username is all numeric. Look up the username as it - * could be a PSTN number in the contact database. - * - * @param context the query context - * @param number the original phone number, could be a SIP URI - * @param previousResult the result of previous lookup - * @return previousResult if it's not the case - */ - static CallerInfo doSecondaryLookupIfNecessary(Context context, - String number, CallerInfo previousResult) { - if (!previousResult.contactExists - && PhoneNumberHelper.isUriNumber(number)) { - String username = PhoneNumberHelper.getUsernameFromUriNumber(number); - if (PhoneNumberUtils.isGlobalPhoneNumber(username)) { - previousResult = getCallerInfo(context, - Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, - Uri.encode(username))); - } - } - return previousResult; - } - - // Accessors - - /** - * @return true if the caller info is an emergency number. - */ - public boolean isEmergencyNumber() { - return mIsEmergency; - } - - /** - * @return true if the caller info is a voicemail number. - */ - public boolean isVoiceMailNumber() { - return mIsVoiceMail; - } - - /** - * Mark this CallerInfo as an emergency call. - * @param context To lookup the localized 'Emergency Number' string. - * @return this instance. - */ - /* package */ CallerInfo markAsEmergency(Context context) { - name = context.getString(R.string.emergency_call_dialog_number_for_display); - phoneNumber = null; - - photoResource = R.drawable.img_phone; - mIsEmergency = true; - return this; - } - - - /** - * Mark this CallerInfo as a voicemail call. The voicemail label - * is obtained from the telephony manager. Caller must hold the - * READ_PHONE_STATE permission otherwise the phoneNumber will be - * set to null. - * @return this instance. - */ - /* package */ CallerInfo markAsVoiceMail(Context context) { - mIsVoiceMail = true; - - try { - // For voicemail calls, we display the voice mail tag - // instead of the real phone number in the "number" - // field. - name = TelephonyManagerUtils.getVoiceMailAlphaTag(context); - phoneNumber = null; - } catch (SecurityException se) { - // Should never happen: if this process does not have - // permission to retrieve VM tag, it should not have - // permission to retrieve VM number and would not call - // this method. - // Leave phoneNumber untouched. - Log.e(TAG, "Cannot access VoiceMail.", se); - } - // TODO: There is no voicemail picture? - // FIXME: FIND ANOTHER ICON - // photoResource = android.R.drawable.badge_voicemail; - return this; - } - - private static String normalize(String s) { - if (s == null || s.length() > 0) { - return s; - } else { - return null; - } - } - - /** - * Returns the column index to use to find the "person_id" field in - * the specified cursor, based on the contact URI that was originally - * queried. - * - * This is a helper function for the getCallerInfo() method that takes - * a Cursor. Looking up the person_id is nontrivial (compared to all - * the other CallerInfo fields) since the column we need to use - * depends on what query we originally ran. - * - * Watch out: be sure to not do any database access in this method, since - * it's run from the UI thread (see comments below for more info.) - * - * @return the columnIndex to use (with cursor.getLong()) to get the - * person_id, or -1 if we couldn't figure out what colum to use. - * - * TODO: Add a unittest for this method. (This is a little tricky to - * test, since we'll need a live contacts database to test against, - * preloaded with at least some phone numbers and SIP addresses. And - * we'll probably have to hardcode the column indexes we expect, so - * the test might break whenever the contacts schema changes. But we - * can at least make sure we handle all the URI patterns we claim to, - * and that the mime types match what we expect...) - */ - private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) { - // TODO: This is pretty ugly now, see bug 2269240 for - // more details. The column to use depends upon the type of URL: - // - content://com.android.contacts/data/phones ==> use the "contact_id" column - // - content://com.android.contacts/phone_lookup ==> use the "_ID" column - // - content://com.android.contacts/data ==> use the "contact_id" column - // If it's none of the above, we leave columnIndex=-1 which means - // that the person_id field will be left unset. - // - // The logic here *used* to be based on the mime type of contactRef - // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the - // RawContacts.CONTACT_ID column). But looking up the mime type requires - // a call to context.getContentResolver().getType(contactRef), which - // isn't safe to do from the UI thread since it can cause an ANR if - // the contacts provider is slow or blocked (like during a sync.) - // - // So instead, figure out the column to use for person_id by just - // looking at the URI itself. - - Log.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '" - + contactRef + "'..."); - // Warning: Do not enable the following logging (due to ANR risk.) - // if (VDBG) Rlog.v(TAG, "- MIME type: " - // + context.getContentResolver().getType(contactRef)); - - String url = contactRef.toString(); - String columnName = null; - if (url.startsWith("content://com.android.contacts/data/phones")) { - // Direct lookup in the Phone table. - // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2") - Log.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID"); - columnName = RawContacts.CONTACT_ID; - } else if (url.startsWith("content://com.android.contacts/data")) { - // Direct lookup in the Data table. - // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data") - Log.v(TAG, "'data' URI; using Data.CONTACT_ID"); - // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.) - columnName = Data.CONTACT_ID; - } else if (url.startsWith("content://com.android.contacts/phone_lookup")) { - // Lookup in the PhoneLookup table, which provides "fuzzy matching" - // for phone numbers. - // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup") - Log.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID"); - columnName = PhoneLookupUtil.getContactIdColumnNameForUri(contactRef); - } else { - Log.v(TAG, "Unexpected prefix for contactRef '" + url + "'"); - } - int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1; - Log.v(TAG, "==> Using column '" + columnName - + "' (columnIndex = " + columnIndex + ") for person_id lookup..."); - return columnIndex; - } - - /** - * Updates this CallerInfo's geoDescription field, based on the raw - * phone number in the phoneNumber field. - * - * (Note that the various getCallerInfo() methods do *not* set the - * geoDescription automatically; you need to call this method - * explicitly to get it.) - * - * @param context the context used to look up the current locale / country - * @param fallbackNumber if this CallerInfo's phoneNumber field is empty, - * this specifies a fallback number to use instead. - */ - public void updateGeoDescription(Context context, String fallbackNumber) { - String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber; - geoDescription = com.android.dialer.util.PhoneNumberUtil.getGeoDescription(context, number); - } - - /** - * @return a string debug representation of this instance. - */ - @Override - public String toString() { - // Warning: never check in this file with VERBOSE_DEBUG = true - // because that will result in PII in the system log. - final boolean VERBOSE_DEBUG = false; - - if (VERBOSE_DEBUG) { - return new StringBuilder(384) - .append(super.toString() + " { ") - .append("\nname: " + name) - .append("\nphoneNumber: " + phoneNumber) - .append("\nnormalizedNumber: " + normalizedNumber) - .append("\forwardingNumber: " + forwardingNumber) - .append("\ngeoDescription: " + geoDescription) - .append("\ncnapName: " + cnapName) - .append("\nnumberPresentation: " + numberPresentation) - .append("\nnamePresentation: " + namePresentation) - .append("\ncontactExists: " + contactExists) - .append("\nphoneLabel: " + phoneLabel) - .append("\nnumberType: " + numberType) - .append("\nnumberLabel: " + numberLabel) - .append("\nphotoResource: " + photoResource) - .append("\ncontactIdOrZero: " + contactIdOrZero) - .append("\nneedUpdate: " + needUpdate) - .append("\ncontactRefUri: " + contactRefUri) - .append("\ncontactRingtoneUri: " + contactRingtoneUri) - .append("\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri) - .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail) - .append("\ncachedPhoto: " + cachedPhoto) - .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent) - .append("\nemergency: " + mIsEmergency) - .append("\nvoicemail: " + mIsVoiceMail) - .append("\nuserType: " + userType) - .append(" }") - .toString(); - } else { - return new StringBuilder(128) - .append(super.toString() + " { ") - .append("name " + ((name == null) ? "null" : "non-null")) - .append(", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null")) - .append(" }") - .toString(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java b/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java deleted file mode 100644 index f7f0cbb5dbba87ceec6ca553398f0b44d6078c16..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.google.common.primitives.Longs; - -import android.Manifest; -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.database.SQLException; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Directory; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.compat.DirectoryCompat; -import com.android.contacts.common.util.PermissionsUtil; -import com.android.contacts.common.util.TelephonyManagerUtils; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfoHelper; -import com.android.dialer.service.CachedNumberLookupService; -import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; -import com.android.dialerbind.ObjectFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - -/** - * Helper class to make it easier to run asynchronous caller-id lookup queries. - * @see CallerInfo - * - */ -public class CallerInfoAsyncQuery { - private static final boolean DBG = false; - private static final String LOG_TAG = "CallerInfoAsyncQuery"; - - private static final int EVENT_NEW_QUERY = 1; - private static final int EVENT_ADD_LISTENER = 2; - private static final int EVENT_END_OF_QUEUE = 3; - private static final int EVENT_EMERGENCY_NUMBER = 4; - private static final int EVENT_VOICEMAIL_NUMBER = 5; - - private CallerInfoAsyncQueryHandler mHandler; - - // If the CallerInfo query finds no contacts, should we use the - // PhoneNumberOfflineGeocoder to look up a "geo description"? - // (TODO: This could become a flag in config.xml if it ever needs to be - // configured on a per-product basis.) - private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true; - - /** - * Interface for a CallerInfoAsyncQueryHandler result return. - */ - public interface OnQueryCompleteListener { - /** - * Called when the query is complete. - */ - public void onQueryComplete(int token, Object cookie, CallerInfo ci); - } - - - /** - * Wrap the cookie from the WorkerArgs with additional information needed by our - * classes. - */ - private static final class CookieWrapper { - public OnQueryCompleteListener listener; - public Object cookie; - public int event; - public String number; - } - - /** - * Simple exception used to communicate problems with the query pool. - */ - public static class QueryPoolException extends SQLException { - public QueryPoolException(String error) { - super(error); - } - } - - /** - * Our own implementation of the AsyncQueryHandler. - */ - private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler { - - @Override - public void startQuery(int token, Object cookie, Uri uri, String[] projection, - String selection, String[] selectionArgs, String orderBy) { - if (DBG) { - // Show stack trace with the arguments. - android.util.Log.d(LOG_TAG, "InCall: startQuery: url=" + uri + - " projection=[" + Arrays.toString(projection) + "]" + - " selection=" + selection + " " + - " args=[" + Arrays.toString(selectionArgs) + "]", - new RuntimeException("STACKTRACE")); - } - super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy); - } - - /** - * The information relevant to each CallerInfo query. Each query may have multiple - * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper - * objects in the queue (one with a new query event, and one with a end event, with - * 0 or more additional listeners in between). - */ - private Context mQueryContext; - private Uri mQueryUri; - private CallerInfo mCallerInfo; - - /** - * Our own query worker thread. - * - * This thread handles the messages enqueued in the looper. The normal sequence - * of events is that a new query shows up in the looper queue, followed by 0 or - * more add listener requests, and then an end request. Of course, these requests - * can be interlaced with requests from other tokens, but is irrelevant to this - * handler since the handler has no state. - * - * Note that we depend on the queue to keep things in order; in other words, the - * looper queue must be FIFO with respect to input from the synchronous startQuery - * calls and output to this handleMessage call. - * - * This use of the queue is required because CallerInfo objects may be accessed - * multiple times before the query is complete. All accesses (listeners) must be - * queued up and informed in order when the query is complete. - */ - protected class CallerInfoWorkerHandler extends WorkerHandler { - public CallerInfoWorkerHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - WorkerArgs args = (WorkerArgs) msg.obj; - CookieWrapper cw = (CookieWrapper) args.cookie; - - if (cw == null) { - // Normally, this should never be the case for calls originating - // from within this code. - // However, if there is any code that this Handler calls (such as in - // super.handleMessage) that DOES place unexpected messages on the - // queue, then we need pass these messages on. - Log.d(this, "Unexpected command (CookieWrapper is null): " + msg.what + - " ignored by CallerInfoWorkerHandler, passing onto parent."); - - super.handleMessage(msg); - } else { - Log.d(this, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 + - " command: " + msg.what + " query URI: " + - sanitizeUriToString(args.uri)); - - switch (cw.event) { - case EVENT_NEW_QUERY: - //start the sql command. - super.handleMessage(msg); - break; - - // shortcuts to avoid query for recognized numbers. - case EVENT_EMERGENCY_NUMBER: - case EVENT_VOICEMAIL_NUMBER: - - case EVENT_ADD_LISTENER: - case EVENT_END_OF_QUEUE: - // query was already completed, so just send the reply. - // passing the original token value back to the caller - // on top of the event values in arg1. - Message reply = args.handler.obtainMessage(msg.what); - reply.obj = args; - reply.arg1 = msg.arg1; - - reply.sendToTarget(); - - break; - default: - } - } - } - } - - - /** - * Asynchronous query handler class for the contact / callerinfo object. - */ - private CallerInfoAsyncQueryHandler(Context context) { - super(context.getContentResolver()); - } - - @Override - protected Handler createHandler(Looper looper) { - return new CallerInfoWorkerHandler(looper); - } - - /** - * Overrides onQueryComplete from AsyncQueryHandler. - * - * This method takes into account the state of this class; we construct the CallerInfo - * object only once for each set of listeners. When the query thread has done its work - * and calls this method, we inform the remaining listeners in the queue, until we're - * out of listeners. Once we get the message indicating that we should expect no new - * listeners for this CallerInfo object, we release the AsyncCursorInfo back into the - * pool. - */ - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - try { - Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token); - - //get the cookie and notify the listener. - CookieWrapper cw = (CookieWrapper) cookie; - if (cw == null) { - // Normally, this should never be the case for calls originating - // from within this code. - // However, if there is any code that calls this method, we should - // check the parameters to make sure they're viable. - Log.d(this, "Cookie is null, ignoring onQueryComplete() request."); - return; - } - - if (cw.event == EVENT_END_OF_QUEUE) { - release(); - return; - } - - // check the token and if needed, create the callerinfo object. - if (mCallerInfo == null) { - if ((mQueryContext == null) || (mQueryUri == null)) { - throw new QueryPoolException - ("Bad context or query uri, or CallerInfoAsyncQuery already released."); - } - - // adjust the callerInfo data as needed, and only if it was set from the - // initial query request. - // Change the callerInfo number ONLY if it is an emergency number or the - // voicemail number, and adjust other data (including photoResource) - // accordingly. - if (cw.event == EVENT_EMERGENCY_NUMBER) { - // Note we're setting the phone number here (refer to javadoc - // comments at the top of CallerInfo class). - mCallerInfo = new CallerInfo().markAsEmergency(mQueryContext); - } else if (cw.event == EVENT_VOICEMAIL_NUMBER) { - mCallerInfo = new CallerInfo().markAsVoiceMail(mQueryContext); - } else { - mCallerInfo = CallerInfo.getCallerInfo(mQueryContext, mQueryUri, cursor); - Log.d(this, "==> Got mCallerInfo: " + mCallerInfo); - - CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary( - mQueryContext, cw.number, mCallerInfo); - if (newCallerInfo != mCallerInfo) { - mCallerInfo = newCallerInfo; - Log.d(this, "#####async contact look up with numeric username" - + mCallerInfo); - } - - // Final step: look up the geocoded description. - if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) { - // Note we do this only if we *don't* have a valid name (i.e. if - // no contacts matched the phone number of the incoming call), - // since that's the only case where the incoming-call UI cares - // about this field. - // - // (TODO: But if we ever want the UI to show the geoDescription - // even when we *do* match a contact, we'll need to either call - // updateGeoDescription() unconditionally here, or possibly add a - // new parameter to CallerInfoAsyncQuery.startQuery() to force - // the geoDescription field to be populated.) - - if (TextUtils.isEmpty(mCallerInfo.name)) { - // Actually when no contacts match the incoming phone number, - // the CallerInfo object is totally blank here (i.e. no name - // *or* phoneNumber). So we need to pass in cw.number as - // a fallback number. - mCallerInfo.updateGeoDescription(mQueryContext, cw.number); - } - } - - // Use the number entered by the user for display. - if (!TextUtils.isEmpty(cw.number)) { - mCallerInfo.phoneNumber = PhoneNumberUtils.formatNumber(cw.number, - mCallerInfo.normalizedNumber, - TelephonyManagerUtils.getCurrentCountryIso(mQueryContext, - Locale.getDefault())); - } - } - - Log.d(this, "constructing CallerInfo object for token: " + token); - - //notify that we can clean up the queue after this. - CookieWrapper endMarker = new CookieWrapper(); - endMarker.event = EVENT_END_OF_QUEUE; - startQuery(token, endMarker, null, null, null, null, null); - } - - //notify the listener that the query is complete. - if (cw.listener != null) { - Log.d(this, "notifying listener: " + cw.listener.getClass().toString() + - " for token: " + token + mCallerInfo); - cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo); - } - } finally { - // The cursor may have been closed in CallerInfo.getCallerInfo() - if (cursor != null && !cursor.isClosed()) { - cursor.close(); - } - } - } - } - - /** - * Private constructor for factory methods. - */ - private CallerInfoAsyncQuery() { - } - - public static void startQuery(final int token, final Context context, final CallerInfo info, - final OnQueryCompleteListener listener, final Object cookie) { - Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####"); - Log.d(LOG_TAG, "- number: " + info.phoneNumber); - Log.d(LOG_TAG, "- cookie: " + cookie); - if (!PermissionsUtil.hasPermission(context, Manifest.permission.READ_CONTACTS)) { - Log.w(LOG_TAG, "Dialer doesn't have permission to read contacts."); - listener.onQueryComplete(token, cookie, info); - return; - } - - OnQueryCompleteListener contactsProviderQueryCompleteListener = - new OnQueryCompleteListener() { - @Override - public void onQueryComplete(int token, Object cookie, CallerInfo ci) { - Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done"); - // If there are no other directory queries, make sure that the listener is - // notified of this result. see b/27621628 - if ((ci != null && ci.contactExists) || - !startOtherDirectoriesQuery(token, context, info, listener, cookie)) { - if (listener != null && ci != null) { - listener.onQueryComplete(token, cookie, ci); - } - } - } - }; - startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, - cookie); - } - - // Private methods - private static CallerInfoAsyncQuery startDefaultDirectoryQuery(int token, Context context, - CallerInfo info, OnQueryCompleteListener listener, Object cookie) { - // Construct the URI object and query params, and start the query. - Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber); - return startQueryInternal(token, context, info, listener, cookie, uri); - } - - /** - * Factory method to start the query based on a CallerInfo object. - * - * Note: if the number contains an "@" character we treat it - * as a SIP address, and look it up directly in the Data table - * rather than using the PhoneLookup table. - * TODO: But eventually we should expose two separate methods, one for - * numbers and one for SIP addresses, and then have - * PhoneUtils.startGetCallerInfo() decide which one to call based on - * the phone type of the incoming connection. - */ - private static CallerInfoAsyncQuery startQueryInternal(int token, Context context, - CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef) { - if (DBG) { - Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef)); - } - - CallerInfoAsyncQuery c = new CallerInfoAsyncQuery(); - c.allocate(context, contactRef); - - //create cookieWrapper, start query - CookieWrapper cw = new CookieWrapper(); - cw.listener = listener; - cw.cookie = cookie; - cw.number = info.phoneNumber; - - // check to see if these are recognized numbers, and use shortcuts if we can. - if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) { - cw.event = EVENT_EMERGENCY_NUMBER; - } else if (info.isVoiceMailNumber()) { - cw.event = EVENT_VOICEMAIL_NUMBER; - } else { - cw.event = EVENT_NEW_QUERY; - } - - - String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef); - c.mHandler.startQuery(token, - cw, // cookie - contactRef, // uri - proejection, // projection - null, // selection - null, // selectionArgs - null); // orderBy - return c; - } - - // Return value indicates if listener was notified. - private static boolean startOtherDirectoriesQuery(int token, Context context, CallerInfo info, - OnQueryCompleteListener listener, Object cookie) { - long[] directoryIds = getDirectoryIds(context); - int size = directoryIds.length; - if (size == 0) { - return false; - } - - DirectoryQueryCompleteListenerFactory listenerFactory = - new DirectoryQueryCompleteListenerFactory(context, size, listener); - - // The current implementation of multiple async query runs in single handler thread - // in AsyncQueryHandler. - // intermediateListener.onQueryComplete is also called from the same caller thread. - // TODO(b/26019872): use thread pool instead of single thread. - for (int i = 0; i < size; i++) { - long directoryId = directoryIds[i]; - Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId); - if (DBG) { - Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri); - } - OnQueryCompleteListener intermediateListener = - listenerFactory.newListener(directoryId); - startQueryInternal(token, context, info, intermediateListener, cookie, uri); - } - return true; - } - - /* Directory lookup related code - START */ - private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID}; - - private static long[] getDirectoryIds(Context context) { - ArrayList results = new ArrayList<>(); - - Uri uri = Directory.CONTENT_URI; - if (ContactsUtils.FLAG_N_FEATURE) { - uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise"); - } - - ContentResolver cr = context.getContentResolver(); - Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null); - addDirectoryIdsFromCursor(cursor, results); - - return Longs.toArray(results); - } - - private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList results) { - if (cursor != null) { - int idIndex = cursor.getColumnIndex(Directory._ID); - while (cursor.moveToNext()) { - long id = cursor.getLong(idIndex); - if (DirectoryCompat.isRemoteDirectoryId(id)) { - results.add(id); - } - } - cursor.close(); - } - } - - private static final class DirectoryQueryCompleteListenerFactory { - // Make sure listener to be called once and only once - private int mCount; - private boolean mIsListenerCalled; - private final OnQueryCompleteListener mListener; - private final Context mContext; - private final CachedNumberLookupService mCachedNumberLookupService = - ObjectFactory.newCachedNumberLookupService(); - - private class DirectoryQueryCompleteListener implements OnQueryCompleteListener { - private final long mDirectoryId; - - DirectoryQueryCompleteListener(long directoryId) { - mDirectoryId = directoryId; - } - - @Override - public void onQueryComplete(int token, Object cookie, CallerInfo ci) { - onDirectoryQueryComplete(token, cookie, ci, mDirectoryId); - } - } - - DirectoryQueryCompleteListenerFactory(Context context, int size, - OnQueryCompleteListener listener) { - mCount = size; - mListener = listener; - mIsListenerCalled = false; - mContext = context; - } - - private void onDirectoryQueryComplete(int token, Object cookie, CallerInfo ci, - long directoryId) { - boolean shouldCallListener = false; - synchronized (this) { - mCount = mCount - 1; - if (!mIsListenerCalled && (ci.contactExists || mCount == 0)) { - mIsListenerCalled = true; - shouldCallListener = true; - } - } - - // Don't call callback in synchronized block because mListener.onQueryComplete may - // take long time to complete - if (shouldCallListener && mListener != null) { - addCallerInfoIntoCache(ci, directoryId); - mListener.onQueryComplete(token, cookie, ci); - } - } - - private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) { - if (ci.contactExists && mCachedNumberLookupService != null) { - // 1. Cache caller info - CachedContactInfo cachedContactInfo = CallerInfoUtils - .buildCachedContactInfo(mCachedNumberLookupService, ci); - String directoryLabel = mContext.getString(R.string.directory_search_label); - cachedContactInfo.setDirectorySource(directoryLabel, directoryId); - mCachedNumberLookupService.addContact(mContext, cachedContactInfo); - - // 2. Cache photo - if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) { - try (InputStream in = mContext.getContentResolver() - .openInputStream(ci.contactDisplayPhotoUri)) { - if (in != null) { - mCachedNumberLookupService.addPhoto(mContext, ci.normalizedNumber, in); - } - } catch (IOException e) { - Log.e(LOG_TAG, "failed to fetch directory contact photo", e); - } - - } - } - } - - public OnQueryCompleteListener newListener(long directoryId) { - return new DirectoryQueryCompleteListener(directoryId); - } - } - /* Directory lookup related code - END */ - - /** - * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct - * state of context and uri. - */ - private void allocate(Context context, Uri contactRef) { - if ((context == null) || (contactRef == null)){ - throw new QueryPoolException("Bad context or query uri."); - } - mHandler = new CallerInfoAsyncQueryHandler(context); - mHandler.mQueryContext = context; - mHandler.mQueryUri = contactRef; - } - - /** - * Releases the relevant data. - */ - private void release() { - mHandler.mQueryContext = null; - mHandler.mQueryUri = null; - mHandler.mCallerInfo = null; - mHandler = null; - } - - private static String sanitizeUriToString(Uri uri) { - if (uri != null) { - String uriString = uri.toString(); - int indexOfLastSlash = uriString.lastIndexOf('/'); - if (indexOfLastSlash > 0) { - return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx"; - } else { - return uriString; - } - } else { - return ""; - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallerInfoUtils.java b/InCallUI/src/com/android/incallui/CallerInfoUtils.java deleted file mode 100644 index 289b652fc2fca5348d74db1fed11d354ad2e00a9..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CallerInfoUtils.java +++ /dev/null @@ -1,234 +0,0 @@ -package com.android.incallui; - -import android.content.Context; -import android.content.Loader; -import android.content.Loader.OnLoadCompleteListener; -import android.net.Uri; -import android.telecom.PhoneAccount; -import android.telecom.TelecomManager; -import android.text.TextUtils; -import android.util.Log; - -import com.android.contacts.common.model.Contact; -import com.android.contacts.common.model.ContactLoader; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfo; -import com.android.dialer.service.CachedNumberLookupService; -import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; -import com.android.dialer.util.TelecomUtil; - -import java.util.Arrays; - -/** - * Utility methods for contact and caller info related functionality - */ -public class CallerInfoUtils { - - private static final String TAG = CallerInfoUtils.class.getSimpleName(); - - /** Define for not a special CNAP string */ - private static final int CNAP_SPECIAL_CASE_NO = -1; - - public CallerInfoUtils() { - } - - private static final int QUERY_TOKEN = -1; - - /** - * This is called to get caller info for a call. This will return a CallerInfo - * object immediately based off information in the call, but - * more information is returned to the OnQueryCompleteListener (which contains - * information about the phone number label, user's name, etc). - */ - public static CallerInfo getCallerInfoForCall(Context context, Call call, - CallerInfoAsyncQuery.OnQueryCompleteListener listener) { - CallerInfo info = buildCallerInfo(context, call); - - // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call. - - if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) { - // Start the query with the number provided from the call. - Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()..."); - CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, call); - } - return info; - } - - public static CallerInfo buildCallerInfo(Context context, Call call) { - CallerInfo info = new CallerInfo(); - - // Store CNAP information retrieved from the Connection (we want to do this - // here regardless of whether the number is empty or not). - info.cnapName = call.getCnapName(); - info.name = info.cnapName; - info.numberPresentation = call.getNumberPresentation(); - info.namePresentation = call.getCnapNamePresentation(); - info.callSubject = call.getCallSubject(); - - String number = call.getNumber(); - if (!TextUtils.isEmpty(number)) { - final String[] numbers = number.split("&"); - number = numbers[0]; - if (numbers.length > 1) { - info.forwardingNumber = numbers[1]; - } - - number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation); - info.phoneNumber = number; - } - - // Because the InCallUI is immediately launched before the call is connected, occasionally - // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number. - // This call should still be handled as a voicemail call. - if ((call.getHandle() != null && - PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) || - isVoiceMailNumber(context, call)) { - info.markAsVoiceMail(context); - } - - ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call, - info); - - return info; - } - - /** - * Creates a new {@link CachedContactInfo} from a {@link CallerInfo} - * - * @param lookupService the {@link CachedNumberLookupService} used to build a - * new {@link CachedContactInfo} - * @param {@link CallerInfo} object - * @return a CachedContactInfo object created from this CallerInfo - * @throws NullPointerException if lookupService or ci are null - */ - public static CachedContactInfo buildCachedContactInfo(CachedNumberLookupService lookupService, - CallerInfo ci) { - ContactInfo info = new ContactInfo(); - info.name = ci.name; - info.type = ci.numberType; - info.label = ci.phoneLabel; - info.number = ci.phoneNumber; - info.normalizedNumber = ci.normalizedNumber; - info.photoUri = ci.contactDisplayPhotoUri; - info.userType = ci.userType; - - CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info); - cacheInfo.setLookupKey(ci.lookupKeyOrNull); - return cacheInfo; - } - - public static boolean isVoiceMailNumber(Context context, Call call) { - return TelecomUtil.isVoicemailNumber(context, - call.getTelecomCall().getDetails().getAccountHandle(), - call.getNumber()); - } - - /** - * Handles certain "corner cases" for CNAP. When we receive weird phone numbers - * from the network to indicate different number presentations, convert them to - * expected number and presentation values within the CallerInfo object. - * @param number number we use to verify if we are in a corner case - * @param presentation presentation value used to verify if we are in a corner case - * @return the new String that should be used for the phone number - */ - /* package */static String modifyForSpecialCnapCases(Context context, CallerInfo ci, - String number, int presentation) { - // Obviously we return number if ci == null, but still return number if - // number == null, because in these cases the correct string will still be - // displayed/logged after this function returns based on the presentation value. - if (ci == null || number == null) return number; - - Log.d(TAG, "modifyForSpecialCnapCases: initially, number=" - + toLogSafePhoneNumber(number) - + ", presentation=" + presentation + " ci " + ci); - - // "ABSENT NUMBER" is a possible value we could get from the network as the - // phone number, so if this happens, change it to "Unknown" in the CallerInfo - // and fix the presentation to be the same. - final String[] absentNumberValues = - context.getResources().getStringArray(R.array.absent_num); - if (Arrays.asList(absentNumberValues).contains(number) - && presentation == TelecomManager.PRESENTATION_ALLOWED) { - number = context.getString(R.string.unknown); - ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN; - } - - // Check for other special "corner cases" for CNAP and fix them similarly. Corner - // cases only apply if we received an allowed presentation from the network, so check - // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't - // match the presentation passed in for verification (meaning we changed it previously - // because it's a corner case and we're being called from a different entry point). - if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED - || (ci.numberPresentation != presentation - && presentation == TelecomManager.PRESENTATION_ALLOWED)) { - // For all special strings, change number & numberPrentation. - if (isCnapSpecialCaseRestricted(number)) { - number = context.getString(R.string.private_num); - ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED; - } else if (isCnapSpecialCaseUnknown(number)) { - number = context.getString(R.string.unknown); - ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN; - } - Log.d(TAG, "SpecialCnap: number=" + toLogSafePhoneNumber(number) - + "; presentation now=" + ci.numberPresentation); - } - Log.d(TAG, "modifyForSpecialCnapCases: returning number string=" - + toLogSafePhoneNumber(number)); - return number; - } - - private static boolean isCnapSpecialCaseRestricted(String n) { - return n.equals("PRIVATE") || n.equals("P") || n.equals("RES"); - } - - private static boolean isCnapSpecialCaseUnknown(String n) { - return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U"); - } - - /* package */static String toLogSafePhoneNumber(String number) { - // For unknown number, log empty string. - if (number == null) { - return ""; - } - - // Todo: Figure out an equivalent for VDBG - if (false) { - // When VDBG is true we emit PII. - return number; - } - - // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare - // sanitized phone numbers. - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < number.length(); i++) { - char c = number.charAt(i); - if (c == '-' || c == '@' || c == '.' || c == '&') { - builder.append(c); - } else { - builder.append('x'); - } - } - return builder.toString(); - } - - /** - * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are - * viewing a particular contact, so that it can download the high-res photo. - */ - public static void sendViewNotification(Context context, Uri contactUri) { - final ContactLoader loader = new ContactLoader(context, contactUri, - true /* postViewNotification */); - loader.registerListener(0, new OnLoadCompleteListener() { - @Override - public void onLoadComplete( - Loader loader, Contact contact) { - try { - loader.reset(); - } catch (RuntimeException e) { - Log.e(TAG, "Error resetting loader", e); - } - } - }); - loader.startLoading(); - } -} diff --git a/InCallUI/src/com/android/incallui/CircularRevealFragment.java b/InCallUI/src/com/android/incallui/CircularRevealFragment.java deleted file mode 100644 index 01bd253ec2a58d0136452d80d4a54ac91113f3db..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/CircularRevealFragment.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.app.Activity; -import android.app.Fragment; -import android.app.FragmentManager; -import android.graphics.Outline; -import android.graphics.Point; -import android.os.Bundle; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnPreDrawListener; - -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; - -public class CircularRevealFragment extends Fragment { - static final String TAG = "CircularRevealFragment"; - - private Point mTouchPoint; - private OnCircularRevealCompleteListener mListener; - private boolean mAnimationStarted; - - interface OnCircularRevealCompleteListener { - public void onCircularRevealComplete(FragmentManager fm); - } - - public static void startCircularReveal(FragmentManager fm, Point touchPoint, - OnCircularRevealCompleteListener listener) { - if (fm.findFragmentByTag(TAG) == null) { - fm.beginTransaction().add(R.id.main, - new CircularRevealFragment(touchPoint, listener), TAG) - .commitAllowingStateLoss(); - } else { - Log.w(TAG, "An instance of CircularRevealFragment already exists"); - } - } - - public static void endCircularReveal(FragmentManager fm) { - final Fragment fragment = fm.findFragmentByTag(TAG); - if (fragment != null) { - fm.beginTransaction().remove(fragment).commitAllowingStateLoss(); - } - } - - /** - * Empty constructor used only by the {@link FragmentManager}. - */ - public CircularRevealFragment() {} - - public CircularRevealFragment(Point touchPoint, OnCircularRevealCompleteListener listener) { - mTouchPoint = touchPoint; - mListener = listener; - } - - @Override - public void onResume() { - super.onResume(); - if (!mAnimationStarted) { - // Only run the animation once for each instance of the fragment - startOutgoingAnimation(InCallPresenter.getInstance().getThemeColors()); - } - mAnimationStarted = true; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.outgoing_call_animation, container, false); - } - - public void startOutgoingAnimation(MaterialPalette palette) { - final Activity activity = getActivity(); - if (activity == null) { - Log.w(this, "Asked to do outgoing call animation when not attached"); - return; - } - - final View view = activity.getWindow().getDecorView(); - - // The circle starts from an initial size of 0 so clip it such that it is invisible. - // Otherwise the first frame is drawn with a fully opaque screen which causes jank. When - // the animation later starts, this clip will be clobbered by the circular reveal clip. - // See ViewAnimationUtils.createCircularReveal. - view.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - // Using (0, 0, 0, 0) will not work since the outline will simply be treated as - // an empty outline. - outline.setOval(-1, -1, 0, 0); - } - }); - view.setClipToOutline(true); - - if (palette != null) { - view.findViewById(R.id.outgoing_call_animation_circle).setBackgroundColor( - palette.mPrimaryColor); - activity.getWindow().setStatusBarColor(palette.mSecondaryColor); - } - - view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - final ViewTreeObserver vto = view.getViewTreeObserver(); - if (vto.isAlive()) { - vto.removeOnPreDrawListener(this); - } - final Animator animator = getRevealAnimator(mTouchPoint); - if (animator != null) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setClipToOutline(false); - if (mListener != null) { - mListener.onCircularRevealComplete(getFragmentManager()); - } - } - }); - animator.start(); - } - return false; - } - }); - } - - private Animator getRevealAnimator(Point touchPoint) { - final Activity activity = getActivity(); - if (activity == null) { - return null; - } - final View view = activity.getWindow().getDecorView(); - final Display display = activity.getWindowManager().getDefaultDisplay(); - final Point size = new Point(); - display.getSize(size); - - int startX = size.x / 2; - int startY = size.y / 2; - if (touchPoint != null) { - startX = touchPoint.x; - startY = touchPoint.y; - } - - final Animator valueAnimator = ViewAnimationUtils.createCircularReveal(view, - startX, startY, 0, Math.max(size.x, size.y)); - valueAnimator.setDuration(getResources().getInteger(R.integer.reveal_animation_duration)); - return valueAnimator; - } -} diff --git a/InCallUI/src/com/android/incallui/ConferenceManagerFragment.java b/InCallUI/src/com/android/incallui/ConferenceManagerFragment.java deleted file mode 100644 index fe941c8c500f4dfae47d38ba47b500968307cda1..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ConferenceManagerFragment.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.app.ActionBar; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; - -import com.android.contacts.common.ContactPhotoManager; -import com.android.dialer.R; - -import java.util.List; - -/** - * Fragment that allows the user to manage a conference call. - */ -public class ConferenceManagerFragment - extends BaseFragment - implements ConferenceManagerPresenter.ConferenceManagerUi { - - private static final String KEY_IS_VISIBLE = "key_conference_is_visible"; - - private ListView mConferenceParticipantList; - private int mActionBarElevation; - private ContactPhotoManager mContactPhotoManager; - private LayoutInflater mInflater; - private ConferenceParticipantListAdapter mConferenceParticipantListAdapter; - private boolean mIsVisible; - private boolean mIsRecreating; - - @Override - public ConferenceManagerPresenter createPresenter() { - return new ConferenceManagerPresenter(); - } - - @Override - public ConferenceManagerPresenter.ConferenceManagerUi getUi() { - return this; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mIsRecreating = true; - mIsVisible = savedInstanceState.getBoolean(KEY_IS_VISIBLE); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View parent = - inflater.inflate(R.layout.conference_manager_fragment, container, false); - - mConferenceParticipantList = (ListView) parent.findViewById(R.id.participantList); - mContactPhotoManager = - ContactPhotoManager.getInstance(getActivity().getApplicationContext()); - mActionBarElevation = - (int) getResources().getDimension(R.dimen.incall_action_bar_elevation); - mInflater = LayoutInflater.from(getActivity().getApplicationContext()); - - return parent; - } - - @Override - public void onResume() { - super.onResume(); - if (mIsRecreating) { - onVisibilityChanged(mIsVisible); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putBoolean(KEY_IS_VISIBLE, mIsVisible); - super.onSaveInstanceState(outState); - } - - public void onVisibilityChanged(boolean isVisible) { - mIsVisible = isVisible; - ActionBar actionBar = getActivity().getActionBar(); - if (isVisible) { - actionBar.setTitle(R.string.manageConferenceLabel); - actionBar.setElevation(mActionBarElevation); - actionBar.setHideOffset(0); - actionBar.show(); - - final CallList calls = CallList.getInstance(); - getPresenter().init(getActivity(), calls); - // Request focus on the list of participants for accessibility purposes. This ensures - // that once the list of participants is shown, the first participant is announced. - mConferenceParticipantList.requestFocus(); - } else { - actionBar.setElevation(0); - actionBar.setHideOffset(actionBar.getHeight()); - } - } - - @Override - public boolean isFragmentVisible() { - return isVisible(); - } - - @Override - public void update(Context context, List participants, boolean parentCanSeparate) { - if (mConferenceParticipantListAdapter == null) { - mConferenceParticipantListAdapter = new ConferenceParticipantListAdapter( - mConferenceParticipantList, context, mInflater, mContactPhotoManager); - - mConferenceParticipantList.setAdapter(mConferenceParticipantListAdapter); - } - mConferenceParticipantListAdapter.updateParticipants(participants, parentCanSeparate); - } - - @Override - public void refreshCall(Call call) { - mConferenceParticipantListAdapter.refreshCall(call); - } -} diff --git a/InCallUI/src/com/android/incallui/ConferenceManagerPresenter.java b/InCallUI/src/com/android/incallui/ConferenceManagerPresenter.java deleted file mode 100644 index 6fb6e5dda10142a08f0af7ac7e5911b364945189..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ConferenceManagerPresenter.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; - -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; - -import com.google.common.base.Preconditions; - -import java.util.ArrayList; -import java.util.List; - -/** - * Logic for call buttons. - */ -public class ConferenceManagerPresenter - extends Presenter - implements InCallStateListener, InCallDetailsListener, IncomingCallListener { - - private Context mContext; - - @Override - public void onUiReady(ConferenceManagerUi ui) { - super.onUiReady(ui); - - // register for call state changes last - InCallPresenter.getInstance().addListener(this); - InCallPresenter.getInstance().addIncomingCallListener(this); - } - - @Override - public void onUiUnready(ConferenceManagerUi ui) { - super.onUiUnready(ui); - - InCallPresenter.getInstance().removeListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - } - - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - if (getUi().isFragmentVisible()) { - Log.v(this, "onStateChange" + newState); - if (newState == InCallState.INCALL) { - final Call call = callList.getActiveOrBackgroundCall(); - if (call != null && call.isConferenceCall()) { - Log.v(this, "Number of existing calls is " + - String.valueOf(call.getChildCallIds().size())); - update(callList); - } else { - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } else { - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } - } - - @Override - public void onDetailsChanged(Call call, android.telecom.Call.Details details) { - boolean canDisconnect = details.can( - android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE); - boolean canSeparate = details.can( - android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE); - - if (call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE) - != canDisconnect - || call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE) - != canSeparate) { - getUi().refreshCall(call); - } - - if (!details.can( - android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)) { - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - // When incoming call exists, set conference ui invisible. - if (getUi().isFragmentVisible()) { - Log.d(this, "onIncomingCall()... Conference ui is showing, hide it."); - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } - - public void init(Context context, CallList callList) { - mContext = Preconditions.checkNotNull(context); - mContext = context; - update(callList); - } - - /** - * Updates the conference participant adapter. - * - * @param callList The callList. - */ - private void update(CallList callList) { - // callList is non null, but getActiveOrBackgroundCall() may return null - final Call currentCall = callList.getActiveOrBackgroundCall(); - if (currentCall == null) { - return; - } - - ArrayList calls = new ArrayList<>(currentCall.getChildCallIds().size()); - for (String callerId : currentCall.getChildCallIds()) { - calls.add(callList.getCallById(callerId)); - } - - Log.d(this, "Number of calls is " + String.valueOf(calls.size())); - - // Users can split out a call from the conference call if either the active call or the - // holding call is empty. If both are filled, users can not split out another call. - final boolean hasActiveCall = (callList.getActiveCall() != null); - final boolean hasHoldingCall = (callList.getBackgroundCall() != null); - boolean canSeparate = !(hasActiveCall && hasHoldingCall); - - getUi().update(mContext, calls, canSeparate); - } - - public interface ConferenceManagerUi extends Ui { - boolean isFragmentVisible(); - void update(Context context, List participants, boolean parentCanSeparate); - void refreshCall(Call call); - } -} diff --git a/InCallUI/src/com/android/incallui/ConferenceParticipantListAdapter.java b/InCallUI/src/com/android/incallui/ConferenceParticipantListAdapter.java deleted file mode 100644 index d68ae1f6f7fe745126238037a32434918b685fd4..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ConferenceParticipantListAdapter.java +++ /dev/null @@ -1,533 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.MoreObjects; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import com.android.contacts.common.ContactPhotoManager; -import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; -import com.android.contacts.common.compat.PhoneNumberUtilsCompat; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Adapter for a ListView containing conference call participant information. - */ -public class ConferenceParticipantListAdapter extends BaseAdapter { - - /** - * Internal class which represents a participant. Includes a reference to the {@link Call} and - * the corresponding {@link ContactCacheEntry} for the participant. - */ - private class ParticipantInfo { - private Call mCall; - private ContactCacheEntry mContactCacheEntry; - private boolean mCacheLookupComplete = false; - - public ParticipantInfo(Call call, ContactCacheEntry contactCacheEntry) { - mCall = call; - mContactCacheEntry = contactCacheEntry; - } - - public Call getCall() { - return mCall; - } - - public void setCall(Call call) { - mCall = call; - } - - public ContactCacheEntry getContactCacheEntry() { - return mContactCacheEntry; - } - - public void setContactCacheEntry(ContactCacheEntry entry) { - mContactCacheEntry = entry; - } - - public boolean isCacheLookupComplete() { - return mCacheLookupComplete; - } - - public void setCacheLookupComplete(boolean cacheLookupComplete) { - mCacheLookupComplete = cacheLookupComplete; - } - - @Override - public boolean equals(Object o) { - if (o instanceof ParticipantInfo) { - ParticipantInfo p = (ParticipantInfo) o; - return - Objects.equals(p.getCall().getId(), mCall.getId()); - } - return false; - } - - @Override - public int hashCode() { - return mCall.getId().hashCode(); - } - } - - /** - * Callback class used when making requests to the {@link ContactInfoCache} to resolve contact - * info and contact photos for conference participants. - */ - public static class ContactLookupCallback implements ContactInfoCache.ContactInfoCacheCallback { - private final WeakReference mListAdapter; - - public ContactLookupCallback(ConferenceParticipantListAdapter listAdapter) { - mListAdapter = new WeakReference(listAdapter); - } - - /** - * Called when contact info has been resolved. - * - * @param callId The call id. - * @param entry The new contact information. - */ - @Override - public void onContactInfoComplete(String callId, ContactCacheEntry entry) { - update(callId, entry); - } - - /** - * Called when contact photo has been loaded into the cache. - * - * @param callId The call id. - * @param entry The new contact information. - */ - @Override - public void onImageLoadComplete(String callId, ContactCacheEntry entry) { - update(callId, entry); - } - - @Override - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {} - - /** - * Updates the contact information for a participant. - * - * @param callId The call id. - * @param entry The new contact information. - */ - private void update(String callId, ContactCacheEntry entry) { - ConferenceParticipantListAdapter listAdapter = mListAdapter.get(); - if (listAdapter != null) { - listAdapter.updateContactInfo(callId, entry); - } - } - } - - /** - * Listener used to handle tap of the "disconnect' button for a participant. - */ - private View.OnClickListener mDisconnectListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - View parent = (View) v.getParent(); - String callId = (String) parent.getTag(); - TelecomAdapter.getInstance().disconnectCall(callId); - } - }; - - /** - * Listener used to handle tap of the "separate' button for a participant. - */ - private View.OnClickListener mSeparateListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - View parent = (View) v.getParent(); - String callId = (String) parent.getTag(); - TelecomAdapter.getInstance().separateCall(callId); - } - }; - - /** - * The ListView containing the participant information. - */ - private final ListView mListView; - - /** - * The conference participants to show in the ListView. - */ - private List mConferenceParticipants = new ArrayList<>(); - - /** - * Hashmap to make accessing participant info by call Id faster. - */ - private final HashMap mParticipantsByCallId = new HashMap<>(); - - /** - * The context. - */ - private final Context mContext; - - /** - * ContactsPreferences used to lookup displayName preferences - */ - @Nullable private final ContactsPreferences mContactsPreferences; - - /** - * The layout inflater used to inflate new views. - */ - private final LayoutInflater mLayoutInflater; - - /** - * Contact photo manager to retrieve cached contact photo information. - */ - private final ContactPhotoManager mContactPhotoManager; - - /** - * {@code True} if the conference parent supports separating calls from the conference. - */ - private boolean mParentCanSeparate; - - /** - * Creates an instance of the ConferenceParticipantListAdapter. - * - * @param listView The listview. - * @param context The context. - * @param layoutInflater The layout inflater. - * @param contactPhotoManager The contact photo manager, used to load contact photos. - */ - public ConferenceParticipantListAdapter(ListView listView, Context context, - LayoutInflater layoutInflater, ContactPhotoManager contactPhotoManager) { - - mListView = listView; - mContext = context; - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - mLayoutInflater = layoutInflater; - mContactPhotoManager = contactPhotoManager; - } - - /** - * Updates the adapter with the new conference participant information provided. - * - * @param conferenceParticipants The list of conference participants. - * @param parentCanSeparate {@code True} if the parent supports separating calls from the - * conference. - */ - public void updateParticipants(List conferenceParticipants, boolean parentCanSeparate) { - if (mContactsPreferences != null) { - mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); - mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY); - } - mParentCanSeparate = parentCanSeparate; - updateParticipantInfo(conferenceParticipants); - } - - /** - * Determines the number of participants in the conference. - * - * @return The number of participants. - */ - @Override - public int getCount() { - return mConferenceParticipants.size(); - } - - /** - * Retrieves an item from the list of participants. - * - * @param position Position of the item whose data we want within the adapter's - * data set. - * @return The {@link ParticipantInfo}. - */ - @Override - public Object getItem(int position) { - return mConferenceParticipants.get(position); - } - - /** - * Retreives the adapter-specific item id for an item at a specified position. - * - * @param position The position of the item within the adapter's data set whose row id we want. - * @return The item id. - */ - @Override - public long getItemId(int position) { - return position; - } - - /** - * Refreshes call information for the call passed in. - * - * @param call The new call information. - */ - public void refreshCall(Call call) { - String callId = call.getId(); - - if (mParticipantsByCallId.containsKey(callId)) { - ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); - participantInfo.setCall(call); - refreshView(callId); - } - } - - /** - * Attempts to refresh the view for the specified call ID. This ensures the contact info and - * photo loaded from cache are updated. - * - * @param callId The call id. - */ - private void refreshView(String callId) { - int first = mListView.getFirstVisiblePosition(); - int last = mListView.getLastVisiblePosition(); - - for (int position = 0; position <= last - first; position++) { - View view = mListView.getChildAt(position); - String rowCallId = (String) view.getTag(); - if (rowCallId.equals(callId)) { - getView(position+first, view, mListView); - break; - } - } - } - - /** - * Creates or populates an existing conference participant row. - * - * @param position The position of the item within the adapter's data set of the item whose view - * we want. - * @param convertView The old view to reuse, if possible. - * @param parent The parent that this view will eventually be attached to - * @return The populated view. - */ - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // Make sure we have a valid convertView to start with - final View result = convertView == null - ? mLayoutInflater.inflate(R.layout.caller_in_conference, parent, false) - : convertView; - - ParticipantInfo participantInfo = mConferenceParticipants.get(position); - Call call = participantInfo.getCall(); - ContactCacheEntry contactCache = participantInfo.getContactCacheEntry(); - - final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); - - // If a cache lookup has not yet been performed to retrieve the contact information and - // photo, do it now. - if (!participantInfo.isCacheLookupComplete()) { - cache.findInfo(participantInfo.getCall(), - participantInfo.getCall().getState() == Call.State.INCOMING, - new ContactLookupCallback(this)); - } - - boolean thisRowCanSeparate = mParentCanSeparate && call.getTelecomCall().getDetails().can( - android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE); - boolean thisRowCanDisconnect = call.getTelecomCall().getDetails().can( - android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE); - - setCallerInfoForRow(result, contactCache.namePrimary, - ContactDisplayUtils.getPreferredDisplayName(contactCache.namePrimary, - contactCache.nameAlternative, mContactsPreferences), - contactCache.number, contactCache.label, - contactCache.lookupKey, contactCache.displayPhotoUri, thisRowCanSeparate, - thisRowCanDisconnect); - - // Tag the row in the conference participant list with the call id to make it easier to - // find calls when contact cache information is loaded. - result.setTag(call.getId()); - - return result; - } - - /** - * Replaces the contact info for a participant and triggers a refresh of the UI. - * - * @param callId The call id. - * @param entry The new contact info. - */ - /* package */ void updateContactInfo(String callId, ContactCacheEntry entry) { - if (mParticipantsByCallId.containsKey(callId)) { - ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); - participantInfo.setContactCacheEntry(entry); - participantInfo.setCacheLookupComplete(true); - refreshView(callId); - } - } - - /** - * Sets the caller information for a row in the conference participant list. - * - * @param view The view to set the details on. - * @param callerName The participant's name. - * @param callerNumber The participant's phone number. - * @param callerNumberType The participant's phone number typ.e - * @param lookupKey The lookup key for the participant (for photo lookup). - * @param photoUri The URI of the contact photo. - * @param thisRowCanSeparate {@code True} if this participant can separate from the conference. - * @param thisRowCanDisconnect {@code True} if this participant can be disconnected. - */ - private final void setCallerInfoForRow(View view, String callerName, String preferredName, - String callerNumber, String callerNumberType, String lookupKey, Uri photoUri, - boolean thisRowCanSeparate, boolean thisRowCanDisconnect) { - - final ImageView photoView = (ImageView) view.findViewById(R.id.callerPhoto); - final TextView nameTextView = (TextView) view.findViewById(R.id.conferenceCallerName); - final TextView numberTextView = (TextView) view.findViewById(R.id.conferenceCallerNumber); - final TextView numberTypeTextView = (TextView) view.findViewById( - R.id.conferenceCallerNumberType); - final View endButton = view.findViewById(R.id.conferenceCallerDisconnect); - final View separateButton = view.findViewById(R.id.conferenceCallerSeparate); - - endButton.setVisibility(thisRowCanDisconnect ? View.VISIBLE : View.GONE); - if (thisRowCanDisconnect) { - endButton.setOnClickListener(mDisconnectListener); - } else { - endButton.setOnClickListener(null); - } - - separateButton.setVisibility(thisRowCanSeparate ? View.VISIBLE : View.GONE); - if (thisRowCanSeparate) { - separateButton.setOnClickListener(mSeparateListener); - } else { - separateButton.setOnClickListener(null); - } - - DefaultImageRequest imageRequest = (photoUri != null) ? null : - new DefaultImageRequest(callerName, lookupKey, true /* isCircularPhoto */); - - mContactPhotoManager.loadDirectoryPhoto(photoView, photoUri, false, true, imageRequest); - - // set the caller name - nameTextView.setText(preferredName); - - // set the caller number in subscript, or make the field disappear. - if (TextUtils.isEmpty(callerNumber)) { - numberTextView.setVisibility(View.GONE); - numberTypeTextView.setVisibility(View.GONE); - } else { - numberTextView.setVisibility(View.VISIBLE); - numberTextView.setText(PhoneNumberUtilsCompat.createTtsSpannable( - BidiFormatter.getInstance().unicodeWrap( - callerNumber, TextDirectionHeuristics.LTR))); - numberTypeTextView.setVisibility(View.VISIBLE); - numberTypeTextView.setText(callerNumberType); - } - } - - /** - * Updates the participant info list which is bound to the ListView. Stores the call and - * contact info for all entries. The list is sorted alphabetically by participant name. - * - * @param conferenceParticipants The calls which make up the conference participants. - */ - private void updateParticipantInfo(List conferenceParticipants) { - final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); - boolean newParticipantAdded = false; - HashSet newCallIds = new HashSet<>(conferenceParticipants.size()); - - // Update or add conference participant info. - for (Call call : conferenceParticipants) { - String callId = call.getId(); - newCallIds.add(callId); - ContactCacheEntry contactCache = cache.getInfo(callId); - if (contactCache == null) { - contactCache = ContactInfoCache.buildCacheEntryFromCall(mContext, call, - call.getState() == Call.State.INCOMING); - } - - if (mParticipantsByCallId.containsKey(callId)) { - ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); - participantInfo.setCall(call); - participantInfo.setContactCacheEntry(contactCache); - } else { - newParticipantAdded = true; - ParticipantInfo participantInfo = new ParticipantInfo(call, contactCache); - mConferenceParticipants.add(participantInfo); - mParticipantsByCallId.put(call.getId(), participantInfo); - } - } - - // Remove any participants that no longer exist. - Iterator> it = - mParticipantsByCallId.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - String existingCallId = entry.getKey(); - if (!newCallIds.contains(existingCallId)) { - ParticipantInfo existingInfo = entry.getValue(); - mConferenceParticipants.remove(existingInfo); - it.remove(); - } - } - - if (newParticipantAdded) { - // Sort the list of participants by contact name. - sortParticipantList(); - } - notifyDataSetChanged(); - } - - /** - * Sorts the participant list by contact name. - */ - private void sortParticipantList() { - Collections.sort(mConferenceParticipants, new Comparator() { - public int compare(ParticipantInfo p1, ParticipantInfo p2) { - // Contact names might be null, so replace with empty string. - ContactCacheEntry c1 = p1.getContactCacheEntry(); - String p1Name = MoreObjects.firstNonNull( - ContactDisplayUtils.getPreferredSortName( - c1.namePrimary, - c1.nameAlternative, - mContactsPreferences), - ""); - - ContactCacheEntry c2 = p2.getContactCacheEntry(); - String p2Name = MoreObjects.firstNonNull( - ContactDisplayUtils.getPreferredSortName( - c2.namePrimary, - c2.nameAlternative, - mContactsPreferences), - ""); - - return p1Name.compareToIgnoreCase(p2Name); - } - }); - } -} diff --git a/InCallUI/src/com/android/incallui/ContactInfoCache.java b/InCallUI/src/com/android/incallui/ContactInfoCache.java deleted file mode 100644 index 9d6fc462729cff0e3c37a8cee791ed977f5f9c46..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ContactInfoCache.java +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.location.Address; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Looper; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.DisplayNameSources; -import android.telecom.TelecomManager; -import android.text.TextUtils; -import android.util.Pair; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.util.PhoneNumberHelper; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfo; -import com.android.dialer.service.CachedNumberLookupService; -import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; -import com.android.dialer.util.MoreStrings; -import com.android.incallui.Call.LogState; -import com.android.incallui.service.PhoneNumberService; -import com.android.incalluibind.ObjectFactory; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Calendar; -import java.util.HashMap; -import java.util.List; -import java.util.Set; - -/** - * Class responsible for querying Contact Information for Call objects. Can perform asynchronous - * requests to the Contact Provider for information as well as respond synchronously for any data - * that it currently has cached from previous queries. This class always gets called from the UI - * thread so it does not need thread protection. - */ -public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener { - - private static final String TAG = ContactInfoCache.class.getSimpleName(); - private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0; - - private final Context mContext; - private final PhoneNumberService mPhoneNumberService; - private final CachedNumberLookupService mCachedNumberLookupService; - private final HashMap mInfoMap = Maps.newHashMap(); - private final HashMap> mCallBacks = Maps.newHashMap(); - - private static ContactInfoCache sCache = null; - - private Drawable mDefaultContactPhotoDrawable; - private Drawable mConferencePhotoDrawable; - private ContactUtils mContactUtils; - - public static synchronized ContactInfoCache getInstance(Context mContext) { - if (sCache == null) { - sCache = new ContactInfoCache(mContext.getApplicationContext()); - } - return sCache; - } - - private ContactInfoCache(Context context) { - mContext = context; - mPhoneNumberService = ObjectFactory.newPhoneNumberService(context); - mCachedNumberLookupService = - com.android.dialerbind.ObjectFactory.newCachedNumberLookupService(); - mContactUtils = ObjectFactory.getContactUtilsInstance(context); - - } - - public ContactCacheEntry getInfo(String callId) { - return mInfoMap.get(callId); - } - - public static ContactCacheEntry buildCacheEntryFromCall(Context context, Call call, - boolean isIncoming) { - final ContactCacheEntry entry = new ContactCacheEntry(); - - // TODO: get rid of caller info. - final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call); - ContactInfoCache.populateCacheEntry(context, info, entry, call.getNumberPresentation(), - isIncoming); - return entry; - } - - public void maybeInsertCnapInformationIntoCache(Context context, final Call call, - final CallerInfo info) { - if (mCachedNumberLookupService == null || TextUtils.isEmpty(info.cnapName) - || mInfoMap.get(call.getId()) != null) { - return; - } - final Context applicationContext = context.getApplicationContext(); - Log.i(TAG, "Found contact with CNAP name - inserting into cache"); - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - ContactInfo contactInfo = new ContactInfo(); - CachedContactInfo cacheInfo = mCachedNumberLookupService.buildCachedContactInfo( - contactInfo); - cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0); - contactInfo.name = info.cnapName; - contactInfo.number = call.getNumber(); - contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN; - try { - final JSONObject contactRows = new JSONObject().put(Phone.CONTENT_ITEM_TYPE, - new JSONObject() - .put(Phone.NUMBER, contactInfo.number) - .put(Phone.TYPE, Phone.TYPE_MAIN)); - final String jsonString = new JSONObject() - .put(Contacts.DISPLAY_NAME, contactInfo.name) - .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME) - .put(Contacts.CONTENT_ITEM_TYPE, contactRows).toString(); - cacheInfo.setLookupKey(jsonString); - } catch (JSONException e) { - Log.w(TAG, "Creation of lookup key failed when caching CNAP information"); - } - mCachedNumberLookupService.addContact(applicationContext, cacheInfo); - return null; - } - }.execute(); - } - - private class FindInfoCallback implements CallerInfoAsyncQuery.OnQueryCompleteListener { - private final boolean mIsIncoming; - - public FindInfoCallback(boolean isIncoming) { - mIsIncoming = isIncoming; - } - - @Override - public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) { - findInfoQueryComplete((Call) cookie, callerInfo, mIsIncoming, true); - } - } - - /** - * Requests contact data for the Call object passed in. - * Returns the data through callback. If callback is null, no response is made, however the - * query is still performed and cached. - * - * @param callback The function to call back when the call is found. Can be null. - */ - public void findInfo(final Call call, final boolean isIncoming, - ContactInfoCacheCallback callback) { - Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread()); - Preconditions.checkNotNull(callback); - - final String callId = call.getId(); - final ContactCacheEntry cacheEntry = mInfoMap.get(callId); - Set callBacks = mCallBacks.get(callId); - - // If we have a previously obtained intermediate result return that now - if (cacheEntry != null) { - Log.d(TAG, "Contact lookup. In memory cache hit; lookup " - + (callBacks == null ? "complete" : "still running")); - callback.onContactInfoComplete(callId, cacheEntry); - // If no other callbacks are in flight, we're done. - if (callBacks == null) { - return; - } - } - - // If the entry already exists, add callback - if (callBacks != null) { - callBacks.add(callback); - return; - } - Log.d(TAG, "Contact lookup. In memory cache miss; searching provider."); - // New lookup - callBacks = Sets.newHashSet(); - callBacks.add(callback); - mCallBacks.put(callId, callBacks); - - /** - * Performs a query for caller information. - * Save any immediate data we get from the query. An asynchronous query may also be made - * for any data that we do not already have. Some queries, such as those for voicemail and - * emergency call information, will not perform an additional asynchronous query. - */ - final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall( - mContext, call, new FindInfoCallback(isIncoming)); - - findInfoQueryComplete(call, callerInfo, isIncoming, false); - } - - private void findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming, - boolean didLocalLookup) { - final String callId = call.getId(); - int presentationMode = call.getNumberPresentation(); - if (callerInfo.contactExists || callerInfo.isEmergencyNumber() || - callerInfo.isVoiceMailNumber()) { - presentationMode = TelecomManager.PRESENTATION_ALLOWED; - } - - ContactCacheEntry cacheEntry = mInfoMap.get(callId); - // Ensure we always have a cacheEntry. Replace the existing entry if - // it has no name or if we found a local contact. - if (cacheEntry == null || TextUtils.isEmpty(cacheEntry.namePrimary) || - callerInfo.contactExists) { - cacheEntry = buildEntry(mContext, callId, callerInfo, presentationMode, isIncoming); - mInfoMap.put(callId, cacheEntry); - } - - sendInfoNotifications(callId, cacheEntry); - - if (didLocalLookup) { - // Before issuing a request for more data from other services, we only check that the - // contact wasn't found in the local DB. We don't check the if the cache entry already - // has a name because we allow overriding cnap data with data from other services. - if (!callerInfo.contactExists && mPhoneNumberService != null) { - Log.d(TAG, "Contact lookup. Local contacts miss, checking remote"); - final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId); - mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, - isIncoming); - } else if (cacheEntry.displayPhotoUri != null) { - Log.d(TAG, "Contact lookup. Local contact found, starting image load"); - // Load the image with a callback to update the image state. - // When the load is finished, onImageLoadComplete() will be called. - cacheEntry.isLoadingPhoto = true; - ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, - mContext, cacheEntry.displayPhotoUri, ContactInfoCache.this, callId); - } else { - if (callerInfo.contactExists) { - Log.d(TAG, "Contact lookup done. Local contact found, no image."); - } else { - Log.d(TAG, "Contact lookup done. Local contact not found and" - + " no remote lookup service available."); - } - clearCallbacks(callId); - } - } - } - - class PhoneNumberServiceListener implements PhoneNumberService.NumberLookupListener, - PhoneNumberService.ImageLookupListener, ContactUtils.Listener { - private final String mCallId; - - PhoneNumberServiceListener(String callId) { - mCallId = callId; - } - - @Override - public void onPhoneNumberInfoComplete( - final PhoneNumberService.PhoneNumberInfo info) { - // If we got a miss, this is the end of the lookup pipeline, - // so clear the callbacks and return. - if (info == null) { - Log.d(TAG, "Contact lookup done. Remote contact not found."); - clearCallbacks(mCallId); - return; - } - - ContactCacheEntry entry = new ContactCacheEntry(); - entry.namePrimary = info.getDisplayName(); - entry.number = info.getNumber(); - entry.contactLookupResult = info.getLookupSource(); - final int type = info.getPhoneType(); - final String label = info.getPhoneLabel(); - if (type == Phone.TYPE_CUSTOM) { - entry.label = label; - } else { - final CharSequence typeStr = Phone.getTypeLabel( - mContext.getResources(), type, label); - entry.label = typeStr == null ? null : typeStr.toString(); - } - final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); - if (oldEntry != null) { - // Location is only obtained from local lookup so persist - // the value for remote lookups. Once we have a name this - // field is no longer used; it is persisted here in case - // the UI is ever changed to use it. - entry.location = oldEntry.location; - // Contact specific ringtone is obtained from local lookup. - entry.contactRingtoneUri = oldEntry.contactRingtoneUri; - } - - // If no image and it's a business, switch to using the default business avatar. - if (info.getImageUrl() == null && info.isBusiness()) { - Log.d(TAG, "Business has no image. Using default."); - entry.photo = mContext.getResources().getDrawable(R.drawable.img_business); - } - - mInfoMap.put(mCallId, entry); - sendInfoNotifications(mCallId, entry); - - if (mContactUtils != null) { - // This method will callback "onContactInteractionsFound". - entry.isLoadingContactInteractions = - mContactUtils.retrieveContactInteractionsFromLookupKey( - info.getLookupKey(), this); - } - - entry.isLoadingPhoto = info.getImageUrl() != null; - - // If there is no image or contact interactions then we should not expect another - // callback. - if (!entry.isLoadingPhoto && !entry.isLoadingContactInteractions) { - // We're done, so clear callbacks - clearCallbacks(mCallId); - } - } - - @Override - public void onImageFetchComplete(Bitmap bitmap) { - onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId); - } - - @Override - public void onContactInteractionsFound(Address address, - List> openingHours) { - final ContactCacheEntry entry = mInfoMap.get(mCallId); - if (entry == null) { - Log.e(this, "Contact context received for empty search entry."); - clearCallbacks(mCallId); - return; - } - - entry.isLoadingContactInteractions = false; - - Log.v(ContactInfoCache.this, "Setting contact interactions for entry: ", entry); - - entry.locationAddress = address; - entry.openingHours = openingHours; - sendContactInteractionsNotifications(mCallId, entry); - - if (!entry.isLoadingPhoto) { - clearCallbacks(mCallId); - } - } - } - - /** - * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. - * make sure that the call state is reflected after the image is loaded. - */ - @Override - public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) { - Log.d(this, "Image load complete with context: ", mContext); - // TODO: may be nice to update the image view again once the newer one - // is available on contacts database. - - final String callId = (String) cookie; - final ContactCacheEntry entry = mInfoMap.get(callId); - - if (entry == null) { - Log.e(this, "Image Load received for empty search entry."); - clearCallbacks(callId); - return; - } - - entry.isLoadingPhoto = false; - - Log.d(this, "setting photo for entry: ", entry); - - // Conference call icons are being handled in CallCardPresenter. - if (photo != null) { - Log.v(this, "direct drawable: ", photo); - entry.photo = photo; - } else if (photoIcon != null) { - Log.v(this, "photo icon: ", photoIcon); - entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon); - } else { - Log.v(this, "unknown photo"); - entry.photo = null; - } - - sendImageNotifications(callId, entry); - - if (!entry.isLoadingContactInteractions) { - clearCallbacks(callId); - } - } - - /** - * Blows away the stored cache values. - */ - public void clearCache() { - mInfoMap.clear(); - mCallBacks.clear(); - } - - private ContactCacheEntry buildEntry(Context context, String callId, - CallerInfo info, int presentation, boolean isIncoming) { - // The actual strings we're going to display onscreen: - Drawable photo = null; - - final ContactCacheEntry cce = new ContactCacheEntry(); - populateCacheEntry(context, info, cce, presentation, isIncoming); - - // This will only be true for emergency numbers - if (info.photoResource != 0) { - photo = context.getResources().getDrawable(info.photoResource); - } else if (info.isCachedPhotoCurrent) { - if (info.cachedPhoto != null) { - photo = info.cachedPhoto; - } else { - photo = getDefaultContactPhotoDrawable(); - } - } else if (info.contactDisplayPhotoUri == null) { - photo = getDefaultContactPhotoDrawable(); - } else { - cce.displayPhotoUri = info.contactDisplayPhotoUri; - } - - // Support any contact id in N because QuickContacts in N starts supporting enterprise - // contact id - if (info.lookupKeyOrNull != null - && (ContactsUtils.FLAG_N_FEATURE || info.contactIdOrZero != 0)) { - cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull); - } else { - Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri."); - cce.lookupUri = null; - } - - cce.photo = photo; - cce.lookupKey = info.lookupKeyOrNull; - cce.contactRingtoneUri = info.contactRingtoneUri; - if (cce.contactRingtoneUri == null || cce.contactRingtoneUri == Uri.EMPTY) { - cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); - } - - return cce; - } - - /** - * Populate a cache entry from a call (which got converted into a caller info). - */ - public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce, - int presentation, boolean isIncoming) { - Preconditions.checkNotNull(info); - String displayName = null; - String displayNumber = null; - String displayLocation = null; - String label = null; - boolean isSipCall = false; - - // It appears that there is a small change in behaviour with the - // PhoneUtils' startGetCallerInfo whereby if we query with an - // empty number, we will get a valid CallerInfo object, but with - // fields that are all null, and the isTemporary boolean input - // parameter as true. - - // In the past, we would see a NULL callerinfo object, but this - // ends up causing null pointer exceptions elsewhere down the - // line in other cases, so we need to make this fix instead. It - // appears that this was the ONLY call to PhoneUtils - // .getCallerInfo() that relied on a NULL CallerInfo to indicate - // an unknown contact. - - // Currently, infi.phoneNumber may actually be a SIP address, and - // if so, it might sometimes include the "sip:" prefix. That - // prefix isn't really useful to the user, though, so strip it off - // if present. (For any other URI scheme, though, leave the - // prefix alone.) - // TODO: It would be cleaner for CallerInfo to explicitly support - // SIP addresses instead of overloading the "phoneNumber" field. - // Then we could remove this hack, and instead ask the CallerInfo - // for a "user visible" form of the SIP address. - String number = info.phoneNumber; - - if (!TextUtils.isEmpty(number)) { - isSipCall = PhoneNumberHelper.isUriNumber(number); - if (number.startsWith("sip:")) { - number = number.substring(4); - } - } - - if (TextUtils.isEmpty(info.name)) { - // No valid "name" in the CallerInfo, so fall back to - // something else. - // (Typically, we promote the phone number up to the "name" slot - // onscreen, and possibly display a descriptive string in the - // "number" slot.) - if (TextUtils.isEmpty(number)) { - // No name *or* number! Display a generic "unknown" string - // (or potentially some other default based on the presentation.) - displayName = getPresentationString(context, presentation, info.callSubject); - Log.d(TAG, " ==> no name *or* number! displayName = " + displayName); - } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) { - // This case should never happen since the network should never send a phone # - // AND a restricted presentation. However we leave it here in case of weird - // network behavior - displayName = getPresentationString(context, presentation, info.callSubject); - Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName); - } else if (!TextUtils.isEmpty(info.cnapName)) { - // No name, but we do have a valid CNAP name, so use that. - displayName = info.cnapName; - info.name = info.cnapName; - displayNumber = number; - Log.d(TAG, " ==> cnapName available: displayName '" + displayName + - "', displayNumber '" + displayNumber + "'"); - } else { - // No name; all we have is a number. This is the typical - // case when an incoming call doesn't match any contact, - // or if you manually dial an outgoing number using the - // dialpad. - displayNumber = number; - - // Display a geographical description string if available - // (but only for incoming calls.) - if (isIncoming) { - // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo - // query to only do the geoDescription lookup in the first - // place for incoming calls. - displayLocation = info.geoDescription; // may be null - Log.d(TAG, "Geodescrption: " + info.geoDescription); - } - - Log.d(TAG, " ==> no name; falling back to number:" - + " displayNumber '" + Log.pii(displayNumber) - + "', displayLocation '" + displayLocation + "'"); - } - } else { - // We do have a valid "name" in the CallerInfo. Display that - // in the "name" slot, and the phone number in the "number" slot. - if (presentation != TelecomManager.PRESENTATION_ALLOWED) { - // This case should never happen since the network should never send a name - // AND a restricted presentation. However we leave it here in case of weird - // network behavior - displayName = getPresentationString(context, presentation, info.callSubject); - Log.d(TAG, " ==> valid name, but presentation not allowed!" + - " displayName = " + displayName); - } else { - // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will - // later determine whether to use the name or nameAlternative when presenting - displayName = info.name; - cce.nameAlternative = info.nameAlternative; - displayNumber = number; - label = info.phoneLabel; - Log.d(TAG, " ==> name is present in CallerInfo: displayName '" + displayName - + "', displayNumber '" + displayNumber + "'"); - } - } - - cce.namePrimary = displayName; - cce.number = displayNumber; - cce.location = displayLocation; - cce.label = label; - cce.isSipCall = isSipCall; - cce.userType = info.userType; - - if (info.contactExists) { - cce.contactLookupResult = LogState.LOOKUP_LOCAL_CONTACT; - } - } - - /** - * Sends the updated information to call the callbacks for the entry. - */ - private void sendInfoNotifications(String callId, ContactCacheEntry entry) { - final Set callBacks = mCallBacks.get(callId); - if (callBacks != null) { - for (ContactInfoCacheCallback callBack : callBacks) { - callBack.onContactInfoComplete(callId, entry); - } - } - } - - private void sendImageNotifications(String callId, ContactCacheEntry entry) { - final Set callBacks = mCallBacks.get(callId); - if (callBacks != null && entry.photo != null) { - for (ContactInfoCacheCallback callBack : callBacks) { - callBack.onImageLoadComplete(callId, entry); - } - } - } - - private void sendContactInteractionsNotifications(String callId, ContactCacheEntry entry) { - final Set callBacks = mCallBacks.get(callId); - if (callBacks != null) { - for (ContactInfoCacheCallback callBack : callBacks) { - callBack.onContactInteractionsInfoComplete(callId, entry); - } - } - } - - private void clearCallbacks(String callId) { - mCallBacks.remove(callId); - } - - /** - * Gets name strings based on some special presentation modes and the associated custom label. - */ - private static String getPresentationString(Context context, int presentation, - String customLabel) { - String name = context.getString(R.string.unknown); - if (!TextUtils.isEmpty(customLabel) && - ((presentation == TelecomManager.PRESENTATION_UNKNOWN) || - (presentation == TelecomManager.PRESENTATION_RESTRICTED))) { - name = customLabel; - return name; - } else { - if (presentation == TelecomManager.PRESENTATION_RESTRICTED) { - name = context.getString(R.string.private_num); - } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) { - name = context.getString(R.string.payphone); - } - } - return name; - } - - public Drawable getDefaultContactPhotoDrawable() { - if (mDefaultContactPhotoDrawable == null) { - mDefaultContactPhotoDrawable = - mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored); - } - return mDefaultContactPhotoDrawable; - } - - public Drawable getConferenceDrawable() { - if (mConferencePhotoDrawable == null) { - mConferencePhotoDrawable = - mContext.getResources().getDrawable(R.drawable.img_conference_automirrored); - } - return mConferencePhotoDrawable; - } - - /** - * Callback interface for the contact query. - */ - public interface ContactInfoCacheCallback { - public void onContactInfoComplete(String callId, ContactCacheEntry entry); - public void onImageLoadComplete(String callId, ContactCacheEntry entry); - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry); - } - - public static class ContactCacheEntry { - public String namePrimary; - public String nameAlternative; - public String number; - public String location; - public String label; - public Drawable photo; - public boolean isSipCall; - // Note in cache entry whether this is a pending async loading action to know whether to - // wait for its callback or not. - public boolean isLoadingPhoto; - public boolean isLoadingContactInteractions; - /** This will be used for the "view" notification. */ - public Uri contactUri; - /** Either a display photo or a thumbnail URI. */ - public Uri displayPhotoUri; - public Uri lookupUri; // Sent to NotificationMananger - public String lookupKey; - public Address locationAddress; - public List> openingHours; - public int contactLookupResult = LogState.LOOKUP_NOT_FOUND; - public long userType = ContactsUtils.USER_TYPE_CURRENT; - public Uri contactRingtoneUri; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("name", MoreStrings.toSafeString(namePrimary)) - .add("nameAlternative", MoreStrings.toSafeString(nameAlternative)) - .add("number", MoreStrings.toSafeString(number)) - .add("location", MoreStrings.toSafeString(location)) - .add("label", label) - .add("photo", photo) - .add("isSipCall", isSipCall) - .add("contactUri", contactUri) - .add("displayPhotoUri", displayPhotoUri) - .add("locationAddress", locationAddress) - .add("openingHours", openingHours) - .add("contactLookupResult", contactLookupResult) - .add("userType", userType) - .add("contactRingtoneUri", contactRingtoneUri) - .toString(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/ContactUtils.java b/InCallUI/src/com/android/incallui/ContactUtils.java deleted file mode 100644 index 0750af7317e58e601e968193fc0833eec62d5f14..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ContactUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -package com.android.incallui; - -import android.content.Context; -import android.location.Address; -import android.util.Pair; - -import com.android.incalluibind.ObjectFactory; - -import java.util.Calendar; -import java.util.List; - -/** - * Utility functions to help manipulate contact data. - */ -public abstract class ContactUtils { - protected Context mContext; - - public static ContactUtils getInstance(Context context) { - return ObjectFactory.getContactUtilsInstance(context); - } - - protected ContactUtils(Context context) { - mContext = context; - } - - public interface Listener { - public void onContactInteractionsFound(Address address, - List> openingHours); - } - - public abstract boolean retrieveContactInteractionsFromLookupKey(String lookupKey, - Listener listener); -} diff --git a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java deleted file mode 100644 index d959fadd4d48f53c6e21004b9e7550e0e352e074..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.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.incallui; - -import android.app.Notification; -import android.content.ContentUris; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.provider.ContactsContract.Contacts; - -import com.android.dialer.R; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Helper class for loading contacts photo asynchronously. - */ -public class ContactsAsyncHelper { - - /** - * Interface for a WorkerHandler result return. - */ - public interface OnImageLoadCompleteListener { - /** - * Called when the image load is complete. - * - * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, - * Context, Uri, OnImageLoadCompleteListener, Object)}. - * @param photo Drawable object obtained by the async load. - * @param photoIcon Bitmap object obtained by the async load. - * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, - * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original - * cookie is null. - */ - public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, - Object cookie); - } - - // constants - private static final int EVENT_LOAD_IMAGE = 1; - - private final Handler mResultHandler = new Handler() { - /** Called when loading is done. */ - @Override - public void handleMessage(Message msg) { - WorkerArgs args = (WorkerArgs) msg.obj; - switch (msg.arg1) { - case EVENT_LOAD_IMAGE: - if (args.listener != null) { - Log.d(this, "Notifying listener: " + args.listener.toString() + - " image: " + args.displayPhotoUri + " completed"); - args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon, - args.cookie); - } - break; - default: - } - } - }; - - /** Handler run on a worker thread to load photo asynchronously. */ - private static Handler sThreadHandler; - - /** For forcing the system to call its constructor */ - @SuppressWarnings("unused") - private static ContactsAsyncHelper sInstance; - - static { - sInstance = new ContactsAsyncHelper(); - } - - private static final class WorkerArgs { - public Context context; - public Uri displayPhotoUri; - public Drawable photo; - public Bitmap photoIcon; - public Object cookie; - public OnImageLoadCompleteListener listener; - } - - /** - * Thread worker class that handles the task of opening the stream and loading - * the images. - */ - private class WorkerHandler extends Handler { - public WorkerHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - WorkerArgs args = (WorkerArgs) msg.obj; - - switch (msg.arg1) { - case EVENT_LOAD_IMAGE: - InputStream inputStream = null; - try { - try { - inputStream = args.context.getContentResolver() - .openInputStream(args.displayPhotoUri); - } catch (Exception e) { - Log.e(this, "Error opening photo input stream", e); - } - - if (inputStream != null) { - args.photo = Drawable.createFromStream(inputStream, - args.displayPhotoUri.toString()); - - // This assumes Drawable coming from contact database is usually - // BitmapDrawable and thus we can have (down)scaled version of it. - args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo); - - Log.d(ContactsAsyncHelper.this, "Loading image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.displayPhotoUri); - } else { - args.photo = null; - args.photoIcon = null; - Log.d(ContactsAsyncHelper.this, "Problem with image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.displayPhotoUri + - ", using default image."); - } - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - Log.e(this, "Unable to close input stream.", e); - } - } - } - break; - default: - } - - // send the reply to the enclosing class. - Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what); - reply.arg1 = msg.arg1; - reply.obj = msg.obj; - reply.sendToTarget(); - } - - /** - * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might - * return null when the given Drawable isn't BitmapDrawable, or if the system fails to - * create a scaled Bitmap for the Drawable. - */ - private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) { - if (!(photo instanceof BitmapDrawable)) { - return null; - } - int iconSize = context.getResources() - .getDimensionPixelSize(R.dimen.notification_icon_size); - Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap(); - int orgWidth = orgBitmap.getWidth(); - int orgHeight = orgBitmap.getHeight(); - int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight; - // We want downscaled one only when the original icon is too big. - if (longerEdge > iconSize) { - float ratio = ((float) longerEdge) / iconSize; - int newWidth = (int) (orgWidth / ratio); - int newHeight = (int) (orgHeight / ratio); - // If the longer edge is much longer than the shorter edge, the latter may - // become 0 which will cause a crash. - if (newWidth <= 0 || newHeight <= 0) { - Log.w(this, "Photo icon's width or height become 0."); - return null; - } - - // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap - // should be smaller than the original. - return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true); - } else { - return orgBitmap; - } - } - } - - /** - * Private constructor for static class - */ - private ContactsAsyncHelper() { - HandlerThread thread = new HandlerThread("ContactsAsyncWorker"); - thread.start(); - sThreadHandler = new WorkerHandler(thread.getLooper()); - } - - /** - * Starts an asynchronous image load. After finishing the load, - * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} - * will be called. - * - * @param token Arbitrary integer which will be returned as the first argument of - * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} - * @param context Context object used to do the time-consuming operation. - * @param displayPhotoUri Uri to be used to fetch the photo - * @param listener Callback object which will be used when the asynchronous load is done. - * Can be null, which means only the asynchronous load is done while there's no way to - * obtain the loaded photos. - * @param cookie Arbitrary object the caller wants to remember, which will become the - * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, - * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument. - */ - public static final void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri, - OnImageLoadCompleteListener listener, Object cookie) { - // in case the source caller info is null, the URI will be null as well. - // just update using the placeholder image in this case. - if (displayPhotoUri == null) { - Log.wtf("startObjectPhotoAsync", "Uri is missing"); - return; - } - - // Added additional Cookie field in the callee to handle arguments - // sent to the callback function. - - // setup arguments - WorkerArgs args = new WorkerArgs(); - args.cookie = cookie; - args.context = context; - args.displayPhotoUri = displayPhotoUri; - args.listener = listener; - - // setup message arguments - Message msg = sThreadHandler.obtainMessage(token); - msg.arg1 = EVENT_LOAD_IMAGE; - msg.obj = args; - - Log.d("startObjectPhotoAsync", "Begin loading image: " + args.displayPhotoUri + - ", displaying default image for now."); - - // notify the thread to begin working - sThreadHandler.sendMessage(msg); - } - - -} diff --git a/InCallUI/src/com/android/incallui/ContactsPreferencesFactory.java b/InCallUI/src/com/android/incallui/ContactsPreferencesFactory.java deleted file mode 100644 index a9cc93bda2ea446a17a67987b5fe58f6e6d44f73..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ContactsPreferencesFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.support.annotation.Nullable; -import com.android.dialer.compat.UserManagerCompat; - -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.testing.NeededForTesting; - -/** - * Factory class for {@link ContactsPreferences}. - */ -public class ContactsPreferencesFactory { - - private static boolean sUseTestInstance; - private static ContactsPreferences sTestInstance; - - /** - * Creates a new {@link ContactsPreferences} object if possible. - * - * @param context the context to use when creating the ContactsPreferences. - * @return a new ContactsPreferences object or {@code null} if the user is locked. - */ - @Nullable - public static ContactsPreferences newContactsPreferences(Context context) { - if (sUseTestInstance) { - return sTestInstance; - } - if (UserManagerCompat.isUserUnlocked(context)) { - return new ContactsPreferences(context); - } - return null; - } - - /** - * Sets the instance to be returned by all calls to {@link #newContactsPreferences(Context)}. - * - * @param testInstance the instance to return. - */ - @NeededForTesting - static void setTestInstance(ContactsPreferences testInstance) { - sUseTestInstance = true; - sTestInstance = testInstance; - } -} diff --git a/InCallUI/src/com/android/incallui/DialpadFragment.java b/InCallUI/src/com/android/incallui/DialpadFragment.java deleted file mode 100644 index ad288bdc6c2449c3c443c9b604569f65da15da2d..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/DialpadFragment.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.Editable; -import android.text.method.DialerKeyListener; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.contacts.common.compat.PhoneNumberUtilsCompat; -import com.android.dialer.R; -import com.android.phone.common.dialpad.DialpadKeyButton; -import com.android.phone.common.dialpad.DialpadView; - -import java.util.HashMap; - -/** - * Fragment for call control buttons - */ -public class DialpadFragment extends BaseFragment - implements DialpadPresenter.DialpadUi, View.OnTouchListener, View.OnKeyListener, - View.OnHoverListener, View.OnClickListener { - - private static final int ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS = 50; - - private final int[] mButtonIds = new int[] {R.id.zero, R.id.one, R.id.two, R.id.three, - R.id.four, R.id.five, R.id.six, R.id.seven, R.id.eight, R.id.nine, R.id.star, - R.id.pound}; - - /** - * LinearLayout with getter and setter methods for the translationY property using floats, - * for animation purposes. - */ - public static class DialpadSlidingLinearLayout extends LinearLayout { - - public DialpadSlidingLinearLayout(Context context) { - super(context); - } - - public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public float getYFraction() { - final int height = getHeight(); - if (height == 0) return 0; - return getTranslationY() / height; - } - - public void setYFraction(float yFraction) { - setTranslationY(yFraction * getHeight()); - } - } - - private EditText mDtmfDialerField; - - /** Hash Map to map a view id to a character*/ - private static final HashMap mDisplayMap = - new HashMap(); - - private static final Handler sHandler = new Handler(Looper.getMainLooper()); - - - /** Set up the static maps*/ - static { - // Map the buttons to the display characters - mDisplayMap.put(R.id.one, '1'); - mDisplayMap.put(R.id.two, '2'); - mDisplayMap.put(R.id.three, '3'); - mDisplayMap.put(R.id.four, '4'); - mDisplayMap.put(R.id.five, '5'); - mDisplayMap.put(R.id.six, '6'); - mDisplayMap.put(R.id.seven, '7'); - mDisplayMap.put(R.id.eight, '8'); - mDisplayMap.put(R.id.nine, '9'); - mDisplayMap.put(R.id.zero, '0'); - mDisplayMap.put(R.id.pound, '#'); - mDisplayMap.put(R.id.star, '*'); - } - - // KeyListener used with the "dialpad digits" EditText widget. - private DTMFKeyListener mDialerKeyListener; - - private DialpadView mDialpadView; - - private int mCurrentTextColor; - - /** - * Our own key listener, specialized for dealing with DTMF codes. - * 1. Ignore the backspace since it is irrelevant. - * 2. Allow ONLY valid DTMF characters to generate a tone and be - * sent as a DTMF code. - * 3. All other remaining characters are handled by the superclass. - * - * This code is purely here to handle events from the hardware keyboard - * while the DTMF dialpad is up. - */ - private class DTMFKeyListener extends DialerKeyListener { - - private DTMFKeyListener() { - super(); - } - - /** - * Overriden to return correct DTMF-dialable characters. - */ - @Override - protected char[] getAcceptedChars(){ - return DTMF_CHARACTERS; - } - - /** special key listener ignores backspace. */ - @Override - public boolean backspace(View view, Editable content, int keyCode, - KeyEvent event) { - return false; - } - - /** - * Return true if the keyCode is an accepted modifier key for the - * dialer (ALT or SHIFT). - */ - private boolean isAcceptableModifierKey(int keyCode) { - switch (keyCode) { - case KeyEvent.KEYCODE_ALT_LEFT: - case KeyEvent.KEYCODE_ALT_RIGHT: - case KeyEvent.KEYCODE_SHIFT_LEFT: - case KeyEvent.KEYCODE_SHIFT_RIGHT: - return true; - default: - return false; - } - } - - /** - * Overriden so that with each valid button press, we start sending - * a dtmf code and play a local dtmf tone. - */ - @Override - public boolean onKeyDown(View view, Editable content, - int keyCode, KeyEvent event) { - // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view); - - // find the character - char c = (char) lookup(event, content); - - // if not a long press, and parent onKeyDown accepts the input - if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) { - - boolean keyOK = ok(getAcceptedChars(), c); - - // if the character is a valid dtmf code, start playing the tone and send the - // code. - if (keyOK) { - Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); - getPresenter().processDtmf(c); - } else { - Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); - } - return true; - } - return false; - } - - /** - * Overriden so that with each valid button up, we stop sending - * a dtmf code and the dtmf tone. - */ - @Override - public boolean onKeyUp(View view, Editable content, - int keyCode, KeyEvent event) { - // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view); - - super.onKeyUp(view, content, keyCode, event); - - // find the character - char c = (char) lookup(event, content); - - boolean keyOK = ok(getAcceptedChars(), c); - - if (keyOK) { - Log.d(this, "Stopping the tone for '" + c + "'"); - getPresenter().stopDtmf(); - return true; - } - - return false; - } - - /** - * Handle individual keydown events when we DO NOT have an Editable handy. - */ - public boolean onKeyDown(KeyEvent event) { - char c = lookup(event); - Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'"); - - // if not a long press, and parent onKeyDown accepts the input - if (event.getRepeatCount() == 0 && c != 0) { - // if the character is a valid dtmf code, start playing the tone and send the - // code. - if (ok(getAcceptedChars(), c)) { - Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); - getPresenter().processDtmf(c); - return true; - } else { - Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); - } - } - return false; - } - - /** - * Handle individual keyup events. - * - * @param event is the event we are trying to stop. If this is null, - * then we just force-stop the last tone without checking if the event - * is an acceptable dialer event. - */ - public boolean onKeyUp(KeyEvent event) { - if (event == null) { - //the below piece of code sends stopDTMF event unnecessarily even when a null event - //is received, hence commenting it. - /*if (DBG) log("Stopping the last played tone."); - stopTone();*/ - return true; - } - - char c = lookup(event); - Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'"); - - // TODO: stopTone does not take in character input, we may want to - // consider checking for this ourselves. - if (ok(getAcceptedChars(), c)) { - Log.d(this, "Stopping the tone for '" + c + "'"); - getPresenter().stopDtmf(); - return true; - } - - return false; - } - - /** - * Find the Dialer Key mapped to this event. - * - * @return The char value of the input event, otherwise - * 0 if no matching character was found. - */ - private char lookup(KeyEvent event) { - // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup} - int meta = event.getMetaState(); - int number = event.getNumber(); - - if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) { - int match = event.getMatch(getAcceptedChars(), meta); - number = (match != 0) ? match : number; - } - - return (char) number; - } - - /** - * Check to see if the keyEvent is dialable. - */ - boolean isKeyEventAcceptable (KeyEvent event) { - return (ok(getAcceptedChars(), lookup(event))); - } - - /** - * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} - * These are the valid dtmf characters. - */ - public final char[] DTMF_CHARACTERS = new char[] { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*' - }; - } - - @Override - public void onClick(View v) { - final AccessibilityManager accessibilityManager = (AccessibilityManager) - v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - // When accessibility is on, simulate press and release to preserve the - // semantic meaning of performClick(). Required for Braille support. - if (accessibilityManager.isEnabled()) { - final int id = v.getId(); - // Checking the press state prevents double activation. - if (!v.isPressed() && mDisplayMap.containsKey(id)) { - getPresenter().processDtmf(mDisplayMap.get(id)); - sHandler.postDelayed(new Runnable() { - @Override - public void run() { - getPresenter().stopDtmf(); - } - }, ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS); - } - } - if (v.getId() == R.id.dialpad_back) { - getActivity().onBackPressed(); - } - } - - @Override - public boolean onHover(View v, MotionEvent event) { - // When touch exploration is turned on, lifting a finger while inside - // the button's hover target bounds should perform a click action. - final AccessibilityManager accessibilityManager = (AccessibilityManager) - v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - - if (accessibilityManager.isEnabled() - && accessibilityManager.isTouchExplorationEnabled()) { - final int left = v.getPaddingLeft(); - final int right = (v.getWidth() - v.getPaddingRight()); - final int top = v.getPaddingTop(); - final int bottom = (v.getHeight() - v.getPaddingBottom()); - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_HOVER_ENTER: - // Lift-to-type temporarily disables double-tap activation. - v.setClickable(false); - break; - case MotionEvent.ACTION_HOVER_EXIT: - final int x = (int) event.getX(); - final int y = (int) event.getY(); - if ((x > left) && (x < right) && (y > top) && (y < bottom)) { - v.performClick(); - } - v.setClickable(true); - break; - } - } - - return false; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - Log.d(this, "onKey: keyCode " + keyCode + ", view " + v); - - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { - int viewId = v.getId(); - if (mDisplayMap.containsKey(viewId)) { - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - if (event.getRepeatCount() == 0) { - getPresenter().processDtmf(mDisplayMap.get(viewId)); - } - break; - case KeyEvent.ACTION_UP: - getPresenter().stopDtmf(); - break; - } - // do not return true [handled] here, since we want the - // press / click animation to be handled by the framework. - } - } - return false; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - Log.d(this, "onTouch"); - int viewId = v.getId(); - - // if the button is recognized - if (mDisplayMap.containsKey(viewId)) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - // Append the character mapped to this button, to the display. - // start the tone - getPresenter().processDtmf(mDisplayMap.get(viewId)); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - // stop the tone on ANY other event, except for MOVE. - getPresenter().stopDtmf(); - break; - } - // do not return true [handled] here, since we want the - // press / click animation to be handled by the framework. - } - return false; - } - - // TODO(klp) Adds hardware keyboard listener - - @Override - public DialpadPresenter createPresenter() { - return new DialpadPresenter(); - } - - @Override - public DialpadPresenter.DialpadUi getUi() { - return this; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View parent = inflater.inflate( - R.layout.incall_dialpad_fragment, container, false); - mDialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view); - mDialpadView.setCanDigitsBeEdited(false); - mDialpadView.setBackgroundResource(R.color.incall_dialpad_background); - mDtmfDialerField = (EditText) parent.findViewById(R.id.digits); - if (mDtmfDialerField != null) { - mDialerKeyListener = new DTMFKeyListener(); - mDtmfDialerField.setKeyListener(mDialerKeyListener); - // remove the long-press context menus that support - // the edit (copy / paste / select) functions. - mDtmfDialerField.setLongClickable(false); - mDtmfDialerField.setElegantTextHeight(false); - configureKeypadListeners(); - } - View backButton = mDialpadView.findViewById(R.id.dialpad_back); - backButton.setVisibility(View.VISIBLE); - backButton.setOnClickListener(this); - - return parent; - } - - @Override - public void onResume() { - super.onResume(); - updateColors(); - } - - public void updateColors() { - int textColor = InCallPresenter.getInstance().getThemeColors().mPrimaryColor; - - if (mCurrentTextColor == textColor) { - return; - } - - DialpadKeyButton dialpadKey; - for (int i = 0; i < mButtonIds.length; i++) { - dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); - ((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor); - } - - mCurrentTextColor = textColor; - } - - @Override - public void onDestroyView() { - mDialerKeyListener = null; - super.onDestroyView(); - } - - /** - * Getter for Dialpad text. - * - * @return String containing current Dialpad EditText text. - */ - public String getDtmfText() { - return mDtmfDialerField.getText().toString(); - } - - /** - * Sets the Dialpad text field with some text. - * - * @param text Text to set Dialpad EditText to. - */ - public void setDtmfText(String text) { - mDtmfDialerField.setText(PhoneNumberUtilsCompat.createTtsSpannable(text)); - } - - @Override - public void setVisible(boolean on) { - if (on) { - getView().setVisibility(View.VISIBLE); - } else { - getView().setVisibility(View.INVISIBLE); - } - } - - /** - * Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. - */ - public void animateShowDialpad() { - final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view); - dialpadView.animateShow(); - } - - @Override - public void appendDigitsToField(char digit) { - if (mDtmfDialerField != null) { - // TODO: maybe *don't* manually append this digit if - // mDialpadDigits is focused and this key came from the HW - // keyboard, since in that case the EditText field will - // get the key event directly and automatically appends - // whetever the user types. - // (Or, a cleaner fix would be to just make mDialpadDigits - // *not* handle HW key presses. That seems to be more - // complicated than just setting focusable="false" on it, - // though.) - mDtmfDialerField.getText().append(digit); - } - } - - /** - * Called externally (from InCallScreen) to play a DTMF Tone. - */ - /* package */ boolean onDialerKeyDown(KeyEvent event) { - Log.d(this, "Notifying dtmf key down."); - if (mDialerKeyListener != null) { - return mDialerKeyListener.onKeyDown(event); - } else { - return false; - } - } - - /** - * Called externally (from InCallScreen) to cancel the last DTMF Tone played. - */ - public boolean onDialerKeyUp(KeyEvent event) { - Log.d(this, "Notifying dtmf key up."); - if (mDialerKeyListener != null) { - return mDialerKeyListener.onKeyUp(event); - } else { - return false; - } - } - - private void configureKeypadListeners() { - DialpadKeyButton dialpadKey; - for (int i = 0; i < mButtonIds.length; i++) { - dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); - dialpadKey.setOnTouchListener(this); - dialpadKey.setOnKeyListener(this); - dialpadKey.setOnHoverListener(this); - dialpadKey.setOnClickListener(this); - } - } -} diff --git a/InCallUI/src/com/android/incallui/DialpadPresenter.java b/InCallUI/src/com/android/incallui/DialpadPresenter.java deleted file mode 100644 index 5e24bedefc0f789dd8a802c58f30abf1f3a5c0ce..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/DialpadPresenter.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telephony.PhoneNumberUtils; - -/** - * Logic for call buttons. - */ -public class DialpadPresenter extends Presenter - implements InCallPresenter.InCallStateListener { - - private Call mCall; - - @Override - public void onUiReady(DialpadUi ui) { - super.onUiReady(ui); - InCallPresenter.getInstance().addListener(this); - mCall = CallList.getInstance().getOutgoingOrActive(); - } - - @Override - public void onUiUnready(DialpadUi ui) { - super.onUiUnready(ui); - InCallPresenter.getInstance().removeListener(this); - } - - @Override - public void onStateChange(InCallPresenter.InCallState oldState, - InCallPresenter.InCallState newState, CallList callList) { - mCall = callList.getOutgoingOrActive(); - Log.d(this, "DialpadPresenter mCall = " + mCall); - } - - /** - * Processes the specified digit as a DTMF key, by playing the - * appropriate DTMF tone, and appending the digit to the EditText - * field that displays the DTMF digits sent so far. - * - */ - public final void processDtmf(char c) { - Log.d(this, "Processing dtmf key " + c); - // if it is a valid key, then update the display and send the dtmf tone. - if (PhoneNumberUtils.is12Key(c) && mCall != null) { - Log.d(this, "updating display and sending dtmf tone for '" + c + "'"); - - // Append this key to the "digits" widget. - getUi().appendDigitsToField(c); - // Plays the tone through Telecom. - TelecomAdapter.getInstance().playDtmfTone(mCall.getId(), c); - } else { - Log.d(this, "ignoring dtmf request for '" + c + "'"); - } - } - - /** - * Stops the local tone based on the phone type. - */ - public void stopDtmf() { - if (mCall != null) { - Log.d(this, "stopping remote tone"); - TelecomAdapter.getInstance().stopDtmfTone(mCall.getId()); - } - } - - public interface DialpadUi extends Ui { - void setVisible(boolean on); - void appendDigitsToField(char digit); - } -} diff --git a/InCallUI/src/com/android/incallui/DistanceHelper.java b/InCallUI/src/com/android/incallui/DistanceHelper.java deleted file mode 100644 index a4db5fed3b04010d73551a4db221b917880e07ff..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/DistanceHelper.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.incallui; - -import android.location.Address; - -/** - * Superclass for a helper class to get the current location and distance to other locations. - */ -public abstract class DistanceHelper { - public static final float DISTANCE_NOT_FOUND = -1; - public static final float MILES_PER_METER = (float) 0.000621371192; - public static final float KILOMETERS_PER_METER = (float) 0.001; - - public interface Listener { - public void onLocationReady(); - } - - public void cleanUp() {} - - public float calculateDistance(Address address) { - return DISTANCE_NOT_FOUND; - } -} diff --git a/InCallUI/src/com/android/incallui/ExternalCallList.java b/InCallUI/src/com/android/incallui/ExternalCallList.java deleted file mode 100644 index 06e0bb975a2cc9cc6680d4e51ea040e968a07e0b..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ExternalCallList.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import com.android.contacts.common.compat.CallSdkCompat; - -import android.os.Handler; -import android.os.Looper; -import android.telecom.Call; -import android.util.ArraySet; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Tracks the external calls known to the InCall UI. - * - * External calls are those with {@link android.telecom.Call.Details#PROPERTY_IS_EXTERNAL_CALL}. - */ -public class ExternalCallList { - - public interface ExternalCallListener { - void onExternalCallAdded(Call call); - void onExternalCallRemoved(Call call); - void onExternalCallUpdated(Call call); - } - - /** - * Handles {@link android.telecom.Call.Callback} callbacks. - */ - private final Call.Callback mTelecomCallCallback = new Call.Callback() { - @Override - public void onDetailsChanged(Call call, Call.Details details) { - notifyExternalCallUpdated(call); - } - }; - - private final Set mExternalCalls = new ArraySet<>(); - private final Set mExternalCallListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - - /** - * Begins tracking an external call and notifies listeners of the new call. - */ - public void onCallAdded(Call telecomCall) { - Preconditions.checkArgument(telecomCall.getDetails() - .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)); - mExternalCalls.add(telecomCall); - telecomCall.registerCallback(mTelecomCallCallback, new Handler(Looper.getMainLooper())); - notifyExternalCallAdded(telecomCall); - } - - /** - * Stops tracking an external call and notifies listeners of the removal of the call. - */ - public void onCallRemoved(Call telecomCall) { - Preconditions.checkArgument(mExternalCalls.contains(telecomCall)); - mExternalCalls.remove(telecomCall); - telecomCall.unregisterCallback(mTelecomCallCallback); - notifyExternalCallRemoved(telecomCall); - } - - /** - * Adds a new listener to external call events. - */ - public void addExternalCallListener(ExternalCallListener listener) { - mExternalCallListeners.add(Preconditions.checkNotNull(listener)); - } - - /** - * Removes a listener to external call events. - */ - public void removeExternalCallListener(ExternalCallListener listener) { - Preconditions.checkArgument(mExternalCallListeners.contains(listener)); - mExternalCallListeners.remove(Preconditions.checkNotNull(listener)); - } - - /** - * Notifies listeners of the addition of a new external call. - */ - private void notifyExternalCallAdded(Call call) { - for (ExternalCallListener listener : mExternalCallListeners) { - listener.onExternalCallAdded(call); - } - } - - /** - * Notifies listeners of the removal of an external call. - */ - private void notifyExternalCallRemoved(Call call) { - for (ExternalCallListener listener : mExternalCallListeners) { - listener.onExternalCallRemoved(call); - } - } - - /** - * Notifies listeners of changes to an external call. - */ - private void notifyExternalCallUpdated(Call call) { - for (ExternalCallListener listener : mExternalCallListeners) { - listener.onExternalCallUpdated(call); - } - } -} diff --git a/InCallUI/src/com/android/incallui/ExternalCallNotifier.java b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java deleted file mode 100644 index 639a46da0194a95636fb77d93f8f987b326a5258..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ExternalCallNotifier.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.BitmapUtil; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.incallui.util.TelecomCallUtil; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.telecom.Call; -import android.telecom.PhoneAccount; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; -import android.util.ArrayMap; - -import java.util.Map; - -/** - * Handles the display of notifications for "external calls". - * - * External calls are a representation of a call which is in progress on the user's other device - * (e.g. another phone, or a watch). - */ -public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener { - - /** - * Tag used with the notification manager to uniquely identify external call notifications. - */ - private static final String NOTIFICATION_TAG = "EXTERNAL_CALL"; - - /** - * Represents a call and associated cached notification data. - */ - private static class NotificationInfo { - private final Call mCall; - private final int mNotificationId; - @Nullable private String mContentTitle; - @Nullable private Bitmap mLargeIcon; - @Nullable private String mPersonReference; - - public NotificationInfo(Call call, int notificationId) { - Preconditions.checkNotNull(call); - mCall = call; - mNotificationId = notificationId; - } - - public Call getCall() { - return mCall; - } - - public int getNotificationId() { - return mNotificationId; - } - - public @Nullable String getContentTitle() { - return mContentTitle; - } - - public @Nullable Bitmap getLargeIcon() { - return mLargeIcon; - } - - public @Nullable String getPersonReference() { - return mPersonReference; - } - - public void setContentTitle(@Nullable String contentTitle) { - mContentTitle = contentTitle; - } - - public void setLargeIcon(@Nullable Bitmap largeIcon) { - mLargeIcon = largeIcon; - } - - public void setPersonReference(@Nullable String personReference) { - mPersonReference = personReference; - } - } - - private final Context mContext; - private final ContactInfoCache mContactInfoCache; - private Map mNotifications = new ArrayMap<>(); - private int mNextUniqueNotificationId; - private ContactsPreferences mContactsPreferences; - - /** - * Initializes a new instance of the external call notifier. - */ - public ExternalCallNotifier(Context context, ContactInfoCache contactInfoCache) { - mContext = Preconditions.checkNotNull(context); - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - mContactInfoCache = Preconditions.checkNotNull(contactInfoCache); - } - - /** - * Handles the addition of a new external call by showing a new notification. - * Triggered by {@link CallList#onCallAdded(android.telecom.Call)}. - */ - @Override - public void onExternalCallAdded(android.telecom.Call call) { - Log.i(this, "onExternalCallAdded " + call); - Preconditions.checkArgument(!mNotifications.containsKey(call)); - NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++); - mNotifications.put(call, info); - - showNotifcation(info); - } - - /** - * Handles the removal of an external call by hiding its associated notification. - * Triggered by {@link CallList#onCallRemoved(android.telecom.Call)}. - */ - @Override - public void onExternalCallRemoved(android.telecom.Call call) { - Log.i(this, "onExternalCallRemoved " + call); - - dismissNotification(call); - } - - /** - * Handles updates to an external call. - */ - @Override - public void onExternalCallUpdated(Call call) { - Preconditions.checkArgument(mNotifications.containsKey(call)); - postNotification(mNotifications.get(call)); - } - - /** - * Initiates a call pull given a notification ID. - * - * @param notificationId The notification ID associated with the external call which is to be - * pulled. - */ - public void pullExternalCall(int notificationId) { - for (NotificationInfo info : mNotifications.values()) { - if (info.getNotificationId() == notificationId) { - CallSdkCompat.pullExternalCall(info.getCall()); - return; - } - } - } - - /** - * Shows a notification for a new external call. Performs a contact cache lookup to find any - * associated photo and information for the call. - */ - private void showNotifcation(final NotificationInfo info) { - // We make a call to the contact info cache to query for supplemental data to what the - // call provides. This includes the contact name and photo. - // This callback will always get called immediately and synchronously with whatever data - // it has available, and may make a subsequent call later (same thread) if it had to - // call into the contacts provider for more data. - com.android.incallui.Call incallCall = new com.android.incallui.Call(info.getCall(), - new LatencyReport(), false /* registerCallback */); - - mContactInfoCache.findInfo(incallCall, false /* isIncoming */, - new ContactInfoCache.ContactInfoCacheCallback() { - @Override - public void onContactInfoComplete(String callId, - ContactInfoCache.ContactCacheEntry entry) { - - // Ensure notification still exists as the external call could have been - // removed during async contact info lookup. - if (mNotifications.containsKey(info.getCall())) { - saveContactInfo(info, entry); - } - } - - @Override - public void onImageLoadComplete(String callId, - ContactInfoCache.ContactCacheEntry entry) { - - // Ensure notification still exists as the external call could have been - // removed during async contact info lookup. - if (mNotifications.containsKey(info.getCall())) { - savePhoto(info, entry); - } - } - - @Override - public void onContactInteractionsInfoComplete(String callId, - ContactInfoCache.ContactCacheEntry entry) { - } - }); - } - - /** - * Dismisses a notification for an external call. - */ - private void dismissNotification(Call call) { - Preconditions.checkArgument(mNotifications.containsKey(call)); - - NotificationManager notificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId()); - - mNotifications.remove(call); - } - - /** - * Attempts to build a large icon to use for the notification based on the contact info and - * post the updated notification to the notification manager. - */ - private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { - Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall()); - if (largeIcon != null) { - largeIcon = getRoundedIcon(mContext, largeIcon); - } - info.setLargeIcon(largeIcon); - postNotification(info); - } - - /** - * Builds and stores the contact information the notification will display and posts the updated - * notification to the notification manager. - */ - private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { - info.setContentTitle(getContentTitle(mContext, mContactsPreferences, - entry, info.getCall())); - info.setPersonReference(getPersonReference(entry, info.getCall())); - postNotification(info); - } - - /** - * Rebuild an existing or show a new notification given {@link NotificationInfo}. - */ - private void postNotification(NotificationInfo info) { - Log.i(this, "postNotification : " + info.getContentTitle()); - Notification.Builder builder = new Notification.Builder(mContext); - // Set notification as ongoing since calls are long-running versus a point-in-time notice. - builder.setOngoing(true); - // Make the notification prioritized over the other normal notifications. - builder.setPriority(Notification.PRIORITY_HIGH); - // Set the content ("Ongoing call on another device") - builder.setContentText(mContext.getString(R.string.notification_external_call)); - builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(info.getContentTitle()); - builder.setLargeIcon(info.getLargeIcon()); - builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); - builder.addPerson(info.getPersonReference()); - - // Where the external call supports being transferred to the local device, add an action - // to the notification to initiate the call pull process. - if ((info.getCall().getDetails().getCallCapabilities() - & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) - == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) { - - Intent intent = new Intent( - NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL, null, mContext, - NotificationBroadcastReceiver.class); - intent.putExtra(NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, - info.getNotificationId()); - - builder.addAction(new Notification.Action.Builder(R.drawable.ic_call_white_24dp, - mContext.getText(R.string.notification_transfer_call), - PendingIntent.getBroadcast(mContext, 0, intent, 0)).build()); - } - - /** - * This builder is used for the notification shown when the device is locked and the user - * has set their notification settings to 'hide sensitive content' - * {@see Notification.Builder#setPublicVersion}. - */ - Notification.Builder publicBuilder = new Notification.Builder(mContext); - publicBuilder.setSmallIcon(R.drawable.ic_call_white_24dp); - publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); - - builder.setPublicVersion(publicBuilder.build()); - Notification notification = builder.build(); - - NotificationManager notificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification); - } - - /** - * Finds a large icon to display in a notification for a call. For conference calls, a - * conference call icon is used, otherwise if contact info is specified, the user's contact - * photo or avatar is used. - * - * @param context The context. - * @param contactInfo The contact cache info. - * @param call The call. - * @return The large icon to use for the notification. - */ - private @Nullable Bitmap getLargeIconToDisplay(Context context, - ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { - - Bitmap largeIcon = null; - if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && - !call.getDetails() - .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { - - largeIcon = BitmapFactory.decodeResource(context.getResources(), - R.drawable.img_conference); - } - if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { - largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); - } - return largeIcon; - } - - /** - * Given a bitmap, returns a rounded version of the icon suitable for display in a notification. - * - * @param context The context. - * @param bitmap The bitmap to round. - * @return The rounded bitmap. - */ - private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) { - if (bitmap == null) { - return null; - } - final int height = (int) context.getResources().getDimension( - android.R.dimen.notification_large_icon_height); - final int width = (int) context.getResources().getDimension( - android.R.dimen.notification_large_icon_width); - return BitmapUtil.getRoundedBitmap(bitmap, width, height); - } - - /** - * Builds a notification content title for a call. If the call is a conference call, it is - * identified as such. Otherwise an attempt is made to show an associated contact name or - * phone number. - * - * @param context The context. - * @param contactsPreferences Contacts preferences, used to determine the preferred formatting - * for contact names. - * @param contactInfo The contact info which was looked up in the contact cache. - * @param call The call to generate a title for. - * @return The content title. - */ - private @Nullable String getContentTitle(Context context, - @Nullable ContactsPreferences contactsPreferences, - ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { - - if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && - !call.getDetails() - .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { - - return context.getResources().getString(R.string.card_title_conf_call); - } - - String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, - contactInfo.nameAlternative, contactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() - .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); - } - return preferredName; - } - - /** - * Gets a "person reference" for a notification, used by the system to determine whether the - * notification should be allowed past notification interruption filters. - * - * @param contactInfo The contact info from cache. - * @param call The call. - * @return the person reference. - */ - private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, - Call call) { - - String number = TelecomCallUtil.getNumber(call); - // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. - // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid - // NotificationManager using it. - if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { - return contactInfo.lookupUri.toString(); - } else if (!TextUtils.isEmpty(number)) { - return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString(); - } - return ""; - } -} diff --git a/InCallUI/src/com/android/incallui/FragmentDisplayManager.java b/InCallUI/src/com/android/incallui/FragmentDisplayManager.java deleted file mode 100644 index 045d999a0834fd1bbaee48211a2db0f83fc34879..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/FragmentDisplayManager.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.app.Fragment; - -interface FragmentDisplayManager { - public void onFragmentAttached(Fragment fragment); -} diff --git a/InCallUI/src/com/android/incallui/GlowPadAnswerFragment.java b/InCallUI/src/com/android/incallui/GlowPadAnswerFragment.java deleted file mode 100644 index 62a8e7829da7c51642528dcc7ea379184e17d397..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/GlowPadAnswerFragment.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.telecom.VideoProfile; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.dialer.R; - -public class GlowPadAnswerFragment extends AnswerFragment { - - private GlowPadWrapper mGlowpad; - - public GlowPadAnswerFragment() { - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mGlowpad = (GlowPadWrapper) inflater.inflate(R.layout.answer_fragment, - container, false); - - Log.d(this, "Creating view for answer fragment ", this); - Log.d(this, "Created from activity", getActivity()); - mGlowpad.setAnswerFragment(this); - - return mGlowpad; - } - - @Override - public void onResume() { - super.onResume(); - mGlowpad.requestFocus(); - } - - @Override - public void onDestroyView() { - Log.d(this, "onDestroyView"); - if (mGlowpad != null) { - mGlowpad.stopPing(); - mGlowpad = null; - } - super.onDestroyView(); - } - - @Override - public void onShowAnswerUi(boolean shown) { - Log.d(this, "Show answer UI: " + shown); - if (shown) { - mGlowpad.startPing(); - } else { - mGlowpad.stopPing(); - } - } - - /** - * Sets targets on the glowpad according to target set identified by the parameter. - * - * @param targetSet Integer identifying the set of targets to use. - */ - public void showTargets(int targetSet) { - showTargets(targetSet, VideoProfile.STATE_BIDIRECTIONAL); - } - - /** - * Sets targets on the glowpad according to target set identified by the parameter. - * - * @param targetSet Integer identifying the set of targets to use. - */ - @Override - public void showTargets(int targetSet, int videoState) { - final int targetResourceId; - final int targetDescriptionsResourceId; - final int directionDescriptionsResourceId; - final int handleDrawableResourceId; - mGlowpad.setVideoState(videoState); - - switch (targetSet) { - case TARGET_SET_FOR_AUDIO_WITH_SMS: - targetResourceId = R.array.incoming_call_widget_audio_with_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_audio_with_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_audio_with_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_audio_handle; - break; - case TARGET_SET_FOR_VIDEO_WITHOUT_SMS: - targetResourceId = R.array.incoming_call_widget_video_without_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_video_without_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_video_without_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_video_handle; - break; - case TARGET_SET_FOR_VIDEO_WITH_SMS: - targetResourceId = R.array.incoming_call_widget_video_with_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_video_with_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_video_with_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_video_handle; - break; - case TARGET_SET_FOR_VIDEO_ACCEPT_REJECT_REQUEST: - targetResourceId = - R.array.incoming_call_widget_video_request_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_video_request_target_descriptions; - directionDescriptionsResourceId = R.array - .incoming_call_widget_video_request_target_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_video_handle; - break; - case TARGET_SET_FOR_AUDIO_WITHOUT_SMS: - default: - targetResourceId = R.array.incoming_call_widget_audio_without_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_audio_without_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_audio_without_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_audio_handle; - break; - } - - if (targetResourceId != mGlowpad.getTargetResourceId()) { - mGlowpad.setTargetResources(targetResourceId); - mGlowpad.setTargetDescriptionsResourceId(targetDescriptionsResourceId); - mGlowpad.setDirectionDescriptionsResourceId(directionDescriptionsResourceId); - mGlowpad.setHandleDrawable(handleDrawableResourceId); - mGlowpad.reset(false); - } - } - - @Override - protected void onMessageDialogCancel() { - if (mGlowpad != null) { - mGlowpad.startPing(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/GlowPadWrapper.java b/InCallUI/src/com/android/incallui/GlowPadWrapper.java deleted file mode 100644 index 342f6b4fd667a38b6ff21a3b26572ec5c6ef0da9..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/GlowPadWrapper.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.os.Handler; -import android.os.Message; -import android.telecom.VideoProfile; -import android.util.AttributeSet; -import android.view.View; - -import com.android.dialer.R; -import com.android.incallui.widget.multiwaveview.GlowPadView; - -/** - * - */ -public class GlowPadWrapper extends GlowPadView implements GlowPadView.OnTriggerListener { - - // Parameters for the GlowPadView "ping" animation; see triggerPing(). - private static final int PING_MESSAGE_WHAT = 101; - private static final boolean ENABLE_PING_AUTO_REPEAT = true; - private static final long PING_REPEAT_DELAY_MS = 1200; - - private final Handler mPingHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case PING_MESSAGE_WHAT: - triggerPing(); - break; - } - } - }; - - private AnswerFragment mAnswerFragment; - private boolean mPingEnabled = true; - private boolean mTargetTriggered = false; - private int mVideoState = VideoProfile.STATE_BIDIRECTIONAL; - - public GlowPadWrapper(Context context) { - super(context); - Log.d(this, "class created " + this + " "); - } - - public GlowPadWrapper(Context context, AttributeSet attrs) { - super(context, attrs); - Log.d(this, "class created " + this); - } - - @Override - protected void onFinishInflate() { - Log.d(this, "onFinishInflate()"); - super.onFinishInflate(); - setOnTriggerListener(this); - } - - public void startPing() { - Log.d(this, "startPing"); - mPingEnabled = true; - triggerPing(); - } - - public void stopPing() { - Log.d(this, "stopPing"); - mPingEnabled = false; - mPingHandler.removeMessages(PING_MESSAGE_WHAT); - } - - private void triggerPing() { - Log.d(this, "triggerPing(): " + mPingEnabled + " " + this); - if (mPingEnabled && !mPingHandler.hasMessages(PING_MESSAGE_WHAT)) { - ping(); - - if (ENABLE_PING_AUTO_REPEAT) { - mPingHandler.sendEmptyMessageDelayed(PING_MESSAGE_WHAT, PING_REPEAT_DELAY_MS); - } - } - } - - @Override - public void onGrabbed(View v, int handle) { - Log.d(this, "onGrabbed()"); - stopPing(); - } - - @Override - public void onReleased(View v, int handle) { - Log.d(this, "onReleased()"); - if (mTargetTriggered) { - mTargetTriggered = false; - } else { - startPing(); - } - } - - @Override - public void onTrigger(View v, int target) { - Log.d(this, "onTrigger() view=" + v + " target=" + target); - final int resId = getResourceIdForTarget(target); - if (resId == R.drawable.ic_lockscreen_answer) { - mAnswerFragment.onAnswer(VideoProfile.STATE_AUDIO_ONLY, getContext()); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_lockscreen_decline) { - mAnswerFragment.onDecline(getContext()); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_lockscreen_text) { - mAnswerFragment.onText(); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_videocam || resId == R.drawable.ic_lockscreen_answer_video) { - mAnswerFragment.onAnswer(mVideoState, getContext()); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_lockscreen_decline_video) { - mAnswerFragment.onDeclineUpgradeRequest(getContext()); - mTargetTriggered = true; - } else { - // Code should never reach here. - Log.e(this, "Trigger detected on unhandled resource. Skipping."); - } - } - - @Override - public void onGrabbedStateChange(View v, int handle) { - - } - - @Override - public void onFinishFinalAnimation() { - - } - - public void setAnswerFragment(AnswerFragment fragment) { - mAnswerFragment = fragment; - } - - /** - * Sets the video state represented by the "video" icon on the glow pad. - * - * @param videoState The new video state. - */ - public void setVideoState(int videoState) { - mVideoState = videoState; - } -} diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java deleted file mode 100644 index eaaedff2ccd9a57b3e550e5067a149c09ad54522..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallActivity.java +++ /dev/null @@ -1,980 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.app.ActionBar; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.DialogFragment; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.Point; -import android.hardware.SensorManager; -import android.os.Bundle; -import android.os.Trace; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccountHandle; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.OrientationEventListener; -import android.view.Surface; -import android.view.View; -import android.view.View.OnTouchListener; -import android.view.Window; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; - -import com.android.contacts.common.activity.TransactionSafeActivity; -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.interactions.TouchPointManager; -import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; -import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; -import com.android.dialer.R; -import com.android.dialer.logging.Logger; -import com.android.dialer.logging.ScreenEvent; -import com.android.incallui.Call.State; -import com.android.incallui.util.AccessibilityUtil; -import com.android.phone.common.animation.AnimUtils; -import com.android.phone.common.animation.AnimationListenerAdapter; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * Main activity that the user interacts with while in a live call. - */ -public class InCallActivity extends TransactionSafeActivity implements FragmentDisplayManager { - - public static final String TAG = InCallActivity.class.getSimpleName(); - - public static final String SHOW_DIALPAD_EXTRA = "InCallActivity.show_dialpad"; - public static final String DIALPAD_TEXT_EXTRA = "InCallActivity.dialpad_text"; - public static final String NEW_OUTGOING_CALL_EXTRA = "InCallActivity.new_outgoing_call"; - public static final String FOR_FULL_SCREEN_INTENT = "InCallActivity.for_full_screen_intent"; - - private static final String TAG_DIALPAD_FRAGMENT = "tag_dialpad_fragment"; - private static final String TAG_CONFERENCE_FRAGMENT = "tag_conference_manager_fragment"; - private static final String TAG_CALLCARD_FRAGMENT = "tag_callcard_fragment"; - private static final String TAG_ANSWER_FRAGMENT = "tag_answer_fragment"; - private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment"; - - private static final int DIALPAD_REQUEST_NONE = 1; - private static final int DIALPAD_REQUEST_SHOW = 2; - private static final int DIALPAD_REQUEST_HIDE = 3; - - /** - * This is used to relaunch the activity if resizing beyond which it needs to load different - * layout file. - */ - private static final int SCREEN_HEIGHT_RESIZE_THRESHOLD = 500; - - private CallButtonFragment mCallButtonFragment; - private CallCardFragment mCallCardFragment; - private AnswerFragment mAnswerFragment; - private DialpadFragment mDialpadFragment; - private ConferenceManagerFragment mConferenceManagerFragment; - private FragmentManager mChildFragmentManager; - - private AlertDialog mDialog; - private InCallOrientationEventListener mInCallOrientationEventListener; - - /** - * Used to indicate whether the dialpad should be hidden or shown {@link #onResume}. - * {@code #DIALPAD_REQUEST_SHOW} indicates that the dialpad should be shown. - * {@code #DIALPAD_REQUEST_HIDE} indicates that the dialpad should be hidden. - * {@code #DIALPAD_REQUEST_NONE} indicates no change should be made to dialpad visibility. - */ - private int mShowDialpadRequest = DIALPAD_REQUEST_NONE; - - /** - * Use to determine if the dialpad should be animated on show. - */ - private boolean mAnimateDialpadOnShow; - - /** - * Use to determine the DTMF Text which should be pre-populated in the dialpad. - */ - private String mDtmfText; - - /** - * Use to pass parameters for showing the PostCharDialog to {@link #onResume} - */ - private boolean mShowPostCharWaitDialogOnResume; - private String mShowPostCharWaitDialogCallId; - private String mShowPostCharWaitDialogChars; - - private boolean mIsLandscape; - private Animation mSlideIn; - private Animation mSlideOut; - private boolean mDismissKeyguard = false; - - AnimationListenerAdapter mSlideOutListener = new AnimationListenerAdapter() { - @Override - public void onAnimationEnd(Animation animation) { - showFragment(TAG_DIALPAD_FRAGMENT, false, true); - } - }; - - private OnTouchListener mDispatchTouchEventListener; - - private SelectPhoneAccountListener mSelectAcctListener = new SelectPhoneAccountListener() { - @Override - public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle, - boolean setDefault) { - InCallPresenter.getInstance().handleAccountSelection(selectedAccountHandle, - setDefault); - } - - @Override - public void onDialogDismissed() { - InCallPresenter.getInstance().cancelAccountSelection(); - } - }; - - @Override - protected void onCreate(Bundle icicle) { - Log.d(this, "onCreate()... this = " + this); - - super.onCreate(icicle); - - // set this flag so this activity will stay in front of the keyguard - // Have the WindowManager filter out touch events that are "too fat". - int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; - - getWindow().addFlags(flags); - - // Setup action bar for the conference call manager. - requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); - ActionBar actionBar = getActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setDisplayShowTitleEnabled(true); - actionBar.hide(); - } - - // TODO(klp): Do we need to add this back when prox sensor is not available? - // lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; - - setContentView(R.layout.incall_screen); - - internalResolveIntent(getIntent()); - - mIsLandscape = getResources().getConfiguration().orientation == - Configuration.ORIENTATION_LANDSCAPE; - - final boolean isRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == - View.LAYOUT_DIRECTION_RTL; - - if (mIsLandscape) { - mSlideIn = AnimationUtils.loadAnimation(this, - isRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); - mSlideOut = AnimationUtils.loadAnimation(this, - isRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right); - } else { - mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom); - mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom); - } - - mSlideIn.setInterpolator(AnimUtils.EASE_IN); - mSlideOut.setInterpolator(AnimUtils.EASE_OUT); - - mSlideOut.setAnimationListener(mSlideOutListener); - - // If the dialpad fragment already exists, retrieve it. This is important when rotating as - // we will not be able to hide or show the dialpad after the rotation otherwise. - Fragment existingFragment = - getFragmentManager().findFragmentByTag(DialpadFragment.class.getName()); - if (existingFragment != null) { - mDialpadFragment = (DialpadFragment) existingFragment; - } - - if (icicle != null) { - // If the dialpad was shown before, set variables indicating it should be shown and - // populated with the previous DTMF text. The dialpad is actually shown and populated - // in onResume() to ensure the hosting CallCardFragment has been inflated and is ready - // to receive it. - if (icicle.containsKey(SHOW_DIALPAD_EXTRA)) { - boolean showDialpad = icicle.getBoolean(SHOW_DIALPAD_EXTRA); - mShowDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_HIDE; - mAnimateDialpadOnShow = false; - } - mDtmfText = icicle.getString(DIALPAD_TEXT_EXTRA); - - SelectPhoneAccountDialogFragment dialogFragment = (SelectPhoneAccountDialogFragment) - getFragmentManager().findFragmentByTag(TAG_SELECT_ACCT_FRAGMENT); - if (dialogFragment != null) { - dialogFragment.setListener(mSelectAcctListener); - } - } - mInCallOrientationEventListener = new InCallOrientationEventListener(this); - - Log.d(this, "onCreate(): exit"); - } - - @Override - protected void onSaveInstanceState(Bundle out) { - // TODO: The dialpad fragment should handle this as part of its own state - out.putBoolean(SHOW_DIALPAD_EXTRA, - mCallButtonFragment != null && mCallButtonFragment.isDialpadVisible()); - if (mDialpadFragment != null) { - out.putString(DIALPAD_TEXT_EXTRA, mDialpadFragment.getDtmfText()); - } - super.onSaveInstanceState(out); - } - - @Override - protected void onStart() { - Log.d(this, "onStart()..."); - super.onStart(); - - // setting activity should be last thing in setup process - InCallPresenter.getInstance().setActivity(this); - enableInCallOrientationEventListener(getRequestedOrientation() == - InCallOrientationEventListener.FULL_SENSOR_SCREEN_ORIENTATION); - - InCallPresenter.getInstance().onActivityStarted(); - } - - @Override - protected void onResume() { - Log.i(this, "onResume()..."); - super.onResume(); - - InCallPresenter.getInstance().setThemeColors(); - InCallPresenter.getInstance().onUiShowing(true); - - // Clear fullscreen state onResume; the stored value may not match reality. - InCallPresenter.getInstance().clearFullscreen(); - - // If there is a pending request to show or hide the dialpad, handle that now. - if (mShowDialpadRequest != DIALPAD_REQUEST_NONE) { - if (mShowDialpadRequest == DIALPAD_REQUEST_SHOW) { - // Exit fullscreen so that the user has access to the dialpad hide/show button and - // can hide the dialpad. Important when showing the dialpad from within dialer. - InCallPresenter.getInstance().setFullScreen(false, true /* force */); - - mCallButtonFragment.displayDialpad(true /* show */, - mAnimateDialpadOnShow /* animate */); - mAnimateDialpadOnShow = false; - - if (mDialpadFragment != null) { - mDialpadFragment.setDtmfText(mDtmfText); - mDtmfText = null; - } - } else { - Log.v(this, "onResume : force hide dialpad"); - if (mDialpadFragment != null) { - mCallButtonFragment.displayDialpad(false /* show */, false /* animate */); - } - } - mShowDialpadRequest = DIALPAD_REQUEST_NONE; - } - - if (mShowPostCharWaitDialogOnResume) { - showPostCharWaitDialog(mShowPostCharWaitDialogCallId, mShowPostCharWaitDialogChars); - } - - CallList.getInstance().onInCallUiShown( - getIntent().getBooleanExtra(FOR_FULL_SCREEN_INTENT, false)); - } - - // onPause is guaranteed to be called when the InCallActivity goes - // in the background. - @Override - protected void onPause() { - Log.d(this, "onPause()..."); - if (mDialpadFragment != null) { - mDialpadFragment.onDialerKeyUp(null); - } - - InCallPresenter.getInstance().onUiShowing(false); - if (isFinishing()) { - InCallPresenter.getInstance().unsetActivity(this); - } - super.onPause(); - } - - @Override - protected void onStop() { - Log.d(this, "onStop()..."); - enableInCallOrientationEventListener(false); - InCallPresenter.getInstance().updateIsChangingConfigurations(); - InCallPresenter.getInstance().onActivityStopped(); - super.onStop(); - } - - @Override - protected void onDestroy() { - Log.d(this, "onDestroy()... this = " + this); - InCallPresenter.getInstance().unsetActivity(this); - InCallPresenter.getInstance().updateIsChangingConfigurations(); - super.onDestroy(); - } - - /** - * When fragments have a parent fragment, onAttachFragment is not called on the parent - * activity. To fix this, register our own callback instead that is always called for - * all fragments. - * - * @see {@link BaseFragment#onAttach(Activity)} - */ - @Override - public void onFragmentAttached(Fragment fragment) { - if (fragment instanceof DialpadFragment) { - mDialpadFragment = (DialpadFragment) fragment; - } else if (fragment instanceof AnswerFragment) { - mAnswerFragment = (AnswerFragment) fragment; - } else if (fragment instanceof CallCardFragment) { - mCallCardFragment = (CallCardFragment) fragment; - mChildFragmentManager = mCallCardFragment.getChildFragmentManager(); - } else if (fragment instanceof ConferenceManagerFragment) { - mConferenceManagerFragment = (ConferenceManagerFragment) fragment; - } else if (fragment instanceof CallButtonFragment) { - mCallButtonFragment = (CallButtonFragment) fragment; - } - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - Configuration oldConfig = getResources().getConfiguration(); - Log.v(this, String.format( - "incallui config changed, screen size: w%ddp x h%ddp old:w%ddp x h%ddp", - newConfig.screenWidthDp, newConfig.screenHeightDp, - oldConfig.screenWidthDp, oldConfig.screenHeightDp)); - // Recreate this activity if height is changing beyond the threshold to load different - // layout file. - if (oldConfig.screenHeightDp < SCREEN_HEIGHT_RESIZE_THRESHOLD && - newConfig.screenHeightDp > SCREEN_HEIGHT_RESIZE_THRESHOLD || - oldConfig.screenHeightDp > SCREEN_HEIGHT_RESIZE_THRESHOLD && - newConfig.screenHeightDp < SCREEN_HEIGHT_RESIZE_THRESHOLD) { - Log.i(this, String.format( - "Recreate activity due to resize beyond threshold: %d dp", - SCREEN_HEIGHT_RESIZE_THRESHOLD)); - recreate(); - } - } - - /** - * Returns true when the Activity is currently visible. - */ - /* package */ boolean isVisible() { - return isSafeToCommitTransactions(); - } - - private boolean hasPendingDialogs() { - return mDialog != null || (mAnswerFragment != null && mAnswerFragment.hasPendingDialogs()); - } - - @Override - public void finish() { - Log.i(this, "finish(). Dialog showing: " + (mDialog != null)); - - // skip finish if we are still showing a dialog. - if (!hasPendingDialogs()) { - super.finish(); - } - } - - @Override - protected void onNewIntent(Intent intent) { - Log.d(this, "onNewIntent: intent = " + intent); - - // We're being re-launched with a new Intent. Since it's possible for a - // single InCallActivity instance to persist indefinitely (even if we - // finish() ourselves), this sequence can potentially happen any time - // the InCallActivity needs to be displayed. - - // Stash away the new intent so that we can get it in the future - // by calling getIntent(). (Otherwise getIntent() will return the - // original Intent from when we first got created!) - setIntent(intent); - - // Activities are always paused before receiving a new intent, so - // we can count on our onResume() method being called next. - - // Just like in onCreate(), handle the intent. - internalResolveIntent(intent); - } - - @Override - public void onBackPressed() { - Log.i(this, "onBackPressed"); - - // BACK is also used to exit out of any "special modes" of the - // in-call UI: - if (!isVisible()) { - return; - } - - if ((mConferenceManagerFragment == null || !mConferenceManagerFragment.isVisible()) - && (mCallCardFragment == null || !mCallCardFragment.isVisible())) { - return; - } - - if (mDialpadFragment != null && mDialpadFragment.isVisible()) { - mCallButtonFragment.displayDialpad(false /* show */, true /* animate */); - return; - } else if (mConferenceManagerFragment != null && mConferenceManagerFragment.isVisible()) { - showConferenceFragment(false); - return; - } - - // Always disable the Back key while an incoming call is ringing - final Call call = CallList.getInstance().getIncomingCall(); - if (call != null) { - Log.i(this, "Consume Back press for an incoming call"); - return; - } - - // Nothing special to do. Fall back to the default behavior. - super.onBackPressed(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - // push input to the dialer. - if (mDialpadFragment != null && (mDialpadFragment.isVisible()) && - (mDialpadFragment.onDialerKeyUp(event))) { - return true; - } else if (keyCode == KeyEvent.KEYCODE_CALL) { - // Always consume CALL to be sure the PhoneWindow won't do anything with it - return true; - } - return super.onKeyUp(keyCode, event); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - if (mDispatchTouchEventListener != null) { - boolean handled = mDispatchTouchEventListener.onTouch(null, ev); - if (handled) { - return true; - } - } - return super.dispatchTouchEvent(ev); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_CALL: - boolean handled = InCallPresenter.getInstance().handleCallKey(); - if (!handled) { - Log.w(this, "InCallActivity should always handle KEYCODE_CALL in onKeyDown"); - } - // Always consume CALL to be sure the PhoneWindow won't do anything with it - return true; - - // Note there's no KeyEvent.KEYCODE_ENDCALL case here. - // The standard system-wide handling of the ENDCALL key - // (see PhoneWindowManager's handling of KEYCODE_ENDCALL) - // already implements exactly what the UI spec wants, - // namely (1) "hang up" if there's a current active call, - // or (2) "don't answer" if there's a current ringing call. - - case KeyEvent.KEYCODE_CAMERA: - // Disable the CAMERA button while in-call since it's too - // easy to press accidentally. - return true; - - case KeyEvent.KEYCODE_VOLUME_UP: - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_MUTE: - // Ringer silencing handled by PhoneWindowManager. - break; - - case KeyEvent.KEYCODE_MUTE: - // toggle mute - TelecomAdapter.getInstance().mute(!AudioModeProvider.getInstance().getMute()); - return true; - - // Various testing/debugging features, enabled ONLY when VERBOSE == true. - case KeyEvent.KEYCODE_SLASH: - if (Log.VERBOSE) { - Log.v(this, "----------- InCallActivity View dump --------------"); - // Dump starting from the top-level view of the entire activity: - Window w = this.getWindow(); - View decorView = w.getDecorView(); - Log.d(this, "View dump:" + decorView); - return true; - } - break; - case KeyEvent.KEYCODE_EQUALS: - // TODO: Dump phone state? - break; - } - - if (event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event)) { - return true; - } - return super.onKeyDown(keyCode, event); - } - - private boolean handleDialerKeyDown(int keyCode, KeyEvent event) { - Log.v(this, "handleDialerKeyDown: keyCode " + keyCode + ", event " + event + "..."); - - // As soon as the user starts typing valid dialable keys on the - // keyboard (presumably to type DTMF tones) we start passing the - // key events to the DTMFDialer's onDialerKeyDown. - if (mDialpadFragment != null && mDialpadFragment.isVisible()) { - return mDialpadFragment.onDialerKeyDown(event); - } - - return false; - } - - public CallButtonFragment getCallButtonFragment() { - return mCallButtonFragment; - } - - public CallCardFragment getCallCardFragment() { - return mCallCardFragment; - } - - public AnswerFragment getAnswerFragment() { - return mAnswerFragment; - } - - private void internalResolveIntent(Intent intent) { - final String action = intent.getAction(); - if (action.equals(Intent.ACTION_MAIN)) { - // This action is the normal way to bring up the in-call UI. - // - // But we do check here for one extra that can come along with the - // ACTION_MAIN intent: - - if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) { - // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF - // dialpad should be initially visible. If the extra isn't - // present at all, we just leave the dialpad in its previous state. - - final boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false); - Log.d(this, "- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad); - - relaunchedFromDialer(showDialpad); - } - - boolean newOutgoingCall = false; - if (intent.getBooleanExtra(NEW_OUTGOING_CALL_EXTRA, false)) { - intent.removeExtra(NEW_OUTGOING_CALL_EXTRA); - Call call = CallList.getInstance().getOutgoingCall(); - if (call == null) { - call = CallList.getInstance().getPendingOutgoingCall(); - } - - Bundle extras = null; - if (call != null) { - extras = call.getTelecomCall().getDetails().getIntentExtras(); - } - if (extras == null) { - // Initialize the extras bundle to avoid NPE - extras = new Bundle(); - } - - Point touchPoint = null; - if (TouchPointManager.getInstance().hasValidPoint()) { - // Use the most immediate touch point in the InCallUi if available - touchPoint = TouchPointManager.getInstance().getPoint(); - } else { - // Otherwise retrieve the touch point from the call intent - if (call != null) { - touchPoint = (Point) extras.getParcelable(TouchPointManager.TOUCH_POINT); - } - } - - // Start animation for new outgoing call - CircularRevealFragment.startCircularReveal(getFragmentManager(), touchPoint, - InCallPresenter.getInstance()); - - // InCallActivity is responsible for disconnecting a new outgoing call if there - // is no way of making it (i.e. no valid call capable accounts). - // If the version is not MSIM compatible, then ignore this code. - if (CompatUtils.isMSIMCompatible() - && InCallPresenter.isCallWithNoValidAccounts(call)) { - TelecomAdapter.getInstance().disconnectCall(call.getId()); - } - - dismissKeyguard(true); - newOutgoingCall = true; - } - - Call pendingAccountSelectionCall = CallList.getInstance().getWaitingForAccountCall(); - if (pendingAccountSelectionCall != null) { - showCallCardFragment(false); - Bundle extras = - pendingAccountSelectionCall.getTelecomCall().getDetails().getIntentExtras(); - - final List phoneAccountHandles; - if (extras != null) { - phoneAccountHandles = extras.getParcelableArrayList( - android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); - } else { - phoneAccountHandles = new ArrayList<>(); - } - - DialogFragment dialogFragment = SelectPhoneAccountDialogFragment.newInstance( - R.string.select_phone_account_for_calls, true, phoneAccountHandles, - mSelectAcctListener); - dialogFragment.show(getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT); - } else if (!newOutgoingCall) { - showCallCardFragment(true); - } - return; - } - } - - /** - * When relaunching from the dialer app, {@code showDialpad} indicates whether the dialpad - * should be shown on launch. - * - * @param showDialpad {@code true} to indicate the dialpad should be shown on launch, and - * {@code false} to indicate no change should be made to the - * dialpad visibility. - */ - private void relaunchedFromDialer(boolean showDialpad) { - mShowDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_NONE; - mAnimateDialpadOnShow = true; - - if (mShowDialpadRequest == DIALPAD_REQUEST_SHOW) { - // If there's only one line in use, AND it's on hold, then we're sure the user - // wants to use the dialpad toward the exact line, so un-hold the holding line. - final Call call = CallList.getInstance().getActiveOrBackgroundCall(); - if (call != null && call.getState() == State.ONHOLD) { - TelecomAdapter.getInstance().unholdCall(call.getId()); - } - } - } - - public void dismissKeyguard(boolean dismiss) { - if (mDismissKeyguard == dismiss) { - return; - } - mDismissKeyguard = dismiss; - if (dismiss) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); - } - } - - private void showFragment(String tag, boolean show, boolean executeImmediately) { - Trace.beginSection("showFragment - " + tag); - final FragmentManager fm = getFragmentManagerForTag(tag); - - if (fm == null) { - Log.w(TAG, "Fragment manager is null for : " + tag); - return; - } - - Fragment fragment = fm.findFragmentByTag(tag); - if (!show && fragment == null) { - // Nothing to show, so bail early. - return; - } - - final FragmentTransaction transaction = fm.beginTransaction(); - if (show) { - if (fragment == null) { - fragment = createNewFragmentForTag(tag); - transaction.add(getContainerIdForFragment(tag), fragment, tag); - } else { - transaction.show(fragment); - } - Logger.logScreenView(getScreenTypeForTag(tag), this); - } else { - transaction.hide(fragment); - } - - transaction.commitAllowingStateLoss(); - if (executeImmediately) { - fm.executePendingTransactions(); - } - Trace.endSection(); - } - - private Fragment createNewFragmentForTag(String tag) { - if (TAG_DIALPAD_FRAGMENT.equals(tag)) { - mDialpadFragment = new DialpadFragment(); - return mDialpadFragment; - } else if (TAG_ANSWER_FRAGMENT.equals(tag)) { - if (AccessibilityUtil.isTalkBackEnabled(this)) { - mAnswerFragment = new AccessibleAnswerFragment(); - } else { - mAnswerFragment = new GlowPadAnswerFragment(); - } - return mAnswerFragment; - } else if (TAG_CONFERENCE_FRAGMENT.equals(tag)) { - mConferenceManagerFragment = new ConferenceManagerFragment(); - return mConferenceManagerFragment; - } else if (TAG_CALLCARD_FRAGMENT.equals(tag)) { - mCallCardFragment = new CallCardFragment(); - return mCallCardFragment; - } - throw new IllegalStateException("Unexpected fragment: " + tag); - } - - private FragmentManager getFragmentManagerForTag(String tag) { - if (TAG_DIALPAD_FRAGMENT.equals(tag)) { - return mChildFragmentManager; - } else if (TAG_ANSWER_FRAGMENT.equals(tag)) { - return mChildFragmentManager; - } else if (TAG_CONFERENCE_FRAGMENT.equals(tag)) { - return getFragmentManager(); - } else if (TAG_CALLCARD_FRAGMENT.equals(tag)) { - return getFragmentManager(); - } - throw new IllegalStateException("Unexpected fragment: " + tag); - } - - private int getScreenTypeForTag(String tag) { - switch (tag) { - case TAG_DIALPAD_FRAGMENT: - return ScreenEvent.INCALL_DIALPAD; - case TAG_CALLCARD_FRAGMENT: - return ScreenEvent.INCALL; - case TAG_CONFERENCE_FRAGMENT: - return ScreenEvent.CONFERENCE_MANAGEMENT; - case TAG_ANSWER_FRAGMENT: - return ScreenEvent.INCOMING_CALL; - default: - return ScreenEvent.UNKNOWN; - } - } - - private int getContainerIdForFragment(String tag) { - if (TAG_DIALPAD_FRAGMENT.equals(tag)) { - return R.id.answer_and_dialpad_container; - } else if (TAG_ANSWER_FRAGMENT.equals(tag)) { - return R.id.answer_and_dialpad_container; - } else if (TAG_CONFERENCE_FRAGMENT.equals(tag)) { - return R.id.main; - } else if (TAG_CALLCARD_FRAGMENT.equals(tag)) { - return R.id.main; - } - throw new IllegalStateException("Unexpected fragment: " + tag); - } - - /** - * @return {@code true} while the visibility of the dialpad has actually changed. - */ - public boolean showDialpadFragment(boolean show, boolean animate) { - // If the dialpad is already visible, don't animate in. If it's gone, don't animate out. - if ((show && isDialpadVisible()) || (!show && !isDialpadVisible())) { - return false; - } - // We don't do a FragmentTransaction on the hide case because it will be dealt with when - // the listener is fired after an animation finishes. - if (!animate) { - showFragment(TAG_DIALPAD_FRAGMENT, show, true); - } else { - if (show) { - showFragment(TAG_DIALPAD_FRAGMENT, true, true); - mDialpadFragment.animateShowDialpad(); - } - mDialpadFragment.getView().startAnimation(show ? mSlideIn : mSlideOut); - } - // Note: onDialpadVisibilityChange is called here to ensure that the dialpad FAB - // repositions itself. - mCallCardFragment.onDialpadVisibilityChange(show); - - final ProximitySensor sensor = InCallPresenter.getInstance().getProximitySensor(); - if (sensor != null) { - sensor.onDialpadVisible(show); - } - return true; - } - - public boolean isDialpadVisible() { - return mDialpadFragment != null && mDialpadFragment.isVisible(); - } - - public void showCallCardFragment(boolean show) { - showFragment(TAG_CALLCARD_FRAGMENT, show, true); - } - - /** - * Hides or shows the conference manager fragment. - * - * @param show {@code true} if the conference manager should be shown, {@code false} if it - * should be hidden. - */ - public void showConferenceFragment(boolean show) { - showFragment(TAG_CONFERENCE_FRAGMENT, show, true); - mConferenceManagerFragment.onVisibilityChanged(show); - - // Need to hide the call card fragment to ensure that accessibility service does not try to - // give focus to the call card when the conference manager is visible. - mCallCardFragment.getView().setVisibility(show ? View.GONE : View.VISIBLE); - } - - public void showAnswerFragment(boolean show) { - // CallCardFragment is the parent fragment of AnswerFragment. - // Must create the CallCardFragment first before creating - // AnswerFragment if CallCardFragment is null. - if (show && getCallCardFragment() == null) { - showCallCardFragment(true); - } - showFragment(TAG_ANSWER_FRAGMENT, show, true); - } - - public void showPostCharWaitDialog(String callId, String chars) { - if (isVisible()) { - final PostCharDialogFragment fragment = new PostCharDialogFragment(callId, chars); - fragment.show(getFragmentManager(), "postCharWait"); - - mShowPostCharWaitDialogOnResume = false; - mShowPostCharWaitDialogCallId = null; - mShowPostCharWaitDialogChars = null; - } else { - mShowPostCharWaitDialogOnResume = true; - mShowPostCharWaitDialogCallId = callId; - mShowPostCharWaitDialogChars = chars; - } - } - - @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (mCallCardFragment != null) { - mCallCardFragment.dispatchPopulateAccessibilityEvent(event); - } - return super.dispatchPopulateAccessibilityEvent(event); - } - - public void maybeShowErrorDialogOnDisconnect(DisconnectCause disconnectCause) { - Log.d(this, "maybeShowErrorDialogOnDisconnect"); - - if (!isFinishing() && !TextUtils.isEmpty(disconnectCause.getDescription()) - && (disconnectCause.getCode() == DisconnectCause.ERROR || - disconnectCause.getCode() == DisconnectCause.RESTRICTED)) { - showErrorDialog(disconnectCause.getDescription()); - } - } - - public void dismissPendingDialogs() { - if (mDialog != null) { - mDialog.dismiss(); - mDialog = null; - } - if (mAnswerFragment != null) { - mAnswerFragment.dismissPendingDialogs(); - } - - SelectPhoneAccountDialogFragment dialogFragment = (SelectPhoneAccountDialogFragment) - getFragmentManager().findFragmentByTag(TAG_SELECT_ACCT_FRAGMENT); - if (dialogFragment != null) { - dialogFragment.dismiss(); - } - } - - /** - * Utility function to bring up a generic "error" dialog. - */ - private void showErrorDialog(CharSequence msg) { - Log.i(this, "Show Dialog: " + msg); - - dismissPendingDialogs(); - - mDialog = new AlertDialog.Builder(this) - .setMessage(msg) - .setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - onDialogDismissed(); - } - }) - .setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - onDialogDismissed(); - } - }) - .create(); - - mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); - mDialog.show(); - } - - private void onDialogDismissed() { - mDialog = null; - CallList.getInstance().onErrorDialogDismissed(); - InCallPresenter.getInstance().onDismissDialog(); - } - - public void setExcludeFromRecents(boolean exclude) { - ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - List tasks = am.getAppTasks(); - int taskId = getTaskId(); - for (int i = 0; i < tasks.size(); i++) { - ActivityManager.AppTask task = tasks.get(i); - if (task.getTaskInfo().id == taskId) { - try { - task.setExcludeFromRecents(exclude); - } catch (RuntimeException e) { - Log.e(TAG, "RuntimeException when excluding task from recents.", e); - } - } - } - } - - - public OnTouchListener getDispatchTouchEventListener() { - return mDispatchTouchEventListener; - } - - public void setDispatchTouchEventListener(OnTouchListener mDispatchTouchEventListener) { - this.mDispatchTouchEventListener = mDispatchTouchEventListener; - } - - /** - * Enables the OrientationEventListener if enable flag is true. Disables it if enable is - * false - * @param enable true or false. - */ - public void enableInCallOrientationEventListener(boolean enable) { - if (enable) { - mInCallOrientationEventListener.enable(enable); - } else { - mInCallOrientationEventListener.disable(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallAnimationUtils.java b/InCallUI/src/com/android/incallui/InCallAnimationUtils.java deleted file mode 100644 index 44bb369e61aac9d2faf23f760183ab99e14d415e..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallAnimationUtils.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.view.ViewPropertyAnimator; -import android.widget.ImageView; - -/** - * Utilities for Animation. - */ -public class InCallAnimationUtils { - private static final String LOG_TAG = InCallAnimationUtils.class.getSimpleName(); - /** - * Turn on when you're interested in fading animation. Intentionally untied from other debug - * settings. - */ - private static final boolean FADE_DBG = false; - - /** - * Duration for animations in msec, which can be used with - * {@link ViewPropertyAnimator#setDuration(long)} for example. - */ - public static final int ANIMATION_DURATION = 250; - - private InCallAnimationUtils() { - } - - /** - * Drawable achieving cross-fade, just like TransitionDrawable. We can have - * call-backs via animator object (see also {@link CrossFadeDrawable#getAnimator()}). - */ - private static class CrossFadeDrawable extends LayerDrawable { - private final ObjectAnimator mAnimator; - - public CrossFadeDrawable(Drawable[] layers) { - super(layers); - mAnimator = ObjectAnimator.ofInt(this, "crossFadeAlpha", 0xff, 0); - } - - private int mCrossFadeAlpha; - - /** - * This will be used from ObjectAnimator. - * Note: this method is protected by proguard.flags so that it won't be removed - * automatically. - */ - @SuppressWarnings("unused") - public void setCrossFadeAlpha(int alpha) { - mCrossFadeAlpha = alpha; - invalidateSelf(); - } - - public ObjectAnimator getAnimator() { - return mAnimator; - } - - @Override - public void draw(Canvas canvas) { - Drawable first = getDrawable(0); - Drawable second = getDrawable(1); - - if (mCrossFadeAlpha > 0) { - first.setAlpha(mCrossFadeAlpha); - first.draw(canvas); - first.setAlpha(255); - } - - if (mCrossFadeAlpha < 0xff) { - second.setAlpha(0xff - mCrossFadeAlpha); - second.draw(canvas); - second.setAlpha(0xff); - } - } - } - - private static CrossFadeDrawable newCrossFadeDrawable(Drawable first, Drawable second) { - Drawable[] layers = new Drawable[2]; - layers[0] = first; - layers[1] = second; - return new CrossFadeDrawable(layers); - } - - /** - * Starts cross-fade animation using TransitionDrawable. Nothing will happen if "from" and "to" - * are the same. - */ - public static void startCrossFade( - final ImageView imageView, final Drawable from, final Drawable to) { - // We skip the cross-fade when those two Drawables are equal, or they are BitmapDrawables - // pointing to the same Bitmap. - final boolean drawableIsEqual = (from != null && to != null && from.equals(to)); - final boolean hasFromImage = ((from instanceof BitmapDrawable) && - ((BitmapDrawable) from).getBitmap() != null); - final boolean hasToImage = ((to instanceof BitmapDrawable) && - ((BitmapDrawable) to).getBitmap() != null); - final boolean areSameImage = drawableIsEqual || (hasFromImage && hasToImage && - ((BitmapDrawable) from).getBitmap().equals(((BitmapDrawable) to).getBitmap())); - - if (!areSameImage) { - if (FADE_DBG) { - log("Start cross-fade animation for " + imageView - + "(" + Integer.toHexString(from.hashCode()) + " -> " - + Integer.toHexString(to.hashCode()) + ")"); - } - - CrossFadeDrawable crossFadeDrawable = newCrossFadeDrawable(from, to); - ObjectAnimator animator = crossFadeDrawable.getAnimator(); - imageView.setImageDrawable(crossFadeDrawable); - animator.setDuration(ANIMATION_DURATION); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (FADE_DBG) { - log("cross-fade animation start (" - + Integer.toHexString(from.hashCode()) + " -> " - + Integer.toHexString(to.hashCode()) + ")"); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (FADE_DBG) { - log("cross-fade animation ended (" - + Integer.toHexString(from.hashCode()) + " -> " - + Integer.toHexString(to.hashCode()) + ")"); - } - animation.removeAllListeners(); - // Workaround for issue 6300562; this will force the drawable to the - // resultant one regardless of animation glitch. - imageView.setImageDrawable(to); - } - }); - animator.start(); - - /* We could use TransitionDrawable here, but it may cause some weird animation in - * some corner cases. See issue 6300562 - * TODO: decide which to be used in the long run. TransitionDrawable is old but system - * one. Ours uses new animation framework and thus have callback (great for testing), - * while no framework support for the exact class. - - Drawable[] layers = new Drawable[2]; - layers[0] = from; - layers[1] = to; - TransitionDrawable transitionDrawable = new TransitionDrawable(layers); - imageView.setImageDrawable(transitionDrawable); - transitionDrawable.startTransition(ANIMATION_DURATION); */ - imageView.setTag(to); - } else if (!hasFromImage && hasToImage) { - imageView.setImageDrawable(to); - imageView.setTag(to); - } else { - if (FADE_DBG) { - log("*Not* start cross-fade. " + imageView); - } - } - } - - // Debugging / testing code - - private static void log(String msg) { - Log.d(LOG_TAG, msg); - } -} \ No newline at end of file diff --git a/InCallUI/src/com/android/incallui/InCallCameraManager.java b/InCallUI/src/com/android/incallui/InCallCameraManager.java deleted file mode 100644 index 53000f1ddd2d9ea8a6dc870ba9bfa30656bee1ed..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallCameraManager.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.util.Size; - -import java.lang.String; -import java.util.Collections; -import java.util.concurrent.ConcurrentHashMap; -import java.util.Set; - -/** - * Used to track which camera is used for outgoing video. - */ -public class InCallCameraManager { - - public interface Listener { - void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera); - } - - private final Set mCameraSelectionListeners = Collections. - newSetFromMap(new ConcurrentHashMap(8,0.9f,1)); - - /** - * The camera ID for the front facing camera. - */ - private String mFrontFacingCameraId; - - /** - * The camera ID for the rear facing camera. - */ - private String mRearFacingCameraId; - - /** - * The currently active camera. - */ - private boolean mUseFrontFacingCamera; - - /** - * Indicates whether the list of cameras has been initialized yet. Initialization is delayed - * until a video call is present. - */ - private boolean mIsInitialized = false; - - /** - * The context. - */ - private Context mContext; - - /** - * Initializes the InCall CameraManager. - * - * @param context The current context. - */ - public InCallCameraManager(Context context) { - mUseFrontFacingCamera = true; - mContext = context; - } - - /** - * Sets whether the front facing camera should be used or not. - * - * @param useFrontFacingCamera {@code True} if the front facing camera is to be used. - */ - public void setUseFrontFacingCamera(boolean useFrontFacingCamera) { - mUseFrontFacingCamera = useFrontFacingCamera; - for (Listener listener : mCameraSelectionListeners) { - listener.onActiveCameraSelectionChanged(mUseFrontFacingCamera); - } - } - - /** - * Determines whether the front facing camera is currently in use. - * - * @return {@code True} if the front facing camera is in use. - */ - public boolean isUsingFrontFacingCamera() { - return mUseFrontFacingCamera; - } - - /** - * Determines the active camera ID. - * - * @return The active camera ID. - */ - public String getActiveCameraId() { - maybeInitializeCameraList(mContext); - - if (mUseFrontFacingCamera) { - return mFrontFacingCameraId; - } else { - return mRearFacingCameraId; - } - } - - /** - * Get the list of cameras available for use. - * - * @param context The context. - */ - private void maybeInitializeCameraList(Context context) { - if (mIsInitialized || context == null) { - return; - } - - Log.v(this, "initializeCameraList"); - - CameraManager cameraManager = null; - try { - cameraManager = (CameraManager) context.getSystemService( - Context.CAMERA_SERVICE); - } catch (Exception e) { - Log.e(this, "Could not get camera service."); - return; - } - - if (cameraManager == null) { - return; - } - - String[] cameraIds = {}; - try { - cameraIds = cameraManager.getCameraIdList(); - } catch (CameraAccessException e) { - Log.d(this, "Could not access camera: "+e); - // Camera disabled by device policy. - return; - } - - for (int i = 0; i < cameraIds.length; i++) { - CameraCharacteristics c = null; - try { - c = cameraManager.getCameraCharacteristics(cameraIds[i]); - } catch (IllegalArgumentException e) { - // Device Id is unknown. - } catch (CameraAccessException e) { - // Camera disabled by device policy. - } - if (c != null) { - int facingCharacteristic = c.get(CameraCharacteristics.LENS_FACING); - if (facingCharacteristic == CameraCharacteristics.LENS_FACING_FRONT) { - mFrontFacingCameraId = cameraIds[i]; - } else if (facingCharacteristic == CameraCharacteristics.LENS_FACING_BACK) { - mRearFacingCameraId = cameraIds[i]; - } - } - } - - mIsInitialized = true; - Log.v(this, "initializeCameraList : done"); - } - - public void addCameraSelectionListener(Listener listener) { - if (listener != null) { - mCameraSelectionListeners.add(listener); - } - } - - public void removeCameraSelectionListener(Listener listener) { - if (listener != null) { - mCameraSelectionListeners.remove(listener); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallContactInteractions.java b/InCallUI/src/com/android/incallui/InCallContactInteractions.java deleted file mode 100644 index 88070fe379283f81821951ed64f31616ad8f5166..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallContactInteractions.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.annotations.VisibleForTesting; - -import android.content.Context; -import android.location.Address; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; -import android.widget.TextView; - -import com.android.dialer.R; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; - -/** - * Wrapper class for objects that are used in generating the context about the contact in the InCall - * screen. - * - * This handles generating the appropriate resource for the ListAdapter based on whether the contact - * is a business contact or not and logic for the manipulation of data for the call context. - */ -public class InCallContactInteractions { - private static final String TAG = InCallContactInteractions.class.getSimpleName(); - - private Context mContext; - private InCallContactInteractionsListAdapter mListAdapter; - private boolean mIsBusiness; - private View mBusinessHeaderView; - private LayoutInflater mInflater; - - public InCallContactInteractions(Context context, boolean isBusiness) { - mContext = context; - mInflater = (LayoutInflater) - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - switchContactType(isBusiness); - } - - public InCallContactInteractionsListAdapter getListAdapter() { - return mListAdapter; - } - - /** - * Switches the "isBusiness" value, if applicable. Recreates the list adapter with the resource - * corresponding to the new isBusiness value if the "isBusiness" value is switched. - * - * @param isBusiness Whether or not the contact is a business. - * - * @return {@code true} if a new list adapter was created, {@code} otherwise. - */ - public boolean switchContactType(boolean isBusiness) { - if (mIsBusiness != isBusiness || mListAdapter == null) { - mIsBusiness = isBusiness; - mListAdapter = new InCallContactInteractionsListAdapter(mContext, - mIsBusiness ? R.layout.business_context_info_list_item - : R.layout.person_context_info_list_item); - return true; - } - return false; - } - - public View getBusinessListHeaderView() { - if (mBusinessHeaderView == null) { - mBusinessHeaderView = mInflater.inflate( - R.layout.business_contact_context_list_header, null); - } - return mBusinessHeaderView; - } - - public void setBusinessInfo(Address address, float distance, - List> openingHours) { - mListAdapter.clear(); - List info = new ArrayList(); - - // Hours of operation - if (openingHours != null) { - BusinessContextInfo hoursInfo = constructHoursInfo(openingHours); - if (hoursInfo != null) { - info.add(hoursInfo); - } - } - - // Location information - if (address != null) { - BusinessContextInfo locationInfo = constructLocationInfo(address, distance); - info.add(locationInfo); - } - - mListAdapter.addAll(info); - } - - /** - * Construct a BusinessContextInfo object containing hours of operation information. - * The format is: - * [Open now/Closed now] - * [Hours] - * - * @param openingHours - * @return BusinessContextInfo object with the schedule icon, the heading set to whether the - * business is open or not and the details set to the hours of operation. - */ - private BusinessContextInfo constructHoursInfo(List> openingHours) { - try { - return constructHoursInfo(Calendar.getInstance(), openingHours); - } catch (Exception e) { - // Catch all exceptions here because we don't want any crashes if something goes wrong. - Log.e(TAG, "Error constructing hours info: ", e); - } - return null; - } - - /** - * Pass in arbitrary current calendar time. - */ - @VisibleForTesting - BusinessContextInfo constructHoursInfo(Calendar currentTime, - List> openingHours) { - if (currentTime == null || openingHours == null || openingHours.size() == 0) { - return null; - } - - BusinessContextInfo hoursInfo = new BusinessContextInfo(); - hoursInfo.iconId = R.drawable.ic_schedule_white_24dp; - - boolean isOpenNow = false; - // This variable records which interval the current time is after. 0 denotes none of the - // intervals, 1 after the first interval, etc. It is also the index of the interval the - // current time is in (if open) or the next interval (if closed). - int afterInterval = 0; - // This variable counts the number of time intervals in today's opening hours. - int todaysIntervalCount = 0; - - for (Pair hours : openingHours) { - if (hours.first.compareTo(currentTime) <= 0 - && currentTime.compareTo(hours.second) < 0) { - // If the current time is on or after the opening time and strictly before the - // closing time, then this business is open. - isOpenNow = true; - } - - if (currentTime.get(Calendar.DAY_OF_YEAR) == hours.first.get(Calendar.DAY_OF_YEAR)) { - todaysIntervalCount += 1; - } - - if (currentTime.compareTo(hours.second) > 0) { - // This assumes that the list of intervals is sorted by time. - afterInterval += 1; - } - } - - hoursInfo.heading = isOpenNow ? mContext.getString(R.string.open_now) - : mContext.getString(R.string.closed_now); - - /* - * The following logic determines what to display in various cases for hours of operation. - * - * - Display all intervals if open now and number of intervals is <=2. - * - Display next closing time if open now and number of intervals is >2. - * - Display next opening time if currently closed but opens later today. - * - Display last time it closed today if closed now and tomorrow's hours are unknown. - * - Display tomorrow's first open time if closed today and tomorrow's hours are known. - * - * NOTE: The logic below assumes that the intervals are sorted by ascending time. Possible - * TODO to modify the logic above and ensure this is true. - */ - if (isOpenNow) { - if (todaysIntervalCount == 1) { - hoursInfo.detail = getTimeSpanStringForHours(openingHours.get(0)); - } else if (todaysIntervalCount == 2) { - hoursInfo.detail = mContext.getString( - R.string.opening_hours, - getTimeSpanStringForHours(openingHours.get(0)), - getTimeSpanStringForHours(openingHours.get(1))); - } else if (afterInterval < openingHours.size()) { - // This check should not be necessary since if it is currently open, we should not - // be after the last interval, but just in case, we don't want to crash. - hoursInfo.detail = mContext.getString( - R.string.closes_today_at, - getFormattedTimeForCalendar(openingHours.get(afterInterval).second)); - } - } else { // Currently closed - final int lastIntervalToday = todaysIntervalCount - 1; - if (todaysIntervalCount == 0) { // closed today - hoursInfo.detail = mContext.getString( - R.string.opens_tomorrow_at, - getFormattedTimeForCalendar(openingHours.get(0).first)); - } else if (currentTime.after(openingHours.get(lastIntervalToday).second)) { - // Passed hours for today - if (todaysIntervalCount < openingHours.size()) { - // If all of today's intervals are exhausted, assume the next are tomorrow's. - hoursInfo.detail = mContext.getString( - R.string.opens_tomorrow_at, - getFormattedTimeForCalendar( - openingHours.get(todaysIntervalCount).first)); - } else { - // Grab the last time it was open today. - hoursInfo.detail = mContext.getString( - R.string.closed_today_at, - getFormattedTimeForCalendar( - openingHours.get(lastIntervalToday).second)); - } - } else if (afterInterval < openingHours.size()) { - // This check should not be necessary since if it is currently before the last - // interval, afterInterval should be less than the count of intervals, but just in - // case, we don't want to crash. - hoursInfo.detail = mContext.getString( - R.string.opens_today_at, - getFormattedTimeForCalendar(openingHours.get(afterInterval).first)); - } - } - - return hoursInfo; - } - - String getFormattedTimeForCalendar(Calendar calendar) { - return DateFormat.getTimeFormat(mContext).format(calendar.getTime()); - } - - String getTimeSpanStringForHours(Pair hours) { - return mContext.getString(R.string.open_time_span, - getFormattedTimeForCalendar(hours.first), - getFormattedTimeForCalendar(hours.second)); - } - - /** - * Construct a BusinessContextInfo object with the location information of the business. - * The format is: - * [Straight line distance in miles or kilometers] - * [Address without state/country/etc.] - * - * @param address An Address object containing address details of the business - * @param distance The distance to the location in meters - * @return A BusinessContextInfo object with the location icon, the heading as the distance to - * the business and the details containing the address. - */ - private BusinessContextInfo constructLocationInfo(Address address, float distance) { - return constructLocationInfo(Locale.getDefault(), address, distance); - } - - @VisibleForTesting - BusinessContextInfo constructLocationInfo(Locale locale, Address address, - float distance) { - if (address == null) { - return null; - } - - BusinessContextInfo locationInfo = new BusinessContextInfo(); - locationInfo.iconId = R.drawable.ic_location_on_white_24dp; - if (distance != DistanceHelper.DISTANCE_NOT_FOUND) { - //TODO: add a setting to allow the user to select "KM" or "MI" as their distance units. - if (Locale.US.equals(locale)) { - locationInfo.heading = mContext.getString(R.string.distance_imperial_away, - distance * DistanceHelper.MILES_PER_METER); - } else { - locationInfo.heading = mContext.getString(R.string.distance_metric_away, - distance * DistanceHelper.KILOMETERS_PER_METER); - } - } - if (address.getLocality() != null) { - locationInfo.detail = mContext.getString( - R.string.display_address, - address.getAddressLine(0), - address.getLocality()); - } else { - locationInfo.detail = address.getAddressLine(0); - } - return locationInfo; - } - - /** - * Get the appropriate title for the context. - * @return The "Business info" title for a business contact and the "Recent messages" title for - * personal contacts. - */ - public String getContactContextTitle() { - return mIsBusiness - ? mContext.getResources().getString(R.string.business_contact_context_title) - : mContext.getResources().getString(R.string.person_contact_context_title); - } - - public static abstract class ContactContextInfo { - public abstract void bindView(View listItem); - } - - public static class BusinessContextInfo extends ContactContextInfo { - int iconId; - String heading; - String detail; - - @Override - public void bindView(View listItem) { - ImageView imageView = (ImageView) listItem.findViewById(R.id.icon); - TextView headingTextView = (TextView) listItem.findViewById(R.id.heading); - TextView detailTextView = (TextView) listItem.findViewById(R.id.detail); - - if (this.iconId == 0 || (this.heading == null && this.detail == null)) { - return; - } - - imageView.setImageDrawable(listItem.getContext().getDrawable(this.iconId)); - - headingTextView.setText(this.heading); - headingTextView.setVisibility(TextUtils.isEmpty(this.heading) - ? View.GONE : View.VISIBLE); - - detailTextView.setText(this.detail); - detailTextView.setVisibility(TextUtils.isEmpty(this.detail) - ? View.GONE : View.VISIBLE); - - } - } - - public static class PersonContextInfo extends ContactContextInfo { - boolean isIncoming; - String message; - String detail; - - @Override - public void bindView(View listItem) { - TextView messageTextView = (TextView) listItem.findViewById(R.id.message); - TextView detailTextView = (TextView) listItem.findViewById(R.id.detail); - - if (this.message == null || this.detail == null) { - return; - } - - messageTextView.setBackgroundResource(this.isIncoming ? - R.drawable.incoming_sms_background : R.drawable.outgoing_sms_background); - messageTextView.setText(this.message); - LayoutParams messageLayoutParams = (LayoutParams) messageTextView.getLayoutParams(); - messageLayoutParams.addRule(this.isIncoming? - RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END); - messageTextView.setLayoutParams(messageLayoutParams); - - LayoutParams detailLayoutParams = (LayoutParams) detailTextView.getLayoutParams(); - detailLayoutParams.addRule(this.isIncoming ? - RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END); - detailTextView.setLayoutParams(detailLayoutParams); - detailTextView.setText(this.detail); - } - } - - /** - * A list adapter for call context information. We use the same adapter for both business and - * contact context. - */ - private class InCallContactInteractionsListAdapter extends ArrayAdapter { - // The resource id of the list item layout. - int mResId; - - public InCallContactInteractionsListAdapter(Context context, int resource) { - super(context, resource); - mResId = resource; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View listItem = mInflater.inflate(mResId, null); - ContactContextInfo item = getItem(position); - - if (item == null) { - return listItem; - } - - item.bindView(listItem); - return listItem; - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallDateUtils.java b/InCallUI/src/com/android/incallui/InCallDateUtils.java deleted file mode 100644 index 3401692ea99c3868a0e74712f8cadfe7ea0caf00..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallDateUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.android.incallui; - -import android.icu.text.MeasureFormat; -import android.icu.text.MeasureFormat.FormatWidth; -import android.icu.util.Measure; -import android.icu.util.MeasureUnit; - -import java.util.ArrayList; -import java.util.Locale; - -/** - * Methods to parse time and date information in the InCallUi - */ -public class InCallDateUtils { - - /** - * Return given duration in a human-friendly format. For example, "4 minutes 3 seconds" or - * "3 hours 1 second". Returns the hours, minutes and seconds in that order if they exist. - */ - public static String formatDuration(long millis) { - int hours = 0; - int minutes = 0; - int seconds = 0; - int elapsedSeconds = (int) (millis / 1000); - if (elapsedSeconds >= 3600) { - hours = elapsedSeconds / 3600; - elapsedSeconds -= hours * 3600; - } - if (elapsedSeconds >= 60) { - minutes = elapsedSeconds / 60; - elapsedSeconds -= minutes * 60; - } - seconds = elapsedSeconds; - - final ArrayList measures = new ArrayList(); - if (hours > 0) { - measures.add(new Measure(hours, MeasureUnit.HOUR)); - } - if (minutes > 0) { - measures.add(new Measure(minutes, MeasureUnit.MINUTE)); - } - if (seconds > 0) { - measures.add(new Measure(seconds, MeasureUnit.SECOND)); - } - - if (measures.isEmpty()) { - return ""; - } else { - return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE) - .formatMeasures(measures.toArray(new Measure[measures.size()])); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallOrientationEventListener.java b/InCallUI/src/com/android/incallui/InCallOrientationEventListener.java deleted file mode 100644 index 3cab6dc3bf947176e50ab0b2204a5dc9ac58b06f..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallOrientationEventListener.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.content.res.Configuration; -import android.view.OrientationEventListener; -import android.hardware.SensorManager; -import android.view.Surface; -import android.content.pm.ActivityInfo; - -/** - * This class listens to Orientation events and overrides onOrientationChanged which gets - * invoked when an orientation change occurs. When that happens, we notify InCallUI registrants - * of the change. - */ -public class InCallOrientationEventListener extends OrientationEventListener { - - /** - * Screen orientation angles one of 0, 90, 180, 270, 360 in degrees. - */ - public static int SCREEN_ORIENTATION_0 = 0; - public static int SCREEN_ORIENTATION_90 = 90; - public static int SCREEN_ORIENTATION_180 = 180; - public static int SCREEN_ORIENTATION_270 = 270; - public static int SCREEN_ORIENTATION_360 = 360; - - public static int FULL_SENSOR_SCREEN_ORIENTATION = - ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; - - public static int NO_SENSOR_SCREEN_ORIENTATION = - ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; - - /** - * This is to identify dead zones where we won't notify others of orientation changed. - * Say for e.g our threshold is x degrees. We will only notify UI when our current rotation is - * within x degrees right or left of the screen orientation angles. If it's not within those - * ranges, we return SCREEN_ORIENTATION_UNKNOWN and ignore it. - */ - private static int SCREEN_ORIENTATION_UNKNOWN = -1; - - // Rotation threshold is 10 degrees. So if the rotation angle is within 10 degrees of any of - // the above angles, we will notify orientation changed. - private static int ROTATION_THRESHOLD = 10; - - - /** - * Cache the current rotation of the device. - */ - private static int sCurrentOrientation = SCREEN_ORIENTATION_0; - private boolean mEnabled = false; - - public InCallOrientationEventListener(Context context) { - super(context); - } - - /** - * Handles changes in device orientation. Notifies InCallPresenter of orientation changes. - * - * Note that this API receives sensor rotation in degrees as a param and we convert that to - * one of our screen orientation constants - (one of: {@link SCREEN_ORIENTATION_0}, - * {@link SCREEN_ORIENTATION_90}, {@link SCREEN_ORIENTATION_180}, - * {@link SCREEN_ORIENTATION_270}). - * - * @param rotation The new device sensor rotation in degrees - */ - @Override - public void onOrientationChanged(int rotation) { - if (rotation == OrientationEventListener.ORIENTATION_UNKNOWN) { - return; - } - - final int orientation = toScreenOrientation(rotation); - - if (orientation != SCREEN_ORIENTATION_UNKNOWN && sCurrentOrientation != orientation) { - sCurrentOrientation = orientation; - InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation); - } - } - - /** - * Enables the OrientationEventListener and notifies listeners of current orientation if - * notify flag is true - * @param notify true or false. Notify device orientation changed if true. - */ - public void enable(boolean notify) { - if (mEnabled) { - Log.v(this, "enable: Orientation listener is already enabled. Ignoring..."); - return; - } - - super.enable(); - mEnabled = true; - if (notify) { - InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation); - } - } - - /** - * Enables the OrientationEventListener with notify flag defaulting to false. - */ - public void enable() { - enable(false); - } - - /** - * Disables the OrientationEventListener. - */ - public void disable() { - if (!mEnabled) { - Log.v(this, "enable: Orientation listener is already disabled. Ignoring..."); - return; - } - - mEnabled = false; - super.disable(); - } - - /** - * Returns true the OrientationEventListener is enabled, false otherwise. - */ - public boolean isEnabled() { - return mEnabled; - } - - /** - * Converts sensor rotation in degrees to screen orientation constants. - * @param rotation sensor rotation angle in degrees - * @return Screen orientation angle in degrees (0, 90, 180, 270). Returns -1 for degrees not - * within threshold to identify zones where orientation change should not be trigerred. - */ - private int toScreenOrientation(int rotation) { - // Sensor orientation 90 is equivalent to screen orientation 270 and vice versa. This - // function returns the screen orientation. So we convert sensor rotation 90 to 270 and - // vice versa here. - if (isInLeftRange(rotation, SCREEN_ORIENTATION_360, ROTATION_THRESHOLD) || - isInRightRange(rotation, SCREEN_ORIENTATION_0, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_0; - } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_90, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_270; - } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_180, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_180; - } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_270, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_90; - } - return SCREEN_ORIENTATION_UNKNOWN; - } - - private static boolean isWithinRange(int value, int begin, int end) { - return value >= begin && value < end; - } - - private static boolean isWithinThreshold(int value, int center, int threshold) { - return isWithinRange(value, center - threshold, center + threshold); - } - - private static boolean isInLeftRange(int value, int center, int threshold) { - return isWithinRange(value, center - threshold, center); - } - - private static boolean isInRightRange(int value, int center, int threshold) { - return isWithinRange(value, center, center + threshold); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java deleted file mode 100644 index 0103f61ed00ef201b609bc840fead6cc74d75652..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallPresenter.java +++ /dev/null @@ -1,1938 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.app.ActivityManager.TaskDescription; -import android.app.FragmentManager; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.graphics.Point; -import android.os.Bundle; -import android.os.Handler; -import android.os.SystemClock; -import android.provider.CallLog; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; -import android.telecom.VideoProfile; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import com.android.contacts.common.GeoUtil; -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.compat.telecom.TelecomManagerCompat; -import com.android.contacts.common.interactions.TouchPointManager; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; -import com.android.dialer.calllog.CallLogAsyncTaskUtil; -import com.android.dialer.calllog.CallLogAsyncTaskUtil.OnCallLogQueryFinishedListener; -import com.android.dialer.database.FilteredNumberAsyncQueryHandler; -import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener; -import com.android.dialer.filterednumber.FilteredNumbersUtil; -import com.android.dialer.logging.InteractionEvent; -import com.android.dialer.logging.Logger; -import com.android.dialer.util.TelecomUtil; -import com.android.incallui.spam.SpamCallListListener; -import com.android.incallui.util.TelecomCallUtil; -import com.android.incalluibind.ObjectFactory; - -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Takes updates from the CallList and notifies the InCallActivity (UI) - * of the changes. - * Responsible for starting the activity for a new call and finishing the activity when all calls - * are disconnected. - * Creates and manages the in-call state and provides a listener pattern for the presenters - * that want to listen in on the in-call state changes. - * TODO: This class has become more of a state machine at this point. Consider renaming. - */ -public class InCallPresenter implements CallList.Listener, - CircularRevealFragment.OnCircularRevealCompleteListener, - InCallVideoCallCallbackNotifier.SessionModificationListener { - - private static final String EXTRA_FIRST_TIME_SHOWN = - "com.android.incallui.intent.extra.FIRST_TIME_SHOWN"; - - private static final long BLOCK_QUERY_TIMEOUT_MS = 1000; - - private static final Bundle EMPTY_EXTRAS = new Bundle(); - - private static InCallPresenter sInCallPresenter; - - /** - * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is - * load factor before resizing, 1 means we only expect a single thread to - * access the map so make only a single shard - */ - private final Set mListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final List mIncomingCallListeners = new CopyOnWriteArrayList<>(); - private final Set mDetailsListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mCanAddCallListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mInCallUiListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mOrientationListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mInCallEventListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - - private AudioModeProvider mAudioModeProvider; - private StatusBarNotifier mStatusBarNotifier; - private ExternalCallNotifier mExternalCallNotifier; - private ContactInfoCache mContactInfoCache; - private Context mContext; - private CallList mCallList; - private ExternalCallList mExternalCallList; - private InCallActivity mInCallActivity; - private InCallState mInCallState = InCallState.NO_CALLS; - private ProximitySensor mProximitySensor; - private boolean mServiceConnected = false; - private boolean mAccountSelectionCancelled = false; - private InCallCameraManager mInCallCameraManager = null; - private AnswerPresenter mAnswerPresenter = new AnswerPresenter(); - private FilteredNumberAsyncQueryHandler mFilteredQueryHandler; - private CallList.Listener mSpamCallListListener; - - /** - * Whether or not we are currently bound and waiting for Telecom to send us a new call. - */ - private boolean mBoundAndWaitingForOutgoingCall; - - /** - * If there is no actual call currently in the call list, this will be used as a fallback - * to determine the theme color for InCallUI. - */ - private PhoneAccountHandle mPendingPhoneAccountHandle; - - /** - * Determines if the InCall UI is in fullscreen mode or not. - */ - private boolean mIsFullScreen = false; - - private final android.telecom.Call.Callback mCallCallback = new android.telecom.Call.Callback() { - @Override - public void onPostDialWait(android.telecom.Call telecomCall, - String remainingPostDialSequence) { - final Call call = mCallList.getCallByTelecomCall(telecomCall); - if (call == null) { - Log.w(this, "Call not found in call list: " + telecomCall); - return; - } - onPostDialCharWait(call.getId(), remainingPostDialSequence); - } - - @Override - public void onDetailsChanged(android.telecom.Call telecomCall, - android.telecom.Call.Details details) { - final Call call = mCallList.getCallByTelecomCall(telecomCall); - if (call == null) { - Log.w(this, "Call not found in call list: " + telecomCall); - return; - } - for (InCallDetailsListener listener : mDetailsListeners) { - listener.onDetailsChanged(call, details); - } - } - - @Override - public void onConferenceableCallsChanged(android.telecom.Call telecomCall, - List conferenceableCalls) { - Log.i(this, "onConferenceableCallsChanged: " + telecomCall); - onDetailsChanged(telecomCall, telecomCall.getDetails()); - } - }; - - private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { - public void onCallStateChanged(int state, String incomingNumber) { - if (state == TelephonyManager.CALL_STATE_RINGING) { - if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) { - return; - } - // Check if the number is blocked, to silence the ringer. - String countryIso = GeoUtil.getCurrentCountryIso(mContext); - mFilteredQueryHandler.isBlockedNumber( - mOnCheckBlockedListener, incomingNumber, countryIso); - } - } - }; - - private final OnCheckBlockedListener mOnCheckBlockedListener = new OnCheckBlockedListener() { - @Override - public void onCheckComplete(final Integer id) { - if (id != null) { - // Silence the ringer now to prevent ringing and vibration before the call is - // terminated when Telecom attempts to add it. - TelecomUtil.silenceRinger(mContext); - } - } - }; - - /** - * Observes the CallLog to delete the call log entry for the blocked call after it is added. - * Times out if too much time has passed. - */ - private class BlockedNumberContentObserver extends ContentObserver { - private static final int TIMEOUT_MS = 5000; - - private Handler mHandler; - private String mNumber; - private long mTimeAddedMs; - - private Runnable mTimeoutRunnable = new Runnable() { - @Override - public void run() { - unregister(); - } - }; - - public BlockedNumberContentObserver(Handler handler, String number, long timeAddedMs) { - super(handler); - - mHandler = handler; - mNumber = number; - mTimeAddedMs = timeAddedMs; - } - - @Override - public void onChange(boolean selfChange) { - CallLogAsyncTaskUtil.deleteBlockedCall(mContext, mNumber, mTimeAddedMs, - new OnCallLogQueryFinishedListener() { - @Override - public void onQueryFinished(boolean hasEntry) { - if (mContext != null && hasEntry) { - unregister(); - } - } - }); - } - - public void register() { - if (mContext != null) { - mContext.getContentResolver().registerContentObserver( - CallLog.CONTENT_URI, true, this); - mHandler.postDelayed(mTimeoutRunnable, TIMEOUT_MS); - } - } - - private void unregister() { - if (mContext != null) { - mHandler.removeCallbacks(mTimeoutRunnable); - mContext.getContentResolver().unregisterContentObserver(this); - } - } - }; - - /** - * Is true when the activity has been previously started. Some code needs to know not just if - * the activity is currently up, but if it had been previously shown in foreground for this - * in-call session (e.g., StatusBarNotifier). This gets reset when the session ends in the - * tear-down method. - */ - private boolean mIsActivityPreviouslyStarted = false; - - /** - * Whether or not InCallService is bound to Telecom. - */ - private boolean mServiceBound = false; - - /** - * When configuration changes Android kills the current activity and starts a new one. - * The flag is used to check if full clean up is necessary (activity is stopped and new - * activity won't be started), or if a new activity will be started right after the current one - * is destroyed, and therefore no need in release all resources. - */ - private boolean mIsChangingConfigurations = false; - - /** Display colors for the UI. Consists of a primary color and secondary (darker) color */ - private MaterialPalette mThemeColors; - - private TelecomManager mTelecomManager; - private TelephonyManager mTelephonyManager; - - public static synchronized InCallPresenter getInstance() { - if (sInCallPresenter == null) { - sInCallPresenter = new InCallPresenter(); - } - return sInCallPresenter; - } - - @NeededForTesting - static synchronized void setInstance(InCallPresenter inCallPresenter) { - sInCallPresenter = inCallPresenter; - } - - public InCallState getInCallState() { - return mInCallState; - } - - public CallList getCallList() { - return mCallList; - } - - public void setUp(Context context, - CallList callList, - ExternalCallList externalCallList, - AudioModeProvider audioModeProvider, - StatusBarNotifier statusBarNotifier, - ExternalCallNotifier externalCallNotifier, - ContactInfoCache contactInfoCache, - ProximitySensor proximitySensor) { - if (mServiceConnected) { - Log.i(this, "New service connection replacing existing one."); - // retain the current resources, no need to create new ones. - Preconditions.checkState(context == mContext); - Preconditions.checkState(callList == mCallList); - Preconditions.checkState(audioModeProvider == mAudioModeProvider); - return; - } - - Preconditions.checkNotNull(context); - mContext = context; - - mContactInfoCache = contactInfoCache; - - mStatusBarNotifier = statusBarNotifier; - mExternalCallNotifier = externalCallNotifier; - addListener(mStatusBarNotifier); - - mAudioModeProvider = audioModeProvider; - - mProximitySensor = proximitySensor; - addListener(mProximitySensor); - - addIncomingCallListener(mAnswerPresenter); - addInCallUiListener(mAnswerPresenter); - - mCallList = callList; - mExternalCallList = externalCallList; - externalCallList.addExternalCallListener(mExternalCallNotifier); - - // This only gets called by the service so this is okay. - mServiceConnected = true; - - // The final thing we do in this set up is add ourselves as a listener to CallList. This - // will kick off an update and the whole process can start. - mCallList.addListener(this); - - // Create spam call list listener and add it to the list of listeners - mSpamCallListListener = new SpamCallListListener(context); - mCallList.addListener(mSpamCallListListener); - - VideoPauseController.getInstance().setUp(this); - InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this); - - mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context.getContentResolver()); - mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - mCallList.setExtendedCallInfoService( - com.android.dialerbind.ObjectFactory.newExtendedCallInfoService(context)); - - Log.d(this, "Finished InCallPresenter.setUp"); - } - - /** - * Called when the telephony service has disconnected from us. This will happen when there are - * no more active calls. However, we may still want to continue showing the UI for - * certain cases like showing "Call Ended". - * What we really want is to wait for the activity and the service to both disconnect before we - * tear things down. This method sets a serviceConnected boolean and calls a secondary method - * that performs the aforementioned logic. - */ - public void tearDown() { - Log.d(this, "tearDown"); - mCallList.clearOnDisconnect(); - - mServiceConnected = false; - attemptCleanup(); - - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); - VideoPauseController.getInstance().tearDown(); - InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this); - } - - private void attemptFinishActivity() { - final boolean doFinish = (mInCallActivity != null && isActivityStarted()); - Log.i(this, "Hide in call UI: " + doFinish); - if (doFinish) { - mInCallActivity.setExcludeFromRecents(true); - mInCallActivity.finish(); - - if (mAccountSelectionCancelled) { - // This finish is a result of account selection cancellation - // do not include activity ending transition - mInCallActivity.overridePendingTransition(0, 0); - } - } - } - - /** - * Called when the UI begins, and starts the callstate callbacks if necessary. - */ - public void setActivity(InCallActivity inCallActivity) { - if (inCallActivity == null) { - throw new IllegalArgumentException("registerActivity cannot be called with null"); - } - if (mInCallActivity != null && mInCallActivity != inCallActivity) { - Log.w(this, "Setting a second activity before destroying the first."); - } - updateActivity(inCallActivity); - } - - /** - * Called when the UI ends. Attempts to tear down everything if necessary. See - * {@link #tearDown()} for more insight on the tear-down process. - */ - public void unsetActivity(InCallActivity inCallActivity) { - if (inCallActivity == null) { - throw new IllegalArgumentException("unregisterActivity cannot be called with null"); - } - if (mInCallActivity == null) { - Log.i(this, "No InCallActivity currently set, no need to unset."); - return; - } - if (mInCallActivity != inCallActivity) { - Log.w(this, "Second instance of InCallActivity is trying to unregister when another" - + " instance is active. Ignoring."); - return; - } - updateActivity(null); - } - - /** - * Updates the current instance of {@link InCallActivity} with the provided one. If a - * {@code null} activity is provided, it means that the activity was finished and we should - * attempt to cleanup. - */ - private void updateActivity(InCallActivity inCallActivity) { - boolean updateListeners = false; - boolean doAttemptCleanup = false; - - if (inCallActivity != null) { - if (mInCallActivity == null) { - updateListeners = true; - Log.i(this, "UI Initialized"); - } else { - // since setActivity is called onStart(), it can be called multiple times. - // This is fine and ignorable, but we do not want to update the world every time - // this happens (like going to/from background) so we do not set updateListeners. - } - - mInCallActivity = inCallActivity; - mInCallActivity.setExcludeFromRecents(false); - - // By the time the UI finally comes up, the call may already be disconnected. - // If that's the case, we may need to show an error dialog. - if (mCallList != null && mCallList.getDisconnectedCall() != null) { - maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall()); - } - - // When the UI comes up, we need to first check the in-call state. - // If we are showing NO_CALLS, that means that a call probably connected and - // then immediately disconnected before the UI was able to come up. - // If we dont have any calls, start tearing down the UI instead. - // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after - // it has been set. - if (mInCallState == InCallState.NO_CALLS) { - Log.i(this, "UI Initialized, but no calls left. shut down."); - attemptFinishActivity(); - return; - } - } else { - Log.i(this, "UI Destroyed"); - updateListeners = true; - mInCallActivity = null; - - // We attempt cleanup for the destroy case but only after we recalculate the state - // to see if we need to come back up or stay shut down. This is why we do the - // cleanup after the call to onCallListChange() instead of directly here. - doAttemptCleanup = true; - } - - // Messages can come from the telephony layer while the activity is coming up - // and while the activity is going down. So in both cases we need to recalculate what - // state we should be in after they complete. - // Examples: (1) A new incoming call could come in and then get disconnected before - // the activity is created. - // (2) All calls could disconnect and then get a new incoming call before the - // activity is destroyed. - // - // b/1122139 - We previously had a check for mServiceConnected here as well, but there are - // cases where we need to recalculate the current state even if the service in not - // connected. In particular the case where startOrFinish() is called while the app is - // already finish()ing. In that case, we skip updating the state with the knowledge that - // we will check again once the activity has finished. That means we have to recalculate the - // state here even if the service is disconnected since we may not have finished a state - // transition while finish()ing. - if (updateListeners) { - onCallListChange(mCallList); - } - - if (doAttemptCleanup) { - attemptCleanup(); - } - } - - private boolean mAwaitingCallListUpdate = false; - - public void onBringToForeground(boolean showDialpad) { - Log.i(this, "Bringing UI to foreground."); - bringToForeground(showDialpad); - } - - public void onCallAdded(final android.telecom.Call call) { - LatencyReport latencyReport = new LatencyReport(call); - if (shouldAttemptBlocking(call)) { - maybeBlockCall(call, latencyReport); - } else { - latencyReport.onCallBlockingDone(); - if (call.getDetails() - .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { - mExternalCallList.onCallAdded(call); - } else { - mCallList.onCallAdded(call, latencyReport); - } - } - - // Since a call has been added we are no longer waiting for Telecom to send us a call. - setBoundAndWaitingForOutgoingCall(false, null); - call.registerCallback(mCallCallback); - } - - private boolean shouldAttemptBlocking(android.telecom.Call call) { - if (call.getState() != android.telecom.Call.STATE_RINGING) { - return false; - } - if (TelecomCallUtil.isEmergencyCall(call)) { - Log.i(this, "Not attempting to block incoming emergency call"); - return false; - } - if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) { - Log.i(this, "Not attempting to block incoming call due to recent emergency call"); - return false; - } - if (call.getDetails().hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { - return false; - } - - return true; - } - - /** - * Checks whether a call should be blocked, and blocks it if so. Otherwise, it adds the call - * to the CallList so it can proceed as normal. There is a timeout, so if the function for - * checking whether a function is blocked does not return in a reasonable time, we proceed - * with adding the call anyways. - */ - private void maybeBlockCall(final android.telecom.Call call, - final LatencyReport latencyReport) { - final String countryIso = GeoUtil.getCurrentCountryIso(mContext); - final String number = TelecomCallUtil.getNumber(call); - final long timeAdded = System.currentTimeMillis(); - - // Though AtomicBoolean's can be scary, don't fear, as in this case it is only used on the - // main UI thread. It is needed so we can change its value within different scopes, since - // that cannot be done with a final boolean. - final AtomicBoolean hasTimedOut = new AtomicBoolean(false); - - final Handler handler = new Handler(); - - // Proceed if the query is slow; the call may still be blocked after the query returns. - final Runnable runnable = new Runnable() { - public void run() { - hasTimedOut.set(true); - latencyReport.onCallBlockingDone(); - mCallList.onCallAdded(call, latencyReport); - } - }; - handler.postDelayed(runnable, BLOCK_QUERY_TIMEOUT_MS); - - OnCheckBlockedListener onCheckBlockedListener = new OnCheckBlockedListener() { - @Override - public void onCheckComplete(final Integer id) { - if (!hasTimedOut.get()) { - handler.removeCallbacks(runnable); - } - if (id == null) { - if (!hasTimedOut.get()) { - latencyReport.onCallBlockingDone(); - mCallList.onCallAdded(call, latencyReport); - } - } else { - Log.i(this, "Rejecting incoming call from blocked number"); - call.reject(false, null); - Logger.logInteraction(InteractionEvent.CALL_BLOCKED); - - mFilteredQueryHandler.incrementFilteredCount(id); - - // Register observer to update the call log. - // BlockedNumberContentObserver will unregister after successful log or timeout. - BlockedNumberContentObserver contentObserver = - new BlockedNumberContentObserver(new Handler(), number, timeAdded); - contentObserver.register(); - } - } - }; - - final boolean success = mFilteredQueryHandler.isBlockedNumber( - onCheckBlockedListener, number, countryIso); - if (!success) { - Log.d(this, "checkForBlockedCall: invalid number, skipping block checking"); - if (!hasTimedOut.get()) { - handler.removeCallbacks(runnable); - - latencyReport.onCallBlockingDone(); - mCallList.onCallAdded(call, latencyReport); - } - } - } - - public void onCallRemoved(android.telecom.Call call) { - if (call.getDetails() - .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { - mExternalCallList.onCallRemoved(call); - } else { - mCallList.onCallRemoved(call); - call.unregisterCallback(mCallCallback); - } - } - - public void onCanAddCallChanged(boolean canAddCall) { - for (CanAddCallListener listener : mCanAddCallListeners) { - listener.onCanAddCallChanged(canAddCall); - } - } - - /** - * Called when there is a change to the call list. - * Sets the In-Call state for the entire in-call app based on the information it gets from - * CallList. Dispatches the in-call state to all listeners. Can trigger the creation or - * destruction of the UI based on the states that is calculates. - */ - @Override - public void onCallListChange(CallList callList) { - if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null && - mInCallActivity.getCallCardFragment().isAnimating()) { - mAwaitingCallListUpdate = true; - return; - } - if (callList == null) { - return; - } - - mAwaitingCallListUpdate = false; - - InCallState newState = getPotentialStateFromCallList(callList); - InCallState oldState = mInCallState; - Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState); - newState = startOrFinishUi(newState); - Log.d(this, "onCallListChange newState changed to " + newState); - - // Set the new state before announcing it to the world - Log.i(this, "Phone switching state: " + oldState + " -> " + newState); - mInCallState = newState; - - // notify listeners of new state - for (InCallStateListener listener : mListeners) { - Log.d(this, "Notify " + listener + " of state " + mInCallState.toString()); - listener.onStateChange(oldState, mInCallState, callList); - } - - if (isActivityStarted()) { - final boolean hasCall = callList.getActiveOrBackgroundCall() != null || - callList.getOutgoingCall() != null; - mInCallActivity.dismissKeyguard(hasCall); - } - } - - /** - * Called when there is a new incoming call. - * - * @param call - */ - @Override - public void onIncomingCall(Call call) { - InCallState newState = startOrFinishUi(InCallState.INCOMING); - InCallState oldState = mInCallState; - - Log.i(this, "Phone switching state: " + oldState + " -> " + newState); - mInCallState = newState; - - for (IncomingCallListener listener : mIncomingCallListeners) { - listener.onIncomingCall(oldState, mInCallState, call); - } - } - - @Override - public void onUpgradeToVideo(Call call) { - //NO-OP - } - /** - * Called when a call becomes disconnected. Called everytime an existing call - * changes from being connected (incoming/outgoing/active) to disconnected. - */ - @Override - public void onDisconnect(Call call) { - maybeShowErrorDialogOnDisconnect(call); - - // We need to do the run the same code as onCallListChange. - onCallListChange(mCallList); - - if (isActivityStarted()) { - mInCallActivity.dismissKeyguard(false); - } - - if (call.isEmergencyCall()) { - FilteredNumbersUtil.recordLastEmergencyCallTime(mContext); - } - } - - @Override - public void onUpgradeToVideoRequest(Call call, int videoState) { - Log.d(this, "onUpgradeToVideoRequest call = " + call + " video state = " + videoState); - - if (call == null) { - return; - } - - call.setRequestedVideoState(videoState); - } - - /** - * Given the call list, return the state in which the in-call screen should be. - */ - public InCallState getPotentialStateFromCallList(CallList callList) { - - InCallState newState = InCallState.NO_CALLS; - - if (callList == null) { - return newState; - } - if (callList.getIncomingCall() != null) { - newState = InCallState.INCOMING; - } else if (callList.getWaitingForAccountCall() != null) { - newState = InCallState.WAITING_FOR_ACCOUNT; - } else if (callList.getPendingOutgoingCall() != null) { - newState = InCallState.PENDING_OUTGOING; - } else if (callList.getOutgoingCall() != null) { - newState = InCallState.OUTGOING; - } else if (callList.getActiveCall() != null || - callList.getBackgroundCall() != null || - callList.getDisconnectedCall() != null || - callList.getDisconnectingCall() != null) { - newState = InCallState.INCALL; - } - - if (newState == InCallState.NO_CALLS) { - if (mBoundAndWaitingForOutgoingCall) { - return InCallState.OUTGOING; - } - } - - return newState; - } - - public boolean isBoundAndWaitingForOutgoingCall() { - return mBoundAndWaitingForOutgoingCall; - } - - public void setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle) { - // NOTE: It is possible for there to be a race and have handle become null before - // the circular reveal starts. This should not cause any problems because CallCardFragment - // should fallback to the actual call in the CallList at that point in time to determine - // the theme color. - Log.i(this, "setBoundAndWaitingForOutgoingCall: " + isBound); - mBoundAndWaitingForOutgoingCall = isBound; - mPendingPhoneAccountHandle = handle; - if (isBound && mInCallState == InCallState.NO_CALLS) { - mInCallState = InCallState.OUTGOING; - } - } - - @Override - public void onCircularRevealComplete(FragmentManager fm) { - if (mInCallActivity != null) { - mInCallActivity.showCallCardFragment(true); - mInCallActivity.getCallCardFragment().animateForNewOutgoingCall(); - CircularRevealFragment.endCircularReveal(mInCallActivity.getFragmentManager()); - } - } - - public void onShrinkAnimationComplete() { - if (mAwaitingCallListUpdate) { - onCallListChange(mCallList); - } - } - - public void addIncomingCallListener(IncomingCallListener listener) { - Preconditions.checkNotNull(listener); - mIncomingCallListeners.add(listener); - } - - public void removeIncomingCallListener(IncomingCallListener listener) { - if (listener != null) { - mIncomingCallListeners.remove(listener); - } - } - - public void addListener(InCallStateListener listener) { - Preconditions.checkNotNull(listener); - mListeners.add(listener); - } - - public void removeListener(InCallStateListener listener) { - if (listener != null) { - mListeners.remove(listener); - } - } - - public void addDetailsListener(InCallDetailsListener listener) { - Preconditions.checkNotNull(listener); - mDetailsListeners.add(listener); - } - - public void removeDetailsListener(InCallDetailsListener listener) { - if (listener != null) { - mDetailsListeners.remove(listener); - } - } - - public void addCanAddCallListener(CanAddCallListener listener) { - Preconditions.checkNotNull(listener); - mCanAddCallListeners.add(listener); - } - - public void removeCanAddCallListener(CanAddCallListener listener) { - if (listener != null) { - mCanAddCallListeners.remove(listener); - } - } - - public void addOrientationListener(InCallOrientationListener listener) { - Preconditions.checkNotNull(listener); - mOrientationListeners.add(listener); - } - - public void removeOrientationListener(InCallOrientationListener listener) { - if (listener != null) { - mOrientationListeners.remove(listener); - } - } - - public void addInCallEventListener(InCallEventListener listener) { - Preconditions.checkNotNull(listener); - mInCallEventListeners.add(listener); - } - - public void removeInCallEventListener(InCallEventListener listener) { - if (listener != null) { - mInCallEventListeners.remove(listener); - } - } - - public ProximitySensor getProximitySensor() { - return mProximitySensor; - } - - public void handleAccountSelection(PhoneAccountHandle accountHandle, boolean setDefault) { - if (mCallList != null) { - Call call = mCallList.getWaitingForAccountCall(); - if (call != null) { - String callId = call.getId(); - TelecomAdapter.getInstance().phoneAccountSelected(callId, accountHandle, setDefault); - } - } - } - - public void cancelAccountSelection() { - mAccountSelectionCancelled = true; - if (mCallList != null) { - Call call = mCallList.getWaitingForAccountCall(); - if (call != null) { - String callId = call.getId(); - TelecomAdapter.getInstance().disconnectCall(callId); - } - } - } - - /** - * Hangs up any active or outgoing calls. - */ - public void hangUpOngoingCall(Context context) { - // By the time we receive this intent, we could be shut down and call list - // could be null. Bail in those cases. - if (mCallList == null) { - if (mStatusBarNotifier == null) { - // The In Call UI has crashed but the notification still stayed up. We should not - // come to this stage. - StatusBarNotifier.clearAllCallNotifications(context); - } - return; - } - - Call call = mCallList.getOutgoingCall(); - if (call == null) { - call = mCallList.getActiveOrBackgroundCall(); - } - - if (call != null) { - TelecomAdapter.getInstance().disconnectCall(call.getId()); - call.setState(Call.State.DISCONNECTING); - mCallList.onUpdate(call); - } - } - - /** - * Answers any incoming call. - */ - public void answerIncomingCall(Context context, int videoState) { - // By the time we receive this intent, we could be shut down and call list - // could be null. Bail in those cases. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - return; - } - - Call call = mCallList.getIncomingCall(); - if (call != null) { - TelecomAdapter.getInstance().answerCall(call.getId(), videoState); - showInCall(false, false/* newOutgoingCall */); - } - } - - /** - * Declines any incoming call. - */ - public void declineIncomingCall(Context context) { - // By the time we receive this intent, we could be shut down and call list - // could be null. Bail in those cases. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - return; - } - - Call call = mCallList.getIncomingCall(); - if (call != null) { - TelecomAdapter.getInstance().rejectCall(call.getId(), false, null); - } - } - - public void acceptUpgradeRequest(int videoState, Context context) { - Log.d(this, " acceptUpgradeRequest videoState " + videoState); - // Bail if we have been shut down and the call list is null. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - Log.e(this, " acceptUpgradeRequest mCallList is empty so returning"); - return; - } - - Call call = mCallList.getVideoUpgradeRequestCall(); - if (call != null) { - VideoProfile videoProfile = new VideoProfile(videoState); - call.getVideoCall().sendSessionModifyResponse(videoProfile); - call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - } - - public void declineUpgradeRequest(Context context) { - Log.d(this, " declineUpgradeRequest"); - // Bail if we have been shut down and the call list is null. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - Log.e(this, " declineUpgradeRequest mCallList is empty so returning"); - return; - } - - Call call = mCallList.getVideoUpgradeRequestCall(); - if (call != null) { - VideoProfile videoProfile = - new VideoProfile(call.getVideoState()); - call.getVideoCall().sendSessionModifyResponse(videoProfile); - call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - } - - /*package*/ - void declineUpgradeRequest() { - // Pass mContext if InCallActivity is destroyed. - // Ex: When user pressed back key while in active call and - // then modify request is received followed by MT call. - declineUpgradeRequest(mInCallActivity != null ? mInCallActivity : mContext); - } - - /** - * Returns true if the incall app is the foreground application. - */ - public boolean isShowingInCallUi() { - return (isActivityStarted() && mInCallActivity.isVisible()); - } - - /** - * Returns true if the activity has been created and is running. - * Returns true as long as activity is not destroyed or finishing. This ensures that we return - * true even if the activity is paused (not in foreground). - */ - public boolean isActivityStarted() { - return (mInCallActivity != null && - !mInCallActivity.isDestroyed() && - !mInCallActivity.isFinishing()); - } - - public boolean isActivityPreviouslyStarted() { - return mIsActivityPreviouslyStarted; - } - - /** - * Determines if the In-Call app is currently changing configuration. - * - * @return {@code true} if the In-Call app is changing configuration. - */ - public boolean isChangingConfigurations() { - return mIsChangingConfigurations; - } - - /** - * Tracks whether the In-Call app is currently in the process of changing configuration (i.e. - * screen orientation). - */ - /*package*/ - void updateIsChangingConfigurations() { - mIsChangingConfigurations = false; - if (mInCallActivity != null) { - mIsChangingConfigurations = mInCallActivity.isChangingConfigurations(); - } - Log.v(this, "updateIsChangingConfigurations = " + mIsChangingConfigurations); - } - - - /** - * Called when the activity goes in/out of the foreground. - */ - public void onUiShowing(boolean showing) { - // We need to update the notification bar when we leave the UI because that - // could trigger it to show again. - if (mStatusBarNotifier != null) { - mStatusBarNotifier.updateNotification(mInCallState, mCallList); - } - - if (mProximitySensor != null) { - mProximitySensor.onInCallShowing(showing); - } - - Intent broadcastIntent = ObjectFactory.getUiReadyBroadcastIntent(mContext); - if (broadcastIntent != null) { - broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted); - - if (showing) { - Log.d(this, "Sending sticky broadcast: ", broadcastIntent); - mContext.sendStickyBroadcast(broadcastIntent); - } else { - Log.d(this, "Removing sticky broadcast: ", broadcastIntent); - mContext.removeStickyBroadcast(broadcastIntent); - } - } - - if (showing) { - mIsActivityPreviouslyStarted = true; - } else { - updateIsChangingConfigurations(); - } - - for (InCallUiListener listener : mInCallUiListeners) { - listener.onUiShowing(showing); - } - } - - public void addInCallUiListener(InCallUiListener listener) { - mInCallUiListeners.add(listener); - } - - public boolean removeInCallUiListener(InCallUiListener listener) { - return mInCallUiListeners.remove(listener); - } - - /*package*/ - void onActivityStarted() { - Log.d(this, "onActivityStarted"); - notifyVideoPauseController(true); - } - - /*package*/ - void onActivityStopped() { - Log.d(this, "onActivityStopped"); - notifyVideoPauseController(false); - } - - private void notifyVideoPauseController(boolean showing) { - Log.d(this, "notifyVideoPauseController: mIsChangingConfigurations=" + - mIsChangingConfigurations); - if (!mIsChangingConfigurations) { - VideoPauseController.getInstance().onUiShowing(showing); - } - } - - /** - * Brings the app into the foreground if possible. - */ - public void bringToForeground(boolean showDialpad) { - // Before we bring the incall UI to the foreground, we check to see if: - // 1. It is not currently in the foreground - // 2. We are in a state where we want to show the incall ui (i.e. there are calls to - // be displayed) - // If the activity hadn't actually been started previously, yet there are still calls - // present (e.g. a call was accepted by a bluetooth or wired headset), we want to - // bring it up the UI regardless. - if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) { - showInCall(showDialpad, false /* newOutgoingCall */); - } - } - - public void onPostDialCharWait(String callId, String chars) { - if (isActivityStarted()) { - mInCallActivity.showPostCharWaitDialog(callId, chars); - } - } - - /** - * Handles the green CALL key while in-call. - * @return true if we consumed the event. - */ - public boolean handleCallKey() { - Log.v(this, "handleCallKey"); - - // The green CALL button means either "Answer", "Unhold", or - // "Swap calls", or can be a no-op, depending on the current state - // of the Phone. - - /** - * INCOMING CALL - */ - final CallList calls = mCallList; - final Call incomingCall = calls.getIncomingCall(); - Log.v(this, "incomingCall: " + incomingCall); - - // (1) Attempt to answer a call - if (incomingCall != null) { - TelecomAdapter.getInstance().answerCall( - incomingCall.getId(), VideoProfile.STATE_AUDIO_ONLY); - return true; - } - - /** - * STATE_ACTIVE CALL - */ - final Call activeCall = calls.getActiveCall(); - if (activeCall != null) { - // TODO: This logic is repeated from CallButtonPresenter.java. We should - // consolidate this logic. - final boolean canMerge = activeCall.can( - android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); - final boolean canSwap = activeCall.can( - android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); - - Log.v(this, "activeCall: " + activeCall + ", canMerge: " + canMerge + - ", canSwap: " + canSwap); - - // (2) Attempt actions on conference calls - if (canMerge) { - TelecomAdapter.getInstance().merge(activeCall.getId()); - return true; - } else if (canSwap) { - TelecomAdapter.getInstance().swap(activeCall.getId()); - return true; - } - } - - /** - * BACKGROUND CALL - */ - final Call heldCall = calls.getBackgroundCall(); - if (heldCall != null) { - // We have a hold call so presumeable it will always support HOLD...but - // there is no harm in double checking. - final boolean canHold = heldCall.can(android.telecom.Call.Details.CAPABILITY_HOLD); - - Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold); - - // (4) unhold call - if (heldCall.getState() == Call.State.ONHOLD && canHold) { - TelecomAdapter.getInstance().unholdCall(heldCall.getId()); - return true; - } - } - - // Always consume hard keys - return true; - } - - /** - * A dialog could have prevented in-call screen from being previously finished. - * This function checks to see if there should be any UI left and if not attempts - * to tear down the UI. - */ - public void onDismissDialog() { - Log.i(this, "Dialog dismissed"); - if (mInCallState == InCallState.NO_CALLS) { - attemptFinishActivity(); - attemptCleanup(); - } - } - - /** - * Toggles whether the application is in fullscreen mode or not. - * - * @return {@code true} if in-call is now in fullscreen mode. - */ - public boolean toggleFullscreenMode() { - boolean isFullScreen = !mIsFullScreen; - Log.v(this, "toggleFullscreenMode = " + isFullScreen); - setFullScreen(isFullScreen); - return mIsFullScreen; - } - - /** - * Clears the previous fullscreen state. - */ - public void clearFullscreen() { - mIsFullScreen = false; - } - - /** - * Changes the fullscreen mode of the in-call UI. - * - * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false} - * otherwise. - */ - public void setFullScreen(boolean isFullScreen) { - setFullScreen(isFullScreen, false /* force */); - } - - /** - * Changes the fullscreen mode of the in-call UI. - * - * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false} - * otherwise. - * @param force {@code true} if fullscreen mode should be set regardless of its current state. - */ - public void setFullScreen(boolean isFullScreen, boolean force) { - Log.v(this, "setFullScreen = " + isFullScreen); - - // As a safeguard, ensure we cannot enter fullscreen if the dialpad is shown. - if (isDialpadVisible()) { - isFullScreen = false; - Log.v(this, "setFullScreen overridden as dialpad is shown = " + isFullScreen); - } - - if (mIsFullScreen == isFullScreen && !force) { - Log.v(this, "setFullScreen ignored as already in that state."); - return; - } - mIsFullScreen = isFullScreen; - notifyFullscreenModeChange(mIsFullScreen); - } - - /** - * @return {@code true} if the in-call ui is currently in fullscreen mode, {@code false} - * otherwise. - */ - public boolean isFullscreen() { - return mIsFullScreen; - } - - - /** - * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status. - * - * @param isFullscreenMode {@code True} if entering full screen mode. - */ - public void notifyFullscreenModeChange(boolean isFullscreenMode) { - for (InCallEventListener listener : mInCallEventListeners) { - listener.onFullscreenModeChanged(isFullscreenMode); - } - } - - /** - * Called by the {@link CallCardPresenter} to inform of a change in visibility of the secondary - * caller info bar. - * - * @param isVisible {@code true} if the secondary caller info is visible, {@code false} - * otherwise. - * @param height the height of the secondary caller info bar. - */ - public void notifySecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { - for (InCallEventListener listener : mInCallEventListeners) { - listener.onSecondaryCallerInfoVisibilityChanged(isVisible, height); - } - } - - - /** - * For some disconnected causes, we show a dialog. This calls into the activity to show - * the dialog if appropriate for the call. - */ - private void maybeShowErrorDialogOnDisconnect(Call call) { - // For newly disconnected calls, we may want to show a dialog on specific error conditions - if (isActivityStarted() && call.getState() == Call.State.DISCONNECTED) { - if (call.getAccountHandle() == null && !call.isConferenceCall()) { - setDisconnectCauseForMissingAccounts(call); - } - mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause()); - } - } - - /** - * When the state of in-call changes, this is the first method to get called. It determines if - * the UI needs to be started or finished depending on the new state and does it. - */ - private InCallState startOrFinishUi(InCallState newState) { - Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState); - - // TODO: Consider a proper state machine implementation - - // If the state isn't changing we have already done any starting/stopping of activities in - // a previous pass...so lets cut out early - if (newState == mInCallState) { - return newState; - } - - // A new Incoming call means that the user needs to be notified of the the call (since - // it wasn't them who initiated it). We do this through full screen notifications and - // happens indirectly through {@link StatusBarNotifier}. - // - // The process for incoming calls is as follows: - // - // 1) CallList - Announces existence of new INCOMING call - // 2) InCallPresenter - Gets announcement and calculates that the new InCallState - // - should be set to INCOMING. - // 3) InCallPresenter - This method is called to see if we need to start or finish - // the app given the new state. - // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls - // StatusBarNotifier explicitly to issue a FullScreen Notification - // that will either start the InCallActivity or show the user a - // top-level notification dialog if the user is in an immersive app. - // That notification can also start the InCallActivity. - // 5) InCallActivity - Main activity starts up and at the end of its onCreate will - // call InCallPresenter::setActivity() to let the presenter - // know that start-up is complete. - // - // [ AND NOW YOU'RE IN THE CALL. voila! ] - // - // Our app is started using a fullScreen notification. We need to do this whenever - // we get an incoming call. Depending on the current context of the device, either a - // incoming call HUN or the actual InCallActivity will be shown. - final boolean startIncomingCallSequence = (InCallState.INCOMING == newState); - - // A dialog to show on top of the InCallUI to select a PhoneAccount - final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState); - - // A new outgoing call indicates that the user just now dialed a number and when that - // happens we need to display the screen immediately or show an account picker dialog if - // no default is set. However, if the main InCallUI is already visible, we do not want to - // re-initiate the start-up animation, so we do not need to do anything here. - // - // It is also possible to go into an intermediate state where the call has been initiated - // but Telecom has not yet returned with the details of the call (handle, gateway, etc.). - // This pending outgoing state can also launch the call screen. - // - // This is different from the incoming call sequence because we do not need to shock the - // user with a top-level notification. Just show the call UI normally. - final boolean mainUiNotVisible = !isShowingInCallUi() || !getCallCardFragmentVisible(); - boolean showCallUi = InCallState.OUTGOING == newState && mainUiNotVisible; - - // Direct transition from PENDING_OUTGOING -> INCALL means that there was an error in the - // outgoing call process, so the UI should be brought up to show an error dialog. - showCallUi |= (InCallState.PENDING_OUTGOING == mInCallState - && InCallState.INCALL == newState && !isShowingInCallUi()); - - // Another exception - InCallActivity is in charge of disconnecting a call with no - // valid accounts set. Bring the UI up if this is true for the current pending outgoing - // call so that: - // 1) The call can be disconnected correctly - // 2) The UI comes up and correctly displays the error dialog. - // TODO: Remove these special case conditions by making InCallPresenter a true state - // machine. Telecom should also be the component responsible for disconnecting a call - // with no valid accounts. - showCallUi |= InCallState.PENDING_OUTGOING == newState && mainUiNotVisible - && isCallWithNoValidAccounts(mCallList.getPendingOutgoingCall()); - - // The only time that we have an instance of mInCallActivity and it isn't started is - // when it is being destroyed. In that case, lets avoid bringing up another instance of - // the activity. When it is finally destroyed, we double check if we should bring it back - // up so we aren't going to lose anything by avoiding a second startup here. - boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted(); - if (activityIsFinishing) { - Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState); - return mInCallState; - } - - if (showCallUi || showAccountPicker) { - Log.i(this, "Start in call UI"); - showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */); - } else if (startIncomingCallSequence) { - Log.i(this, "Start Full Screen in call UI"); - - // We're about the bring up the in-call UI for an incoming call. If we still have - // dialogs up, we need to clear them out before showing incoming screen. - if (isActivityStarted()) { - mInCallActivity.dismissPendingDialogs(); - } - if (!startUi(newState)) { - // startUI refused to start the UI. This indicates that it needed to restart the - // activity. When it finally restarts, it will call us back, so we do not actually - // change the state yet (we return mInCallState instead of newState). - return mInCallState; - } - } else if (newState == InCallState.NO_CALLS) { - // The new state is the no calls state. Tear everything down. - attemptFinishActivity(); - attemptCleanup(); - } - - return newState; - } - - /** - * Determines whether or not a call has no valid phone accounts that can be used to make the - * call with. Emergency calls do not require a phone account. - * - * @param call to check accounts for. - * @return {@code true} if the call has no call capable phone accounts set, {@code false} if - * the call contains a phone account that could be used to initiate it with, or is an emergency - * call. - */ - public static boolean isCallWithNoValidAccounts(Call call) { - if (call != null && !call.isEmergencyCall()) { - Bundle extras = call.getIntentExtras(); - - if (extras == null) { - extras = EMPTY_EXTRAS; - } - - final List phoneAccountHandles = extras - .getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); - - if ((call.getAccountHandle() == null && - (phoneAccountHandles == null || phoneAccountHandles.isEmpty()))) { - Log.i(InCallPresenter.getInstance(), "No valid accounts for call " + call); - return true; - } - } - return false; - } - - /** - * Sets the DisconnectCause for a call that was disconnected because it was missing a - * PhoneAccount or PhoneAccounts to select from. - * @param call - */ - private void setDisconnectCauseForMissingAccounts(Call call) { - android.telecom.Call telecomCall = call.getTelecomCall(); - - Bundle extras = telecomCall.getDetails().getIntentExtras(); - // Initialize the extras bundle to avoid NPE - if (extras == null) { - extras = new Bundle(); - } - - final List phoneAccountHandles = extras.getParcelableArrayList( - android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); - - if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) { - String scheme = telecomCall.getDetails().getHandle().getScheme(); - final String errorMsg = PhoneAccount.SCHEME_TEL.equals(scheme) ? - mContext.getString(R.string.callFailed_simError) : - mContext.getString(R.string.incall_error_supp_service_unknown); - DisconnectCause disconnectCause = - new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg); - call.setDisconnectCause(disconnectCause); - } - } - - private boolean startUi(InCallState inCallState) { - boolean isCallWaiting = mCallList.getActiveCall() != null && - mCallList.getIncomingCall() != null; - - // If the screen is off, we need to make sure it gets turned on for incoming calls. - // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works - // when the activity is first created. Therefore, to ensure the screen is turned on - // for the call waiting case, we finish() the current activity and start a new one. - // There should be no jank from this since the screen is already off and will remain so - // until our new activity is up. - - if (isCallWaiting) { - if (mProximitySensor.isScreenReallyOff() && isActivityStarted()) { - Log.i(this, "Restarting InCallActivity to turn screen on for call waiting"); - mInCallActivity.finish(); - // When the activity actually finishes, we will start it again if there are - // any active calls, so we do not need to start it explicitly here. Note, we - // actually get called back on this function to restart it. - - // We return false to indicate that we did not actually start the UI. - return false; - } else { - showInCall(false, false); - } - } else { - mStatusBarNotifier.updateNotification(inCallState, mCallList); - } - return true; - } - - /** - * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all - * down. - */ - private void attemptCleanup() { - boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected && - mInCallState == InCallState.NO_CALLS); - Log.i(this, "attemptCleanup? " + shouldCleanup); - - if (shouldCleanup) { - mIsActivityPreviouslyStarted = false; - mIsChangingConfigurations = false; - - // blow away stale contact info so that we get fresh data on - // the next set of calls - if (mContactInfoCache != null) { - mContactInfoCache.clearCache(); - } - mContactInfoCache = null; - - if (mProximitySensor != null) { - removeListener(mProximitySensor); - mProximitySensor.tearDown(); - } - mProximitySensor = null; - - mAudioModeProvider = null; - - if (mStatusBarNotifier != null) { - removeListener(mStatusBarNotifier); - } - if (mExternalCallNotifier != null && mExternalCallList != null) { - mExternalCallList.removeExternalCallListener(mExternalCallNotifier); - } - mStatusBarNotifier = null; - - if (mCallList != null) { - mCallList.removeListener(this); - mCallList.removeListener(mSpamCallListListener); - } - mCallList = null; - - mContext = null; - mInCallActivity = null; - - mListeners.clear(); - mIncomingCallListeners.clear(); - mDetailsListeners.clear(); - mCanAddCallListeners.clear(); - mOrientationListeners.clear(); - mInCallEventListeners.clear(); - - Log.d(this, "Finished InCallPresenter.CleanUp"); - } - } - - public void showInCall(final boolean showDialpad, final boolean newOutgoingCall) { - Log.i(this, "Showing InCallActivity"); - mContext.startActivity(getInCallIntent(showDialpad, newOutgoingCall)); - } - - public void onServiceBind() { - mServiceBound = true; - } - - public void onServiceUnbind() { - InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(false, null); - mServiceBound = false; - } - - public boolean isServiceBound() { - return mServiceBound; - } - - public void maybeStartRevealAnimation(Intent intent) { - if (intent == null || mInCallActivity != null) { - return; - } - final Bundle extras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); - if (extras == null) { - // Incoming call, just show the in-call UI directly. - return; - } - - if (extras.containsKey(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS)) { - // Account selection dialog will show up so don't show the animation. - return; - } - - final PhoneAccountHandle accountHandle = - intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); - final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT); - - InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle); - - final Intent incallIntent = getInCallIntent(false, true); - incallIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint); - mContext.startActivity(incallIntent); - } - - public Intent getInCallIntent(boolean showDialpad, boolean newOutgoingCall) { - final Intent intent = new Intent(Intent.ACTION_MAIN, null); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK); - - intent.setClass(mContext, InCallActivity.class); - if (showDialpad) { - intent.putExtra(InCallActivity.SHOW_DIALPAD_EXTRA, true); - } - intent.putExtra(InCallActivity.NEW_OUTGOING_CALL_EXTRA, newOutgoingCall); - return intent; - } - - /** - * Retrieves the current in-call camera manager instance, creating if necessary. - * - * @return The {@link InCallCameraManager}. - */ - public InCallCameraManager getInCallCameraManager() { - synchronized(this) { - if (mInCallCameraManager == null) { - mInCallCameraManager = new InCallCameraManager(mContext); - } - - return mInCallCameraManager; - } - } - - /** - * Notifies listeners of changes in orientation and notify calls of rotation angle change. - * - * @param orientation The screen orientation of the device (one of: - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - */ - public void onDeviceOrientationChange(int orientation) { - Log.d(this, "onDeviceOrientationChange: orientation= " + orientation); - - if (mCallList != null) { - mCallList.notifyCallsOfDeviceRotation(orientation); - } else { - Log.w(this, "onDeviceOrientationChange: CallList is null."); - } - - // Notify listeners of device orientation changed. - for (InCallOrientationListener listener : mOrientationListeners) { - listener.onDeviceOrientationChanged(orientation); - } - } - - /** - * Configures the in-call UI activity so it can change orientations or not. Enables the - * orientation event listener if allowOrientationChange is true, disables it if false. - * - * @param allowOrientationChange {@code True} if the in-call UI can change between portrait - * and landscape. {@Code False} if the in-call UI should be locked in portrait. - */ - public void setInCallAllowsOrientationChange(boolean allowOrientationChange) { - if (mInCallActivity == null) { - Log.e(this, "InCallActivity is null. Can't set requested orientation."); - return; - } - - if (!allowOrientationChange) { - mInCallActivity.setRequestedOrientation( - InCallOrientationEventListener.NO_SENSOR_SCREEN_ORIENTATION); - } else { - // Using SCREEN_ORIENTATION_FULL_SENSOR allows for reverse-portrait orientation, where - // SCREEN_ORIENTATION_SENSOR does not. - mInCallActivity.setRequestedOrientation( - InCallOrientationEventListener.FULL_SENSOR_SCREEN_ORIENTATION); - } - mInCallActivity.enableInCallOrientationEventListener(allowOrientationChange); - } - - public void enableScreenTimeout(boolean enable) { - Log.v(this, "enableScreenTimeout: value=" + enable); - if (mInCallActivity == null) { - Log.e(this, "enableScreenTimeout: InCallActivity is null."); - return; - } - - final Window window = mInCallActivity.getWindow(); - if (enable) { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - - /** - * Returns the space available beside the call card. - * - * @return The space beside the call card. - */ - public float getSpaceBesideCallCard() { - if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) { - return mInCallActivity.getCallCardFragment().getSpaceBesideCallCard(); - } - return 0; - } - - /** - * Returns whether the call card fragment is currently visible. - * - * @return True if the call card fragment is visible. - */ - public boolean getCallCardFragmentVisible() { - if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) { - return mInCallActivity.getCallCardFragment().isVisible(); - } - return false; - } - - /** - * Hides or shows the conference manager fragment. - * - * @param show {@code true} if the conference manager should be shown, {@code false} if it - * should be hidden. - */ - public void showConferenceCallManager(boolean show) { - if (mInCallActivity == null) { - return; - } - - mInCallActivity.showConferenceFragment(show); - } - - /** - * Determines if the dialpad is visible. - * - * @return {@code true} if the dialpad is visible, {@code false} otherwise. - */ - public boolean isDialpadVisible() { - if (mInCallActivity == null) { - return false; - } - return mInCallActivity.isDialpadVisible(); - } - - /** - * @return True if the application is currently running in a right-to-left locale. - */ - public static boolean isRtl() { - return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == - View.LAYOUT_DIRECTION_RTL; - } - - /** - * Extract background color from call object. The theme colors will include a primary color - * and a secondary color. - */ - public void setThemeColors() { - // This method will set the background to default if the color is PhoneAccount.NO_COLOR. - mThemeColors = getColorsFromCall(mCallList.getFirstCall()); - - if (mInCallActivity == null) { - return; - } - - final Resources resources = mInCallActivity.getResources(); - final int color; - if (resources.getBoolean(R.bool.is_layout_landscape)) { - // TODO use ResourcesCompat.getColor(Resources, int, Resources.Theme) when available - // {@link Resources#getColor(int)} used for compatibility - color = resources.getColor(R.color.statusbar_background_color); - } else { - color = mThemeColors.mSecondaryColor; - } - - mInCallActivity.getWindow().setStatusBarColor(color); - final TaskDescription td = new TaskDescription( - resources.getString(R.string.notification_ongoing_call), null, color); - mInCallActivity.setTaskDescription(td); - } - - /** - * @return A palette for colors to display in the UI. - */ - public MaterialPalette getThemeColors() { - return mThemeColors; - } - - private MaterialPalette getColorsFromCall(Call call) { - if (call == null) { - return getColorsFromPhoneAccountHandle(mPendingPhoneAccountHandle); - } else { - if (call.isSpam()) { - Resources resources = mContext.getResources(); - return new InCallUIMaterialColorMapUtils( - resources).calculatePrimaryAndSecondaryColor( - resources.getColor(R.color.incall_call_spam_background_color)); - } else { - return getColorsFromPhoneAccountHandle(call.getAccountHandle()); - } - } - } - - private MaterialPalette getColorsFromPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) { - int highlightColor = PhoneAccount.NO_HIGHLIGHT_COLOR; - if (phoneAccountHandle != null) { - final TelecomManager tm = getTelecomManager(); - - if (tm != null) { - final PhoneAccount account = - TelecomManagerCompat.getPhoneAccount(tm, phoneAccountHandle); - // For single-sim devices, there will be no selected highlight color, so the phone - // account will default to NO_HIGHLIGHT_COLOR. - if (account != null && CompatUtils.isLollipopMr1Compatible()) { - highlightColor = account.getHighlightColor(); - } - } - } - return new InCallUIMaterialColorMapUtils( - mContext.getResources()).calculatePrimaryAndSecondaryColor(highlightColor); - } - - /** - * @return An instance of TelecomManager. - */ - public TelecomManager getTelecomManager() { - if (mTelecomManager == null) { - mTelecomManager = (TelecomManager) - mContext.getSystemService(Context.TELECOM_SERVICE); - } - return mTelecomManager; - } - - /** - * @return An instance of TelephonyManager - */ - public TelephonyManager getTelephonyManager() { - return mTelephonyManager; - } - - InCallActivity getActivity() { - return mInCallActivity; - } - - AnswerPresenter getAnswerPresenter() { - return mAnswerPresenter; - } - - ExternalCallNotifier getExternalCallNotifier() { - return mExternalCallNotifier; - } - - /** - * Private constructor. Must use getInstance() to get this singleton. - */ - private InCallPresenter() { - } - - /** - * All the main states of InCallActivity. - */ - public enum InCallState { - // InCall Screen is off and there are no calls - NO_CALLS, - - // Incoming-call screen is up - INCOMING, - - // In-call experience is showing - INCALL, - - // Waiting for user input before placing outgoing call - WAITING_FOR_ACCOUNT, - - // UI is starting up but no call has been initiated yet. - // The UI is waiting for Telecom to respond. - PENDING_OUTGOING, - - // User is dialing out - OUTGOING; - - public boolean isIncoming() { - return (this == INCOMING); - } - - public boolean isConnectingOrConnected() { - return (this == INCOMING || - this == OUTGOING || - this == INCALL); - } - } - - /** - * Interface implemented by classes that need to know about the InCall State. - */ - public interface InCallStateListener { - // TODO: Enhance state to contain the call objects instead of passing CallList - public void onStateChange(InCallState oldState, InCallState newState, CallList callList); - } - - public interface IncomingCallListener { - public void onIncomingCall(InCallState oldState, InCallState newState, Call call); - } - - public interface CanAddCallListener { - public void onCanAddCallChanged(boolean canAddCall); - } - - public interface InCallDetailsListener { - public void onDetailsChanged(Call call, android.telecom.Call.Details details); - } - - public interface InCallOrientationListener { - public void onDeviceOrientationChanged(int orientation); - } - - /** - * Interface implemented by classes that need to know about events which occur within the - * In-Call UI. Used as a means of communicating between fragments that make up the UI. - */ - public interface InCallEventListener { - public void onFullscreenModeChanged(boolean isFullscreenMode); - public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height); - } - - public interface InCallUiListener { - void onUiShowing(boolean showing); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallServiceImpl.java b/InCallUI/src/com/android/incallui/InCallServiceImpl.java deleted file mode 100644 index 1414bc51de1200aaa16b77904394fb56f7417773..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallServiceImpl.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.telecom.Call; -import android.telecom.CallAudioState; -import android.telecom.InCallService; - -/** - * Used to receive updates about calls from the Telecom component. This service is bound to - * Telecom while there exist calls which potentially require UI. This includes ringing (incoming), - * dialing (outgoing), and active calls. When the last call is disconnected, Telecom will unbind to - * the service triggering InCallActivity (via CallList) to finish soon after. - */ -public class InCallServiceImpl extends InCallService { - - @Override - public void onCallAudioStateChanged(CallAudioState audioState) { - AudioModeProvider.getInstance().onAudioStateChanged(audioState.isMuted(), - audioState.getRoute(), audioState.getSupportedRouteMask()); - } - - @Override - public void onBringToForeground(boolean showDialpad) { - InCallPresenter.getInstance().onBringToForeground(showDialpad); - } - - @Override - public void onCallAdded(Call call) { - InCallPresenter.getInstance().onCallAdded(call); - } - - @Override - public void onCallRemoved(Call call) { - InCallPresenter.getInstance().onCallRemoved(call); - } - - @Override - public void onCanAddCallChanged(boolean canAddCall) { - InCallPresenter.getInstance().onCanAddCallChanged(canAddCall); - } - - @Override - public IBinder onBind(Intent intent) { - final Context context = getApplicationContext(); - final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context); - InCallPresenter.getInstance().setUp( - getApplicationContext(), - CallList.getInstance(), - new ExternalCallList(), - AudioModeProvider.getInstance(), - new StatusBarNotifier(context, contactInfoCache), - new ExternalCallNotifier(context, contactInfoCache), - contactInfoCache, - new ProximitySensor( - context, - AudioModeProvider.getInstance(), - new AccelerometerListener(context)) - ); - InCallPresenter.getInstance().onServiceBind(); - InCallPresenter.getInstance().maybeStartRevealAnimation(intent); - TelecomAdapter.getInstance().setInCallService(this); - - return super.onBind(intent); - } - - @Override - public boolean onUnbind(Intent intent) { - super.onUnbind(intent); - - InCallPresenter.getInstance().onServiceUnbind(); - tearDown(); - - return false; - } - - private void tearDown() { - Log.v(this, "tearDown"); - // Tear down the InCall system - TelecomAdapter.getInstance().clearInCallService(); - InCallPresenter.getInstance().tearDown(); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallServiceListener.java b/InCallUI/src/com/android/incallui/InCallServiceListener.java deleted file mode 100644 index 11a5b08ef9b44efb1d98841464167935d1dc5bde..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallServiceListener.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.telecom.InCallService; - -/** - * Interface implemented by In-Call components that maintain a reference to the Telecom API - * {@code InCallService} object. Clarifies the expectations associated with the relevant method - * calls. - */ -public interface InCallServiceListener { - - /** - * Called once at {@code InCallService} startup time with a valid instance. At - * that time, there will be no existing {@code Call}s. - * - * @param inCallService The {@code InCallService} object. - */ - void setInCallService(InCallService inCallService); - - /** - * Called once at {@code InCallService} shutdown time. At that time, any {@code Call}s - * will have transitioned through the disconnected state and will no longer exist. - */ - void clearInCallService(); -} diff --git a/InCallUI/src/com/android/incallui/InCallUIMaterialColorMapUtils.java b/InCallUI/src/com/android/incallui/InCallUIMaterialColorMapUtils.java deleted file mode 100644 index 9c108b8557f1fd69f3fc073f953018bcf3b9aceb..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallUIMaterialColorMapUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.android.incallui; - -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.telecom.PhoneAccount; - -import com.android.contacts.common.util.MaterialColorMapUtils; -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; - -public class InCallUIMaterialColorMapUtils extends MaterialColorMapUtils { - private final TypedArray sPrimaryColors; - private final TypedArray sSecondaryColors; - private final Resources mResources; - - public InCallUIMaterialColorMapUtils(Resources resources) { - super(resources); - sPrimaryColors = resources.obtainTypedArray(R.array.background_colors); - sSecondaryColors = resources.obtainTypedArray(R.array.background_colors_dark); - mResources = resources; - } - - /** - * Currently the InCallUI color will only vary by SIM color which is a list of colors - * defined in the background_colors array, so first search the list for the matching color and - * fall back to the closest matching color if an exact match does not exist. - */ - @Override - public MaterialPalette calculatePrimaryAndSecondaryColor(int color) { - if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) { - return getDefaultPrimaryAndSecondaryColors(mResources); - } - - for (int i = 0; i < sPrimaryColors.length(); i++) { - if (sPrimaryColors.getColor(i, 0) == color) { - return new MaterialPalette( - sPrimaryColors.getColor(i, 0), - sSecondaryColors.getColor(i, 0)); - } - } - - // The color isn't in the list, so use the superclass to find an approximate color. - return super.calculatePrimaryAndSecondaryColor(color); - } - - /** - * {@link Resources#getColor(int) used for compatibility - */ - @SuppressWarnings("deprecation") - public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) { - final int primaryColor = resources.getColor(R.color.dialer_theme_color); - final int secondaryColor = resources.getColor(R.color.dialer_theme_color_dark); - return new MaterialPalette(primaryColor, secondaryColor); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallVideoCallCallback.java b/InCallUI/src/com/android/incallui/InCallVideoCallCallback.java deleted file mode 100644 index 99e6d5129bd54b6e3f342acd5bd13addf70e0f68..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallVideoCallCallback.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telecom.Connection; -import android.telecom.Connection.VideoProvider; -import android.telecom.InCallService.VideoCall; -import android.telecom.VideoProfile; -import android.telecom.VideoProfile.CameraCapabilities; - -/** - * Implements the InCallUI VideoCall Callback. - */ -public class InCallVideoCallCallback extends VideoCall.Callback { - - /** - * The call associated with this {@link InCallVideoCallCallback}. - */ - private Call mCall; - - /** - * Creates an instance of the call video client, specifying the call it is related to. - * - * @param call The call. - */ - public InCallVideoCallCallback(Call call) { - mCall = call; - } - - /** - * Handles an incoming session modification request. - * - * @param videoProfile The requested video call profile. - */ - @Override - public void onSessionModifyRequestReceived(VideoProfile videoProfile) { - Log.d(this, " onSessionModifyRequestReceived videoProfile=" + videoProfile); - int previousVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState()); - int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState()); - - boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState); - boolean isVideoCall = VideoUtils.isVideoCall(newVideoState); - - // Check for upgrades to video. - if (!wasVideoCall && isVideoCall && previousVideoState != newVideoState) { - InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(mCall, - newVideoState); - } - } - - /** - * Handles a session modification response. - * - * @param status Status of the session modify request. Valid values are - * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, - * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, - * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID} - * @param requestedProfile - * @param responseProfile The actual profile changes made by the peer device. - */ - @Override - public void onSessionModifyResponseReceived(int status, VideoProfile requestedProfile, - VideoProfile responseProfile) { - Log.d(this, "onSessionModifyResponseReceived status=" + status + " requestedProfile=" - + requestedProfile + " responseProfile=" + responseProfile); - if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) { - // Report the reason the upgrade failed as the new session modification state. - if (status == VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT) { - mCall.setSessionModificationState( - Call.SessionModificationState.UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT); - } else { - if (status == VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE) { - mCall.setSessionModificationState( - Call.SessionModificationState.REQUEST_REJECTED); - } else { - mCall.setSessionModificationState( - Call.SessionModificationState.REQUEST_FAILED); - } - } - } - - // Finally clear the outstanding request. - mCall.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - - /** - * Handles a call session event. - * - * @param event The event. - */ - @Override - public void onCallSessionEvent(int event) { - InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event); - } - - /** - * Handles a change to the peer video dimensions. - * - * @param width The updated peer video width. - * @param height The updated peer video height. - */ - @Override - public void onPeerDimensionsChanged(int width, int height) { - InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(mCall, width, height); - } - - /** - * Handles a change to the video quality of the call. - * - * @param videoQuality The updated video call quality. - */ - @Override - public void onVideoQualityChanged(int videoQuality) { - InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(mCall, videoQuality); - } - - /** - * Handles a change to the call data usage. No implementation as the in-call UI does not - * display data usage. - * - * @param dataUsage The updated data usage. - */ - @Override - public void onCallDataUsageChanged(long dataUsage) { - Log.d(this, "onCallDataUsageChanged: dataUsage = " + dataUsage); - InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage); - } - - /** - * Handles changes to the camera capabilities. No implementation as the in-call UI does not - * make use of camera capabilities. - * - * @param cameraCapabilities The changed camera capabilities. - */ - @Override - public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) { - if (cameraCapabilities != null) { - InCallVideoCallCallbackNotifier.getInstance().cameraDimensionsChanged( - mCall, cameraCapabilities.getWidth(), cameraCapabilities.getHeight()); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallVideoCallCallbackNotifier.java b/InCallUI/src/com/android/incallui/InCallVideoCallCallbackNotifier.java deleted file mode 100644 index bb7529205a7a483dbc57cdf747f774556fba122f..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/InCallVideoCallCallbackNotifier.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming - * events. - */ -public class InCallVideoCallCallbackNotifier { - /** - * Singleton instance of this class. - */ - private static InCallVideoCallCallbackNotifier sInstance = - new InCallVideoCallCallbackNotifier(); - - /** - * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is - * load factor before resizing, 1 means we only expect a single thread to - * access the map so make only a single shard - */ - private final Set mSessionModificationListeners = - Collections.newSetFromMap(new ConcurrentHashMap - (8, 0.9f, 1)); - private final Set mVideoEventListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mSurfaceChangeListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - - /** - * Static singleton accessor method. - */ - public static InCallVideoCallCallbackNotifier getInstance() { - return sInstance; - } - - /** - * Private constructor. Instance should only be acquired through getInstance(). - */ - private InCallVideoCallCallbackNotifier() { - } - - /** - * Adds a new {@link SessionModificationListener}. - * - * @param listener The listener. - */ - public void addSessionModificationListener(SessionModificationListener listener) { - Preconditions.checkNotNull(listener); - mSessionModificationListeners.add(listener); - } - - /** - * Remove a {@link SessionModificationListener}. - * - * @param listener The listener. - */ - public void removeSessionModificationListener(SessionModificationListener listener) { - if (listener != null) { - mSessionModificationListeners.remove(listener); - } - } - - /** - * Adds a new {@link VideoEventListener}. - * - * @param listener The listener. - */ - public void addVideoEventListener(VideoEventListener listener) { - Preconditions.checkNotNull(listener); - mVideoEventListeners.add(listener); - } - - /** - * Remove a {@link VideoEventListener}. - * - * @param listener The listener. - */ - public void removeVideoEventListener(VideoEventListener listener) { - if (listener != null) { - mVideoEventListeners.remove(listener); - } - } - - /** - * Adds a new {@link SurfaceChangeListener}. - * - * @param listener The listener. - */ - public void addSurfaceChangeListener(SurfaceChangeListener listener) { - Preconditions.checkNotNull(listener); - mSurfaceChangeListeners.add(listener); - } - - /** - * Remove a {@link SurfaceChangeListener}. - * - * @param listener The listener. - */ - public void removeSurfaceChangeListener(SurfaceChangeListener listener) { - if (listener != null) { - mSurfaceChangeListeners.remove(listener); - } - } - - /** - * Inform listeners of an upgrade to video request for a call. - * @param call The call. - * @param videoState The video state we want to upgrade to. - */ - public void upgradeToVideoRequest(Call call, int videoState) { - Log.d(this, "upgradeToVideoRequest call = " + call + " new video state = " + videoState); - for (SessionModificationListener listener : mSessionModificationListeners) { - listener.onUpgradeToVideoRequest(call, videoState); - } - } - - /** - * Inform listeners of a call session event. - * - * @param event The call session event. - */ - public void callSessionEvent(int event) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onCallSessionEvent(event); - } - } - - /** - * Inform listeners of a downgrade to audio. - * - * @param call The call. - * @param paused The paused state. - */ - public void peerPausedStateChanged(Call call, boolean paused) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onPeerPauseStateChanged(call, paused); - } - } - - /** - * Inform listeners of any change in the video quality of the call - * - * @param call The call. - * @param videoQuality The updated video quality of the call. - */ - public void videoQualityChanged(Call call, int videoQuality) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onVideoQualityChanged(call, videoQuality); - } - } - - /** - * Inform listeners of a change to peer dimensions. - * - * @param call The call. - * @param width New peer width. - * @param height New peer height. - */ - public void peerDimensionsChanged(Call call, int width, int height) { - for (SurfaceChangeListener listener : mSurfaceChangeListeners) { - listener.onUpdatePeerDimensions(call, width, height); - } - } - - /** - * Inform listeners of a change to camera dimensions. - * - * @param call The call. - * @param width The new camera video width. - * @param height The new camera video height. - */ - public void cameraDimensionsChanged(Call call, int width, int height) { - for (SurfaceChangeListener listener : mSurfaceChangeListeners) { - listener.onCameraDimensionsChange(call, width, height); - } - } - - /** - * Inform listeners of a change to call data usage. - * - * @param dataUsage data usage value - */ - public void callDataUsageChanged(long dataUsage) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onCallDataUsageChange(dataUsage); - } - } - - /** - * Listener interface for any class that wants to be notified of upgrade to video request. - */ - public interface SessionModificationListener { - /** - * Called when a peer request is received to upgrade an audio-only call to a video call. - * - * @param call The call the request was received for. - * @param videoState The requested video state. - */ - public void onUpgradeToVideoRequest(Call call, int videoState); - } - - /** - * Listener interface for any class that wants to be notified of video events, including pause - * and un-pause of peer video, video quality changes. - */ - public interface VideoEventListener { - /** - * Called when the peer pauses or un-pauses video transmission. - * - * @param call The call which paused or un-paused video transmission. - * @param paused {@code True} when the video transmission is paused, {@code false} - * otherwise. - */ - public void onPeerPauseStateChanged(Call call, boolean paused); - - /** - * Called when the video quality changes. - * - * @param call The call whose video quality changes. - * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN. - */ - public void onVideoQualityChanged(Call call, int videoCallQuality); - - /* - * Called when call data usage value is requested or when call data usage value is updated - * because of a call state change - * - * @param dataUsage call data usage value - */ - public void onCallDataUsageChange(long dataUsage); - - /** - * Called when call session event is raised. - * - * @param event The call session event. - */ - public void onCallSessionEvent(int event); - } - - /** - * Listener interface for any class that wants to be notified of changes to the video surfaces. - */ - public interface SurfaceChangeListener { - /** - * Called when the peer video feed changes dimensions. This can occur when the peer rotates - * their device, changing the aspect ratio of the video signal. - * - * @param call The call which experienced a peer video - * @param width - * @param height - */ - public void onUpdatePeerDimensions(Call call, int width, int height); - - /** - * Called when the local camera changes dimensions. This occurs when a change in camera - * occurs. - * - * @param call The call which experienced the camera dimension change. - * @param width The new camera video width. - * @param height The new camera video height. - */ - public void onCameraDimensionsChange(Call call, int width, int height); - } -} diff --git a/InCallUI/src/com/android/incallui/LatencyReport.java b/InCallUI/src/com/android/incallui/LatencyReport.java deleted file mode 100644 index 655372a8f949f2beb077d9759795343e87d29969..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/LatencyReport.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.os.SystemClock; - -import com.android.incalluibind.ObjectFactory; - -/** - * Tracks latency information for a call. - */ -public class LatencyReport { - // The following are hidden constants from android.telecom.TelecomManager. - private static final String EXTRA_CALL_CREATED_TIME_MILLIS = - "android.telecom.extra.CALL_CREATED_TIME_MILLIS"; - private static final String EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS = - "android.telecom.extra.CALL_TELECOM_ROUTING_START_TIME_MILLIS"; - private static final String EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS = - "android.telecom.extra.CALL_TELECOM_ROUTING_END_TIME_MILLIS"; - - public static final long INVALID_TIME = -1; - - private final boolean mWasIncoming; - - // Time elapsed since boot when the call was created by the connection service. - private final long mCreatedTimeMillis; - - // Time elapsed since boot when telecom began processing the call. - private final long mTelecomRoutingStartTimeMillis; - - // Time elapsed since boot when telecom finished processing the call. This includes things like - // looking up contact info and call blocking but before showing any UI. - private final long mTelecomRoutingEndTimeMillis; - - // Time elapsed since boot when the call was added to the InCallUi. - private final long mCallAddedTimeMillis; - - // Time elapsed since boot when the call was added and call blocking evaluation was completed. - private long mCallBlockingTimeMillis = INVALID_TIME; - - // Time elapsed since boot when the call notification was shown. - private long mCallNotificationTimeMillis = INVALID_TIME; - - // Time elapsed since boot when the InCallUI was shown. - private long mInCallUiShownTimeMillis = INVALID_TIME; - - // Whether the call was shown to the user as a heads up notification instead of a full screen - // UI. - private boolean mDidDisplayHeadsUpNotification; - - public LatencyReport() { - mWasIncoming = false; - mCreatedTimeMillis = INVALID_TIME; - mTelecomRoutingStartTimeMillis = INVALID_TIME; - mTelecomRoutingEndTimeMillis = INVALID_TIME; - mCallAddedTimeMillis = SystemClock.elapsedRealtime(); - } - - public LatencyReport(android.telecom.Call telecomCall) { - mWasIncoming = telecomCall.getState() == android.telecom.Call.STATE_RINGING; - Bundle extras = telecomCall.getDetails().getIntentExtras(); - if (extras == null) { - mCreatedTimeMillis = INVALID_TIME; - mTelecomRoutingStartTimeMillis = INVALID_TIME; - mTelecomRoutingEndTimeMillis = INVALID_TIME; - } else { - mCreatedTimeMillis = extras.getLong(EXTRA_CALL_CREATED_TIME_MILLIS, INVALID_TIME); - mTelecomRoutingStartTimeMillis = extras.getLong( - EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS, INVALID_TIME); - mTelecomRoutingEndTimeMillis = extras.getLong( - EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS, INVALID_TIME); - } - mCallAddedTimeMillis = SystemClock.elapsedRealtime(); - } - - public boolean getWasIncoming() { - return mWasIncoming; - } - - public long getCreatedTimeMillis() { - return mCreatedTimeMillis; - } - - public long getTelecomRoutingStartTimeMillis() { - return mTelecomRoutingStartTimeMillis; - } - - public long getTelecomRoutingEndTimeMillis() { - return mTelecomRoutingEndTimeMillis; - } - - public long getCallAddedTimeMillis() { - return mCallAddedTimeMillis; - } - - public long getCallBlockingTimeMillis() { - return mCallBlockingTimeMillis; - } - - public void onCallBlockingDone() { - if (mCallBlockingTimeMillis == INVALID_TIME) { - mCallBlockingTimeMillis = SystemClock.elapsedRealtime(); - } - } - - public long getCallNotificationTimeMillis() { - return mCallNotificationTimeMillis; - } - - public void onNotificationShown() { - if (mCallNotificationTimeMillis == INVALID_TIME) { - mCallNotificationTimeMillis = SystemClock.elapsedRealtime(); - } - } - - public long getInCallUiShownTimeMillis() { - return mInCallUiShownTimeMillis; - } - - public void onInCallUiShown(boolean forFullScreenIntent) { - if (mInCallUiShownTimeMillis == INVALID_TIME) { - mInCallUiShownTimeMillis = SystemClock.elapsedRealtime(); - mDidDisplayHeadsUpNotification = mWasIncoming && !forFullScreenIntent; - } - } - - public boolean getDidDisplayHeadsUpNotification() { - return mDidDisplayHeadsUpNotification; - } -} diff --git a/InCallUI/src/com/android/incallui/Log.java b/InCallUI/src/com/android/incallui/Log.java deleted file mode 100644 index 07a0e61ca7e65094193d2dc6c5a72020b93f8008..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/Log.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.net.Uri; -import android.telecom.PhoneAccount; -import android.telephony.PhoneNumberUtils; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** - * Manages logging for the entire class. - */ -public class Log { - - // Generic tag for all In Call logging - public static final String TAG = "InCall"; - - public static final boolean FORCE_DEBUG = false; /* STOPSHIP if true */ - public static final boolean DEBUG = FORCE_DEBUG || - android.util.Log.isLoggable(TAG, android.util.Log.DEBUG); - public static final boolean VERBOSE = FORCE_DEBUG || - android.util.Log.isLoggable(TAG, android.util.Log.VERBOSE); - public static final String TAG_DELIMETER = " - "; - - public static void d(String tag, String msg) { - if (DEBUG) { - android.util.Log.d(TAG, delimit(tag) + msg); - } - } - - public static void d(Object obj, String msg) { - if (DEBUG) { - android.util.Log.d(TAG, getPrefix(obj) + msg); - } - } - - public static void d(Object obj, String str1, Object str2) { - if (DEBUG) { - android.util.Log.d(TAG, getPrefix(obj) + str1 + str2); - } - } - - public static void v(Object obj, String msg) { - if (VERBOSE) { - android.util.Log.v(TAG, getPrefix(obj) + msg); - } - } - - public static void v(Object obj, String str1, Object str2) { - if (VERBOSE) { - android.util.Log.d(TAG, getPrefix(obj) + str1 + str2); - } - } - - public static void e(String tag, String msg, Exception e) { - android.util.Log.e(TAG, delimit(tag) + msg, e); - } - - public static void e(String tag, String msg) { - android.util.Log.e(TAG, delimit(tag) + msg); - } - - public static void e(Object obj, String msg, Exception e) { - android.util.Log.e(TAG, getPrefix(obj) + msg, e); - } - - public static void e(Object obj, String msg) { - android.util.Log.e(TAG, getPrefix(obj) + msg); - } - - public static void i(String tag, String msg) { - android.util.Log.i(TAG, delimit(tag) + msg); - } - - public static void i(Object obj, String msg) { - android.util.Log.i(TAG, getPrefix(obj) + msg); - } - - public static void w(Object obj, String msg) { - android.util.Log.w(TAG, getPrefix(obj) + msg); - } - - public static void wtf(Object obj, String msg) { - android.util.Log.wtf(TAG, getPrefix(obj) + msg); - } - - public static String piiHandle(Object pii) { - if (pii == null || VERBOSE) { - return String.valueOf(pii); - } - - if (pii instanceof Uri) { - Uri uri = (Uri) pii; - - // All Uri's which are not "tel" go through normal pii() method. - if (!PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) { - return pii(pii); - } else { - pii = uri.getSchemeSpecificPart(); - } - } - - String originalString = String.valueOf(pii); - StringBuilder stringBuilder = new StringBuilder(originalString.length()); - for (char c : originalString.toCharArray()) { - if (PhoneNumberUtils.isDialable(c)) { - stringBuilder.append('*'); - } else { - stringBuilder.append(c); - } - } - return stringBuilder.toString(); - } - - /** - * Redact personally identifiable information for production users. - * If we are running in verbose mode, return the original string, otherwise - * return a SHA-1 hash of the input string. - */ - public static String pii(Object pii) { - if (pii == null || VERBOSE) { - return String.valueOf(pii); - } - return "[" + secureHash(String.valueOf(pii).getBytes()) + "]"; - } - - private static String secureHash(byte[] input) { - MessageDigest messageDigest; - try { - messageDigest = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - messageDigest.update(input); - byte[] result = messageDigest.digest(); - return encodeHex(result); - } - - private static String encodeHex(byte[] bytes) { - StringBuffer hex = new StringBuffer(bytes.length * 2); - - for (int i = 0; i < bytes.length; i++) { - int byteIntValue = bytes[i] & 0xff; - if (byteIntValue < 0x10) { - hex.append("0"); - } - hex.append(Integer.toString(byteIntValue, 16)); - } - - return hex.toString(); - } - - private static String getPrefix(Object obj) { - return (obj == null ? "" : (obj.getClass().getSimpleName() + TAG_DELIMETER)); - } - - private static String delimit(String tag) { - return tag + TAG_DELIMETER; - } -} diff --git a/InCallUI/src/com/android/incallui/NeededForReflection.java b/InCallUI/src/com/android/incallui/NeededForReflection.java deleted file mode 100644 index 363a0a548cc8213f617ad7a4ec47a998aa7f3b97..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/NeededForReflection.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Denotes that the class, constructor, method or field is used for reflection and therefore cannot - * be removed by tools like ProGuard. - */ -@Retention(RetentionPolicy.CLASS) -@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD}) -public @interface NeededForReflection {} diff --git a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java b/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java deleted file mode 100644 index 27f71159d0f0922dfec174ac730063f407d58776..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.telecom.VideoProfile; - -/** - * Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus - * sent from the notification manager. - * This should be visible from outside, but shouldn't be exported. - */ -public class NotificationBroadcastReceiver extends BroadcastReceiver { - - /** - * Intent Action used for hanging up the current call from Notification bar. This will - * choose first ringing call, first active call, or first background call (typically in - * STATE_HOLDING state). - */ - public static final String ACTION_DECLINE_INCOMING_CALL = - "com.android.incallui.ACTION_DECLINE_INCOMING_CALL"; - public static final String ACTION_HANG_UP_ONGOING_CALL = - "com.android.incallui.ACTION_HANG_UP_ONGOING_CALL"; - public static final String ACTION_ANSWER_VIDEO_INCOMING_CALL = - "com.android.incallui.ACTION_ANSWER_VIDEO_INCOMING_CALL"; - public static final String ACTION_ANSWER_VOICE_INCOMING_CALL = - "com.android.incallui.ACTION_ANSWER_VOICE_INCOMING_CALL"; - public static final String ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST = - "com.android.incallui.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST"; - public static final String ACTION_DECLINE_VIDEO_UPGRADE_REQUEST = - "com.android.incallui.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST"; - public static final String ACTION_PULL_EXTERNAL_CALL = - "com.android.incallui.ACTION_PULL_EXTERNAL_CALL"; - public static final String EXTRA_NOTIFICATION_ID = - "com.android.incallui.extra.EXTRA_NOTIFICATION_ID"; - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - Log.i(this, "Broadcast from Notification: " + action); - - // TODO: Commands of this nature should exist in the CallList. - if (action.equals(ACTION_ANSWER_VIDEO_INCOMING_CALL)) { - InCallPresenter.getInstance().answerIncomingCall( - context, VideoProfile.STATE_BIDIRECTIONAL); - } else if (action.equals(ACTION_ANSWER_VOICE_INCOMING_CALL)) { - InCallPresenter.getInstance().answerIncomingCall( - context, VideoProfile.STATE_AUDIO_ONLY); - } else if (action.equals(ACTION_DECLINE_INCOMING_CALL)) { - InCallPresenter.getInstance().declineIncomingCall(context); - } else if (action.equals(ACTION_HANG_UP_ONGOING_CALL)) { - InCallPresenter.getInstance().hangUpOngoingCall(context); - } else if (action.equals(ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST)) { - //TODO: Change calltype after adding support for TX and RX - InCallPresenter.getInstance().acceptUpgradeRequest( - VideoProfile.STATE_BIDIRECTIONAL, context); - } else if (action.equals(ACTION_DECLINE_VIDEO_UPGRADE_REQUEST)) { - InCallPresenter.getInstance().declineUpgradeRequest(context); - } else if (action.equals(ACTION_PULL_EXTERNAL_CALL)) { - int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); - InCallPresenter.getInstance().getExternalCallNotifier() - .pullExternalCall(notificationId); - } - } - -} diff --git a/InCallUI/src/com/android/incallui/PostCharDialogFragment.java b/InCallUI/src/com/android/incallui/PostCharDialogFragment.java deleted file mode 100644 index 6f904ad9e5ab22f673fb86017e61502f4aeaffe2..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/PostCharDialogFragment.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.WindowManager; - -import com.android.dialer.R; - -/** - * Pop up an alert dialog with OK and Cancel buttons to allow user to Accept or Reject the WAIT - * inserted as part of the Dial string. - */ -public class PostCharDialogFragment extends DialogFragment { - - private static final String STATE_CALL_ID = "CALL_ID"; - private static final String STATE_POST_CHARS = "POST_CHARS"; - - private String mCallId; - private String mPostDialStr; - - public PostCharDialogFragment() { - } - - public PostCharDialogFragment(String callId, String postDialStr) { - mCallId = callId; - mPostDialStr = postDialStr; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreateDialog(savedInstanceState); - - if (mPostDialStr == null && savedInstanceState != null) { - mCallId = savedInstanceState.getString(STATE_CALL_ID); - mPostDialStr = savedInstanceState.getString(STATE_POST_CHARS); - } - - final StringBuilder buf = new StringBuilder(); - buf.append(getResources().getText(R.string.wait_prompt_str)); - buf.append(mPostDialStr); - - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(buf.toString()); - - builder.setPositiveButton(R.string.pause_prompt_yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int whichButton) { - TelecomAdapter.getInstance().postDialContinue(mCallId, true); - } - }); - builder.setNegativeButton(R.string.pause_prompt_no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int whichButton) { - dialog.cancel(); - } - }); - - final AlertDialog dialog = builder.create(); - return dialog; - } - - @Override - public void onCancel(DialogInterface dialog) { - super.onCancel(dialog); - - TelecomAdapter.getInstance().postDialContinue(mCallId, false); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putString(STATE_CALL_ID, mCallId); - outState.putString(STATE_POST_CHARS, mPostDialStr); - } -} diff --git a/InCallUI/src/com/android/incallui/Presenter.java b/InCallUI/src/com/android/incallui/Presenter.java deleted file mode 100644 index 4e1fa978d76c88a0a9673ee5c553fbaee3780288..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/Presenter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; - -/** - * Base class for Presenters. - */ -public abstract class Presenter { - - private U mUi; - - /** - * Called after the UI view has been created. That is when fragment.onViewCreated() is called. - * - * @param ui The Ui implementation that is now ready to be used. - */ - public void onUiReady(U ui) { - mUi = ui; - } - - /** - * Called when the UI view is destroyed in Fragment.onDestroyView(). - */ - public final void onUiDestroy(U ui) { - onUiUnready(ui); - mUi = null; - } - - /** - * To be overriden by Presenter implementations. Called when the fragment is being - * destroyed but before ui is set to null. - */ - public void onUiUnready(U ui) { - } - - public void onSaveInstanceState(Bundle outState) {} - - public void onRestoreInstanceState(Bundle savedInstanceState) {} - - public U getUi() { - return mUi; - } -} diff --git a/InCallUI/src/com/android/incallui/ProximitySensor.java b/InCallUI/src/com/android/incallui/ProximitySensor.java deleted file mode 100644 index 3c9fd937019e31cf171fad48225d952283a0bfb9..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ProximitySensor.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Objects; - -import android.content.Context; -import android.content.res.Configuration; -import android.hardware.display.DisplayManager; -import android.hardware.display.DisplayManager.DisplayListener; -import android.os.PowerManager; -import android.telecom.CallAudioState; -import android.view.Display; - -import com.android.incallui.AudioModeProvider.AudioModeListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; - -/** - * Class manages the proximity sensor for the in-call UI. - * We enable the proximity sensor while the user in a phone call. The Proximity sensor turns off - * the touchscreen and display when the user is close to the screen to prevent user's cheek from - * causing touch events. - * The class requires special knowledge of the activity and device state to know when the proximity - * sensor should be enabled and disabled. Most of that state is fed into this class through - * public methods. - */ -public class ProximitySensor implements AccelerometerListener.OrientationListener, - InCallStateListener, AudioModeListener { - private static final String TAG = ProximitySensor.class.getSimpleName(); - - private final PowerManager mPowerManager; - private final PowerManager.WakeLock mProximityWakeLock; - private final AudioModeProvider mAudioModeProvider; - private final AccelerometerListener mAccelerometerListener; - private final ProximityDisplayListener mDisplayListener; - private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; - private boolean mUiShowing = false; - private boolean mIsPhoneOffhook = false; - private boolean mDialpadVisible; - - // True if the keyboard is currently *not* hidden - // Gets updated whenever there is a Configuration change - private boolean mIsHardKeyboardOpen; - - public ProximitySensor(Context context, AudioModeProvider audioModeProvider, - AccelerometerListener accelerometerListener) { - mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - if (mPowerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { - mProximityWakeLock = mPowerManager.newWakeLock( - PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); - } else { - Log.w(TAG, "Device does not support proximity wake lock."); - mProximityWakeLock = null; - } - mAccelerometerListener = accelerometerListener; - mAccelerometerListener.setListener(this); - - mDisplayListener = new ProximityDisplayListener( - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)); - mDisplayListener.register(); - - mAudioModeProvider = audioModeProvider; - mAudioModeProvider.addListener(this); - } - - public void tearDown() { - mAudioModeProvider.removeListener(this); - - mAccelerometerListener.enable(false); - mDisplayListener.unregister(); - - turnOffProximitySensor(true); - } - - /** - * Called to identify when the device is laid down flat. - */ - @Override - public void orientationChanged(int orientation) { - mOrientation = orientation; - updateProximitySensorMode(); - } - - /** - * Called to keep track of the overall UI state. - */ - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - // We ignore incoming state because we do not want to enable proximity - // sensor during incoming call screen. We check hasLiveCall() because a disconnected call - // can also put the in-call screen in the INCALL state. - boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall(); - boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall; - - if (isOffhook != mIsPhoneOffhook) { - mIsPhoneOffhook = isOffhook; - - mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; - mAccelerometerListener.enable(mIsPhoneOffhook); - - updateProximitySensorMode(); - } - } - - @Override - public void onSupportedAudioMode(int modeMask) { - } - - @Override - public void onMute(boolean muted) { - } - - /** - * Called when the audio mode changes during a call. - */ - @Override - public void onAudioMode(int mode) { - updateProximitySensorMode(); - } - - public void onDialpadVisible(boolean visible) { - mDialpadVisible = visible; - updateProximitySensorMode(); - } - - /** - * Called by InCallActivity to listen for hard keyboard events. - */ - public void onConfigurationChanged(Configuration newConfig) { - mIsHardKeyboardOpen = newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO; - - // Update the Proximity sensor based on keyboard state - updateProximitySensorMode(); - } - - /** - * Used to save when the UI goes in and out of the foreground. - */ - public void onInCallShowing(boolean showing) { - if (showing) { - mUiShowing = true; - - // We only consider the UI not showing for instances where another app took the foreground. - // If we stopped showing because the screen is off, we still consider that showing. - } else if (mPowerManager.isScreenOn()) { - mUiShowing = false; - } - updateProximitySensorMode(); - } - - void onDisplayStateChanged(boolean isDisplayOn) { - Log.i(this, "isDisplayOn: " + isDisplayOn); - mAccelerometerListener.enable(isDisplayOn); - } - - /** - * TODO: There is no way to determine if a screen is off due to proximity or if it is - * legitimately off, but if ever we can do that in the future, it would be useful here. - * Until then, this function will simply return true of the screen is off. - * TODO: Investigate whether this can be replaced with the ProximityDisplayListener. - */ - public boolean isScreenReallyOff() { - return !mPowerManager.isScreenOn(); - } - - private void turnOnProximitySensor() { - if (mProximityWakeLock != null) { - if (!mProximityWakeLock.isHeld()) { - Log.i(this, "Acquiring proximity wake lock"); - mProximityWakeLock.acquire(); - } else { - Log.i(this, "Proximity wake lock already acquired"); - } - } - } - - private void turnOffProximitySensor(boolean screenOnImmediately) { - if (mProximityWakeLock != null) { - if (mProximityWakeLock.isHeld()) { - Log.i(this, "Releasing proximity wake lock"); - int flags = - (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - mProximityWakeLock.release(flags); - } else { - Log.i(this, "Proximity wake lock already released"); - } - } - } - - /** - * Updates the wake lock used to control proximity sensor behavior, - * based on the current state of the phone. - * - * On devices that have a proximity sensor, to avoid false touches - * during a call, we hold a PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock - * whenever the phone is off hook. (When held, that wake lock causes - * the screen to turn off automatically when the sensor detects an - * object close to the screen.) - * - * This method is a no-op for devices that don't have a proximity - * sensor. - * - * Proximity wake lock will *not* be held if any one of the - * conditions is true while on a call: - * 1) If the audio is routed via Bluetooth - * 2) If a wired headset is connected - * 3) if the speaker is ON - * 4) If the slider is open(i.e. the hardkeyboard is *not* hidden) - */ - private synchronized void updateProximitySensorMode() { - final int audioMode = mAudioModeProvider.getAudioMode(); - - // turn proximity sensor off and turn screen on immediately if - // we are using a headset, the keyboard is open, or the device - // is being held in a horizontal position. - boolean screenOnImmediately = (CallAudioState.ROUTE_WIRED_HEADSET == audioMode - || CallAudioState.ROUTE_SPEAKER == audioMode - || CallAudioState.ROUTE_BLUETOOTH == audioMode - || mIsHardKeyboardOpen); - - // We do not keep the screen off when the user is outside in-call screen and we are - // horizontal, but we do not force it on when we become horizontal until the - // proximity sensor goes negative. - final boolean horizontal = - (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL); - screenOnImmediately |= !mUiShowing && horizontal; - - // We do not keep the screen off when dialpad is visible, we are horizontal, and - // the in-call screen is being shown. - // At that moment we're pretty sure users want to use it, instead of letting the - // proximity sensor turn off the screen by their hands. - screenOnImmediately |= mDialpadVisible && horizontal; - - Log.v(this, "screenonImmediately: ", screenOnImmediately); - - Log.i(this, Objects.toStringHelper(this) - .add("keybrd", mIsHardKeyboardOpen ? 1 : 0) - .add("dpad", mDialpadVisible ? 1 : 0) - .add("offhook", mIsPhoneOffhook ? 1 : 0) - .add("hor", horizontal ? 1 : 0) - .add("ui", mUiShowing ? 1 : 0) - .add("aud", CallAudioState.audioRouteToString(audioMode)) - .toString()); - - if (mIsPhoneOffhook && !screenOnImmediately) { - Log.d(this, "Turning on proximity sensor"); - // Phone is in use! Arrange for the screen to turn off - // automatically when the sensor detects a close object. - turnOnProximitySensor(); - } else { - Log.d(this, "Turning off proximity sensor"); - // Phone is either idle, or ringing. We don't want any special proximity sensor - // behavior in either case. - turnOffProximitySensor(screenOnImmediately); - } - } - - /** - * Implementation of a {@link DisplayListener} that maintains a binary state: - * Screen on vs screen off. Used by the proximity sensor manager to decide whether or not - * it needs to listen to accelerometer events. - */ - public class ProximityDisplayListener implements DisplayListener { - private DisplayManager mDisplayManager; - private boolean mIsDisplayOn = true; - - ProximityDisplayListener(DisplayManager displayManager) { - mDisplayManager = displayManager; - } - - void register() { - mDisplayManager.registerDisplayListener(this, null); - } - - void unregister() { - mDisplayManager.unregisterDisplayListener(this); - } - - @Override - public void onDisplayRemoved(int displayId) { - } - - @Override - public void onDisplayChanged(int displayId) { - if (displayId == Display.DEFAULT_DISPLAY) { - final Display display = mDisplayManager.getDisplay(displayId); - - final boolean isDisplayOn = display.getState() != Display.STATE_OFF; - // For call purposes, we assume that as long as the screen is not truly off, it is - // considered on, even if it is in an unknown or low power idle state. - if (isDisplayOn != mIsDisplayOn) { - mIsDisplayOn = isDisplayOn; - onDisplayStateChanged(mIsDisplayOn); - } - } - } - - @Override - public void onDisplayAdded(int displayId) { - } - } -} diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java deleted file mode 100644 index cc87dd414e9ec0bfbb72772bf17ed59faf63dd61..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java +++ /dev/null @@ -1,793 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL; - -import com.google.common.base.Preconditions; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.media.AudioAttributes; -import android.net.Uri; -import android.provider.ContactsContract.Contacts; -import android.support.annotation.Nullable; -import android.telecom.Call.Details; -import android.telecom.PhoneAccount; -import android.telecom.TelecomManager; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.ContactsUtils.UserType; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.contacts.common.util.BitmapUtil; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.dialer.service.ExtendedCallInfoService; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; -import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.async.PausableExecutorImpl; -import com.android.incallui.ringtone.DialerRingtoneManager; -import com.android.incallui.ringtone.InCallTonePlayer; -import com.android.incallui.ringtone.ToneGeneratorFactory; - -import java.util.Objects; - -/** - * This class adds Notifications to the status bar for the in-call experience. - */ -public class StatusBarNotifier implements InCallPresenter.InCallStateListener, - CallList.CallUpdateListener { - - // Notification types - // Indicates that no notification is currently showing. - private static final int NOTIFICATION_NONE = 0; - // Notification for an active call. This is non-interruptive, but cannot be dismissed. - private static final int NOTIFICATION_IN_CALL = 1; - // Notification for incoming calls. This is interruptive and will show up as a HUN. - private static final int NOTIFICATION_INCOMING_CALL = 2; - - private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0; - private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1; - - private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; - - private final Context mContext; - @Nullable private ContactsPreferences mContactsPreferences; - private final ContactInfoCache mContactInfoCache; - private final NotificationManager mNotificationManager; - private final DialerRingtoneManager mDialerRingtoneManager; - private int mCurrentNotification = NOTIFICATION_NONE; - private int mCallState = Call.State.INVALID; - private int mSavedIcon = 0; - private String mSavedContent = null; - private Bitmap mSavedLargeIcon; - private String mSavedContentTitle; - private String mCallId = null; - private InCallState mInCallState; - private Uri mRingtone; - - public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache) { - Preconditions.checkNotNull(context); - mContext = context; - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - mContactInfoCache = contactInfoCache; - mNotificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - mDialerRingtoneManager = new DialerRingtoneManager( - new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()), - CallList.getInstance()); - mCurrentNotification = NOTIFICATION_NONE; - } - - /** - * Creates notifications according to the state we receive from {@link InCallPresenter}. - */ - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - Log.d(this, "onStateChange"); - mInCallState = newState; - updateNotification(newState, callList); - } - - /** - * Updates the phone app's status bar notification *and* launches the - * incoming call UI in response to a new incoming call. - * - * If an incoming call is ringing (or call-waiting), the notification - * will also include a "fullScreenIntent" that will cause the - * InCallScreen to be launched, unless the current foreground activity - * is marked as "immersive". - * - * (This is the mechanism that actually brings up the incoming call UI - * when we receive a "new ringing connection" event from the telephony - * layer.) - * - * Also note that this method is safe to call even if the phone isn't - * actually ringing (or, more likely, if an incoming call *was* - * ringing briefly but then disconnected). In that case, we'll simply - * update or cancel the in-call notification based on the current - * phone state. - * - * @see #updateInCallNotification(InCallState,CallList) - */ - public void updateNotification(InCallState state, CallList callList) { - updateInCallNotification(state, callList); - } - - /** - * Take down the in-call notification. - * @see #updateInCallNotification(InCallState,CallList) - */ - private void cancelNotification() { - if (!TextUtils.isEmpty(mCallId)) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - mCallId = null; - } - if (mCurrentNotification != NOTIFICATION_NONE) { - Log.d(this, "cancelInCall()..."); - mNotificationManager.cancel(mCurrentNotification); - } - mCurrentNotification = NOTIFICATION_NONE; - } - - /** - * Should only be called from a irrecoverable state where it is necessary to dismiss all - * notifications. - */ - static void clearAllCallNotifications(Context backupContext) { - Log.i(StatusBarNotifier.class.getSimpleName(), - "Something terrible happened. Clear all InCall notifications"); - - NotificationManager notificationManager = - (NotificationManager) backupContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFICATION_IN_CALL); - notificationManager.cancel(NOTIFICATION_INCOMING_CALL); - } - - /** - * Helper method for updateInCallNotification() and - * updateNotification(): Update the phone app's - * status bar notification based on the current telephony state, or - * cancels the notification if the phone is totally idle. - */ - private void updateInCallNotification(final InCallState state, CallList callList) { - Log.d(this, "updateInCallNotification..."); - - final Call call = getCallToShow(callList); - - if (call != null) { - showNotification(call); - } else { - cancelNotification(); - } - } - - private void showNotification(final Call call) { - final boolean isIncoming = (call.getState() == Call.State.INCOMING || - call.getState() == Call.State.CALL_WAITING); - if (!TextUtils.isEmpty(mCallId)) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - mCallId = call.getId(); - CallList.getInstance().addCallUpdateListener(call.getId(), this); - - // we make a call to the contact info cache to query for supplemental data to what the - // call provides. This includes the contact name and photo. - // This callback will always get called immediately and synchronously with whatever data - // it has available, and may make a subsequent call later (same thread) if it had to - // call into the contacts provider for more data. - mContactInfoCache.findInfo(call, isIncoming, new ContactInfoCacheCallback() { - @Override - public void onContactInfoComplete(String callId, ContactCacheEntry entry) { - Call call = CallList.getInstance().getCallById(callId); - if (call != null) { - call.getLogState().contactLookupResult = entry.contactLookupResult; - buildAndSendNotification(call, entry); - } - } - - @Override - public void onImageLoadComplete(String callId, ContactCacheEntry entry) { - Call call = CallList.getInstance().getCallById(callId); - if (call != null) { - buildAndSendNotification(call, entry); - } - } - - @Override - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {} - }); - } - - /** - * Sets up the main Ui for the notification - */ - private void buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo) { - // This can get called to update an existing notification after contact information has come - // back. However, it can happen much later. Before we continue, we need to make sure that - // the call being passed in is still the one we want to show in the notification. - final Call call = getCallToShow(CallList.getInstance()); - if (call == null || !call.getId().equals(originalCall.getId())) { - return; - } - - final int callState = call.getState(); - // Dont' show as spam if the number is in local contact. - if (contactInfo.contactLookupResult == Call.LogState.LOOKUP_LOCAL_CONTACT) { - call.setSpam(false); - } - - // Check if data has changed; if nothing is different, don't issue another notification. - final int iconResId = getIconToDisplay(call); - Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call); - final String content = - getContentString(call, contactInfo.userType); - final String contentTitle = getContentTitle(contactInfo, call); - - final boolean isVideoUpgradeRequest = call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - final int notificationType; - if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING - || isVideoUpgradeRequest) { - notificationType = NOTIFICATION_INCOMING_CALL; - } else { - notificationType = NOTIFICATION_IN_CALL; - } - - if (!checkForChangeAndSaveData(iconResId, content, largeIcon, contentTitle, callState, - notificationType, contactInfo.contactRingtoneUri)) { - return; - } - - if (largeIcon != null) { - largeIcon = getRoundedIcon(largeIcon); - } - - /* - * This builder is used for the notification shown when the device is locked and the user - * has set their notification settings to 'hide sensitive content' - * {@see Notification.Builder#setPublicVersion}. - */ - Notification.Builder publicBuilder = new Notification.Builder(mContext); - publicBuilder.setSmallIcon(iconResId) - .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) - // Hide work call state for the lock screen notification - .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT)); - setNotificationWhen(call, callState, publicBuilder); - - /* - * Builder for the notification shown when the device is unlocked or the user has set their - * notification settings to 'show all notification content'. - */ - final Notification.Builder builder = getNotificationBuilder(); - builder.setPublicVersion(publicBuilder.build()); - - // Set up the main intent to send the user to the in-call screen - builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */)); - - // Set the intent as a full screen intent as well if a call is incoming - if (notificationType == NOTIFICATION_INCOMING_CALL - && !InCallPresenter.getInstance().isShowingInCallUi()) { - configureFullScreenIntent( - builder, createLaunchPendingIntent(true /* isFullScreen */), call); - // Set the notification category for incoming calls - builder.setCategory(Notification.CATEGORY_CALL); - } - - // Set the content - builder.setContentText(content); - builder.setSmallIcon(iconResId); - builder.setContentTitle(contentTitle); - builder.setLargeIcon(largeIcon); - builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); - - if (isVideoUpgradeRequest) { - builder.setUsesChronometer(false); - addDismissUpgradeRequestAction(builder); - addAcceptUpgradeRequestAction(builder); - } else { - createIncomingCallNotification(call, callState, builder); - } - - addPersonReference(builder, contactInfo, call); - - /* - * Fire off the notification - */ - Notification notification = builder.build(); - - if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) { - notification.flags |= Notification.FLAG_INSISTENT; - notification.sound = contactInfo.contactRingtoneUri; - AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder(); - audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC); - audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); - notification.audioAttributes = audioAttributes.build(); - if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) { - notification.vibrate = VIBRATE_PATTERN; - } - } - if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) { - Log.v(this, "Playing call waiting tone"); - mDialerRingtoneManager.playCallWaitingTone(); - } - if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) { - Log.i(this, "Previous notification already showing - cancelling " - + mCurrentNotification); - mNotificationManager.cancel(mCurrentNotification); - } - - Log.i(this, "Displaying notification for " + notificationType); - mNotificationManager.notify(notificationType, notification); - call.getLatencyReport().onNotificationShown(); - mCurrentNotification = notificationType; - } - - private void createIncomingCallNotification( - Call call, int state, Notification.Builder builder) { - setNotificationWhen(call, state, builder); - - // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). - if (state == Call.State.ACTIVE || - state == Call.State.ONHOLD || - Call.State.isDialing(state)) { - addHangupAction(builder); - } else if (state == Call.State.INCOMING || state == Call.State.CALL_WAITING) { - addDismissAction(builder); - if (call.isVideoCall(mContext)) { - addVoiceAction(builder); - addVideoCallAction(builder); - } else { - addAnswerAction(builder); - } - } - } - - /* - * Sets the notification's when section as needed. For active calls, this is explicitly set as - * the duration of the call. For all other states, the notification will automatically show the - * time at which the notification was created. - */ - private void setNotificationWhen(Call call, int state, Notification.Builder builder) { - if (state == Call.State.ACTIVE) { - builder.setUsesChronometer(true); - builder.setWhen(call.getConnectTimeMillis()); - } else { - builder.setUsesChronometer(false); - } - } - - /** - * Checks the new notification data and compares it against any notification that we - * are already displaying. If the data is exactly the same, we return false so that - * we do not issue a new notification for the exact same data. - */ - private boolean checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, - String contentTitle, int state, int notificationType, Uri ringtone) { - - // The two are different: - // if new title is not null, it should be different from saved version OR - // if new title is null, the saved version should not be null - final boolean contentTitleChanged = - (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) || - (contentTitle == null && mSavedContentTitle != null); - - // any change means we are definitely updating - boolean retval = (mSavedIcon != icon) || !Objects.equals(mSavedContent, content) - || (mCallState != state) || (mSavedLargeIcon != largeIcon) - || contentTitleChanged || !Objects.equals(mRingtone, ringtone); - - // If we aren't showing a notification right now or the notification type is changing, - // definitely do an update. - if (mCurrentNotification != notificationType) { - if (mCurrentNotification == NOTIFICATION_NONE) { - Log.d(this, "Showing notification for first time."); - } - retval = true; - } - - mSavedIcon = icon; - mSavedContent = content; - mCallState = state; - mSavedLargeIcon = largeIcon; - mSavedContentTitle = contentTitle; - mRingtone = ringtone; - - if (retval) { - Log.d(this, "Data changed. Showing notification"); - } - - return retval; - } - - /** - * Returns the main string to use in the notification. - */ - @NeededForTesting - String getContentTitle(ContactCacheEntry contactInfo, Call call) { - if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { - return mContext.getResources().getString(R.string.card_title_conf_call); - } - - String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, - contactInfo.nameAlternative, mContactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() - .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); - } - return preferredName; - } - - private void addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, - Call call) { - // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. - // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid - // NotificationManager using it. - if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { - builder.addPerson(contactInfo.lookupUri.toString()); - } else if (!TextUtils.isEmpty(call.getNumber())) { - builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, - call.getNumber(), null).toString()); - } - } - - /** - * Gets a large icon from the contact info object to display in the notification. - */ - private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call) { - Bitmap largeIcon = null; - if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { - largeIcon = BitmapFactory.decodeResource(mContext.getResources(), - R.drawable.img_conference); - } - if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { - largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); - } - if (call.isSpam()) { - Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact); - largeIcon = CallCardFragment.drawableToBitmap(drawable); - } - return largeIcon; - } - - private Bitmap getRoundedIcon(Bitmap bitmap) { - if (bitmap == null) { - return null; - } - final int height = (int) mContext.getResources().getDimension( - android.R.dimen.notification_large_icon_height); - final int width = (int) mContext.getResources().getDimension( - android.R.dimen.notification_large_icon_width); - return BitmapUtil.getRoundedBitmap(bitmap, width, height); - } - - /** - * Returns the appropriate icon res Id to display based on the call for which - * we want to display information. - */ - private int getIconToDisplay(Call call) { - // Even if both lines are in use, we only show a single item in - // the expanded Notifications UI. It's labeled "Ongoing call" - // (or "On hold" if there's only one call, and it's on hold.) - // Also, we don't have room to display caller-id info from two - // different calls. So if both lines are in use, display info - // from the foreground call. And if there's a ringing call, - // display that regardless of the state of the other calls. - if (call.getState() == Call.State.ONHOLD) { - return R.drawable.ic_phone_paused_white_24dp; - } else if (call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - return R.drawable.ic_videocam; - } - return R.drawable.ic_call_white_24dp; - } - - /** - * Returns the message to use with the notification. - */ - private String getContentString(Call call, @UserType long userType) { - boolean isIncomingOrWaiting = call.getState() == Call.State.INCOMING || - call.getState() == Call.State.CALL_WAITING; - - if (isIncomingOrWaiting && - call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) { - - if (!TextUtils.isEmpty(call.getChildNumber())) { - return mContext.getString(R.string.child_number, call.getChildNumber()); - } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) { - return call.getCallSubject(); - } - } - - int resId = R.string.notification_ongoing_call; - if (call.hasProperty(Details.PROPERTY_WIFI)) { - resId = R.string.notification_ongoing_call_wifi; - } - - if (isIncomingOrWaiting) { - if (call.hasProperty(Details.PROPERTY_WIFI)) { - resId = R.string.notification_incoming_call_wifi; - } else { - if (call.isSpam()) { - resId = R.string.notification_incoming_spam_call; - } else { - resId = R.string.notification_incoming_call; - } - } - } else if (call.getState() == Call.State.ONHOLD) { - resId = R.string.notification_on_hold; - } else if (Call.State.isDialing(call.getState())) { - resId = R.string.notification_dialing; - } else if (call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - resId = R.string.notification_requesting_video_call; - } - - // Is the call placed through work connection service. - boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL); - if(userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) { - resId = getWorkStringFromPersonalString(resId); - } - - return mContext.getString(resId); - } - - private static int getWorkStringFromPersonalString(int resId) { - if (resId == R.string.notification_ongoing_call) { - return R.string.notification_ongoing_work_call; - } else if (resId == R.string.notification_ongoing_call_wifi) { - return R.string.notification_ongoing_work_call_wifi; - } else if (resId == R.string.notification_incoming_call_wifi) { - return R.string.notification_incoming_work_call_wifi; - } else if (resId == R.string.notification_incoming_call) { - return R.string.notification_incoming_work_call; - } else { - return resId; - } - } - - /** - * Gets the most relevant call to display in the notification. - */ - private Call getCallToShow(CallList callList) { - if (callList == null) { - return null; - } - Call call = callList.getIncomingCall(); - if (call == null) { - call = callList.getOutgoingCall(); - } - if (call == null) { - call = callList.getVideoUpgradeRequestCall(); - } - if (call == null) { - call = callList.getActiveOrBackgroundCall(); - } - return call; - } - - private void addAnswerAction(Notification.Builder builder) { - Log.d(this, "Will show \"answer\" action in the incoming call Notification"); - - PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( - mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); - builder.addAction(R.drawable.ic_call_white_24dp, - mContext.getText(R.string.notification_action_answer), - answerVoicePendingIntent); - } - - private void addDismissAction(Notification.Builder builder) { - Log.d(this, "Will show \"dismiss\" action in the incoming call Notification"); - - PendingIntent declinePendingIntent = - createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL); - builder.addAction(R.drawable.ic_close_dk, - mContext.getText(R.string.notification_action_dismiss), - declinePendingIntent); - } - - private void addHangupAction(Notification.Builder builder) { - Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification"); - - PendingIntent hangupPendingIntent = - createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL); - builder.addAction(R.drawable.ic_call_end_white_24dp, - mContext.getText(R.string.notification_action_end_call), - hangupPendingIntent); - } - - private void addVideoCallAction(Notification.Builder builder) { - Log.i(this, "Will show \"video\" action in the incoming call Notification"); - - PendingIntent answerVideoPendingIntent = createNotificationPendingIntent( - mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL); - builder.addAction(R.drawable.ic_videocam, - mContext.getText(R.string.notification_action_answer_video), - answerVideoPendingIntent); - } - - private void addVoiceAction(Notification.Builder builder) { - Log.d(this, "Will show \"voice\" action in the incoming call Notification"); - - PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( - mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); - builder.addAction(R.drawable.ic_call_white_24dp, - mContext.getText(R.string.notification_action_answer_voice), - answerVoicePendingIntent); - } - - private void addAcceptUpgradeRequestAction(Notification.Builder builder) { - Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification"); - - PendingIntent acceptVideoPendingIntent = createNotificationPendingIntent( - mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST); - builder.addAction(0, mContext.getText(R.string.notification_action_accept), - acceptVideoPendingIntent); - } - - private void addDismissUpgradeRequestAction(Notification.Builder builder) { - Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification"); - - PendingIntent declineVideoPendingIntent = createNotificationPendingIntent( - mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST); - builder.addAction(0, mContext.getText(R.string.notification_action_dismiss), - declineVideoPendingIntent); - } - - /** - * Adds fullscreen intent to the builder. - */ - private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, - Call call) { - // Ok, we actually want to launch the incoming call - // UI at this point (in addition to simply posting a notification - // to the status bar). Setting fullScreenIntent will cause - // the InCallScreen to be launched immediately *unless* the - // current foreground activity is marked as "immersive". - Log.d(this, "- Setting fullScreenIntent: " + intent); - builder.setFullScreenIntent(intent, true); - - // Ugly hack alert: - // - // The NotificationManager has the (undocumented) behavior - // that it will *ignore* the fullScreenIntent field if you - // post a new Notification that matches the ID of one that's - // already active. Unfortunately this is exactly what happens - // when you get an incoming call-waiting call: the - // "ongoing call" notification is already visible, so the - // InCallScreen won't get launched in this case! - // (The result: if you bail out of the in-call UI while on a - // call and then get a call-waiting call, the incoming call UI - // won't come up automatically.) - // - // The workaround is to just notice this exact case (this is a - // call-waiting call *and* the InCallScreen is not in the - // foreground) and manually cancel the in-call notification - // before (re)posting it. - // - // TODO: there should be a cleaner way of avoiding this - // problem (see discussion in bug 3184149.) - - // If a call is onhold during an incoming call, the call actually comes in as - // INCOMING. For that case *and* traditional call-waiting, we want to - // cancel the notification. - boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING || - (call.getState() == Call.State.INCOMING && - CallList.getInstance().getBackgroundCall() != null)); - - if (isCallWaiting) { - Log.i(this, "updateInCallNotification: call-waiting! force relaunch..."); - // Cancel the IN_CALL_NOTIFICATION immediately before - // (re)posting it; this seems to force the - // NotificationManager to launch the fullScreenIntent. - mNotificationManager.cancel(NOTIFICATION_IN_CALL); - } - } - - private Notification.Builder getNotificationBuilder() { - final Notification.Builder builder = new Notification.Builder(mContext); - builder.setOngoing(true); - - // Make the notification prioritized over the other normal notifications. - builder.setPriority(Notification.PRIORITY_HIGH); - - return builder; - } - - private PendingIntent createLaunchPendingIntent(boolean isFullScreen) { - Intent intent = InCallPresenter.getInstance().getInCallIntent( - false /* showDialpad */, false /* newOutgoingCall */); - - int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN; - if (isFullScreen) { - intent.putExtra(InCallActivity.FOR_FULL_SCREEN_INTENT, true); - // Use a unique request code so that the pending intent isn't clobbered by the - // non-full screen pending intent. - requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN; - } - - // PendingIntent that can be used to launch the InCallActivity. The - // system fires off this intent if the user pulls down the windowshade - // and clicks the notification's expanded view. It's also used to - // launch the InCallActivity immediately when when there's an incoming - // call (see the "fullScreenIntent" field below). - return PendingIntent.getActivity(mContext, requestCode, intent, 0); - } - - /** - * Returns PendingIntent for answering a phone call. This will typically be used from - * Notification context. - */ - private static PendingIntent createNotificationPendingIntent(Context context, String action) { - final Intent intent = new Intent(action, null, - context, NotificationBroadcastReceiver.class); - return PendingIntent.getBroadcast(context, 0, intent, 0); - } - - @Override - public void onCallChanged(Call call) { - if (CallList.getInstance().getIncomingCall() == null) { - mDialerRingtoneManager.stopCallWaitingTone(); - } - } - - /** - * Responds to changes in the session modification state for the call by dismissing the - * status bar notification as required. - * - * @param sessionModificationState The new session modification state. - */ - @Override - public void onSessionModificationStateChange(int sessionModificationState) { - if (sessionModificationState == Call.SessionModificationState.NO_REQUEST) { - if (mCallId != null) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - - updateNotification(mInCallState, CallList.getInstance()); - } - } - - @Override - public void onLastForwardedNumberChange() { - // no-op - } - - @Override - public void onChildNumberChange() { - // no-op - } -} diff --git a/InCallUI/src/com/android/incallui/TelecomAdapter.java b/InCallUI/src/com/android/incallui/TelecomAdapter.java deleted file mode 100644 index f172270ddfc07a937da435a3d437a8b938a6c291..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/TelecomAdapter.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.os.Looper; -import android.telecom.InCallService; -import android.telecom.PhoneAccountHandle; - -import java.util.List; - -final class TelecomAdapter implements InCallServiceListener { - private static final String ADD_CALL_MODE_KEY = "add_call_mode"; - - private static TelecomAdapter sInstance; - private InCallService mInCallService; - - static TelecomAdapter getInstance() { - Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread()); - if (sInstance == null) { - sInstance = new TelecomAdapter(); - } - return sInstance; - } - - private TelecomAdapter() { - } - - @Override - public void setInCallService(InCallService inCallService) { - mInCallService = inCallService; - } - - @Override - public void clearInCallService() { - mInCallService = null; - } - - private android.telecom.Call getTelecomCallById(String callId) { - Call call = CallList.getInstance().getCallById(callId); - return call == null ? null : call.getTelecomCall(); - } - - void answerCall(String callId, int videoState) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.answer(videoState); - } else { - Log.e(this, "error answerCall, call not in call list: " + callId); - } - } - - void rejectCall(String callId, boolean rejectWithMessage, String message) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.reject(rejectWithMessage, message); - } else { - Log.e(this, "error rejectCall, call not in call list: " + callId); - } - } - - void disconnectCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.disconnect(); - } else { - Log.e(this, "error disconnectCall, call not in call list " + callId); - } - } - - void holdCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.hold(); - } else { - Log.e(this, "error holdCall, call not in call list " + callId); - } - } - - void unholdCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.unhold(); - } else { - Log.e(this, "error unholdCall, call not in call list " + callId); - } - } - - void mute(boolean shouldMute) { - if (mInCallService != null) { - mInCallService.setMuted(shouldMute); - } else { - Log.e(this, "error mute, mInCallService is null"); - } - } - - void setAudioRoute(int route) { - if (mInCallService != null) { - mInCallService.setAudioRoute(route); - } else { - Log.e(this, "error setAudioRoute, mInCallService is null"); - } - } - - void separateCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.splitFromConference(); - } else { - Log.e(this, "error separateCall, call not in call list " + callId); - } - } - - void merge(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - List conferenceable = call.getConferenceableCalls(); - if (!conferenceable.isEmpty()) { - call.conference(conferenceable.get(0)); - } else { - if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE)) { - call.mergeConference(); - } - } - } else { - Log.e(this, "error merge, call not in call list " + callId); - } - } - - void swap(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE)) { - call.swapConference(); - } - } else { - Log.e(this, "error swap, call not in call list " + callId); - } - } - - void addCall() { - if (mInCallService != null) { - Intent intent = new Intent(Intent.ACTION_DIAL); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - // when we request the dialer come up, we also want to inform - // it that we're going through the "add call" option from the - // InCallScreen / PhoneUtils. - intent.putExtra(ADD_CALL_MODE_KEY, true); - try { - Log.d(this, "Sending the add Call intent"); - mInCallService.startActivity(intent); - } catch (ActivityNotFoundException e) { - // This is rather rare but possible. - // Note: this method is used even when the phone is encrypted. At that moment - // the system may not find any Activity which can accept this Intent. - Log.e(this, "Activity for adding calls isn't found.", e); - } - } - } - - void playDtmfTone(String callId, char digit) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.playDtmfTone(digit); - } else { - Log.e(this, "error playDtmfTone, call not in call list " + callId); - } - } - - void stopDtmfTone(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.stopDtmfTone(); - } else { - Log.e(this, "error stopDtmfTone, call not in call list " + callId); - } - } - - void postDialContinue(String callId, boolean proceed) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.postDialContinue(proceed); - } else { - Log.e(this, "error postDialContinue, call not in call list " + callId); - } - } - - void phoneAccountSelected(String callId, PhoneAccountHandle accountHandle, boolean setDefault) { - if (accountHandle == null) { - Log.e(this, "error phoneAccountSelected, accountHandle is null"); - // TODO: Do we really want to send null accountHandle? - } - - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.phoneAccountSelected(accountHandle, setDefault); - } else { - Log.e(this, "error phoneAccountSelected, call not in call list " + callId); - } - } - - boolean canAddCall() { - if (mInCallService != null) { - return mInCallService.canAddCall(); - } - return false; - } -} diff --git a/InCallUI/src/com/android/incallui/VideoCallFragment.java b/InCallUI/src/com/android/incallui/VideoCallFragment.java deleted file mode 100644 index 6a46a423d3668d0522fe9317035cc43ca2e3f004..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/VideoCallFragment.java +++ /dev/null @@ -1,901 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.graphics.Matrix; -import android.graphics.Point; -import android.graphics.SurfaceTexture; -import android.os.Bundle; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.Surface; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import com.android.dialer.R; -import com.android.phone.common.animation.AnimUtils; -import com.google.common.base.Objects; - -/** - * Fragment containing video calling surfaces. - */ -public class VideoCallFragment extends BaseFragment implements VideoCallPresenter.VideoCallUi { - private static final String TAG = VideoCallFragment.class.getSimpleName(); - private static final boolean DEBUG = false; - - /** - * Used to indicate that the surface dimensions are not set. - */ - private static final int DIMENSIONS_NOT_SET = -1; - - /** - * Surface ID for the display surface. - */ - public static final int SURFACE_DISPLAY = 1; - - /** - * Surface ID for the preview surface. - */ - public static final int SURFACE_PREVIEW = 2; - - /** - * Used to indicate that the UI rotation is unknown. - */ - public static final int ORIENTATION_UNKNOWN = -1; - - // Static storage used to retain the video surfaces across Activity restart. - // TextureViews are not parcelable, so it is not possible to store them in the saved state. - private static boolean sVideoSurfacesInUse = false; - private static VideoCallSurface sPreviewSurface = null; - private static VideoCallSurface sDisplaySurface = null; - private static Point sDisplaySize = null; - - /** - * {@link ViewStub} holding the video call surfaces. This is the parent for the - * {@link VideoCallFragment}. Used to ensure that the video surfaces are only inflated when - * required. - */ - private ViewStub mVideoViewsStub; - - /** - * Inflated view containing the video call surfaces represented by the {@link ViewStub}. - */ - private View mVideoViews; - - /** - * The {@link FrameLayout} containing the preview surface. - */ - private View mPreviewVideoContainer; - - /** - * Icon shown to indicate that the outgoing camera has been turned off. - */ - private View mCameraOff; - - /** - * {@link ImageView} containing the user's profile photo. - */ - private ImageView mPreviewPhoto; - - /** - * {@code True} when the layout of the activity has been completed. - */ - private boolean mIsLayoutComplete = false; - - /** - * {@code True} if in landscape mode. - */ - private boolean mIsLandscape; - - private int mAnimationDuration; - - /** - * Inner-class representing a {@link TextureView} and its associated {@link SurfaceTexture} and - * {@link Surface}. Used to manage the lifecycle of these objects across device orientation - * changes. - */ - private static class VideoCallSurface implements TextureView.SurfaceTextureListener, - View.OnClickListener, View.OnAttachStateChangeListener { - private int mSurfaceId; - private VideoCallPresenter mPresenter; - private TextureView mTextureView; - private SurfaceTexture mSavedSurfaceTexture; - private Surface mSavedSurface; - private boolean mIsDoneWithSurface; - private int mWidth = DIMENSIONS_NOT_SET; - private int mHeight = DIMENSIONS_NOT_SET; - - /** - * Creates an instance of a {@link VideoCallSurface}. - * - * @param surfaceId The surface ID of the surface. - * @param textureView The {@link TextureView} for the surface. - */ - public VideoCallSurface(VideoCallPresenter presenter, int surfaceId, - TextureView textureView) { - this(presenter, surfaceId, textureView, DIMENSIONS_NOT_SET, DIMENSIONS_NOT_SET); - } - - /** - * Creates an instance of a {@link VideoCallSurface}. - * - * @param surfaceId The surface ID of the surface. - * @param textureView The {@link TextureView} for the surface. - * @param width The width of the surface. - * @param height The height of the surface. - */ - public VideoCallSurface(VideoCallPresenter presenter,int surfaceId, TextureView textureView, - int width, int height) { - Log.d(this, "VideoCallSurface: surfaceId=" + surfaceId + - " width=" + width + " height=" + height); - mPresenter = presenter; - mWidth = width; - mHeight = height; - mSurfaceId = surfaceId; - - recreateView(textureView); - } - - /** - * Recreates a {@link VideoCallSurface} after a device orientation change. Re-applies the - * saved {@link SurfaceTexture} to the - * - * @param view The {@link TextureView}. - */ - public void recreateView(TextureView view) { - if (DEBUG) { - Log.i(TAG, "recreateView: " + view); - } - - if (mTextureView == view) { - return; - } - - mTextureView = view; - mTextureView.setSurfaceTextureListener(this); - mTextureView.setOnClickListener(this); - - final boolean areSameSurfaces = - Objects.equal(mSavedSurfaceTexture, mTextureView.getSurfaceTexture()); - Log.d(this, "recreateView: SavedSurfaceTexture=" + mSavedSurfaceTexture - + " areSameSurfaces=" + areSameSurfaces); - if (mSavedSurfaceTexture != null && !areSameSurfaces) { - mTextureView.setSurfaceTexture(mSavedSurfaceTexture); - if (createSurface(mWidth, mHeight)) { - onSurfaceCreated(); - } - } - mIsDoneWithSurface = false; - } - - public void resetPresenter(VideoCallPresenter presenter) { - Log.d(this, "resetPresenter: CurrentPresenter=" + mPresenter + " NewPresenter=" - + presenter); - mPresenter = presenter; - } - - /** - * Handles {@link SurfaceTexture} callback to indicate that a {@link SurfaceTexture} has - * been successfully created. - * - * @param surfaceTexture The {@link SurfaceTexture} which has been created. - * @param width The width of the {@link SurfaceTexture}. - * @param height The height of the {@link SurfaceTexture}. - */ - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, - int height) { - boolean surfaceCreated; - if (DEBUG) { - Log.i(TAG, "onSurfaceTextureAvailable: " + surfaceTexture); - } - // Where there is no saved {@link SurfaceTexture} available, use the newly created one. - // If a saved {@link SurfaceTexture} is available, we are re-creating after an - // orientation change. - Log.d(this, " onSurfaceTextureAvailable mSurfaceId=" + mSurfaceId + " surfaceTexture=" - + surfaceTexture + " width=" + width - + " height=" + height + " mSavedSurfaceTexture=" + mSavedSurfaceTexture); - Log.d(this, " onSurfaceTextureAvailable VideoCallPresenter=" + mPresenter); - if (mSavedSurfaceTexture == null) { - mSavedSurfaceTexture = surfaceTexture; - surfaceCreated = createSurface(width, height); - } else { - // A saved SurfaceTexture was found. - Log.d(this, " onSurfaceTextureAvailable: Replacing with cached surface..."); - mTextureView.setSurfaceTexture(mSavedSurfaceTexture); - surfaceCreated = true; - } - - // Inform presenter that the surface is available. - if (surfaceCreated) { - onSurfaceCreated(); - } - } - - private void onSurfaceCreated() { - if (mPresenter != null) { - mPresenter.onSurfaceCreated(mSurfaceId); - } else { - Log.e(this, "onSurfaceTextureAvailable: Presenter is null"); - } - } - - /** - * Handles a change in the {@link SurfaceTexture}'s size. - * - * @param surfaceTexture The {@link SurfaceTexture}. - * @param width The new width. - * @param height The new height. - */ - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, - int height) { - // Not handled - } - - /** - * Handles {@link SurfaceTexture} destruct callback, indicating that it has been destroyed. - * - * @param surfaceTexture The {@link SurfaceTexture}. - * @return {@code True} if the {@link TextureView} can release the {@link SurfaceTexture}. - */ - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - /** - * Destroying the surface texture; inform the presenter so it can null the surfaces. - */ - Log.d(this, " onSurfaceTextureDestroyed mSurfaceId=" + mSurfaceId + " surfaceTexture=" - + surfaceTexture + " SavedSurfaceTexture=" + mSavedSurfaceTexture - + " SavedSurface=" + mSavedSurface); - Log.d(this, " onSurfaceTextureDestroyed VideoCallPresenter=" + mPresenter); - - // Notify presenter if it is not null. - onSurfaceDestroyed(); - - if (mIsDoneWithSurface) { - onSurfaceReleased(); - if (mSavedSurface != null) { - mSavedSurface.release(); - mSavedSurface = null; - } - } - return mIsDoneWithSurface; - } - - private void onSurfaceDestroyed() { - if (mPresenter != null) { - mPresenter.onSurfaceDestroyed(mSurfaceId); - } else { - Log.e(this, "onSurfaceTextureDestroyed: Presenter is null."); - } - } - - /** - * Handles {@link SurfaceTexture} update callback. - * @param surface - */ - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - // Not Handled - } - - @Override - public void onViewAttachedToWindow(View v) { - if (DEBUG) { - Log.i(TAG, "OnViewAttachedToWindow"); - } - if (mSavedSurfaceTexture != null) { - mTextureView.setSurfaceTexture(mSavedSurfaceTexture); - } - } - - @Override - public void onViewDetachedFromWindow(View v) {} - - /** - * Retrieves the current {@link TextureView}. - * - * @return The {@link TextureView}. - */ - public TextureView getTextureView() { - return mTextureView; - } - - /** - * Called by the user presenter to indicate that the surface is no longer required due to a - * change in video state. Releases and clears out the saved surface and surface textures. - */ - public void setDoneWithSurface() { - Log.d(this, "setDoneWithSurface: SavedSurface=" + mSavedSurface - + " SavedSurfaceTexture=" + mSavedSurfaceTexture); - mIsDoneWithSurface = true; - if (mTextureView != null && mTextureView.isAvailable()) { - return; - } - - if (mSavedSurface != null) { - onSurfaceReleased(); - mSavedSurface.release(); - mSavedSurface = null; - } - if (mSavedSurfaceTexture != null) { - mSavedSurfaceTexture.release(); - mSavedSurfaceTexture = null; - } - } - - private void onSurfaceReleased() { - if (mPresenter != null) { - mPresenter.onSurfaceReleased(mSurfaceId); - } else { - Log.d(this, "setDoneWithSurface: Presenter is null."); - } - } - - /** - * Retrieves the saved surface instance. - * - * @return The surface. - */ - public Surface getSurface() { - return mSavedSurface; - } - - /** - * Sets the dimensions of the surface. - * - * @param width The width of the surface, in pixels. - * @param height The height of the surface, in pixels. - */ - public void setSurfaceDimensions(int width, int height) { - Log.d(this, "setSurfaceDimensions, width=" + width + " height=" + height); - mWidth = width; - mHeight = height; - - if (width != DIMENSIONS_NOT_SET && height != DIMENSIONS_NOT_SET - && mSavedSurfaceTexture != null) { - Log.d(this, "setSurfaceDimensions, mSavedSurfaceTexture is NOT equal to null."); - mSavedSurfaceTexture.setDefaultBufferSize(width, height); - } - } - - /** - * Creates the {@link Surface}, adjusting the {@link SurfaceTexture} buffer size. - * @param width The width of the surface to create. - * @param height The height of the surface to create. - */ - private boolean createSurface(int width, int height) { - Log.d(this, "createSurface mSavedSurfaceTexture=" + mSavedSurfaceTexture - + " mSurfaceId =" + mSurfaceId + " mWidth " + width + " mHeight=" + height); - if (width != DIMENSIONS_NOT_SET && height != DIMENSIONS_NOT_SET - && mSavedSurfaceTexture != null) { - mSavedSurfaceTexture.setDefaultBufferSize(width, height); - mSavedSurface = new Surface(mSavedSurfaceTexture); - return true; - } - return false; - } - - /** - * Handles a user clicking the surface, which is the trigger to toggle the full screen - * Video UI. - * - * @param view The view receiving the click. - */ - @Override - public void onClick(View view) { - if (mPresenter != null) { - mPresenter.onSurfaceClick(mSurfaceId); - } else { - Log.e(this, "onClick: Presenter is null."); - } - } - - /** - * Returns the dimensions of the surface. - * - * @return The dimensions of the surface. - */ - public Point getSurfaceDimensions() { - return new Point(mWidth, mHeight); - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mAnimationDuration = getResources().getInteger(R.integer.video_animation_duration); - } - - /** - * Handles creation of the activity and initialization of the presenter. - * - * @param savedInstanceState The saved instance state. - */ - @Override - public void onActivityCreated(Bundle savedInstanceState) { - mIsLandscape = getResources().getBoolean(R.bool.is_layout_landscape); - Log.d(this, "onActivityCreated: IsLandscape=" + mIsLandscape); - getPresenter().init(getActivity()); - - super.onActivityCreated(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - - final View view = inflater.inflate(R.layout.video_call_fragment, container, false); - - return view; - } - - /** - * Centers the display view vertically for portrait orientations. The view is centered within - * the available space not occupied by the call card. This is a no-op for landscape mode. - * - * @param displayVideo The video view to center. - */ - private void centerDisplayView(View displayVideo) { - if (!mIsLandscape) { - ViewGroup.LayoutParams p = displayVideo.getLayoutParams(); - int height = p.height; - - float spaceBesideCallCard = InCallPresenter.getInstance().getSpaceBesideCallCard(); - // If space beside call card is zeo, layout hasn't happened yet so there is no point - // in attempting to center the view. - if (Math.abs(spaceBesideCallCard - 0.0f) < 0.0001) { - return; - } - float videoViewTranslation = height / 2 - spaceBesideCallCard / 2; - displayVideo.setTranslationY(videoViewTranslation); - } - } - - /** - * After creation of the fragment view, retrieves the required views. - * - * @param view The fragment view. - * @param savedInstanceState The saved instance state. - */ - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - Log.d(this, "onViewCreated: VideoSurfacesInUse=" + sVideoSurfacesInUse); - - mVideoViewsStub = (ViewStub) view.findViewById(R.id.videoCallViewsStub); - } - - @Override - public void onStop() { - super.onStop(); - Log.d(this, "onStop:"); - } - - @Override - public void onPause() { - super.onPause(); - Log.d(this, "onPause:"); - getPresenter().cancelAutoFullScreen(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - Log.d(this, "onDestroyView:"); - } - - /** - * Creates the presenter for the {@link VideoCallFragment}. - * @return The presenter instance. - */ - @Override - public VideoCallPresenter createPresenter() { - Log.d(this, "createPresenter"); - VideoCallPresenter presenter = new VideoCallPresenter(); - onPresenterChanged(presenter); - return presenter; - } - - /** - * @return The user interface for the presenter, which is this fragment. - */ - @Override - public VideoCallPresenter.VideoCallUi getUi() { - return this; - } - - /** - * Inflate video surfaces. - * - * @param show {@code True} if the video surfaces should be shown. - */ - private void inflateVideoUi(boolean show) { - int visibility = show ? View.VISIBLE : View.GONE; - getView().setVisibility(visibility); - - if (show) { - inflateVideoCallViews(); - } - - if (mVideoViews != null) { - mVideoViews.setVisibility(visibility); - } - } - - /** - * Hides and shows the incoming video view and changes the outgoing video view's state based on - * whether outgoing view is enabled or not. - */ - @Override - public void showVideoViews(boolean previewPaused, boolean showIncoming) { - inflateVideoUi(true); - - View incomingVideoView = mVideoViews.findViewById(R.id.incomingVideo); - if (incomingVideoView != null) { - incomingVideoView.setVisibility(showIncoming ? View.VISIBLE : View.INVISIBLE); - } - if (mCameraOff != null) { - mCameraOff.setVisibility(!previewPaused ? View.VISIBLE : View.INVISIBLE); - } - if (mPreviewPhoto != null) { - mPreviewPhoto.setVisibility(!previewPaused ? View.VISIBLE : View.INVISIBLE); - } - } - - /** - * Hide all video views. - */ - @Override - public void hideVideoUi() { - inflateVideoUi(false); - } - - /** - * Cleans up the video telephony surfaces. Used when the presenter indicates a change to an - * audio-only state. Since the surfaces are static, it is important to ensure they are cleaned - * up promptly. - */ - @Override - public void cleanupSurfaces() { - Log.d(this, "cleanupSurfaces"); - if (sDisplaySurface != null) { - sDisplaySurface.setDoneWithSurface(); - sDisplaySurface = null; - } - if (sPreviewSurface != null) { - sPreviewSurface.setDoneWithSurface(); - sPreviewSurface = null; - } - sVideoSurfacesInUse = false; - } - - @Override - public ImageView getPreviewPhotoView() { - return mPreviewPhoto; - } - - /** - * Adjusts the location of the video preview view by the specified offset. - * - * @param shiftUp {@code true} if the preview should shift up, {@code false} if it should shift - * down. - * @param offset The offset. - */ - @Override - public void adjustPreviewLocation(boolean shiftUp, int offset) { - if (sPreviewSurface == null || mPreviewVideoContainer == null) { - return; - } - - // Set the position of the secondary call info card to its starting location. - mPreviewVideoContainer.setTranslationY(shiftUp ? 0 : -offset); - - // Animate the secondary card info slide up/down as it appears and disappears. - mPreviewVideoContainer.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mAnimationDuration) - .translationY(shiftUp ? -offset : 0) - .start(); - } - - private void onPresenterChanged(VideoCallPresenter presenter) { - Log.d(this, "onPresenterChanged: Presenter=" + presenter); - if (sDisplaySurface != null) { - sDisplaySurface.resetPresenter(presenter);; - } - if (sPreviewSurface != null) { - sPreviewSurface.resetPresenter(presenter); - } - } - - /** - * @return {@code True} if the display video surface has been created. - */ - @Override - public boolean isDisplayVideoSurfaceCreated() { - boolean ret = sDisplaySurface != null && sDisplaySurface.getSurface() != null; - Log.d(this, " isDisplayVideoSurfaceCreated returns " + ret); - return ret; - } - - /** - * @return {@code True} if the preview video surface has been created. - */ - @Override - public boolean isPreviewVideoSurfaceCreated() { - boolean ret = sPreviewSurface != null && sPreviewSurface.getSurface() != null; - Log.d(this, " isPreviewVideoSurfaceCreated returns " + ret); - return ret; - } - - /** - * {@link android.view.Surface} on which incoming video for a video call is displayed. - * {@code Null} until the video views {@link android.view.ViewStub} is inflated. - */ - @Override - public Surface getDisplayVideoSurface() { - return sDisplaySurface == null ? null : sDisplaySurface.getSurface(); - } - - /** - * {@link android.view.Surface} on which a preview of the outgoing video for a video call is - * displayed. {@code Null} until the video views {@link android.view.ViewStub} is inflated. - */ - @Override - public Surface getPreviewVideoSurface() { - return sPreviewSurface == null ? null : sPreviewSurface.getSurface(); - } - - /** - * Changes the dimensions of the preview surface. Called when the dimensions change due to a - * device orientation change. - * - * @param width The new width. - * @param height The new height. - */ - @Override - public void setPreviewSize(int width, int height) { - Log.d(this, "setPreviewSize: width=" + width + " height=" + height); - if (sPreviewSurface != null) { - TextureView preview = sPreviewSurface.getTextureView(); - - if (preview == null ) { - return; - } - - // Set the dimensions of both the video surface and the FrameLayout containing it. - ViewGroup.LayoutParams params = preview.getLayoutParams(); - params.width = width; - params.height = height; - preview.setLayoutParams(params); - - if (mPreviewVideoContainer != null) { - ViewGroup.LayoutParams containerParams = mPreviewVideoContainer.getLayoutParams(); - containerParams.width = width; - containerParams.height = height; - mPreviewVideoContainer.setLayoutParams(containerParams); - } - - // The width and height are interchanged outside of this method based on the current - // orientation, so we can transform using "width", which will be either the width or - // the height. - Matrix transform = new Matrix(); - transform.setScale(-1, 1, width/2, 0); - preview.setTransform(transform); - } - } - - /** - * Sets the rotation of the preview surface. Called when the dimensions change due to a - * device orientation change. - * - * Please note that the screen orientation passed in is subtracted from 360 to get the actual - * preview rotation values. - * - * @param rotation The screen orientation. One of - - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - */ - @Override - public void setPreviewRotation(int orientation) { - Log.d(this, "setPreviewRotation: orientation=" + orientation); - if (sPreviewSurface != null) { - TextureView preview = sPreviewSurface.getTextureView(); - - if (preview == null ) { - return; - } - - preview.setRotation(orientation); - } - } - - @Override - public void setPreviewSurfaceSize(int width, int height) { - final boolean isPreviewSurfaceAvailable = sPreviewSurface != null; - Log.d(this, "setPreviewSurfaceSize: width=" + width + " height=" + height + - " isPreviewSurfaceAvailable=" + isPreviewSurfaceAvailable); - if (isPreviewSurfaceAvailable) { - sPreviewSurface.setSurfaceDimensions(width, height); - } - } - - /** - * returns UI's current orientation. - */ - @Override - public int getCurrentRotation() { - try { - return getActivity().getWindowManager().getDefaultDisplay().getRotation(); - } catch (Exception e) { - Log.e(this, "getCurrentRotation: Retrieving current rotation failed. Ex=" + e); - } - return ORIENTATION_UNKNOWN; - } - - /** - * Changes the dimensions of the display video surface. Called when the dimensions change due to - * a peer resolution update - * - * @param width The new width. - * @param height The new height. - */ - @Override - public void setDisplayVideoSize(int width, int height) { - Log.v(this, "setDisplayVideoSize: width=" + width + " height=" + height); - if (sDisplaySurface != null) { - TextureView displayVideo = sDisplaySurface.getTextureView(); - if (displayVideo == null) { - Log.e(this, "Display Video texture view is null. Bail out"); - return; - } - sDisplaySize = new Point(width, height); - setSurfaceSizeAndTranslation(displayVideo, sDisplaySize); - } else { - Log.e(this, "Display Video Surface is null. Bail out"); - } - } - - /** - * Determines the size of the device screen. - * - * @return {@link Point} specifying the width and height of the screen. - */ - @Override - public Point getScreenSize() { - // Get current screen size. - Display display = getActivity().getWindowManager().getDefaultDisplay(); - Point size = new Point(); - display.getSize(size); - - return size; - } - - /** - * Determines the size of the preview surface. - * - * @return {@link Point} specifying the width and height of the preview surface. - */ - @Override - public Point getPreviewSize() { - if (sPreviewSurface == null) { - return null; - } - return sPreviewSurface.getSurfaceDimensions(); - } - - /** - * Inflates the {@link ViewStub} containing the incoming and outgoing surfaces, if necessary, - * and creates {@link VideoCallSurface} instances to track the surfaces. - */ - private void inflateVideoCallViews() { - Log.d(this, "inflateVideoCallViews"); - if (mVideoViews == null ) { - mVideoViews = mVideoViewsStub.inflate(); - } - - if (mVideoViews != null) { - mPreviewVideoContainer = mVideoViews.findViewById(R.id.previewVideoContainer); - mCameraOff = mVideoViews.findViewById(R.id.previewCameraOff); - mPreviewPhoto = (ImageView) mVideoViews.findViewById(R.id.previewProfilePhoto); - - TextureView displaySurface = (TextureView) mVideoViews.findViewById(R.id.incomingVideo); - - Log.d(this, "inflateVideoCallViews: sVideoSurfacesInUse=" + sVideoSurfacesInUse); - //If peer adjusted screen size is not available, set screen size to default display size - Point screenSize = sDisplaySize == null ? getScreenSize() : sDisplaySize; - setSurfaceSizeAndTranslation(displaySurface, screenSize); - - if (!sVideoSurfacesInUse) { - // Where the video surfaces are not already in use (first time creating them), - // setup new VideoCallSurface instances to track them. - Log.d(this, " inflateVideoCallViews screenSize" + screenSize); - - sDisplaySurface = new VideoCallSurface(getPresenter(), SURFACE_DISPLAY, - (TextureView) mVideoViews.findViewById(R.id.incomingVideo), screenSize.x, - screenSize.y); - sPreviewSurface = new VideoCallSurface(getPresenter(), SURFACE_PREVIEW, - (TextureView) mVideoViews.findViewById(R.id.previewVideo)); - sVideoSurfacesInUse = true; - } else { - // In this case, the video surfaces are already in use (we are recreating the - // Fragment after a destroy/create cycle resulting from a rotation. - sDisplaySurface.recreateView((TextureView) mVideoViews.findViewById( - R.id.incomingVideo)); - sPreviewSurface.recreateView((TextureView) mVideoViews.findViewById( - R.id.previewVideo)); - } - - // Attempt to center the incoming video view, if it is in the layout. - final ViewTreeObserver observer = mVideoViews.getViewTreeObserver(); - observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - // Check if the layout includes the incoming video surface -- this will only be the - // case for a video call. - View displayVideo = mVideoViews.findViewById(R.id.incomingVideo); - if (displayVideo != null) { - centerDisplayView(displayVideo); - } - mIsLayoutComplete = true; - - // Remove the listener so we don't continually re-layout. - ViewTreeObserver observer = mVideoViews.getViewTreeObserver(); - if (observer.isAlive()) { - observer.removeOnGlobalLayoutListener(this); - } - } - }); - } - } - - /** - * Resizes a surface so that it has the same size as the full screen and so that it is - * centered vertically below the call card. - * - * @param textureView The {@link TextureView} to resize and position. - * @param size The size of the screen. - */ - private void setSurfaceSizeAndTranslation(TextureView textureView, Point size) { - // Set the surface to have that size. - ViewGroup.LayoutParams params = textureView.getLayoutParams(); - params.width = size.x; - params.height = size.y; - textureView.setLayoutParams(params); - Log.d(this, "setSurfaceSizeAndTranslation: Size=" + size + "IsLayoutComplete=" + - mIsLayoutComplete + "IsLandscape=" + mIsLandscape); - - // It is only possible to center the display view if layout of the views has completed. - // It is only after layout is complete that the dimensions of the Call Card has been - // established, which is a prerequisite to centering the view. - // Incoming video calls will center the view - if (mIsLayoutComplete) { - centerDisplayView(textureView); - } - } -} diff --git a/InCallUI/src/com/android/incallui/VideoCallPresenter.java b/InCallUI/src/com/android/incallui/VideoCallPresenter.java deleted file mode 100644 index 06e3e444070959a4829634e6c11636d092e4039d..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/VideoCallPresenter.java +++ /dev/null @@ -1,1306 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Point; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Looper; -import android.provider.ContactsContract; -import android.telecom.Connection; -import android.telecom.InCallService.VideoCall; -import android.telecom.VideoProfile; -import android.telecom.VideoProfile.CameraCapabilities; -import android.view.Surface; -import android.widget.ImageView; - -import com.android.contacts.common.ContactPhotoManager; -import com.android.contacts.common.compat.CompatUtils; -import com.android.dialer.R; -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallOrientationListener; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; -import com.android.incallui.InCallVideoCallCallbackNotifier.SurfaceChangeListener; -import com.android.incallui.InCallVideoCallCallbackNotifier.VideoEventListener; - -import java.util.Objects; - -/** - * Logic related to the {@link VideoCallFragment} and for managing changes to the video calling - * surfaces based on other user interface events and incoming events from the - * {@class VideoCallListener}. - *

- * When a call's video state changes to bi-directional video, the - * {@link com.android.incallui.VideoCallPresenter} performs the following negotiation with the - * telephony layer: - *

    - *
  • {@code VideoCallPresenter} creates and informs telephony of the display surface.
  • - *
  • {@code VideoCallPresenter} creates the preview surface.
  • - *
  • {@code VideoCallPresenter} informs telephony of the currently selected camera.
  • - *
  • Telephony layer sends {@link CameraCapabilities}, including the - * dimensions of the video for the current camera.
  • - *
  • {@code VideoCallPresenter} adjusts size of the preview surface to match the aspect - * ratio of the camera.
  • - *
  • {@code VideoCallPresenter} informs telephony of the new preview surface.
  • - *
- *

- * When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both - * surfaces. - */ -public class VideoCallPresenter extends Presenter implements - IncomingCallListener, InCallOrientationListener, InCallStateListener, - InCallDetailsListener, SurfaceChangeListener, VideoEventListener, - InCallPresenter.InCallEventListener { - public static final String TAG = "VideoCallPresenter"; - - public static final boolean DEBUG = false; - - /** - * Runnable which is posted to schedule automatically entering fullscreen mode. Will not auto - * enter fullscreen mode if the dialpad is visible (doing so would make it impossible to exit - * the dialpad). - */ - private Runnable mAutoFullscreenRunnable = new Runnable() { - @Override - public void run() { - if (mAutoFullScreenPending && !InCallPresenter.getInstance().isDialpadVisible() - && mIsVideoMode) { - - Log.v(this, "Automatically entering fullscreen mode."); - InCallPresenter.getInstance().setFullScreen(true); - mAutoFullScreenPending = false; - } else { - Log.v(this, "Skipping scheduled fullscreen mode."); - } - } - }; - - /** - * Defines the state of the preview surface negotiation with the telephony layer. - */ - private class PreviewSurfaceState { - /** - * The camera has not yet been set on the {@link VideoCall}; negotiation has not yet - * started. - */ - private static final int NONE = 0; - - /** - * The camera has been set on the {@link VideoCall}, but camera capabilities have not yet - * been received. - */ - private static final int CAMERA_SET = 1; - - /** - * The camera capabilties have been received from telephony, but the surface has not yet - * been set on the {@link VideoCall}. - */ - private static final int CAPABILITIES_RECEIVED = 2; - - /** - * The surface has been set on the {@link VideoCall}. - */ - private static final int SURFACE_SET = 3; - } - - /** - * The minimum width or height of the preview surface. Used when re-sizing the preview surface - * to match the aspect ratio of the currently selected camera. - */ - private float mMinimumVideoDimension; - - /** - * The current context. - */ - private Context mContext; - - /** - * The call the video surfaces are currently related to - */ - private Call mPrimaryCall; - - /** - * The {@link VideoCall} used to inform the video telephony layer of changes to the video - * surfaces. - */ - private VideoCall mVideoCall; - - /** - * Determines if the current UI state represents a video call. - */ - private int mCurrentVideoState; - - /** - * Call's current state - */ - private int mCurrentCallState = Call.State.INVALID; - - /** - * Determines the device orientation (portrait/lanscape). - */ - private int mDeviceOrientation = InCallOrientationEventListener.SCREEN_ORIENTATION_0; - - /** - * Tracks the state of the preview surface negotiation with the telephony layer. - */ - private int mPreviewSurfaceState = PreviewSurfaceState.NONE; - - private static boolean mIsVideoMode = false; - - /** - * Contact photo manager to retrieve cached contact photo information. - */ - private ContactPhotoManager mContactPhotoManager = null; - - /** - * The URI for the user's profile photo, or {@code null} if not specified. - */ - private ContactInfoCache.ContactCacheEntry mProfileInfo = null; - - /** - * UI thread handler used for delayed task execution. - */ - private Handler mHandler; - - /** - * Determines whether video calls should automatically enter full screen mode after - * {@link #mAutoFullscreenTimeoutMillis} milliseconds. - */ - private boolean mIsAutoFullscreenEnabled = false; - - /** - * Determines the number of milliseconds after which a video call will automatically enter - * fullscreen mode. Requires {@link #mIsAutoFullscreenEnabled} to be {@code true}. - */ - private int mAutoFullscreenTimeoutMillis = 0; - - /** - * Determines if the countdown is currently running to automatically enter full screen video - * mode. - */ - private boolean mAutoFullScreenPending = false; - - /** - * Initializes the presenter. - * - * @param context The current context. - */ - public void init(Context context) { - mContext = context; - mMinimumVideoDimension = mContext.getResources().getDimension( - R.dimen.video_preview_small_dimension); - mHandler = new Handler(Looper.getMainLooper()); - mIsAutoFullscreenEnabled = mContext.getResources() - .getBoolean(R.bool.video_call_auto_fullscreen); - mAutoFullscreenTimeoutMillis = mContext.getResources().getInteger( - R.integer.video_call_auto_fullscreen_timeout); - } - - /** - * Called when the user interface is ready to be used. - * - * @param ui The Ui implementation that is now ready to be used. - */ - @Override - public void onUiReady(VideoCallUi ui) { - super.onUiReady(ui); - Log.d(this, "onUiReady:"); - - // Do not register any listeners if video calling is not compatible to safeguard against - // any accidental calls of video calling code. - if (!CompatUtils.isVideoCompatible()) { - return; - } - - // Register for call state changes last - InCallPresenter.getInstance().addListener(this); - InCallPresenter.getInstance().addDetailsListener(this); - InCallPresenter.getInstance().addIncomingCallListener(this); - InCallPresenter.getInstance().addOrientationListener(this); - // To get updates of video call details changes - InCallPresenter.getInstance().addDetailsListener(this); - InCallPresenter.getInstance().addInCallEventListener(this); - - // Register for surface and video events from {@link InCallVideoCallListener}s. - InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this); - InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this); - mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; - mCurrentCallState = Call.State.INVALID; - - final InCallPresenter.InCallState inCallState = - InCallPresenter.getInstance().getInCallState(); - onStateChange(inCallState, inCallState, CallList.getInstance()); - } - - /** - * Called when the user interface is no longer ready to be used. - * - * @param ui The Ui implementation that is no longer ready to be used. - */ - @Override - public void onUiUnready(VideoCallUi ui) { - super.onUiUnready(ui); - Log.d(this, "onUiUnready:"); - - if (!CompatUtils.isVideoCompatible()) { - return; - } - - cancelAutoFullScreen(); - - InCallPresenter.getInstance().removeListener(this); - InCallPresenter.getInstance().removeDetailsListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - InCallPresenter.getInstance().removeOrientationListener(this); - InCallPresenter.getInstance().removeInCallEventListener(this); - - InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this); - InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this); - } - - /** - * Handles the creation of a surface in the {@link VideoCallFragment}. - * - * @param surface The surface which was created. - */ - public void onSurfaceCreated(int surface) { - Log.d(this, "onSurfaceCreated surface=" + surface + " mVideoCall=" + mVideoCall); - Log.d(this, "onSurfaceCreated PreviewSurfaceState=" + mPreviewSurfaceState); - Log.d(this, "onSurfaceCreated presenter=" + this); - - final VideoCallUi ui = getUi(); - if (ui == null || mVideoCall == null) { - Log.w(this, "onSurfaceCreated: Error bad state VideoCallUi=" + ui + " mVideoCall=" - + mVideoCall); - return; - } - - // If the preview surface has just been created and we have already received camera - // capabilities, but not yet set the surface, we will set the surface now. - if (surface == VideoCallFragment.SURFACE_PREVIEW ) { - if (mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) { - mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; - mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface()); - } else if (mPreviewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()){ - enableCamera(mVideoCall, true); - } - } else if (surface == VideoCallFragment.SURFACE_DISPLAY) { - mVideoCall.setDisplaySurface(ui.getDisplayVideoSurface()); - } - } - - /** - * Handles structural changes (format or size) to a surface. - * - * @param surface The surface which changed. - * @param format The new PixelFormat of the surface. - * @param width The new width of the surface. - * @param height The new height of the surface. - */ - public void onSurfaceChanged(int surface, int format, int width, int height) { - //Do stuff - } - - /** - * Handles the destruction of a surface in the {@link VideoCallFragment}. - * Note: The surface is being released, that is, it is no longer valid. - * - * @param surface The surface which was destroyed. - */ - public void onSurfaceReleased(int surface) { - Log.d(this, "onSurfaceReleased: mSurfaceId=" + surface); - if ( mVideoCall == null) { - Log.w(this, "onSurfaceReleased: VideoCall is null. mSurfaceId=" + - surface); - return; - } - - if (surface == VideoCallFragment.SURFACE_DISPLAY) { - mVideoCall.setDisplaySurface(null); - } else if (surface == VideoCallFragment.SURFACE_PREVIEW) { - mVideoCall.setPreviewSurface(null); - enableCamera(mVideoCall, false); - } - } - - /** - * Called by {@link VideoCallFragment} when the surface is detached from UI (TextureView). - * Note: The surface will be cached by {@link VideoCallFragment}, so we don't immediately - * null out incoming video surface. - * @see VideoCallPresenter#onSurfaceReleased(int) - * - * @param surface The surface which was detached. - */ - public void onSurfaceDestroyed(int surface) { - Log.d(this, "onSurfaceDestroyed: mSurfaceId=" + surface); - if (mVideoCall == null) { - return; - } - - final boolean isChangingConfigurations = - InCallPresenter.getInstance().isChangingConfigurations(); - Log.d(this, "onSurfaceDestroyed: isChangingConfigurations=" + isChangingConfigurations); - - if (surface == VideoCallFragment.SURFACE_PREVIEW) { - if (!isChangingConfigurations) { - enableCamera(mVideoCall, false); - } else { - Log.w(this, "onSurfaceDestroyed: Activity is being destroyed due " - + "to configuration changes. Not closing the camera."); - } - } - } - - /** - * Handles clicks on the video surfaces by toggling full screen state. - * Informs the {@link InCallPresenter} of the change so that it can inform the - * {@link CallCardPresenter} of the change. - * - * @param surfaceId The video surface receiving the click. - */ - public void onSurfaceClick(int surfaceId) { - boolean isFullscreen = InCallPresenter.getInstance().toggleFullscreenMode(); - Log.v(this, "toggleFullScreen = " + isFullscreen); - } - - /** - * Handles incoming calls. - * - * @param oldState The old in call state. - * @param newState The new in call state. - * @param call The call. - */ - @Override - public void onIncomingCall(InCallPresenter.InCallState oldState, - InCallPresenter.InCallState newState, Call call) { - // same logic should happen as with onStateChange() - onStateChange(oldState, newState, CallList.getInstance()); - } - - /** - * Handles state changes (including incoming calls) - * - * @param newState The in call state. - * @param callList The call list. - */ - @Override - public void onStateChange(InCallPresenter.InCallState oldState, - InCallPresenter.InCallState newState, CallList callList) { - Log.d(this, "onStateChange oldState" + oldState + " newState=" + newState + - " isVideoMode=" + isVideoMode()); - - if (newState == InCallPresenter.InCallState.NO_CALLS) { - if (isVideoMode()) { - exitVideoMode(); - } - - cleanupSurfaces(); - } - - // Determine the primary active call). - Call primary = null; - - // Determine the call which is the focus of the user's attention. In the case of an - // incoming call waiting call, the primary call is still the active video call, however - // the determination of whether we should be in fullscreen mode is based on the type of the - // incoming call, not the active video call. - Call currentCall = null; - - if (newState == InCallPresenter.InCallState.INCOMING) { - // We don't want to replace active video call (primary call) - // with a waiting call, since user may choose to ignore/decline the waiting call and - // this should have no impact on current active video call, that is, we should not - // change the camera or UI unless the waiting VT call becomes active. - primary = callList.getActiveCall(); - currentCall = callList.getIncomingCall(); - if (!VideoUtils.isActiveVideoCall(primary)) { - primary = callList.getIncomingCall(); - } - } else if (newState == InCallPresenter.InCallState.OUTGOING) { - currentCall = primary = callList.getOutgoingCall(); - } else if (newState == InCallPresenter.InCallState.PENDING_OUTGOING) { - currentCall = primary = callList.getPendingOutgoingCall(); - } else if (newState == InCallPresenter.InCallState.INCALL) { - currentCall = primary = callList.getActiveCall(); - } - - final boolean primaryChanged = !Objects.equals(mPrimaryCall, primary); - Log.d(this, "onStateChange primaryChanged=" + primaryChanged); - Log.d(this, "onStateChange primary= " + primary); - Log.d(this, "onStateChange mPrimaryCall = " + mPrimaryCall); - if (primaryChanged) { - onPrimaryCallChanged(primary); - } else if (mPrimaryCall != null) { - updateVideoCall(primary); - } - updateCallCache(primary); - - // If the call context changed, potentially exit fullscreen or schedule auto enter of - // fullscreen mode. - // If the current call context is no longer a video call, exit fullscreen mode. - maybeExitFullscreen(currentCall); - // Schedule auto-enter of fullscreen mode if the current call context is a video call - maybeAutoEnterFullscreen(currentCall); - } - - /** - * Handles a change to the fullscreen mode of the app. - * - * @param isFullscreenMode {@code true} if the app is now fullscreen, {@code false} otherwise. - */ - @Override - public void onFullscreenModeChanged(boolean isFullscreenMode) { - cancelAutoFullScreen(); - } - - /** - * Handles changes to the visibility of the secondary caller info bar. - * - * @param isVisible {@code true} if the secondary caller info is showing, {@code false} - * otherwise. - * @param height the height of the secondary caller info bar. - */ - @Override - public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { - Log.d(this, - "onSecondaryCallerInfoVisibilityChanged : isVisible = " + isVisible + " height = " - + height); - getUi().adjustPreviewLocation(isVisible /* shiftUp */, height); - } - - private void checkForVideoStateChange(Call call) { - final boolean isVideoCall = VideoUtils.isVideoCall(call); - final boolean hasVideoStateChanged = mCurrentVideoState != call.getVideoState(); - - Log.d(this, "checkForVideoStateChange: isVideoCall= " + isVideoCall - + " hasVideoStateChanged=" + hasVideoStateChanged + " isVideoMode=" - + isVideoMode() + " previousVideoState: " + - VideoProfile.videoStateToString(mCurrentVideoState) + " newVideoState: " - + VideoProfile.videoStateToString(call.getVideoState())); - - if (!hasVideoStateChanged) { - return; - } - - updateCameraSelection(call); - - if (isVideoCall) { - adjustVideoMode(call); - } else if (isVideoMode()) { - exitVideoMode(); - } - } - - private void checkForCallStateChange(Call call) { - final boolean isVideoCall = VideoUtils.isVideoCall(call); - final boolean hasCallStateChanged = mCurrentCallState != call.getState(); - - Log.d(this, "checkForCallStateChange: isVideoCall= " + isVideoCall - + " hasCallStateChanged=" + - hasCallStateChanged + " isVideoMode=" + isVideoMode()); - - if (!hasCallStateChanged) { - return; - } - - if (isVideoCall) { - final InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - - String prevCameraId = cameraManager.getActiveCameraId(); - updateCameraSelection(call); - String newCameraId = cameraManager.getActiveCameraId(); - - if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) { - enableCamera(call.getVideoCall(), true); - } - } - - // Make sure we hide or show the video UI if needed. - showVideoUi(call.getVideoState(), call.getState()); - } - - private void cleanupSurfaces() { - final VideoCallUi ui = getUi(); - if (ui == null) { - Log.w(this, "cleanupSurfaces"); - return; - } - ui.cleanupSurfaces(); - } - - private void onPrimaryCallChanged(Call newPrimaryCall) { - final boolean isVideoCall = VideoUtils.isVideoCall(newPrimaryCall); - final boolean isVideoMode = isVideoMode(); - - Log.d(this, "onPrimaryCallChanged: isVideoCall=" + isVideoCall + " isVideoMode=" - + isVideoMode); - - if (!isVideoCall && isVideoMode) { - // Terminate video mode if new primary call is not a video call - // and we are currently in video mode. - Log.d(this, "onPrimaryCallChanged: Exiting video mode..."); - exitVideoMode(); - } else if (isVideoCall) { - Log.d(this, "onPrimaryCallChanged: Entering video mode..."); - - updateCameraSelection(newPrimaryCall); - adjustVideoMode(newPrimaryCall); - } - checkForOrientationAllowedChange(newPrimaryCall); - } - - private boolean isVideoMode() { - return mIsVideoMode; - } - - private void updateCallCache(Call call) { - if (call == null) { - mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; - mCurrentCallState = Call.State.INVALID; - mVideoCall = null; - mPrimaryCall = null; - } else { - mCurrentVideoState = call.getVideoState(); - mVideoCall = call.getVideoCall(); - mCurrentCallState = call.getState(); - mPrimaryCall = call; - } - } - - /** - * Handles changes to the details of the call. The {@link VideoCallPresenter} is interested in - * changes to the video state. - * - * @param call The call for which the details changed. - * @param details The new call details. - */ - @Override - public void onDetailsChanged(Call call, android.telecom.Call.Details details) { - Log.d(this, " onDetailsChanged call=" + call + " details=" + details + " mPrimaryCall=" - + mPrimaryCall); - if (call == null) { - return; - } - // If the details change is not for the currently active call no update is required. - if (!call.equals(mPrimaryCall)) { - Log.d(this, " onDetailsChanged: Details not for current active call so returning. "); - return; - } - - updateVideoCall(call); - - updateCallCache(call); - } - - private void updateVideoCall(Call call) { - checkForVideoCallChange(call); - checkForVideoStateChange(call); - checkForCallStateChange(call); - checkForOrientationAllowedChange(call); - } - - private void checkForOrientationAllowedChange(Call call) { - InCallPresenter.getInstance().setInCallAllowsOrientationChange( - VideoUtils.isVideoCall(call)); - } - - /** - * Checks for a change to the video call and changes it if required. - */ - private void checkForVideoCallChange(Call call) { - final VideoCall videoCall = call.getTelecomCall().getVideoCall(); - Log.d(this, "checkForVideoCallChange: videoCall=" + videoCall + " mVideoCall=" - + mVideoCall); - if (!Objects.equals(videoCall, mVideoCall)) { - changeVideoCall(call); - } - } - - /** - * Handles a change to the video call. Sets the surfaces on the previous call to null and sets - * the surfaces on the new video call accordingly. - * - * @param call The new video call. - */ - private void changeVideoCall(Call call) { - final VideoCall videoCall = call.getTelecomCall().getVideoCall(); - Log.d(this, "changeVideoCall to videoCall=" + videoCall + " mVideoCall=" + mVideoCall); - // Null out the surfaces on the previous video call. - if (mVideoCall != null) { - // Log.d(this, "Null out the surfaces on the previous video call."); - // mVideoCall.setDisplaySurface(null); - // mVideoCall.setPreviewSurface(null); - } - - final boolean hasChanged = mVideoCall == null && videoCall != null; - - mVideoCall = videoCall; - if (mVideoCall == null || call == null) { - Log.d(this, "Video call or primary call is null. Return"); - return; - } - - if (VideoUtils.isVideoCall(call) && hasChanged) { - adjustVideoMode(call); - } - } - - private static boolean isCameraRequired(int videoState) { - return VideoProfile.isBidirectional(videoState) - || VideoProfile.isTransmissionEnabled(videoState); - } - - private boolean isCameraRequired() { - return mPrimaryCall != null && isCameraRequired(mPrimaryCall.getVideoState()); - } - - /** - * Adjusts the current video mode by setting up the preview and display surfaces as necessary. - * Expected to be called whenever the video state associated with a call changes (e.g. a user - * turns their camera on or off) to ensure the correct surfaces are shown/hidden. - * TODO(vt): Need to adjust size and orientation of preview surface here. - */ - private void adjustVideoMode(Call call) { - VideoCall videoCall = call.getVideoCall(); - int newVideoState = call.getVideoState(); - - Log.d(this, "adjustVideoMode videoCall= " + videoCall + " videoState: " + newVideoState); - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "Error VideoCallUi is null so returning"); - return; - } - - showVideoUi(newVideoState, call.getState()); - - // Communicate the current camera to telephony and make a request for the camera - // capabilities. - if (videoCall != null) { - if (ui.isDisplayVideoSurfaceCreated()) { - Log.d(this, "Calling setDisplaySurface with " + ui.getDisplayVideoSurface()); - videoCall.setDisplaySurface(ui.getDisplayVideoSurface()); - } - - videoCall.setDeviceOrientation(mDeviceOrientation); - enableCamera(videoCall, isCameraRequired(newVideoState)); - } - int previousVideoState = mCurrentVideoState; - mCurrentVideoState = newVideoState; - mIsVideoMode = true; - - // adjustVideoMode may be called if we are already in a 1-way video state. In this case - // we do not want to trigger auto-fullscreen mode. - if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) { - maybeAutoEnterFullscreen(call); - } - } - - private void enableCamera(VideoCall videoCall, boolean isCameraRequired) { - Log.d(this, "enableCamera: VideoCall=" + videoCall + " enabling=" + isCameraRequired); - if (videoCall == null) { - Log.w(this, "enableCamera: VideoCall is null."); - return; - } - - if (isCameraRequired) { - InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - videoCall.setCamera(cameraManager.getActiveCameraId()); - mPreviewSurfaceState = PreviewSurfaceState.CAMERA_SET; - - videoCall.requestCameraCapabilities(); - } else { - mPreviewSurfaceState = PreviewSurfaceState.NONE; - videoCall.setCamera(null); - } - } - - /** - * Exits video mode by hiding the video surfaces and making other adjustments (eg. audio). - */ - private void exitVideoMode() { - Log.d(this, "exitVideoMode"); - - showVideoUi(VideoProfile.STATE_AUDIO_ONLY, Call.State.ACTIVE); - enableCamera(mVideoCall, false); - InCallPresenter.getInstance().setFullScreen(false); - - mIsVideoMode = false; - } - - /** - * Based on the current video state and call state, show or hide the incoming and - * outgoing video surfaces. The outgoing video surface is shown any time video is transmitting. - * The incoming video surface is shown whenever the video is un-paused and active. - * - * @param videoState The video state. - * @param callState The call state. - */ - private void showVideoUi(int videoState, int callState) { - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "showVideoUi, VideoCallUi is null returning"); - return; - } - boolean showIncomingVideo = showIncomingVideo(videoState, callState); - boolean showOutgoingVideo = showOutgoingVideo(videoState); - Log.v(this, "showVideoUi : showIncoming = " + showIncomingVideo + " showOutgoing = " - + showOutgoingVideo); - if (showIncomingVideo || showOutgoingVideo) { - ui.showVideoViews(showOutgoingVideo, showIncomingVideo); - - if (VideoProfile.isReceptionEnabled(videoState)) { - loadProfilePhotoAsync(); - } - } else { - ui.hideVideoUi(); - } - - InCallPresenter.getInstance().enableScreenTimeout( - VideoProfile.isAudioOnly(videoState)); - } - - /** - * Determines if the incoming video surface should be shown based on the current videoState and - * callState. The video surface is shown when incoming video is not paused, the call is active, - * and video reception is enabled. - * - * @param videoState The current video state. - * @param callState The current call state. - * @return {@code true} if the incoming video surface should be shown, {@code false} otherwise. - */ - public static boolean showIncomingVideo(int videoState, int callState) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - boolean isPaused = VideoProfile.isPaused(videoState); - boolean isCallActive = callState == Call.State.ACTIVE; - - return !isPaused && isCallActive && VideoProfile.isReceptionEnabled(videoState); - } - - /** - * Determines if the outgoing video surface should be shown based on the current videoState. - * The video surface is shown if video transmission is enabled. - * - * @param videoState The current video state. - * @return {@code true} if the the outgoing video surface should be shown, {@code false} - * otherwise. - */ - public static boolean showOutgoingVideo(int videoState) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isTransmissionEnabled(videoState); - } - - /** - * Handles peer video pause state changes. - * - * @param call The call which paused or un-pausedvideo transmission. - * @param paused {@code True} when the video transmission is paused, {@code false} when video - * transmission resumes. - */ - @Override - public void onPeerPauseStateChanged(Call call, boolean paused) { - if (!call.equals(mPrimaryCall)) { - return; - } - - // TODO(vt): Show/hide the peer contact photo. - } - - /** - * Handles peer video dimension changes. - * - * @param call The call which experienced a peer video dimension change. - * @param width The new peer video width . - * @param height The new peer video height. - */ - @Override - public void onUpdatePeerDimensions(Call call, int width, int height) { - Log.d(this, "onUpdatePeerDimensions: width= " + width + " height= " + height); - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "VideoCallUi is null. Bail out"); - return; - } - if (!call.equals(mPrimaryCall)) { - Log.e(this, "Current call is not equal to primary call. Bail out"); - return; - } - - // Change size of display surface to match the peer aspect ratio - if (width > 0 && height > 0) { - setDisplayVideoSize(width, height); - } - } - - /** - * Handles any video quality changes in the call. - * - * @param call The call which experienced a video quality change. - * @param videoQuality The new video call quality. - */ - @Override - public void onVideoQualityChanged(Call call, int videoQuality) { - // No-op - } - - /** - * Handles a change to the dimensions of the local camera. Receiving the camera capabilities - * triggers the creation of the video - * - * @param call The call which experienced the camera dimension change. - * @param width The new camera video width. - * @param height The new camera video height. - */ - @Override - public void onCameraDimensionsChange(Call call, int width, int height) { - Log.d(this, "onCameraDimensionsChange call=" + call + " width=" + width + " height=" - + height); - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "onCameraDimensionsChange ui is null"); - return; - } - - if (!call.equals(mPrimaryCall)) { - Log.e(this, "Call is not primary call"); - return; - } - - mPreviewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED; - changePreviewDimensions(width, height); - - // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}. - // If it not yet ready, it will be set when when creation completes. - if (ui.isPreviewVideoSurfaceCreated()) { - mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; - mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface()); - } - } - - /** - * Changes the dimensions of the preview surface. - * - * @param width The new width. - * @param height The new height. - */ - private void changePreviewDimensions(int width, int height) { - VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - // Resize the surface used to display the preview video - ui.setPreviewSurfaceSize(width, height); - - // Configure the preview surface to the correct aspect ratio. - float aspectRatio = 1.0f; - if (width > 0 && height > 0) { - aspectRatio = (float) width / (float) height; - } - - // Resize the textureview housing the preview video and rotate it appropriately based on - // the device orientation - setPreviewSize(mDeviceOrientation, aspectRatio); - } - - /** - * Called when call session event is raised. - * - * @param event The call session event. - */ - @Override - public void onCallSessionEvent(int event) { - StringBuilder sb = new StringBuilder(); - sb.append("onCallSessionEvent = "); - - switch (event) { - case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE: - sb.append("rx_pause"); - break; - case Connection.VideoProvider.SESSION_EVENT_RX_RESUME: - sb.append("rx_resume"); - break; - case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE: - sb.append("camera_failure"); - break; - case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY: - sb.append("camera_ready"); - break; - default: - sb.append("unknown event = "); - sb.append(event); - break; - } - Log.d(this, sb.toString()); - } - - /** - * Handles a change to the call data usage - * - * @param dataUsage call data usage value - */ - @Override - public void onCallDataUsageChange(long dataUsage) { - Log.d(this, "onCallDataUsageChange dataUsage=" + dataUsage); - } - - /** - * Handles changes to the device orientation. - * @param orientation The screen orientation of the device (one of: - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - */ - @Override - public void onDeviceOrientationChanged(int orientation) { - mDeviceOrientation = orientation; - - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "onDeviceOrientationChanged: VideoCallUi is null"); - return; - } - - Point previewDimensions = ui.getPreviewSize(); - if (previewDimensions == null) { - return; - } - Log.d(this, "onDeviceOrientationChanged: orientation=" + orientation + " size: " - + previewDimensions); - changePreviewDimensions(previewDimensions.x, previewDimensions.y); - - ui.setPreviewRotation(mDeviceOrientation); - } - - /** - * Sets the preview surface size based on the current device orientation. - * See: {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - * - * @param orientation The device orientation - * @param aspectRatio The aspect ratio of the camera (width / height). - */ - private void setPreviewSize(int orientation, float aspectRatio) { - VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - int height; - int width; - - if (orientation == InCallOrientationEventListener.SCREEN_ORIENTATION_90 || - orientation == InCallOrientationEventListener.SCREEN_ORIENTATION_270) { - width = (int) (mMinimumVideoDimension * aspectRatio); - height = (int) mMinimumVideoDimension; - } else { - // Portrait or reverse portrait orientation. - width = (int) mMinimumVideoDimension; - height = (int) (mMinimumVideoDimension * aspectRatio); - } - ui.setPreviewSize(width, height); - } - - /** - * Sets the display video surface size based on peer width and height - * - * @param width peer width - * @param height peer height - */ - private void setDisplayVideoSize(int width, int height) { - Log.v(this, "setDisplayVideoSize: Received peer width=" + width + " height=" + height); - VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - // Get current display size - Point size = ui.getScreenSize(); - Log.v(this, "setDisplayVideoSize: windowmgr width=" + size.x - + " windowmgr height=" + size.y); - if (size.y * width > size.x * height) { - // current display height is too much. Correct it - size.y = (int) (size.x * height / width); - } else if (size.y * width < size.x * height) { - // current display width is too much. Correct it - size.x = (int) (size.y * width / height); - } - ui.setDisplayVideoSize(size.x, size.y); - } - - /** - * Exits fullscreen mode if the current call context has changed to a non-video call. - * - * @param call The call. - */ - protected void maybeExitFullscreen(Call call) { - if (call == null) { - return; - } - - if (!VideoUtils.isVideoCall(call) || call.getState() == Call.State.INCOMING) { - InCallPresenter.getInstance().setFullScreen(false); - } - } - - /** - * Schedules auto-entering of fullscreen mode. - * Will not enter full screen mode if any of the following conditions are met: - * 1. No call - * 2. Call is not active - * 3. Call is not video call - * 4. Already in fullscreen mode - * 5. The current video state is not bi-directional (if the remote party stops transmitting, - * the user's contact photo would dominate in fullscreen mode). - * - * @param call The current call. - */ - protected void maybeAutoEnterFullscreen(Call call) { - if (!mIsAutoFullscreenEnabled) { - return; - } - - if (call == null || ( - call != null && (call.getState() != Call.State.ACTIVE || - !VideoUtils.isVideoCall(call)) || - InCallPresenter.getInstance().isFullscreen()) || - !VideoUtils.isBidirectionalVideoCall(call)) { - // Ensure any previously scheduled attempt to enter fullscreen is cancelled. - cancelAutoFullScreen(); - return; - } - - if (mAutoFullScreenPending) { - Log.v(this, "maybeAutoEnterFullscreen : already pending."); - return; - } - Log.v(this, "maybeAutoEnterFullscreen : scheduled"); - mAutoFullScreenPending = true; - mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis); - } - - /** - * Cancels pending auto fullscreen mode. - */ - public void cancelAutoFullScreen() { - if (!mAutoFullScreenPending) { - Log.v(this, "cancelAutoFullScreen : none pending."); - return; - } - Log.v(this, "cancelAutoFullScreen : cancelling pending"); - mAutoFullScreenPending = false; - } - - private static void updateCameraSelection(Call call) { - Log.d(TAG, "updateCameraSelection: call=" + call); - Log.d(TAG, "updateCameraSelection: call=" + toSimpleString(call)); - - final Call activeCall = CallList.getInstance().getActiveCall(); - int cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - - // this function should never be called with null call object, however if it happens we - // should handle it gracefully. - if (call == null) { - cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - com.android.incallui.Log.e(TAG, "updateCameraSelection: Call object is null." - + " Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)"); - } - - // Clear camera direction if this is not a video call. - else if (VideoUtils.isAudioCall(call)) { - cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - call.getVideoSettings().setCameraDir(cameraDir); - } - - // If this is a waiting video call, default to active call's camera, - // since we don't want to change the current camera for waiting call - // without user's permission. - else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) { - cameraDir = activeCall.getVideoSettings().getCameraDir(); - } - - // Infer the camera direction from the video state and store it, - // if this is an outgoing video call. - else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call) ) { - cameraDir = toCameraDirection(call.getVideoState()); - call.getVideoSettings().setCameraDir(cameraDir); - } - - // Use the stored camera dir if this is an outgoing video call for which camera direction - // is set. - else if (VideoUtils.isOutgoingVideoCall(call)) { - cameraDir = call.getVideoSettings().getCameraDir(); - } - - // Infer the camera direction from the video state and store it, - // if this is an active video call and camera direction is not set. - else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) { - cameraDir = toCameraDirection(call.getVideoState()); - call.getVideoSettings().setCameraDir(cameraDir); - } - - // Use the stored camera dir if this is an active video call for which camera direction - // is set. - else if (VideoUtils.isActiveVideoCall(call)) { - cameraDir = call.getVideoSettings().getCameraDir(); - } - - // For all other cases infer the camera direction but don't store it in the call object. - else { - cameraDir = toCameraDirection(call.getVideoState()); - } - - com.android.incallui.Log.d(TAG, "updateCameraSelection: Setting camera direction to " + - cameraDir + " Call=" + call); - final InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - cameraManager.setUseFrontFacingCamera(cameraDir == - Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING); - } - - private static int toCameraDirection(int videoState) { - return VideoProfile.isTransmissionEnabled(videoState) && - !VideoProfile.isBidirectional(videoState) - ? Call.VideoSettings.CAMERA_DIRECTION_BACK_FACING - : Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING; - } - - private static boolean isCameraDirectionSet(Call call) { - return VideoUtils.isVideoCall(call) && call.getVideoSettings().getCameraDir() - != Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - } - - private static String toSimpleString(Call call) { - return call == null ? null : call.toSimpleString(); - } - - /** - * Starts an asynchronous load of the user's profile photo. - */ - public void loadProfilePhotoAsync() { - final VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - final AsyncTask task = new AsyncTask() { - /** - * Performs asynchronous load of the user profile information. - * - * @param params The parameters of the task. - * - * @return {@code null}. - */ - @Override - protected Void doInBackground(Void... params) { - if (mProfileInfo == null) { - // Try and read the photo URI from the local profile. - mProfileInfo = new ContactInfoCache.ContactCacheEntry(); - final Cursor cursor = mContext.getContentResolver().query( - ContactsContract.Profile.CONTENT_URI, new String[]{ - ContactsContract.CommonDataKinds.Phone._ID, - ContactsContract.CommonDataKinds.Phone.PHOTO_URI, - ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_ALTERNATIVE - }, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - mProfileInfo.lookupKey = cursor.getString(cursor.getColumnIndex( - ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)); - String photoUri = cursor.getString(cursor.getColumnIndex( - ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); - mProfileInfo.displayPhotoUri = photoUri == null ? null - : Uri.parse(photoUri); - mProfileInfo.namePrimary = cursor.getString(cursor.getColumnIndex( - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); - mProfileInfo.nameAlternative = cursor.getString( - cursor.getColumnIndex(ContactsContract.CommonDataKinds - .Phone.DISPLAY_NAME_ALTERNATIVE)); - } - } finally { - cursor.close(); - } - } - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - // If user profile information was found, issue an async request to load the user's - // profile photo. - if (mProfileInfo != null) { - if (mContactPhotoManager == null) { - mContactPhotoManager = ContactPhotoManager.getInstance(mContext); - } - ContactPhotoManager.DefaultImageRequest imageRequest = (mProfileInfo != null) - ? null : - new ContactPhotoManager.DefaultImageRequest(mProfileInfo.namePrimary, - mProfileInfo.lookupKey, false /* isCircularPhoto */); - - ImageView photoView = ui.getPreviewPhotoView(); - if (photoView == null) { - return; - } - mContactPhotoManager.loadDirectoryPhoto(photoView, - mProfileInfo.displayPhotoUri, - false /* darkTheme */, false /* isCircular */, imageRequest); - } - } - }; - - task.execute(); - } - - /** - * Defines the VideoCallUI interactions. - */ - public interface VideoCallUi extends Ui { - void showVideoViews(boolean showPreview, boolean showIncoming); - void hideVideoUi(); - boolean isDisplayVideoSurfaceCreated(); - boolean isPreviewVideoSurfaceCreated(); - Surface getDisplayVideoSurface(); - Surface getPreviewVideoSurface(); - int getCurrentRotation(); - void setPreviewSize(int width, int height); - void setPreviewSurfaceSize(int width, int height); - void setDisplayVideoSize(int width, int height); - Point getScreenSize(); - Point getPreviewSize(); - void cleanupSurfaces(); - ImageView getPreviewPhotoView(); - void adjustPreviewLocation(boolean shiftUp, int offset); - void setPreviewRotation(int orientation); - } -} diff --git a/InCallUI/src/com/android/incallui/VideoPauseController.java b/InCallUI/src/com/android/incallui/VideoPauseController.java deleted file mode 100644 index fb873500ef0d81823082bdcd89075b19b34a147e..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/VideoPauseController.java +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.android.incallui.Call.State; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; -import com.android.incallui.InCallVideoCallCallbackNotifier.SessionModificationListener; -import com.google.common.base.Preconditions; - -import android.telecom.VideoProfile; - -/** - * This class is responsible for generating video pause/resume requests when the InCall UI is sent - * to the background and subsequently brought back to the foreground. - */ -class VideoPauseController implements InCallStateListener, IncomingCallListener { - private static final String TAG = "VideoPauseController"; - - /** - * Keeps track of the current active/foreground call. - */ - private class CallContext { - public CallContext(Call call) { - Preconditions.checkNotNull(call); - update(call); - } - - public void update(Call call) { - mCall = Preconditions.checkNotNull(call); - mState = call.getState(); - mVideoState = call.getVideoState(); - } - - public int getState() { - return mState; - } - - public int getVideoState() { - return mVideoState; - } - - public String toString() { - return String.format("CallContext {CallId=%s, State=%s, VideoState=%d}", - mCall.getId(), mState, mVideoState); - } - - public Call getCall() { - return mCall; - } - - private int mState = State.INVALID; - private int mVideoState; - private Call mCall; - } - - private InCallPresenter mInCallPresenter; - private static VideoPauseController sVideoPauseController; - - /** - * The current call context, if applicable. - */ - private CallContext mPrimaryCallContext = null; - - /** - * Tracks whether the application is in the background. {@code True} if the application is in - * the background, {@code false} otherwise. - */ - private boolean mIsInBackground = false; - - /** - * Singleton accessor for the {@link VideoPauseController}. - * @return Singleton instance of the {@link VideoPauseController}. - */ - /*package*/ - static synchronized VideoPauseController getInstance() { - if (sVideoPauseController == null) { - sVideoPauseController = new VideoPauseController(); - } - return sVideoPauseController; - } - - /** - * Configures the {@link VideoPauseController} to listen to call events. Configured via the - * {@link com.android.incallui.InCallPresenter}. - * - * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}. - */ - public void setUp(InCallPresenter inCallPresenter) { - log("setUp"); - mInCallPresenter = Preconditions.checkNotNull(inCallPresenter); - mInCallPresenter.addListener(this); - mInCallPresenter.addIncomingCallListener(this); - } - - /** - * Cleans up the {@link VideoPauseController} by removing all listeners and clearing its - * internal state. Called from {@link com.android.incallui.InCallPresenter}. - */ - public void tearDown() { - log("tearDown..."); - mInCallPresenter.removeListener(this); - mInCallPresenter.removeIncomingCallListener(this); - clear(); - } - - /** - * Clears the internal state for the {@link VideoPauseController}. - */ - private void clear() { - mInCallPresenter = null; - mPrimaryCallContext = null; - mIsInBackground = false; - } - - /** - * Handles changes in the {@link InCallState}. Triggers pause and resumption of video for the - * current foreground call. - * - * @param oldState The previous {@link InCallState}. - * @param newState The current {@link InCallState}. - * @param callList List of current call. - */ - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - log("onStateChange, OldState=" + oldState + " NewState=" + newState); - - Call call = null; - if (newState == InCallState.INCOMING) { - call = callList.getIncomingCall(); - } else if (newState == InCallState.WAITING_FOR_ACCOUNT) { - call = callList.getWaitingForAccountCall(); - } else if (newState == InCallState.PENDING_OUTGOING) { - call = callList.getPendingOutgoingCall(); - } else if (newState == InCallState.OUTGOING) { - call = callList.getOutgoingCall(); - } else { - call = callList.getActiveCall(); - } - - boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext); - boolean canVideoPause = VideoUtils.canVideoPause(call); - log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged); - log("onStateChange, canVideoPause=" + canVideoPause); - log("onStateChange, IsInBackground=" + mIsInBackground); - - if (hasPrimaryCallChanged) { - onPrimaryCallChanged(call); - return; - } - - if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) { - // Bring UI to foreground if outgoing request becomes active while UI is in - // background. - bringToForeground(); - } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) { - // Bring UI to foreground if VoLTE call becomes active while UI is in - // background. - bringToForeground(); - } - - updatePrimaryCallContext(call); - } - - /** - * Handles a change to the primary call. - *

- * Reject incoming or hangup dialing call: Where the previous call was an incoming call or a - * call in dialing state, resume the new primary call. - * Call swap: Where the new primary call is incoming, pause video on the previous primary call. - * - * @param call The new primary call. - */ - private void onPrimaryCallChanged(Call call) { - log("onPrimaryCallChanged: New call = " + call); - log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext); - log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground); - - Preconditions.checkState(!areSame(call, mPrimaryCallContext)); - final boolean canVideoPause = VideoUtils.canVideoPause(call); - - if ((isIncomingCall(mPrimaryCallContext) || isDialing(mPrimaryCallContext) || - (call != null && VideoProfile.isPaused(call.getVideoState()))) - && canVideoPause && !mIsInBackground) { - // Send resume request for the active call, if user rejects incoming call, ends dialing - // call, or the call was previously in a paused state and UI is in the foreground. - sendRequest(call, true); - } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) { - // Send pause request if there is an active video call, and we just received a new - // incoming call. - sendRequest(mPrimaryCallContext.getCall(), false); - } - - updatePrimaryCallContext(call); - } - - /** - * Handles new incoming calls by triggering a change in the primary call. - * - * @param oldState the old {@link InCallState}. - * @param newState the new {@link InCallState}. - * @param call the incoming call. - */ - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " Call=" + call); - - if (areSame(call, mPrimaryCallContext)) { - return; - } - - onPrimaryCallChanged(call); - } - - /** - * Caches a reference to the primary call and stores its previous state. - * - * @param call The new primary call. - */ - private void updatePrimaryCallContext(Call call) { - if (call == null) { - mPrimaryCallContext = null; - } else if (mPrimaryCallContext != null) { - mPrimaryCallContext.update(call); - } else { - mPrimaryCallContext = new CallContext(call); - } - } - - /** - * Called when UI goes in/out of the foreground. - * @param showing true if UI is in the foreground, false otherwise. - */ - public void onUiShowing(boolean showing) { - // Only send pause/unpause requests if we are in the INCALL state. - if (mInCallPresenter == null) { - return; - } - final boolean isInCall = mInCallPresenter.getInCallState() == InCallState.INCALL; - if (showing) { - onResume(isInCall); - } else { - onPause(isInCall); - } - } - - /** - * Called when UI is brought to the foreground. Sends a session modification request to resume - * the outgoing video. - * @param isInCall true if phone state is INCALL, false otherwise - */ - private void onResume(boolean isInCall) { - log("onResume"); - - mIsInBackground = false; - if (canVideoPause(mPrimaryCallContext) && isInCall) { - sendRequest(mPrimaryCallContext.getCall(), true); - } else { - log("onResume. Ignoring..."); - } - } - - /** - * Called when UI is sent to the background. Sends a session modification request to pause the - * outgoing video. - * @param isInCall true if phone state is INCALL, false otherwise - */ - private void onPause(boolean isInCall) { - log("onPause"); - - mIsInBackground = true; - if (canVideoPause(mPrimaryCallContext) && isInCall) { - sendRequest(mPrimaryCallContext.getCall(), false); - } else { - log("onPause, Ignoring..."); - } - } - - private void bringToForeground() { - if (mInCallPresenter != null) { - log("Bringing UI to foreground"); - mInCallPresenter.bringToForeground(false); - } else { - loge("InCallPresenter is null. Cannot bring UI to foreground"); - } - } - - /** - * Sends Pause/Resume request. - * - * @param call Call to be paused/resumed. - * @param resume If true resume request will be sent, otherwise pause request. - */ - private void sendRequest(Call call, boolean resume) { - // Check if this call supports pause/un-pause. - if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) { - return; - } - - if (resume) { - log("sending resume request, call=" + call); - call.getVideoCall() - .sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call)); - } else { - log("sending pause request, call=" + call); - call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call)); - } - } - - /** - * Determines if a given call is the same one stored in a {@link CallContext}. - * - * @param call The call. - * @param callContext The call context. - * @return {@code true} if the {@link Call} is the same as the one referenced in the - * {@link CallContext}. - */ - private static boolean areSame(Call call, CallContext callContext) { - if (call == null && callContext == null) { - return true; - } else if (call == null || callContext == null) { - return false; - } - return call.equals(callContext.getCall()); - } - - /** - * Determines if a video call can be paused. Only a video call which is active can be paused. - * - * @param callContext The call context to check. - * @return {@code true} if the call is an active video call. - */ - private static boolean canVideoPause(CallContext callContext) { - return isVideoCall(callContext) && callContext.getState() == Call.State.ACTIVE; - } - - /** - * Determines if a call referenced by a {@link CallContext} is a video call. - * - * @param callContext The call context. - * @return {@code true} if the call is a video call, {@code false} otherwise. - */ - private static boolean isVideoCall(CallContext callContext) { - return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState()); - } - - /** - * Determines if call is in incoming/waiting state. - * - * @param call The call context. - * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. - */ - private static boolean isIncomingCall(CallContext call) { - return call != null && isIncomingCall(call.getCall()); - } - - /** - * Determines if a call is in incoming/waiting state. - * - * @param call The call. - * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. - */ - private static boolean isIncomingCall(Call call) { - return call != null && (call.getState() == Call.State.CALL_WAITING - || call.getState() == Call.State.INCOMING); - } - - /** - * Determines if a call is dialing. - * - * @param call The call context. - * @return {@code true} if the call is dialing, {@code false} otherwise. - */ - private static boolean isDialing(CallContext call) { - return call != null && Call.State.isDialing(call.getState()); - } - - /** - * Determines if a call is holding. - * - * @param call The call context. - * @return {@code true} if the call is holding, {@code false} otherwise. - */ - private static boolean isHolding(CallContext call) { - return call != null && call.getState() == Call.State.ONHOLD; - } - - /** - * Logs a debug message. - * - * @param msg The message. - */ - private void log(String msg) { - Log.d(this, TAG + msg); - } - - /** - * Logs an error message. - * - * @param msg The message. - */ - private void loge(String msg) { - Log.e(this, TAG + msg); - } -} diff --git a/InCallUI/src/com/android/incallui/VideoUtils.java b/InCallUI/src/com/android/incallui/VideoUtils.java deleted file mode 100644 index a2eb8bcf23780813dd339431c45490f916c732c6..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/VideoUtils.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telecom.VideoProfile; - -import com.android.contacts.common.compat.CompatUtils; - -import com.google.common.base.Preconditions; - -public class VideoUtils { - - public static boolean isVideoCall(Call call) { - return call != null && isVideoCall(call.getVideoState()); - } - - public static boolean isVideoCall(int videoState) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isTransmissionEnabled(videoState) - || VideoProfile.isReceptionEnabled(videoState); - } - - public static boolean isBidirectionalVideoCall(Call call) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isBidirectional(call.getVideoState()); - } - - public static boolean isTransmissionEnabled(Call call) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isTransmissionEnabled(call.getVideoState()); - } - - public static boolean isIncomingVideoCall(Call call) { - if (!VideoUtils.isVideoCall(call)) { - return false; - } - final int state = call.getState(); - return (state == Call.State.INCOMING) || (state == Call.State.CALL_WAITING); - } - - public static boolean isActiveVideoCall(Call call) { - return VideoUtils.isVideoCall(call) && call.getState() == Call.State.ACTIVE; - } - - public static boolean isOutgoingVideoCall(Call call) { - if (!VideoUtils.isVideoCall(call)) { - return false; - } - final int state = call.getState(); - return Call.State.isDialing(state) || state == Call.State.CONNECTING - || state == Call.State.SELECT_PHONE_ACCOUNT; - } - - public static boolean isAudioCall(Call call) { - if (!CompatUtils.isVideoCompatible()) { - return true; - } - - return call != null && VideoProfile.isAudioOnly(call.getVideoState()); - } - - // TODO (ims-vt) Check if special handling is needed for CONF calls. - public static boolean canVideoPause(Call call) { - return isVideoCall(call) && call.getState() == Call.State.ACTIVE; - } - - public static VideoProfile makeVideoPauseProfile(Call call) { - Preconditions.checkNotNull(call); - Preconditions.checkState(!VideoProfile.isAudioOnly(call.getVideoState())); - return new VideoProfile(getPausedVideoState(call.getVideoState())); - } - - public static VideoProfile makeVideoUnPauseProfile(Call call) { - Preconditions.checkNotNull(call); - return new VideoProfile(getUnPausedVideoState(call.getVideoState())); - } - - public static int getUnPausedVideoState(int videoState) { - return videoState & (~VideoProfile.STATE_PAUSED); - } - - public static int getPausedVideoState(int videoState) { - return videoState | VideoProfile.STATE_PAUSED; - } - -} diff --git a/InCallUI/src/com/android/incallui/async/PausableExecutor.java b/InCallUI/src/com/android/incallui/async/PausableExecutor.java deleted file mode 100644 index 1b8201a796eebd6d0c9103c8470ab1872d18b7b0..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/async/PausableExecutor.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.async; - -import com.android.contacts.common.testing.NeededForTesting; - -import java.util.concurrent.Executor; - -/** - * Executor that can be used to easily synchronize testing and production code. Production code - * should call {@link #milestone()} at points in the code where the state of the system is worthy of - * testing. In a test scenario, this method will pause execution until the test acknowledges the - * milestone through the use of {@link #ackMilestoneForTesting()}. - */ -public interface PausableExecutor extends Executor { - - /** - * Method called from asynchronous production code to inform this executor that it has - * reached a point that puts the system into a state worth testing. TestableExecutors intended - * for use in a testing environment should cause the calling thread to block. In the production - * environment this should be a no-op. - */ - void milestone(); - - /** - * Method called from the test code to inform this executor that the state of the production - * system at the current milestone has been sufficiently tested. Every milestone must be - * acknowledged. - */ - @NeededForTesting - void ackMilestoneForTesting(); - - /** - * Method called from the test code to inform this executor that the tests are finished with all - * milestones. Future calls to {@link #milestone()} or {@link #awaitMilestoneForTesting()} - * should return immediately. - */ - @NeededForTesting - void ackAllMilestonesForTesting(); - - /** - * Method called from the test code to block until a milestone has been reached in the - * production code. - */ - @NeededForTesting - void awaitMilestoneForTesting() throws InterruptedException; -} diff --git a/InCallUI/src/com/android/incallui/async/PausableExecutorImpl.java b/InCallUI/src/com/android/incallui/async/PausableExecutorImpl.java deleted file mode 100644 index 15900e57b2af98c86b48576fb1ebb78574b2c084..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/async/PausableExecutorImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.async; - -import java.util.concurrent.Executors; - -/** - * {@link PausableExecutor} intended for use in production environments. - */ -public class PausableExecutorImpl implements PausableExecutor { - - @Override - public void milestone() {} - - @Override - public void ackMilestoneForTesting() {} - - @Override - public void ackAllMilestonesForTesting() {} - - @Override - public void awaitMilestoneForTesting() {} - - @Override - public void execute(Runnable command) { - Executors.newSingleThreadExecutor().execute(command); - } -} diff --git a/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java b/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java deleted file mode 100644 index 39844e5a2f3f4ddc17c9d402eb7cc75f85136c14..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.ringtone; - -import com.google.common.base.Preconditions; - -import android.content.ContentResolver; -import android.net.Uri; -import android.provider.Settings; -import android.support.annotation.Nullable; - -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.incallui.Call; -import com.android.incallui.Call.State; -import com.android.incallui.CallList; - -/** - * Class that determines when ringtones should be played and can play the call waiting tone when - * necessary. - */ -public class DialerRingtoneManager { - - /* - * Flag used to determine if the Dialer is responsible for playing ringtones for incoming calls. - * Once we're ready to enable Dialer Ringing, these flags should be removed. - */ - private static final boolean IS_DIALER_RINGING_ENABLED = false; - private Boolean mIsDialerRingingEnabledForTesting; - - private final InCallTonePlayer mInCallTonePlayer; - private final CallList mCallList; - - /** - * Creates the DialerRingtoneManager with the given {@link InCallTonePlayer}. - * - * @param inCallTonePlayer the tone player used to play in-call tones. - * @param callList the CallList used to check for {@link State#CALL_WAITING} - * @throws NullPointerException if inCallTonePlayer or callList are null - */ - public DialerRingtoneManager(InCallTonePlayer inCallTonePlayer, CallList callList) { - mInCallTonePlayer = Preconditions.checkNotNull(inCallTonePlayer); - mCallList = Preconditions.checkNotNull(callList); - } - - /** - * Determines if a ringtone should be played for the given call state (see {@link State}) and - * {@link Uri}. - * - * @param callState the call state for the call being checked. - * @param ringtoneUri the ringtone to potentially play. - * @return {@code true} if the ringtone should be played, {@code false} otherwise. - */ - public boolean shouldPlayRingtone(int callState, @Nullable Uri ringtoneUri) { - return isDialerRingingEnabled() - && translateCallStateForCallWaiting(callState) == State.INCOMING - && ringtoneUri != null; - } - - /** - * Determines if an incoming call should vibrate as well as ring. - * - * @param resolver {@link ContentResolver} used to look up the - * {@link Settings.System#VIBRATE_WHEN_RINGING} setting. - * @return {@code true} if the call should vibrate, {@code false} otherwise. - */ - public boolean shouldVibrate(ContentResolver resolver) { - return Settings.System.getInt(resolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0; - } - - /** - * The incoming callState is never set as {@link State#CALL_WAITING} because - * {@link Call#translateState(int)} doesn't account for that case, check for it here - */ - private int translateCallStateForCallWaiting(int callState) { - if (callState != State.INCOMING) { - return callState; - } - return mCallList.getActiveCall() == null ? State.INCOMING : State.CALL_WAITING; - } - - private boolean isDialerRingingEnabled() { - if (mIsDialerRingingEnabledForTesting != null) { - return mIsDialerRingingEnabledForTesting; - } - return CompatUtils.isNCompatible() && IS_DIALER_RINGING_ENABLED; - } - - /** - * Determines if a call waiting tone should be played for the the given call state - * (see {@link State}). - * - * @param callState the call state for the call being checked. - * @return {@code true} if the call waiting tone should be played, {@code false} otherwise. - */ - public boolean shouldPlayCallWaitingTone(int callState) { - return isDialerRingingEnabled() - && translateCallStateForCallWaiting(callState) == State.CALL_WAITING - && !mInCallTonePlayer.isPlayingTone(); - } - - /** - * Plays the call waiting tone. - */ - public void playCallWaitingTone() { - if (!isDialerRingingEnabled()) { - return; - } - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - } - - /** - * Stops playing the call waiting tone. - */ - public void stopCallWaitingTone() { - if (!isDialerRingingEnabled()) { - return; - } - mInCallTonePlayer.stop(); - } - - @NeededForTesting - void setDialerRingingEnabledForTesting(boolean status) { - mIsDialerRingingEnabledForTesting = status; - } -} diff --git a/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java b/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java deleted file mode 100644 index 3a8b03d910900ce8cbe18dc00fc70bcc3e966428..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.ringtone; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; - -import android.media.AudioManager; -import android.media.ToneGenerator; -import android.support.annotation.Nullable; - -import com.android.incallui.Log; -import com.android.incallui.async.PausableExecutor; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Class responsible for playing in-call related tones in a background thread. This class only - * allows one tone to be played at a time. - */ -public class InCallTonePlayer { - - public static final int TONE_CALL_WAITING = 4; - - public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80; - - private final ToneGeneratorFactory mToneGeneratorFactory; - private final PausableExecutor mExecutor; - private @Nullable CountDownLatch mNumPlayingTones; - - /** - * Creates a new InCallTonePlayer. - * - * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create - * {@link ToneGenerator}s. - * @param executor the {@link PausableExecutor} used to play tones in a background thread. - * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are - * {@code null}. - */ - public InCallTonePlayer(ToneGeneratorFactory toneGeneratorFactory, PausableExecutor executor) { - mToneGeneratorFactory = Preconditions.checkNotNull(toneGeneratorFactory); - mExecutor = Preconditions.checkNotNull(executor); - } - - /** - * @return {@code true} if a tone is currently playing, {@code false} otherwise. - */ - public boolean isPlayingTone() { - return mNumPlayingTones != null && mNumPlayingTones.getCount() > 0; - } - - /** - * Plays the given tone in a background thread. - * - * @param tone the tone to play. - * @throws IllegalStateException if a tone is already playing. - * @throws IllegalArgumentException if the tone is invalid. - */ - public void play(int tone) { - if (isPlayingTone()) { - throw new IllegalStateException("Tone already playing"); - } - final ToneGeneratorInfo info = getToneGeneratorInfo(tone); - mNumPlayingTones = new CountDownLatch(1); - mExecutor.execute(new Runnable() { - @Override - public void run() { - playOnBackgroundThread(info); - } - }); - } - - private ToneGeneratorInfo getToneGeneratorInfo(int tone) { - switch (tone) { - case TONE_CALL_WAITING: - /* - * Call waiting tones play until they're stopped either by the user accepting or - * declining the call so the tone length is set at what's effectively forever. The - * tone is played at a high priority volume and through STREAM_VOICE_CALL since it's - * call related and using that stream will route it through bluetooth devices - * appropriately. - */ - return new ToneGeneratorInfo(ToneGenerator.TONE_SUP_CALL_WAITING, - VOLUME_RELATIVE_HIGH_PRIORITY, - Integer.MAX_VALUE, - AudioManager.STREAM_VOICE_CALL); - default: - throw new IllegalArgumentException("Bad tone: " + tone); - } - } - - private void playOnBackgroundThread(ToneGeneratorInfo info) { - ToneGenerator toneGenerator = null; - try { - Log.v(this, "Starting tone " + info); - toneGenerator = mToneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume); - toneGenerator.startTone(info.tone); - /* - * During tests, this will block until the tests call mExecutor.ackMilestone. This call - * allows for synchronization to the point where the tone has started playing. - */ - mExecutor.milestone(); - if (mNumPlayingTones != null) { - mNumPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS); - // Allows for synchronization to the point where the tone has completed playing. - mExecutor.milestone(); - } - } catch (InterruptedException e) { - Log.w(this, "Interrupted while playing in-call tone."); - } finally { - if (toneGenerator != null) { - toneGenerator.release(); - } - if (mNumPlayingTones != null) { - mNumPlayingTones.countDown(); - } - // Allows for synchronization to the point where this background thread has cleaned up. - mExecutor.milestone(); - } - } - - /** - * Stops playback of the current tone. - */ - public void stop() { - if (mNumPlayingTones != null) { - mNumPlayingTones.countDown(); - } - } - - private static class ToneGeneratorInfo { - public final int tone; - public final int volume; - public final int toneLengthMillis; - public final int stream; - - public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, - int stream) { - this.tone = toneGeneratorType; - this.volume = volume; - this.toneLengthMillis = toneLengthMillis; - this.stream = stream; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("tone", tone) - .add("volume", volume) - .add("toneLengthMillis", toneLengthMillis).toString(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/ringtone/ToneGeneratorFactory.java b/InCallUI/src/com/android/incallui/ringtone/ToneGeneratorFactory.java deleted file mode 100644 index ac47c8a7dadee86285b2bb00b8b3e229189dbfa3..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/ringtone/ToneGeneratorFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.ringtone; - -import android.media.ToneGenerator; - -/** - * Factory used to create {@link ToneGenerator}s. - */ -public class ToneGeneratorFactory { - - /** - * Creates a new {@link ToneGenerator} to use while in a call. - * - * @param stream the stream through which to play tones. - * @param volume the volume at which to play tones. - * @return a new ToneGenerator. - */ - public ToneGenerator newInCallToneGenerator(int stream, int volume) { - return new ToneGenerator(stream, volume); - } -} diff --git a/InCallUI/src/com/android/incallui/service/PhoneNumberService.java b/InCallUI/src/com/android/incallui/service/PhoneNumberService.java deleted file mode 100644 index 70da4ef3a75a6f8dd3a67e56f93e7a27e6bc5f06..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/service/PhoneNumberService.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.service; - -import android.graphics.Bitmap; - -/** - * Provides phone number lookup services. - */ -public interface PhoneNumberService { - - /** - * Get a phone number number asynchronously. - * - * @param phoneNumber The phone number to lookup. - * @param listener The listener to notify when the phone number lookup is complete. - * @param imageListener The listener to notify when the image lookup is complete. - */ - public void getPhoneNumberInfo(String phoneNumber, NumberLookupListener listener, - ImageLookupListener imageListener, boolean isIncoming); - - public interface NumberLookupListener { - - /** - * Callback when a phone number has been looked up. - * - * @param info The looked up information. Or (@literal null} if there are no results. - */ - public void onPhoneNumberInfoComplete(PhoneNumberInfo info); - } - - public interface ImageLookupListener { - - /** - * Callback when a image has been fetched. - * - * @param bitmap The fetched image. - */ - public void onImageFetchComplete(Bitmap bitmap); - } - - public interface PhoneNumberInfo { - public String getDisplayName(); - public String getNumber(); - public int getPhoneType(); - public String getPhoneLabel(); - public String getNormalizedNumber(); - public String getImageUrl(); - public String getLookupKey(); - public boolean isBusiness(); - public int getLookupSource(); - } -} diff --git a/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java b/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java deleted file mode 100644 index b97f4d099996a118eb0b736f2bd954f5adafa942..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.spam; - -import com.google.common.annotations.VisibleForTesting; - -import android.content.Context; -import android.telecom.DisconnectCause; -import android.text.TextUtils; - -import com.android.dialer.calllog.CallLogAsyncTaskUtil; -import com.android.incallui.Call; -import com.android.incallui.CallList; -import com.android.incallui.Log; - -public class SpamCallListListener implements CallList.Listener { - private static final String TAG = "SpamCallListListener"; - - private final Context mContext; - - public SpamCallListListener(Context context) { - mContext = context; - } - - @Override - public void onIncomingCall(final Call call) { - String number = call.getNumber(); - if (TextUtils.isEmpty(number)) { - return; - } - CallLogAsyncTaskUtil.getNumberInCallHistory(mContext, number, - new CallLogAsyncTaskUtil.OnGetNumberInCallHistoryListener() { - @Override - public void onComplete(boolean inCallHistory) { - call.setCallHistoryStatus(inCallHistory ? - Call.CALL_HISTORY_STATUS_PRESENT - : Call.CALL_HISTORY_STATUS_NOT_PRESENT); - } - }); - } - - @Override - public void onUpgradeToVideo(Call call) {} - - @Override - public void onCallListChange(CallList callList) {} - - @Override - public void onDisconnect(Call call) { - if (shouldShowAfterCallNotification(call)) { - showNotification(call.getNumber()); - } - } - - /** - * Posts the intent for displaying the after call spam notification to the user. - */ - @VisibleForTesting - /* package */ void showNotification(String number) { - //TODO(mhashmi): build and show notifications here - } - - /** - * Determines if the after call notification should be shown for the specified call. - */ - private boolean shouldShowAfterCallNotification(Call call) { - String number = call.getNumber(); - if (TextUtils.isEmpty(number)) { - return false; - } - - Call.LogState logState = call.getLogState(); - if (!logState.isIncoming) { - return false; - } - - if (logState.duration <= 0) { - return false; - } - - if (logState.contactLookupResult != Call.LogState.LOOKUP_NOT_FOUND - && logState.contactLookupResult != Call.LogState.LOOKUP_UNKNOWN) { - return false; - } - - int callHistoryStatus = call.getCallHistoryStatus(); - if (callHistoryStatus == Call.CALL_HISTORY_STATUS_PRESENT) { - return false; - } else if (callHistoryStatus == Call.CALL_HISTORY_STATUS_UNKNOWN) { - Log.i(TAG, "Call history status is unknown, returning false"); - return false; - } - - // Check if call disconnected because of either user hanging up - int disconnectCause = call.getDisconnectCause().getCode(); - if (disconnectCause != DisconnectCause.LOCAL && disconnectCause != DisconnectCause.REMOTE) { - return false; - } - - Log.i(TAG, "shouldShowAfterCallNotification, returning true"); - return true; - } -} \ No newline at end of file diff --git a/InCallUI/src/com/android/incallui/util/AccessibilityUtil.java b/InCallUI/src/com/android/incallui/util/AccessibilityUtil.java deleted file mode 100644 index 1fdd2bac6a4e0a6825ca4e62120ae56e7c13bbcd..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/util/AccessibilityUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.util; - -import android.content.Context; -import android.view.accessibility.AccessibilityManager; - -public class AccessibilityUtil { - public static boolean isTalkBackEnabled(Context context) { - AccessibilityManager accessibilityManager = (AccessibilityManager) context - .getSystemService(Context.ACCESSIBILITY_SERVICE); - return accessibilityManager != null - && accessibilityManager.isEnabled() - && accessibilityManager.isTouchExplorationEnabled(); - } -} diff --git a/InCallUI/src/com/android/incallui/util/TelecomCallUtil.java b/InCallUI/src/com/android/incallui/util/TelecomCallUtil.java deleted file mode 100644 index 53ecc29e9aba5485c3f8c834f269d065472f230d..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/util/TelecomCallUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.util; - -import android.net.Uri; -import android.telecom.Call; -import android.telephony.PhoneNumberUtils; - -/** - * Class to provide a standard interface for obtaining information from the underlying - * android.telecom.Call. Much of this should be obtained through the incall.Call, but - * on occasion we need to interact with the telecom.Call directly (eg. call blocking, - * before the incall.Call has been created). - */ -public class TelecomCallUtil { - - // Whether the call handle is an emergency number. - public static boolean isEmergencyCall(Call call) { - Uri handle = call.getDetails().getHandle(); - return PhoneNumberUtils.isEmergencyNumber( - handle == null ? "" : handle.getSchemeSpecificPart()); - } - - public static String getNumber(Call call) { - if (call == null) { - return null; - } - if (call.getDetails().getGatewayInfo() != null) { - return call.getDetails().getGatewayInfo() - .getOriginalAddress().getSchemeSpecificPart(); - } - Uri handle = getHandle(call); - return handle == null ? null : handle.getSchemeSpecificPart(); - } - - public static Uri getHandle(Call call) { - return call == null ? null : call.getDetails().getHandle(); - } -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/Ease.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/Ease.java deleted file mode 100644 index 5ef689771851d8b5201416d2b9fb19bee8488119..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/Ease.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.animation.TimeInterpolator; - -class Ease { - private static final float DOMAIN = 1.0f; - private static final float DURATION = 1.0f; - private static final float START = 0.0f; - - static class Linear { - public static final TimeInterpolator easeNone = new TimeInterpolator() { - public float getInterpolation(float input) { - return input; - } - }; - } - - static class Cubic { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1.0f) ? - (DOMAIN/2*input*input*input + START) - : (DOMAIN/2*((input-=2)*input*input + 2) + START); - } - }; - } - - static class Quad { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation (float input) { - return DOMAIN*(input/=DURATION)*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN *(input/=DURATION)*(input-2) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input + START) - : (-DOMAIN/2 * ((--input)*(input-2) - 1) + START); - } - }; - } - - static class Quart { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input*input*input + START) - : (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START); - } - }; - } - - static class Quint { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input*input*input*input + START) - : (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START); - } - }; - } - - static class Sine { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN * (float) Math.cos(input/DURATION * (Math.PI/2)) + DOMAIN + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN * (float) Math.sin(input/DURATION * (Math.PI/2)) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN/2 * ((float)Math.cos(Math.PI*input/DURATION) - 1.0f) + START; - } - }; - } - -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java deleted file mode 100644 index efeb4b7e36c57f758242c71d37291746cd42a3ad..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java +++ /dev/null @@ -1,1473 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.Vibrator; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.accessibility.AccessibilityEventCompat; -import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; -import android.support.v4.widget.ExploreByTouchHelper; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; -import android.view.accessibility.AccessibilityNodeProvider; - -import com.android.dialer.R; - -import java.util.ArrayList; -import java.util.List; - -/** - * This is a copy of com.android.internal.widget.multiwaveview.GlowPadView with minor changes - * to remove dependencies on private api's. - * - * Incoporated the scaling functionality. - * - * A re-usable widget containing a center, outer ring and wave animation. - */ -public class GlowPadView extends View { - private static final String TAG = "GlowPadView"; - private static final boolean DEBUG = false; - - // Wave state machine - private static final int STATE_IDLE = 0; - private static final int STATE_START = 1; - private static final int STATE_FIRST_TOUCH = 2; - private static final int STATE_TRACKING = 3; - private static final int STATE_SNAP = 4; - private static final int STATE_FINISH = 5; - - // Animation properties. - private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it - - public interface OnTriggerListener { - int NO_HANDLE = 0; - int CENTER_HANDLE = 1; - public void onGrabbed(View v, int handle); - public void onReleased(View v, int handle); - public void onTrigger(View v, int target); - public void onGrabbedStateChange(View v, int handle); - public void onFinishFinalAnimation(); - } - - // Tuneable parameters for animation - private static final int WAVE_ANIMATION_DURATION = 1350; - private static final int RETURN_TO_HOME_DELAY = 1200; - private static final int RETURN_TO_HOME_DURATION = 200; - private static final int HIDE_ANIMATION_DELAY = 200; - private static final int HIDE_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DELAY = 50; - private static final int INITIAL_SHOW_HANDLE_DURATION = 200; - private static final int REVEAL_GLOW_DELAY = 0; - private static final int REVEAL_GLOW_DURATION = 0; - - private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; - private static final float TARGET_SCALE_EXPANDED = 1.0f; - private static final float TARGET_SCALE_COLLAPSED = 0.8f; - private static final float RING_SCALE_EXPANDED = 1.0f; - private static final float RING_SCALE_COLLAPSED = 0.5f; - - private ArrayList mTargetDrawables = new ArrayList(); - private AnimationBundle mWaveAnimations = new AnimationBundle(); - private AnimationBundle mTargetAnimations = new AnimationBundle(); - private AnimationBundle mGlowAnimations = new AnimationBundle(); - private ArrayList mTargetDescriptions; - private ArrayList mDirectionDescriptions; - private OnTriggerListener mOnTriggerListener; - private TargetDrawable mHandleDrawable; - private TargetDrawable mOuterRing; - private Vibrator mVibrator; - - private int mFeedbackCount = 3; - private int mVibrationDuration = 0; - private int mGrabbedState; - private int mActiveTarget = -1; - private float mGlowRadius; - private float mWaveCenterX; - private float mWaveCenterY; - private int mMaxTargetHeight; - private int mMaxTargetWidth; - private float mRingScaleFactor = 1f; - private boolean mAllowScaling; - - private float mOuterRadius = 0.0f; - private float mSnapMargin = 0.0f; - private boolean mDragging; - private int mNewTargetResources; - - private AccessibilityNodeProvider mAccessibilityNodeProvider; - private GlowpadExploreByTouchHelper mExploreByTouchHelper; - - private class AnimationBundle extends ArrayList { - private static final long serialVersionUID = 0xA84D78726F127468L; - private boolean mSuspended; - - public void start() { - if (mSuspended) return; // ignore attempts to start animations - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.start(); - } - } - - public void cancel() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.cancel(); - } - clear(); - } - - public void stop() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.end(); - } - clear(); - } - - public void setSuspended(boolean suspend) { - mSuspended = suspend; - } - }; - - private AnimatorListener mResetListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - ping(); - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - invalidate(); - } - }; - - private boolean mAnimatingTargets; - private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - if (mNewTargetResources != 0) { - internalSetTargetResources(mNewTargetResources); - mNewTargetResources = 0; - hideTargets(false, false); - } - mAnimatingTargets = false; - } - }; - private int mTargetResourceId; - private int mTargetDescriptionsResourceId; - private int mDirectionDescriptionsResourceId; - private boolean mAlwaysTrackFinger; - private int mHorizontalInset; - private int mVerticalInset; - private int mGravity = Gravity.TOP; - private boolean mInitialLayout = true; - private Tweener mBackgroundAnimator; - private PointCloud mPointCloud; - private float mInnerRadius; - private int mPointerId; - - public GlowPadView(Context context) { - this(context, null); - } - - public GlowPadView(Context context, AttributeSet attrs) { - super(context, attrs); - Resources res = context.getResources(); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView); - mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius); - mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius); - mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin); - mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration, - mVibrationDuration); - mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount, - mFeedbackCount); - mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false); - TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable); - setHandleDrawable(handle != null ? handle.resourceId : R.drawable.ic_incall_audio_handle); - mOuterRing = new TargetDrawable(res, - getResourceId(a, R.styleable.GlowPadView_outerRingDrawable), 1); - - mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false); - - int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable); - Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null; - mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f); - - TypedValue outValue = new TypedValue(); - - // Read array of target drawables - if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) { - internalSetTargetResources(outValue.resourceId); - } - if (mTargetDrawables == null || mTargetDrawables.size() == 0) { - throw new IllegalStateException("Must specify at least one target drawable"); - } - - // Read array of target descriptions - if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify target descriptions"); - } - setTargetDescriptionsResourceId(resourceId); - } - - // Read array of direction descriptions - if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify direction descriptions"); - } - setDirectionDescriptionsResourceId(resourceId); - } - - // Use gravity attribute from LinearLayout - //a = context.obtainStyledAttributes(attrs, R.styleable.LinearLayout); - mGravity = a.getInt(R.styleable.GlowPadView_android_gravity, Gravity.TOP); - a.recycle(); - - - setVibrateEnabled(mVibrationDuration > 0); - - assignDefaultsIfNeeded(); - - mPointCloud = new PointCloud(pointDrawable); - mPointCloud.makePointCloud(mInnerRadius, mOuterRadius); - mPointCloud.glowManager.setRadius(mGlowRadius); - - mExploreByTouchHelper = new GlowpadExploreByTouchHelper(this); - ViewCompat.setAccessibilityDelegate(this, mExploreByTouchHelper); - } - - private int getResourceId(TypedArray a, int id) { - TypedValue tv = a.peekValue(id); - return tv == null ? 0 : tv.resourceId; - } - - private void dump() { - Log.v(TAG, "Outer Radius = " + mOuterRadius); - Log.v(TAG, "SnapMargin = " + mSnapMargin); - Log.v(TAG, "FeedbackCount = " + mFeedbackCount); - Log.v(TAG, "VibrationDuration = " + mVibrationDuration); - Log.v(TAG, "GlowRadius = " + mGlowRadius); - Log.v(TAG, "WaveCenterX = " + mWaveCenterX); - Log.v(TAG, "WaveCenterY = " + mWaveCenterY); - } - - public void suspendAnimations() { - mWaveAnimations.setSuspended(true); - mTargetAnimations.setSuspended(true); - mGlowAnimations.setSuspended(true); - } - - public void resumeAnimations() { - mWaveAnimations.setSuspended(false); - mTargetAnimations.setSuspended(false); - mGlowAnimations.setSuspended(false); - mWaveAnimations.start(); - mTargetAnimations.start(); - mGlowAnimations.start(); - } - - @Override - protected int getSuggestedMinimumWidth() { - // View should be large enough to contain the background + handle and - // target drawable on either edge. - return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth); - } - - @Override - protected int getSuggestedMinimumHeight() { - // View should be large enough to contain the unlock ring + target and - // target drawable on either edge - return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight); - } - - /** - * This gets the suggested width accounting for the ring's scale factor. - */ - protected int getScaledSuggestedMinimumWidth() { - return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) - + mMaxTargetWidth); - } - - /** - * This gets the suggested height accounting for the ring's scale factor. - */ - protected int getScaledSuggestedMinimumHeight() { - return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) - + mMaxTargetHeight); - } - - private int resolveMeasured(int measureSpec, int desired) - { - int result = 0; - int specSize = MeasureSpec.getSize(measureSpec); - switch (MeasureSpec.getMode(measureSpec)) { - case MeasureSpec.UNSPECIFIED: - result = desired; - break; - case MeasureSpec.AT_MOST: - result = Math.min(specSize, desired); - break; - case MeasureSpec.EXACTLY: - default: - result = specSize; - } - return result; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int minimumWidth = getSuggestedMinimumWidth(); - final int minimumHeight = getSuggestedMinimumHeight(); - int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); - int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); - - mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight, - computedWidth, computedHeight); - - int scaledWidth = getScaledSuggestedMinimumWidth(); - int scaledHeight = getScaledSuggestedMinimumHeight(); - - computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight); - setMeasuredDimension(computedWidth, computedHeight); - } - - private void switchToState(int state, float x, float y) { - switch (state) { - case STATE_IDLE: - deactivateTargets(); - hideGlow(0, 0, 0.0f, null); - startBackgroundAnimation(0, 0.0f); - mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); - mHandleDrawable.setAlpha(1.0f); - break; - - case STATE_START: - startBackgroundAnimation(0, 0.0f); - break; - - case STATE_FIRST_TOUCH: - mHandleDrawable.setAlpha(0.0f); - deactivateTargets(); - showTargets(true); - startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f); - setGrabbedState(OnTriggerListener.CENTER_HANDLE); - - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isEnabled()) { - announceTargets(); - } - break; - - case STATE_TRACKING: - mHandleDrawable.setAlpha(0.0f); - break; - - case STATE_SNAP: - // TODO: Add transition states (see list_selector_background_transition.xml) - mHandleDrawable.setAlpha(0.0f); - showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null); - break; - - case STATE_FINISH: - doFinish(); - break; - } - } - - private void showGlow(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mGlowAnimations.cancel(); - mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, - "ease", Ease.Cubic.easeIn, - "delay", delay, - "alpha", finalAlpha, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mGlowAnimations.start(); - } - - private void hideGlow(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mGlowAnimations.cancel(); - mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, - "ease", Ease.Quart.easeOut, - "delay", delay, - "alpha", finalAlpha, - "x", 0.0f, - "y", 0.0f, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mGlowAnimations.start(); - } - - private void deactivateTargets() { - final int count = mTargetDrawables.size(); - for (int i = 0; i < count; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - } - mActiveTarget = -1; - } - - /** - * Dispatches a trigger event to listener. Ignored if a listener is not set. - * @param whichTarget the target that was triggered. - */ - private void dispatchTriggerEvent(int whichTarget) { - vibrate(); - if (mOnTriggerListener != null) { - mOnTriggerListener.onTrigger(this, whichTarget); - } - } - - private void dispatchOnFinishFinalAnimation() { - if (mOnTriggerListener != null) { - mOnTriggerListener.onFinishFinalAnimation(); - } - } - - private void doFinish() { - final int activeTarget = mActiveTarget; - final boolean targetHit = activeTarget != -1; - - if (targetHit) { - if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); - - highlightSelected(activeTarget); - - // Inform listener of any active targets. Typically only one will be active. - hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener); - dispatchTriggerEvent(activeTarget); - if (!mAlwaysTrackFinger) { - // Force ring and targets to finish animation to final expanded state - mTargetAnimations.stop(); - } - } else { - // Animate handle back to the center based on current state. - hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing); - hideTargets(true, false); - } - - setGrabbedState(OnTriggerListener.NO_HANDLE); - } - - private void highlightSelected(int activeTarget) { - // Highlight the given target and fade others - mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); - hideUnselected(activeTarget); - } - - private void hideUnselected(int active) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - if (i != active) { - mTargetDrawables.get(i).setAlpha(0.0f); - } - } - } - - private void hideTargets(boolean animate, boolean expanded) { - mTargetAnimations.cancel(); - // Note: these animations should complete at the same time so that we can swap out - // the target assets asynchronously from the setTargetResources() call. - mAnimatingTargets = animate; - final int duration = animate ? HIDE_ANIMATION_DURATION : 0; - final int delay = animate ? HIDE_ANIMATION_DELAY : 0; - - final float targetScale = expanded ? - TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; - final int length = mTargetDrawables.size(); - final TimeInterpolator interpolator = Ease.Cubic.easeOut; - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", interpolator, - "alpha", 0.0f, - "scaleX", targetScale, - "scaleY", targetScale, - "delay", delay, - "onUpdate", mUpdateListener)); - } - - float ringScaleTarget = expanded ? - RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; - ringScaleTarget *= mRingScaleFactor; - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", interpolator, - "alpha", 0.0f, - "scaleX", ringScaleTarget, - "scaleY", ringScaleTarget, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void showTargets(boolean animate) { - mTargetAnimations.stop(); - mAnimatingTargets = animate; - final int delay = animate ? SHOW_ANIMATION_DELAY : 0; - final int duration = animate ? SHOW_ANIMATION_DURATION : 0; - final int length = mTargetDrawables.size(); - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", 1.0f, - "scaleY", 1.0f, - "delay", delay, - "onUpdate", mUpdateListener)); - } - float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED; - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", ringScale, - "scaleY", ringScale, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void vibrate() { - if (mVibrator != null) { - mVibrator.vibrate(mVibrationDuration); - } - } - - private ArrayList loadDrawableArray(int resourceId) { - Resources res = getContext().getResources(); - TypedArray array = res.obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList drawables = new ArrayList(count); - for (int i = 0; i < count; i++) { - TypedValue value = array.peekValue(i); - TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0, 3); - drawables.add(target); - } - array.recycle(); - return drawables; - } - - private void internalSetTargetResources(int resourceId) { - final ArrayList targets = loadDrawableArray(resourceId); - mTargetDrawables = targets; - mTargetResourceId = resourceId; - - int maxWidth = mHandleDrawable.getWidth(); - int maxHeight = mHandleDrawable.getHeight(); - final int count = targets.size(); - for (int i = 0; i < count; i++) { - TargetDrawable target = targets.get(i); - maxWidth = Math.max(maxWidth, target.getWidth()); - maxHeight = Math.max(maxHeight, target.getHeight()); - } - if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { - mMaxTargetWidth = maxWidth; - mMaxTargetHeight = maxHeight; - requestLayout(); // required to resize layout and call updateTargetPositions() - } else { - updateTargetPositions(mWaveCenterX, mWaveCenterY); - updatePointCloudPosition(mWaveCenterX, mWaveCenterY); - } - } - /** - * Loads an array of drawables from the given resourceId. - * - * @param resourceId - */ - public void setTargetResources(int resourceId) { - if (mAnimatingTargets) { - // postpone this change until we return to the initial state - mNewTargetResources = resourceId; - } else { - internalSetTargetResources(resourceId); - } - } - - public int getTargetResourceId() { - return mTargetResourceId; - } - - /** - * Sets the handle drawable to the drawable specified by the resource ID. - * @param resourceId - */ - public void setHandleDrawable(int resourceId) { - if (mHandleDrawable != null) { - mHandleDrawable.setDrawable(getResources(), resourceId); - } else { - mHandleDrawable = new TargetDrawable(getResources(), resourceId, 1); - } - mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); - } - - /** - * Sets the resource id specifying the target descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setTargetDescriptionsResourceId(int resourceId) { - mTargetDescriptionsResourceId = resourceId; - if (mTargetDescriptions != null) { - mTargetDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target descriptions for accessibility. - * - * @return The resource id. - */ - public int getTargetDescriptionsResourceId() { - return mTargetDescriptionsResourceId; - } - - /** - * Sets the resource id specifying the target direction descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setDirectionDescriptionsResourceId(int resourceId) { - mDirectionDescriptionsResourceId = resourceId; - if (mDirectionDescriptions != null) { - mDirectionDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target direction descriptions. - * - * @return The resource id. - */ - public int getDirectionDescriptionsResourceId() { - return mDirectionDescriptionsResourceId; - } - - /** - * Enable or disable vibrate on touch. - * - * @param enabled - */ - public void setVibrateEnabled(boolean enabled) { - if (enabled && mVibrator == null) { - mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); - } else { - mVibrator = null; - } - } - - /** - * Starts wave animation. - * - */ - public void ping() { - if (mFeedbackCount > 0) { - boolean doWaveAnimation = true; - final AnimationBundle waveAnimations = mWaveAnimations; - - // Don't do a wave if there's already one in progress - if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) { - long t = waveAnimations.get(0).animator.getCurrentPlayTime(); - if (t < WAVE_ANIMATION_DURATION/2) { - doWaveAnimation = false; - } - } - - if (doWaveAnimation) { - startWaveAnimation(); - } - } - } - - private void stopAndHideWaveAnimation() { - mWaveAnimations.cancel(); - mPointCloud.waveManager.setAlpha(0.0f); - } - - private void startWaveAnimation() { - mWaveAnimations.cancel(); - mPointCloud.waveManager.setAlpha(1.0f); - mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f); - mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION, - "ease", Ease.Quad.easeOut, - "delay", 0, - "radius", 2.0f * mOuterRadius, - "onUpdate", mUpdateListener, - "onComplete", - new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - mPointCloud.waveManager.setRadius(0.0f); - mPointCloud.waveManager.setAlpha(0.0f); - } - })); - mWaveAnimations.start(); - } - - /** - * Resets the widget to default state and cancels all animation. If animate is 'true', will - * animate objects into place. Otherwise, objects will snap back to place. - * - * @param animate - */ - public void reset(boolean animate) { - mGlowAnimations.stop(); - mTargetAnimations.stop(); - startBackgroundAnimation(0, 0.0f); - stopAndHideWaveAnimation(); - hideTargets(animate, false); - hideGlow(0, 0, 0.0f, null); - Tweener.reset(); - } - - private void startBackgroundAnimation(int duration, float alpha) { - final Drawable background = getBackground(); - if (mAlwaysTrackFinger && background != null) { - if (mBackgroundAnimator != null) { - mBackgroundAnimator.animator.cancel(); - } - mBackgroundAnimator = Tweener.to(background, duration, - "ease", Ease.Cubic.easeIn, - "alpha", (int)(255.0f * alpha), - "delay", SHOW_ANIMATION_DELAY); - mBackgroundAnimator.animator.start(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getActionMasked(); - boolean handled = false; - switch (action) { - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_DOWN: - if (DEBUG) Log.v(TAG, "*** DOWN ***"); - handleDown(event); - handleMove(event); - handled = true; - break; - - case MotionEvent.ACTION_MOVE: - if (DEBUG) Log.v(TAG, "*** MOVE ***"); - handleMove(event); - handled = true; - break; - - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: - if (DEBUG) Log.v(TAG, "*** UP ***"); - handleMove(event); - handleUp(event); - handled = true; - break; - - case MotionEvent.ACTION_CANCEL: - if (DEBUG) Log.v(TAG, "*** CANCEL ***"); - handleMove(event); - handleCancel(event); - handled = true; - break; - } - invalidate(); - return handled ? true : super.onTouchEvent(event); - } - - private void updateGlowPosition(float x, float y) { - float dx = x - mOuterRing.getX(); - float dy = y - mOuterRing.getY(); - dx *= 1f / mRingScaleFactor; - dy *= 1f / mRingScaleFactor; - mPointCloud.glowManager.setX(mOuterRing.getX() + dx); - mPointCloud.glowManager.setY(mOuterRing.getY() + dy); - } - - private void handleDown(MotionEvent event) { - int actionIndex = event.getActionIndex(); - float eventX = event.getX(actionIndex); - float eventY = event.getY(actionIndex); - switchToState(STATE_START, eventX, eventY); - if (!trySwitchToFirstTouchState(eventX, eventY)) { - mDragging = false; - } else { - mPointerId = event.getPointerId(actionIndex); - updateGlowPosition(eventX, eventY); - } - } - - private void handleUp(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); - int actionIndex = event.getActionIndex(); - if (event.getPointerId(actionIndex) == mPointerId) { - switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); - } - } - - private void handleCancel(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); - - // We should drop the active target here but it interferes with - // moving off the screen in the direction of the navigation bar. At some point we may - // want to revisit how we handle this. For now we'll allow a canceled event to - // activate the current target. - - // mActiveTarget = -1; // Drop the active target if canceled. - - int actionIndex = event.findPointerIndex(mPointerId); - actionIndex = actionIndex == -1 ? 0 : actionIndex; - switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); - } - - private void handleMove(MotionEvent event) { - int activeTarget = -1; - final int historySize = event.getHistorySize(); - ArrayList targets = mTargetDrawables; - int ntargets = targets.size(); - float x = 0.0f; - float y = 0.0f; - int actionIndex = event.findPointerIndex(mPointerId); - - if (actionIndex == -1) { - return; // no data for this pointer - } - - for (int k = 0; k < historySize + 1; k++) { - float eventX = k < historySize ? event.getHistoricalX(actionIndex, k) - : event.getX(actionIndex); - float eventY = k < historySize ? event.getHistoricalY(actionIndex, k) - :event.getY(actionIndex); - // tx and ty are relative to wave center - float tx = eventX - mWaveCenterX; - float ty = eventY - mWaveCenterY; - float touchRadius = (float) Math.hypot(tx, ty); - final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; - float limitX = tx * scale; - float limitY = ty * scale; - double angleRad = Math.atan2(-ty, tx); - - if (!mDragging) { - trySwitchToFirstTouchState(eventX, eventY); - } - - if (mDragging) { - // For multiple targets, snap to the one that matches - final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin; - final float snapDistance2 = snapRadius * snapRadius; - // Find first target in range - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = targets.get(i); - - double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets; - double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets; - if (target.isEnabled()) { - boolean angleMatches = - (angleRad > targetMinRad && angleRad <= targetMaxRad) || - (angleRad + 2 * Math.PI > targetMinRad && - angleRad + 2 * Math.PI <= targetMaxRad); - if (angleMatches && (dist2(tx, ty) > snapDistance2)) { - activeTarget = i; - } - } - } - } - x = limitX; - y = limitY; - } - - if (!mDragging) { - return; - } - - if (activeTarget != -1) { - switchToState(STATE_SNAP, x,y); - updateGlowPosition(x, y); - } else { - switchToState(STATE_TRACKING, x, y); - updateGlowPosition(x, y); - } - - if (mActiveTarget != activeTarget) { - // Defocus the old target - if (mActiveTarget != -1) { - TargetDrawable target = targets.get(mActiveTarget); - target.setState(TargetDrawable.STATE_INACTIVE); - } - // Focus the new target - if (activeTarget != -1) { - TargetDrawable target = targets.get(activeTarget); - target.setState(TargetDrawable.STATE_FOCUSED); - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isEnabled()) { - String targetContentDescription = getTargetDescription(activeTarget); - announceForAccessibility(targetContentDescription); - } - } - } - mActiveTarget = activeTarget; - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isTouchExplorationEnabled()) { - final int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_HOVER_ENTER: - event.setAction(MotionEvent.ACTION_DOWN); - break; - case MotionEvent.ACTION_HOVER_MOVE: - event.setAction(MotionEvent.ACTION_MOVE); - break; - case MotionEvent.ACTION_HOVER_EXIT: - event.setAction(MotionEvent.ACTION_UP); - break; - } - onTouchEvent(event); - event.setAction(action); - } - super.onHoverEvent(event); - return true; - } - - /** - * Sets the current grabbed state, and dispatches a grabbed state change - * event to our listener. - */ - private void setGrabbedState(int newState) { - if (newState != mGrabbedState) { - if (newState != OnTriggerListener.NO_HANDLE) { - vibrate(); - } - mGrabbedState = newState; - if (mOnTriggerListener != null) { - if (newState == OnTriggerListener.NO_HANDLE) { - mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); - } else { - mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); - } - mOnTriggerListener.onGrabbedStateChange(this, newState); - } - } - } - - private boolean trySwitchToFirstTouchState(float x, float y) { - final float tx = x - mWaveCenterX; - final float ty = y - mWaveCenterY; - if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) { - if (DEBUG) Log.v(TAG, "** Handle HIT"); - switchToState(STATE_FIRST_TOUCH, x, y); - updateGlowPosition(tx, ty); - mDragging = true; - return true; - } - return false; - } - - private void assignDefaultsIfNeeded() { - if (mOuterRadius == 0.0f) { - mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; - } - if (mSnapMargin == 0.0f) { - mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); - } - if (mInnerRadius == 0.0f) { - mInnerRadius = mHandleDrawable.getWidth() / 10.0f; - } - } - - private void computeInsets(int dx, int dy) { - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - mHorizontalInset = 0; - break; - case Gravity.RIGHT: - mHorizontalInset = dx; - break; - case Gravity.CENTER_HORIZONTAL: - default: - mHorizontalInset = dx / 2; - break; - } - switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { - case Gravity.TOP: - mVerticalInset = 0; - break; - case Gravity.BOTTOM: - mVerticalInset = dy; - break; - case Gravity.CENTER_VERTICAL: - default: - mVerticalInset = dy / 2; - break; - } - } - - /** - * Given the desired width and height of the ring and the allocated width and height, compute - * how much we need to scale the ring. - */ - private float computeScaleFactor(int desiredWidth, int desiredHeight, - int actualWidth, int actualHeight) { - - // Return unity if scaling is not allowed. - if (!mAllowScaling) return 1f; - - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - - float scaleX = 1f; - float scaleY = 1f; - - // We use the gravity as a cue for whether we want to scale on a particular axis. - // We only scale to fit horizontally if we're not pinned to the left or right. Likewise, - // we only scale to fit vertically if we're not pinned to the top or bottom. In these - // cases, we want the ring to hang off the side or top/bottom, respectively. - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - case Gravity.RIGHT: - break; - case Gravity.CENTER_HORIZONTAL: - default: - if (desiredWidth > actualWidth) { - scaleX = (1f * actualWidth - mMaxTargetWidth) / - (desiredWidth - mMaxTargetWidth); - } - break; - } - switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { - case Gravity.TOP: - case Gravity.BOTTOM: - break; - case Gravity.CENTER_VERTICAL: - default: - if (desiredHeight > actualHeight) { - scaleY = (1f * actualHeight - mMaxTargetHeight) / - (desiredHeight - mMaxTargetHeight); - } - break; - } - return Math.min(scaleX, scaleY); - } - - private float getRingWidth() { - return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); - } - - private float getRingHeight() { - return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - final int width = right - left; - final int height = bottom - top; - - // Target placement width/height. This puts the targets on the greater of the ring - // width or the specified outer radius. - final float placementWidth = getRingWidth(); - final float placementHeight = getRingHeight(); - float newWaveCenterX = mHorizontalInset - + (mMaxTargetWidth + placementWidth) / 2; - float newWaveCenterY = mVerticalInset - + (mMaxTargetHeight + placementHeight) / 2; - - if (mInitialLayout) { - stopAndHideWaveAnimation(); - hideTargets(false, false); - mInitialLayout = false; - } - - mOuterRing.setPositionX(newWaveCenterX); - mOuterRing.setPositionY(newWaveCenterY); - - mPointCloud.setScale(mRingScaleFactor); - - mHandleDrawable.setPositionX(newWaveCenterX); - mHandleDrawable.setPositionY(newWaveCenterY); - - updateTargetPositions(newWaveCenterX, newWaveCenterY); - updatePointCloudPosition(newWaveCenterX, newWaveCenterY); - updateGlowPosition(newWaveCenterX, newWaveCenterY); - - mWaveCenterX = newWaveCenterX; - mWaveCenterY = newWaveCenterY; - - if (DEBUG) dump(); - } - - private void updateTargetPositions(float centerX, float centerY) { - // Reposition the target drawables if the view changed. - ArrayList targets = mTargetDrawables; - final int size = targets.size(); - final float alpha = (float) (-2.0f * Math.PI / size); - for (int i = 0; i < size; i++) { - final TargetDrawable targetIcon = targets.get(i); - final float angle = alpha * i; - targetIcon.setPositionX(centerX); - targetIcon.setPositionY(centerY); - targetIcon.setX(getRingWidth() / 2 * (float) Math.cos(angle)); - targetIcon.setY(getRingHeight() / 2 * (float) Math.sin(angle)); - } - } - - private void updatePointCloudPosition(float centerX, float centerY) { - mPointCloud.setCenter(centerX, centerY); - } - - @Override - protected void onDraw(Canvas canvas) { - mPointCloud.draw(canvas); - mOuterRing.draw(canvas); - final int ntargets = mTargetDrawables.size(); - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = mTargetDrawables.get(i); - if (target != null) { - target.draw(canvas); - } - } - mHandleDrawable.draw(canvas); - } - - public void setOnTriggerListener(OnTriggerListener listener) { - mOnTriggerListener = listener; - } - - private float square(float d) { - return d * d; - } - - private float dist2(float dx, float dy) { - return dx*dx + dy*dy; - } - - private float getScaledGlowRadiusSquared() { - final float scaledTapRadius; - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isEnabled()) { - scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius; - } else { - scaledTapRadius = mGlowRadius; - } - return square(scaledTapRadius); - } - - private void announceTargets() { - StringBuilder utterance = new StringBuilder(); - final int targetCount = mTargetDrawables.size(); - for (int i = 0; i < targetCount; i++) { - String targetDescription = getTargetDescription(i); - String directionDescription = getDirectionDescription(i); - if (!TextUtils.isEmpty(targetDescription) - && !TextUtils.isEmpty(directionDescription)) { - String text = String.format(directionDescription, targetDescription); - utterance.append(text); - } - } - if (utterance.length() > 0) { - announceForAccessibility(utterance.toString()); - } - } - - private String getTargetDescription(int index) { - if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { - mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); - if (mTargetDrawables.size() != mTargetDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " equal to the number of target descriptions."); - return null; - } - } - return mTargetDescriptions.get(index); - } - - private String getDirectionDescription(int index) { - if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { - mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); - if (mTargetDrawables.size() != mDirectionDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " equal to the number of direction descriptions."); - return null; - } - } - return mDirectionDescriptions.get(index); - } - - private ArrayList loadDescriptions(int resourceId) { - TypedArray array = getContext().getResources().obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList targetContentDescriptions = new ArrayList(count); - for (int i = 0; i < count; i++) { - String contentDescription = array.getString(i); - targetContentDescriptions.add(contentDescription); - } - array.recycle(); - return targetContentDescriptions; - } - - public int getResourceIdForTarget(int index) { - final TargetDrawable drawable = mTargetDrawables.get(index); - return drawable == null ? 0 : drawable.getResourceId(); - } - - public void setEnableTarget(int resourceId, boolean enabled) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - target.setEnabled(enabled); - break; // should never be more than one match - } - } - } - - /** - * Gets the position of a target in the array that matches the given resource. - * @param resourceId - * @return the index or -1 if not found - */ - public int getTargetPosition(int resourceId) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - return i; // should never be more than one match - } - } - return -1; - } - - private boolean replaceTargetDrawables(Resources res, int existingResourceId, - int newResourceId) { - if (existingResourceId == 0 || newResourceId == 0) { - return false; - } - - boolean result = false; - final ArrayList drawables = mTargetDrawables; - final int size = drawables.size(); - for (int i = 0; i < size; i++) { - final TargetDrawable target = drawables.get(i); - if (target != null && target.getResourceId() == existingResourceId) { - target.setDrawable(res, newResourceId); - result = true; - } - } - - if (result) { - requestLayout(); // in case any given drawable's size changes - } - - return result; - } - - /** - * Searches the given package for a resource to use to replace the Drawable on the - * target with the given resource id - * @param component of the .apk that contains the resource - * @param name of the metadata in the .apk - * @param existingResId the resource id of the target to search for - * @return true if found in the given package and replaced at least one target Drawables - */ - public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, - int existingResId) { - if (existingResId == 0) return false; - - boolean replaced = false; - if (component != null) { - try { - PackageManager packageManager = getContext().getPackageManager(); - // Look for the search icon specified in the activity meta-data - Bundle metaData = packageManager.getActivityInfo( - component, PackageManager.GET_META_DATA).metaData; - if (metaData != null) { - int iconResId = metaData.getInt(name); - if (iconResId != 0) { - Resources res = packageManager.getResourcesForActivity(component); - replaced = replaceTargetDrawables(res, existingResId, iconResId); - } - } - } catch (NameNotFoundException e) { - Log.w(TAG, "Failed to swap drawable; " - + component.flattenToShortString() + " not found", e); - } catch (Resources.NotFoundException nfe) { - Log.w(TAG, "Failed to swap drawable from " - + component.flattenToShortString(), nfe); - } - } - if (!replaced) { - // Restore the original drawable - replaceTargetDrawables(getContext().getResources(), existingResId, existingResId); - } - return replaced; - } - - public class GlowpadExploreByTouchHelper extends ExploreByTouchHelper { - - private Rect mBounds = new Rect(); - - public GlowpadExploreByTouchHelper(View forView) { - super(forView); - } - - @Override - protected int getVirtualViewAt(float x, float y) { - if (mGrabbedState == OnTriggerListener.CENTER_HANDLE) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.isEnabled() && target.getBounds().contains((int) x, (int) y)) { - return i; - } - } - return INVALID_ID; - } else { - return HOST_ID; - } - } - - @Override - protected void getVisibleVirtualViews(List virtualViewIds) { - if (mGrabbedState == OnTriggerListener.CENTER_HANDLE) { - // Add virtual views backwards so that accessibility services like switch - // access traverse them in the correct order - for (int i = mTargetDrawables.size() - 1; i >= 0; i--) { - if (mTargetDrawables.get(i).isEnabled()) { - virtualViewIds.add(i); - } - } - } - } - - @Override - protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { - if (virtualViewId >= 0 && virtualViewId < mTargetDescriptions.size()) { - event.setContentDescription(mTargetDescriptions.get(virtualViewId)); - } - } - - @Override - public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { - if (host == GlowPadView.this && event.getEventType() - == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { - event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); - } - super.onInitializeAccessibilityEvent(host, event); - } - - @Override - public void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) { - if (mGrabbedState == OnTriggerListener.NO_HANDLE) { - node.setClickable(true); - node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); - } - mBounds.set(0, 0, GlowPadView.this.getWidth(), GlowPadView.this.getHeight()); - node.setBoundsInParent(mBounds); - } - - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - if (mGrabbedState == OnTriggerListener.NO_HANDLE) { - // Simulate handle being grabbed to expose targets. - trySwitchToFirstTouchState(mWaveCenterX, mWaveCenterY); - invalidateRoot(); - return true; - } - return super.performAccessibilityAction(host, action, args); - } - - @Override - protected void onPopulateNodeForVirtualView(int virtualViewId, - AccessibilityNodeInfoCompat node) { - if (virtualViewId < mTargetDrawables.size()) { - final TargetDrawable target = mTargetDrawables.get(virtualViewId); - node.setBoundsInParent(target.getBounds()); - node.setClickable(true); - node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); - node.setContentDescription(getTargetDescription(virtualViewId)); - } - } - - @Override - protected boolean onPerformActionForVirtualView(int virtualViewId, int action, - Bundle arguments) { - if (action == AccessibilityNodeInfo.ACTION_CLICK) { - if (virtualViewId >= 0 && virtualViewId < mTargetDrawables.size()) { - dispatchTriggerEvent(virtualViewId); - return true; - } - } - return false; - } - - } -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/PointCloud.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/PointCloud.java deleted file mode 100644 index 07a2cb964c489392a983d93bd7d7de1c4efe95ce..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/PointCloud.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.util.Log; - -import java.util.ArrayList; - -public class PointCloud { - private static final float MIN_POINT_SIZE = 2.0f; - private static final float MAX_POINT_SIZE = 4.0f; - private static final int INNER_POINTS = 8; - private static final String TAG = "PointCloud"; - private ArrayList mPointCloud = new ArrayList(); - private Drawable mDrawable; - private float mCenterX; - private float mCenterY; - private Paint mPaint; - private float mScale = 1.0f; - private static final float PI = (float) Math.PI; - - // These allow us to have multiple concurrent animations. - WaveManager waveManager = new WaveManager(); - GlowManager glowManager = new GlowManager(); - private float mOuterRadius; - - public class WaveManager { - private float radius = 50; - private float width = 200.0f; // TODO: Make configurable - private float alpha = 0.0f; - public void setRadius(float r) { - radius = r; - } - - public float getRadius() { - return radius; - } - - public void setAlpha(float a) { - alpha = a; - } - - public float getAlpha() { - return alpha; - } - }; - - public class GlowManager { - private float x; - private float y; - private float radius = 0.0f; - private float alpha = 0.0f; - - public void setX(float x1) { - x = x1; - } - - public float getX() { - return x; - } - - public void setY(float y1) { - y = y1; - } - - public float getY() { - return y; - } - - public void setAlpha(float a) { - alpha = a; - } - - public float getAlpha() { - return alpha; - } - - public void setRadius(float r) { - radius = r; - } - - public float getRadius() { - return radius; - } - } - - class Point { - float x; - float y; - float radius; - - public Point(float x2, float y2, float r) { - x = (float) x2; - y = (float) y2; - radius = r; - } - } - - public PointCloud(Drawable drawable) { - mPaint = new Paint(); - mPaint.setFilterBitmap(true); - mPaint.setColor(Color.rgb(255, 255, 255)); // TODO: make configurable - mPaint.setAntiAlias(true); - mPaint.setDither(true); - - mDrawable = drawable; - if (mDrawable != null) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } - } - - public void setCenter(float x, float y) { - mCenterX = x; - mCenterY = y; - } - - public void makePointCloud(float innerRadius, float outerRadius) { - if (innerRadius == 0) { - Log.w(TAG, "Must specify an inner radius"); - return; - } - mOuterRadius = outerRadius; - mPointCloud.clear(); - final float pointAreaRadius = (outerRadius - innerRadius); - final float ds = (2.0f * PI * innerRadius / INNER_POINTS); - final int bands = (int) Math.round(pointAreaRadius / ds); - final float dr = pointAreaRadius / bands; - float r = innerRadius; - for (int b = 0; b <= bands; b++, r += dr) { - float circumference = 2.0f * PI * r; - final int pointsInBand = (int) (circumference / ds); - float eta = PI/2.0f; - float dEta = 2.0f * PI / pointsInBand; - for (int i = 0; i < pointsInBand; i++) { - float x = r * (float) Math.cos(eta); - float y = r * (float) Math.sin(eta); - eta += dEta; - mPointCloud.add(new Point(x, y, r)); - } - } - } - - public void setScale(float scale) { - mScale = scale; - } - - public float getScale() { - return mScale; - } - - private static float hypot(float x, float y) { - return (float) Math.hypot(x, y); - } - - private static float max(float a, float b) { - return a > b ? a : b; - } - - public int getAlphaForPoint(Point point) { - // Contribution from positional glow - float glowDistance = hypot(glowManager.x - point.x, glowManager.y - point.y); - float glowAlpha = 0.0f; - - if (glowDistance < glowManager.radius) { - double cos = Math.cos(Math.PI * 0.25d * glowDistance / glowManager.radius); - glowAlpha = glowManager.alpha * max(0.0f, (float) Math.pow(cos, 10.0d)); - } - - // Compute contribution from Wave - float radius = hypot(point.x, point.y); - float distanceToWaveRing = (radius - waveManager.radius); - float waveAlpha = 0.0f; - if (distanceToWaveRing < waveManager.width * 0.5f && distanceToWaveRing < 0.0f) { - double cos = Math.cos(Math.PI * 0.25d * distanceToWaveRing / waveManager.width); - waveAlpha = waveManager.alpha * max(0.0f, (float) Math.pow(cos, 20.0d)); - } - - return (int) (max(glowAlpha, waveAlpha) * 255); - } - - private float interp(float min, float max, float f) { - return min + (max - min) * f; - } - - public void draw(Canvas canvas) { - ArrayList points = mPointCloud; - canvas.save(Canvas.MATRIX_SAVE_FLAG); - canvas.scale(mScale, mScale, mCenterX, mCenterY); - for (int i = 0; i < points.size(); i++) { - Point point = points.get(i); - final float pointSize = interp(MAX_POINT_SIZE, MIN_POINT_SIZE, - point.radius / mOuterRadius); - final float px = point.x + mCenterX; - final float py = point.y + mCenterY; - int alpha = getAlphaForPoint(point); - - if (alpha == 0) continue; - - if (mDrawable != null) { - canvas.save(Canvas.MATRIX_SAVE_FLAG); - final float cx = mDrawable.getIntrinsicWidth() * 0.5f; - final float cy = mDrawable.getIntrinsicHeight() * 0.5f; - final float s = pointSize / MAX_POINT_SIZE; - canvas.scale(s, s, px, py); - canvas.translate(px - cx, py - cy); - mDrawable.setAlpha(alpha); - mDrawable.draw(canvas); - canvas.restore(); - } else { - mPaint.setAlpha(alpha); - canvas.drawCircle(px, py, pointSize, mPaint); - } - } - canvas.restore(); - } - -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/TargetDrawable.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/TargetDrawable.java deleted file mode 100644 index adc5324ebaae2ea45dd3b73cdac211a1efefba79..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/TargetDrawable.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.util.Log; - -public class TargetDrawable { - private static final String TAG = "TargetDrawable"; - private static final boolean DEBUG = false; - - public static final int[] STATE_ACTIVE = - { android.R.attr.state_enabled, android.R.attr.state_active }; - public static final int[] STATE_INACTIVE = - { android.R.attr.state_enabled, -android.R.attr.state_active }; - public static final int[] STATE_FOCUSED = - { android.R.attr.state_enabled, -android.R.attr.state_active, - android.R.attr.state_focused }; - - private float mTranslationX = 0.0f; - private float mTranslationY = 0.0f; - private float mPositionX = 0.0f; - private float mPositionY = 0.0f; - private float mScaleX = 1.0f; - private float mScaleY = 1.0f; - private float mAlpha = 1.0f; - private Drawable mDrawable; - private boolean mEnabled = true; - private final int mResourceId; - private int mNumDrawables = 1; - private Rect mBounds; - - /** - * This is changed from the framework version to pass in the number of drawables in the - * container. The framework version relies on private api's to get the count from - * StateListDrawable. - * - * @param res - * @param resId - * @param count The number of drawables in the resource. - */ - public TargetDrawable(Resources res, int resId, int count) { - mResourceId = resId; - setDrawable(res, resId); - mNumDrawables = count; - } - - public void setDrawable(Resources res, int resId) { - // Note we explicitly don't set mResourceId to resId since we allow the drawable to be - // swapped at runtime and want to re-use the existing resource id for identification. - Drawable drawable = resId == 0 ? null : res.getDrawable(resId); - // Mutate the drawable so we can animate shared drawable properties. - mDrawable = drawable != null ? drawable.mutate() : null; - resizeDrawables(); - setState(STATE_INACTIVE); - } - - public TargetDrawable(TargetDrawable other) { - mResourceId = other.mResourceId; - // Mutate the drawable so we can animate shared drawable properties. - mDrawable = other.mDrawable != null ? other.mDrawable.mutate() : null; - resizeDrawables(); - setState(STATE_INACTIVE); - } - - public void setState(int [] state) { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - d.setState(state); - } - } - - /** - * Returns true if the drawable is a StateListDrawable and is in the focused state. - * - * @return - */ - public boolean isActive() { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - int[] states = d.getState(); - for (int i = 0; i < states.length; i++) { - if (states[i] == android.R.attr.state_focused) { - return true; - } - } - } - return false; - } - - /** - * Returns true if this target is enabled. Typically an enabled target contains a valid - * drawable in a valid state. Currently all targets with valid drawables are valid. - * - * @return - */ - public boolean isEnabled() { - return mDrawable != null && mEnabled; - } - - /** - * Makes drawables in a StateListDrawable all the same dimensions. - * If not a StateListDrawable, then justs sets the bounds to the intrinsic size of the - * drawable. - */ - private void resizeDrawables() { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - int maxWidth = 0; - int maxHeight = 0; - - for (int i = 0; i < mNumDrawables; i++) { - d.selectDrawable(i); - Drawable childDrawable = d.getCurrent(); - maxWidth = Math.max(maxWidth, childDrawable.getIntrinsicWidth()); - maxHeight = Math.max(maxHeight, childDrawable.getIntrinsicHeight()); - } - - if (DEBUG) Log.v(TAG, "union of childDrawable rects " + d + " to: " - + maxWidth + "x" + maxHeight); - d.setBounds(0, 0, maxWidth, maxHeight); - - for (int i = 0; i < mNumDrawables; i++) { - d.selectDrawable(i); - Drawable childDrawable = d.getCurrent(); - if (DEBUG) Log.v(TAG, "sizing drawable " + childDrawable + " to: " - + maxWidth + "x" + maxHeight); - childDrawable.setBounds(0, 0, maxWidth, maxHeight); - } - } else if (mDrawable != null) { - mDrawable.setBounds(0, 0, - mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); - } - } - - public void setX(float x) { - mTranslationX = x; - } - - public void setY(float y) { - mTranslationY = y; - } - - public void setScaleX(float x) { - mScaleX = x; - } - - public void setScaleY(float y) { - mScaleY = y; - } - - public void setAlpha(float alpha) { - mAlpha = alpha; - } - - public float getX() { - return mTranslationX; - } - - public float getY() { - return mTranslationY; - } - - public float getScaleX() { - return mScaleX; - } - - public float getScaleY() { - return mScaleY; - } - - public float getAlpha() { - return mAlpha; - } - - public void setPositionX(float x) { - mPositionX = x; - } - - public void setPositionY(float y) { - mPositionY = y; - } - - public float getPositionX() { - return mPositionX; - } - - public float getPositionY() { - return mPositionY; - } - - public int getWidth() { - return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0; - } - - public int getHeight() { - return mDrawable != null ? mDrawable.getIntrinsicHeight() : 0; - } - - public Rect getBounds() { - if (mBounds == null) { - mBounds = new Rect(); - } - mBounds.set((int) (mTranslationX + mPositionX - getWidth() * 0.5), - (int) (mTranslationY + mPositionY - getHeight() * 0.5), - (int) (mTranslationX + mPositionX + getWidth() * 0.5), - (int) (mTranslationY + mPositionY + getHeight() * 0.5)); - return mBounds; - } - - public void draw(Canvas canvas) { - if (mDrawable == null || !mEnabled) { - return; - } - canvas.save(Canvas.MATRIX_SAVE_FLAG); - canvas.scale(mScaleX, mScaleY, mPositionX, mPositionY); - canvas.translate(mTranslationX + mPositionX, mTranslationY + mPositionY); - canvas.translate(-0.5f * getWidth(), -0.5f * getHeight()); - mDrawable.setAlpha((int) Math.round(mAlpha * 255f)); - mDrawable.draw(canvas); - canvas.restore(); - } - - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - - public int getResourceId() { - return mResourceId; - } -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/Tweener.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/Tweener.java deleted file mode 100644 index 7222442fed93a5d5839eabe9973c6fb5b851063f..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/Tweener.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.util.Log; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map.Entry; - -class Tweener { - private static final String TAG = "Tweener"; - private static final boolean DEBUG = false; - - ObjectAnimator animator; - private static HashMap sTweens = new HashMap(); - - public Tweener(ObjectAnimator anim) { - animator = anim; - } - - private static void remove(Animator animator) { - Iterator> iter = sTweens.entrySet().iterator(); - while (iter.hasNext()) { - Entry entry = iter.next(); - if (entry.getValue().animator == animator) { - if (DEBUG) Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey()) - + " sTweens.size() = " + sTweens.size()); - iter.remove(); - break; // an animator can only be attached to one object - } - } - } - - public static Tweener to(Object object, long duration, Object... vars) { - long delay = 0; - AnimatorUpdateListener updateListener = null; - AnimatorListener listener = null; - TimeInterpolator interpolator = null; - - // Iterate through arguments and discover properties to animate - ArrayList props = new ArrayList(vars.length/2); - for (int i = 0; i < vars.length; i+=2) { - if (!(vars[i] instanceof String)) { - throw new IllegalArgumentException("Key must be a string: " + vars[i]); - } - String key = (String) vars[i]; - Object value = vars[i+1]; - - if ("simultaneousTween".equals(key)) { - // TODO - } else if ("ease".equals(key)) { - interpolator = (TimeInterpolator) value; // TODO: multiple interpolators? - } else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) { - updateListener = (AnimatorUpdateListener) value; - } else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) { - listener = (AnimatorListener) value; - } else if ("delay".equals(key)) { - delay = ((Number) value).longValue(); - } else if ("syncWith".equals(key)) { - // TODO - } else if (value instanceof float[]) { - props.add(PropertyValuesHolder.ofFloat(key, - ((float[])value)[0], ((float[])value)[1])); - } else if (value instanceof int[]) { - props.add(PropertyValuesHolder.ofInt(key, - ((int[])value)[0], ((int[])value)[1])); - } else if (value instanceof Number) { - float floatValue = ((Number)value).floatValue(); - props.add(PropertyValuesHolder.ofFloat(key, floatValue)); - } else { - throw new IllegalArgumentException( - "Bad argument for key \"" + key + "\" with value " + value.getClass()); - } - } - - // Re-use existing tween, if present - Tweener tween = sTweens.get(object); - ObjectAnimator anim = null; - if (tween == null) { - anim = ObjectAnimator.ofPropertyValuesHolder(object, - props.toArray(new PropertyValuesHolder[props.size()])); - tween = new Tweener(anim); - sTweens.put(object, tween); - if (DEBUG) Log.v(TAG, "Added new Tweener " + tween); - } else { - anim = sTweens.get(object).animator; - replace(props, object); // Cancel all animators for given object - } - - if (interpolator != null) { - anim.setInterpolator(interpolator); - } - - // Update animation with properties discovered in loop above - anim.setStartDelay(delay); - anim.setDuration(duration); - if (updateListener != null) { - anim.removeAllUpdateListeners(); // There should be only one - anim.addUpdateListener(updateListener); - } - if (listener != null) { - anim.removeAllListeners(); // There should be only one. - anim.addListener(listener); - } - anim.addListener(mCleanupListener); - - return tween; - } - - Tweener from(Object object, long duration, Object... vars) { - // TODO: for v of vars - // toVars[v] = object[v] - // object[v] = vars[v] - return Tweener.to(object, duration, vars); - } - - // Listener to watch for completed animations and remove them. - private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator animation) { - remove(animation); - } - - @Override - public void onAnimationCancel(Animator animation) { - remove(animation); - } - }; - - public static void reset() { - if (DEBUG) { - Log.v(TAG, "Reset()"); - if (sTweens.size() > 0) { - Log.v(TAG, "Cleaning up " + sTweens.size() + " animations"); - } - } - sTweens.clear(); - } - - private static void replace(ArrayList props, Object... args) { - for (final Object killobject : args) { - Tweener tween = sTweens.get(killobject); - if (tween != null) { - tween.animator.cancel(); - if (props != null) { - tween.animator.setValues( - props.toArray(new PropertyValuesHolder[props.size()])); - } else { - sTweens.remove(tween); - } - } - } - } -} diff --git a/InCallUI/src/com/android/incalluibind/ObjectFactory.java b/InCallUI/src/com/android/incalluibind/ObjectFactory.java deleted file mode 100644 index 7e9423acf0fa6b2351d84605ee0fe8808d17daf4..0000000000000000000000000000000000000000 --- a/InCallUI/src/com/android/incalluibind/ObjectFactory.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incalluibind; - -import android.content.Context; -import android.content.Intent; - -import com.android.incallui.CallCardPresenter.EmergencyCallListener; -import com.android.incallui.ContactUtils; -import com.android.incallui.DistanceHelper; -import com.android.incallui.service.PhoneNumberService; - -public class ObjectFactory { - - public static PhoneNumberService newPhoneNumberService(Context context) { - // no phone number service. - return null; - } - - public static EmergencyCallListener newEmergencyCallListener() { - return null; - } - - /** @return An {@link Intent} to be broadcast when the InCallUI is visible. */ - public static Intent getUiReadyBroadcastIntent(Context context) { - return null; - } - - /** - * @return An {@link Intent} to be broadcast when the call state button in the InCallUI is - * touched while in a call. - */ - public static Intent getCallStateButtonBroadcastIntent(Context context) { - return null; - } - - public static DistanceHelper newDistanceHelper(Context context, - DistanceHelper.Listener listener) { - return null; - } - - public static ContactUtils getContactUtilsInstance(Context context) { - return null; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/CallCardPresenterTest.java b/InCallUI/tests/src/com/android/incallui/CallCardPresenterTest.java deleted file mode 100644 index 79545ce4badf5bb3f40c577006ebdf22f88956f0..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/CallCardPresenterTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class CallCardPresenterTest extends AndroidTestCase { - - private static final String NAME_PRIMARY = "Full Name"; - private static final String NAME_ALTERNATIVE = "Name, Full"; - private static final String LOCATION = "US"; - private static final String NUMBER = "8006459001"; - - @Mock private ContactsPreferences mContactsPreferences; - private ContactCacheEntry mUnlockedContactInfo; - private ContactCacheEntry mLockedContactInfo; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - - Mockito.when(mContactsPreferences.getDisplayOrder()) - .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY); - - // Unlocked all contact info is available - mUnlockedContactInfo = new ContactCacheEntry(); - mUnlockedContactInfo.namePrimary = NAME_PRIMARY; - mUnlockedContactInfo.nameAlternative = NAME_ALTERNATIVE; - mUnlockedContactInfo.location = LOCATION; - mUnlockedContactInfo.number = NUMBER; - - // Locked only number and location are available - mLockedContactInfo = new ContactCacheEntry(); - mLockedContactInfo .location = LOCATION; - mLockedContactInfo .number = NUMBER; - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - ContactsPreferencesFactory.setTestInstance(null); - } - - public void testGetNameForCall_Unlocked() { - ContactsPreferencesFactory.setTestInstance(mContactsPreferences); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NAME_PRIMARY, presenter.getNameForCall(mUnlockedContactInfo)); - } - - public void testGetNameForCall_Locked() { - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NUMBER, presenter.getNameForCall(mLockedContactInfo)); - } - - public void testGetNameForCall_EmptyPreferredName() { - ContactCacheEntry contactInfo = new ContactCacheEntry(); - contactInfo.number = NUMBER; - - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NUMBER, presenter.getNameForCall(contactInfo)); - } - - public void testGetNumberForCall_Unlocked() { - ContactsPreferencesFactory.setTestInstance(mContactsPreferences); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NUMBER, presenter.getNumberForCall(mUnlockedContactInfo)); - } - - public void testGetNumberForCall_Locked() { - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(LOCATION, presenter.getNumberForCall(mLockedContactInfo)); - } - - public void testGetNumberForCall_EmptyPreferredName() { - ContactCacheEntry contactInfo = new ContactCacheEntry(); - contactInfo.location = LOCATION; - - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(LOCATION, presenter.getNumberForCall(contactInfo)); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/CallTest.java b/InCallUI/tests/src/com/android/incallui/CallTest.java deleted file mode 100644 index 118ec38da4d5fdb11baac3c8c4abd0eed51da9ca..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/CallTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.telecom.Connection; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import java.util.ArrayList; -import java.util.Arrays; - -// @formatter:off -/** - * Run test with - * adb shell am instrument -e class com.android.incallui.CallTest -w com.google.android.dialer.tests/android.test.InstrumentationTestRunner - */ -// @formatter:on - -@SmallTest -public class CallTest extends AndroidTestCase { - - private TestCall mCall; - - private final static String CHILD_NUMBER = "123"; - private final static ArrayList LAST_FORWARDED_NUMBER_LIST = - new ArrayList(Arrays.asList("456", "789")); - private final static String LAST_FORWARDED_NUMBER = "789"; - private final static String CALL_SUBJECT = "foo"; - - @Override - public void setUp() throws Exception { - super.setUp(); - - mCall = new TestCall(); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - } - - public void testUpdateFromCallExtras() { - mCall.updateFromCallExtras(getTestBundle()); - verifyTestBundleResult(); - } - - public void testUpdateFromCallExtras_corruptedBundle() { - mCall.setBundleCorrupted(true); - mCall.updateFromCallExtras(getTestBundle()); - - assertEquals(mCall.getChildNumber(), null); - assertEquals(mCall.getLastForwardedNumber(), null); - assertEquals(mCall.getCallSubject(), null); - } - - public void testUpdateFromCallExtras_corruptedBundleOverwrite() { - - mCall.updateFromCallExtras(getTestBundle()); - mCall.setBundleCorrupted(true); - Bundle bundle = new Bundle(); - bundle.putString(Connection.EXTRA_CHILD_ADDRESS, "321"); - bundle.putStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER, - new ArrayList(Arrays.asList("654", "987"))); - bundle.putString(Connection.EXTRA_CALL_SUBJECT, "bar"); - mCall.updateFromCallExtras(bundle); - //corrupted bundle should not overwrite existing values. - verifyTestBundleResult(); - } - - private Bundle getTestBundle() { - Bundle bundle = new Bundle(); - bundle.putString(Connection.EXTRA_CHILD_ADDRESS, CHILD_NUMBER); - bundle.putStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER, - LAST_FORWARDED_NUMBER_LIST); - bundle.putString(Connection.EXTRA_CALL_SUBJECT, CALL_SUBJECT); - return bundle; - } - - private void verifyTestBundleResult() { - assertEquals(CHILD_NUMBER, mCall.getChildNumber()); - assertEquals(LAST_FORWARDED_NUMBER, mCall.getLastForwardedNumber()); - assertEquals(CALL_SUBJECT, mCall.getCallSubject()); - } - - private class TestCall extends Call { - - private boolean mBundleCorrupted = false; - - public TestCall() { - super(Call.State.NEW); - } - - @Override - public void updateFromCallExtras(Bundle bundle) { - super.updateFromCallExtras(bundle); - } - - public void setBundleCorrupted(boolean value) { - this.mBundleCorrupted = value; - } - - @Override - protected boolean areCallExtrasCorrupted(Bundle callExtras) { - if (mBundleCorrupted) { - return true; - } - return super.areCallExtrasCorrupted(callExtras); - } - } -} diff --git a/InCallUI/tests/src/com/android/incallui/CallerInfoUtilsTest.java b/InCallUI/tests/src/com/android/incallui/CallerInfoUtilsTest.java deleted file mode 100644 index de5a0239e82ba29d97c780e3437b597ef3926373..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/CallerInfoUtilsTest.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -@SmallTest -public class CallerInfoUtilsTest extends AndroidTestCase { - public void testToLogSafeNumber_email() { - assertEquals("xxx@xxx.xxx", CallerInfoUtils.toLogSafePhoneNumber("foo@foo.com")); - } - - public void testToLogSafeNumber_phoneNumber() { - assertEquals("xxx-xxx-xxxx", CallerInfoUtils.toLogSafePhoneNumber("123-456-6789")); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ContactsPreferencesFactoryTest.java b/InCallUI/tests/src/com/android/incallui/ContactsPreferencesFactoryTest.java deleted file mode 100644 index bf915553be9c1a6fc6e9028ca6bf5a7e20a8c493..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/ContactsPreferencesFactoryTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.android.dialer.compat.UserManagerCompat; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import com.android.contacts.common.preference.ContactsPreferences; - -import org.mockito.Mockito; - -@SmallTest -public class ContactsPreferencesFactoryTest extends AndroidTestCase { - - public void testNewContactsPreferences_Unlocked() { - if (!UserManagerCompat.isUserUnlocked(getContext())) { - return; - } - assertNotNull(ContactsPreferencesFactory.newContactsPreferences(getContext())); - } - - public void testNewContactsPreferences_Locked() { - if (UserManagerCompat.isUserUnlocked(getContext())) { - return; - } - assertNull(ContactsPreferencesFactory.newContactsPreferences(getContext())); - } - - public void testNewContactsPreferences_TestInstance() { - ContactsPreferences testInstance = Mockito.mock(ContactsPreferences.class); - ContactsPreferencesFactory.setTestInstance(testInstance); - // Assert that it returns the same object always - assertSame(testInstance, ContactsPreferencesFactory.newContactsPreferences(getContext())); - assertSame(testInstance, ContactsPreferencesFactory.newContactsPreferences(getContext())); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java deleted file mode 100644 index 59434700cd95370396f9421beb74d5088d5d00d0..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.ComponentName; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.telecom.*; -import android.telecom.Call; -import android.test.AndroidTestCase; - -import com.android.contacts.common.compat.CallSdkCompat; - -import java.lang.reflect.Constructor; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -public class ExternalCallListTest extends AndroidTestCase { - - private static class Listener implements ExternalCallList.ExternalCallListener { - private CountDownLatch mCallAddedLatch = new CountDownLatch(1); - private CountDownLatch mCallRemovedLatch = new CountDownLatch(1); - private CountDownLatch mCallUpdatedLatch = new CountDownLatch(1); - - @Override - public void onExternalCallAdded(Call call) { - mCallAddedLatch.countDown(); - } - - @Override - public void onExternalCallRemoved(Call call) { - mCallRemovedLatch.countDown(); - } - - @Override - public void onExternalCallUpdated(Call call) { - mCallUpdatedLatch.countDown(); - } - - public boolean awaitCallAdded() { - try { - return mCallAddedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return false; - } - } - - public boolean awaitCallRemoved() { - try { - return mCallRemovedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return false; - } - } - - public boolean awaitCallUpdated() { - try { - return mCallUpdatedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return false; - } - } - } - - private static final int WAIT_TIMEOUT_MILLIS = 5000; - - private ExternalCallList mExternalCallList = new ExternalCallList(); - private Listener mExternalCallListener = new Listener(); - - @Override - public void setUp() throws Exception { - super.setUp(); - mExternalCallList.addExternalCallListener(mExternalCallListener); - } - - public void testAddCallSuccess() { - TestTelecomCall call = getTestCall(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - mExternalCallList.onCallAdded(call.getCall()); - assertTrue(mExternalCallListener.awaitCallAdded()); - } - - public void testAddCallFail() { - TestTelecomCall call = getTestCall(0 /* no properties */); - try { - mExternalCallList.onCallAdded(call.getCall()); - fail(); - } catch (IllegalArgumentException e) { - } - } - - public void testUpdateCall() { - TestTelecomCall call = getTestCall(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - mExternalCallList.onCallAdded(call.getCall()); - assertTrue(mExternalCallListener.awaitCallAdded()); - - call.forceDetailsUpdate(); - assertTrue(mExternalCallListener.awaitCallUpdated()); - } - - public void testRemoveCall() { - TestTelecomCall call = getTestCall(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - mExternalCallList.onCallAdded(call.getCall()); - assertTrue(mExternalCallListener.awaitCallAdded()); - - mExternalCallList.onCallRemoved(call.getCall()); - assertTrue(mExternalCallListener.awaitCallRemoved()); - } - - private TestTelecomCall getTestCall(int properties) { - TestTelecomCall testCall = TestTelecomCall.createInstance( - "1", - Uri.parse("tel:650-555-1212"), /* handle */ - TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */ - "Joe", /* callerDisplayName */ - TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */ - new PhoneAccountHandle(new ComponentName("test", "class"), - "handle"), /* accountHandle */ - CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL, /* capabilities */ - properties, /* properties */ - null, /* disconnectCause */ - 0, /* connectTimeMillis */ - null, /* GatewayInfo */ - VideoProfile.STATE_AUDIO_ONLY, /* videoState */ - null, /* statusHints */ - null, /* extras */ - null /* intentExtras */); - return testCall; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java deleted file mode 100644 index 64ddd2ea5509b0fc80c7689278bb63630258008a..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.android.contacts.common.preference.ContactsPreferences; - -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import android.app.Notification; -import android.app.NotificationManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Resources; -import android.net.Uri; -import android.telecom.*; -import android.telecom.Call; -import android.telephony.TelephonyManager; -import android.test.AndroidTestCase; -import android.test.mock.MockContext; - -import com.android.contacts.common.compat.CallSdkCompat; - -/** - * Unit tests for {@link ExternalCallNotifier}. - */ -public class ExternalCallNotifierTest extends AndroidTestCase { - private static final int TIMEOUT_MILLIS = 5000; - private static final String NAME_PRIMARY = "Full Name"; - private static final String NAME_ALTERNATIVE = "Name, Full"; - private static final String LOCATION = "US"; - private static final String NUMBER = "6505551212"; - - @Mock private ContactsPreferences mContactsPreferences; - @Mock private NotificationManager mNotificationManager; - @Mock private MockContext mMockContext; - @Mock private Resources mResources; - @Mock private StatusBarNotifier mStatusBarNotifier; - @Mock private ContactInfoCache mContactInfoCache; - @Mock private TelecomManager mTelecomManager; - @Mock private TelephonyManager mTelephonyManager; - @Mock private ProximitySensor mProximitySensor; - @Mock private CallList mCallList; - private InCallPresenter mInCallPresenter; - private ExternalCallNotifier mExternalCallNotifier; - private ContactInfoCache.ContactCacheEntry mContactInfo; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - - when(mContactsPreferences.getDisplayOrder()) - .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY); - - // Setup the mock context to return mocks for some of the needed services; the notification - // service is especially important as we want to be able to intercept calls into it and - // validate the notifcations. - when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE))) - .thenReturn(mNotificationManager); - when(mMockContext.getSystemService(eq(Context.TELECOM_SERVICE))) - .thenReturn(mTelecomManager); - when(mMockContext.getSystemService(eq(Context.TELEPHONY_SERVICE))) - .thenReturn(mTelephonyManager); - - // These aspects of the context are used by the notification builder to build the actual - // notification; we will rely on the actual implementations of these. - when(mMockContext.getPackageManager()).thenReturn(mContext.getPackageManager()); - when(mMockContext.getResources()).thenReturn(mContext.getResources()); - when(mMockContext.getApplicationInfo()).thenReturn(mContext.getApplicationInfo()); - when(mMockContext.getContentResolver()).thenReturn(mContext.getContentResolver()); - when(mMockContext.getPackageName()).thenReturn(mContext.getPackageName()); - - ContactsPreferencesFactory.setTestInstance(null); - mExternalCallNotifier = new ExternalCallNotifier(mMockContext, mContactInfoCache); - - // We don't directly use the InCallPresenter in the test, or even in ExternalCallNotifier - // itself. However, ExternalCallNotifier needs to make instances of - // com.android.incallui.Call for the purpose of performing contact cache lookups. The - // Call class depends on the static InCallPresenter for a number of things, so we need to - // set it up here to prevent crashes. - mInCallPresenter = InCallPresenter.getInstance(); - mInCallPresenter.setUp(mMockContext, mCallList, new ExternalCallList(), - null, mStatusBarNotifier, mExternalCallNotifier, mContactInfoCache, - mProximitySensor); - - // Unlocked all contact info is available - mContactInfo = new ContactInfoCache.ContactCacheEntry(); - mContactInfo.namePrimary = NAME_PRIMARY; - mContactInfo.nameAlternative = NAME_ALTERNATIVE; - mContactInfo.location = LOCATION; - mContactInfo.number = NUMBER; - - // Given the mock ContactInfoCache cache, we need to mock out what happens when the - // ExternalCallNotifier calls into the contact info cache to do a lookup. We will always - // return mock info stored in mContactInfo. - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - com.android.incallui.Call call = (com.android.incallui.Call) args[0]; - ContactInfoCache.ContactInfoCacheCallback callback - = (ContactInfoCache.ContactInfoCacheCallback) args[2]; - callback.onContactInfoComplete(call.getId(), mContactInfo); - return null; - } - }).when(mContactInfoCache).findInfo(any(com.android.incallui.Call.class), anyBoolean(), - any(ContactInfoCache.ContactInfoCacheCallback.class)); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - ContactsPreferencesFactory.setTestInstance(null); - mInCallPresenter.tearDown(); - } - - public void testPostNonPullable() { - TestTelecomCall call = getTestCall(false); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - Notification notification = verifyNotificationPosted(); - assertNull(notification.actions); - } - - public void testPostPullable() { - TestTelecomCall call = getTestCall(true); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - Notification notification = verifyNotificationPosted(); - assertEquals(1, notification.actions.length); - } - - public void testNotificationDismissed() { - TestTelecomCall call = getTestCall(false); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - verifyNotificationPosted(); - - mExternalCallNotifier.onExternalCallRemoved(call.getCall()); - verify(mNotificationManager, timeout(TIMEOUT_MILLIS)).cancel(eq("EXTERNAL_CALL"), eq(0)); - } - - public void testNotificationUpdated() { - TestTelecomCall call = getTestCall(false); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - verifyNotificationPosted(); - - call.setCapabilities(CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL); - mExternalCallNotifier.onExternalCallUpdated(call.getCall()); - - ArgumentCaptor notificationCaptor = - ArgumentCaptor.forClass(Notification.class); - verify(mNotificationManager, timeout(TIMEOUT_MILLIS).times(2)) - .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture()); - Notification notification1 = notificationCaptor.getAllValues().get(0); - assertNull(notification1.actions); - Notification notification2 = notificationCaptor.getAllValues().get(1); - assertEquals(1, notification2.actions.length); - } - - private Notification verifyNotificationPosted() { - ArgumentCaptor notificationCaptor = - ArgumentCaptor.forClass(Notification.class); - verify(mNotificationManager, timeout(TIMEOUT_MILLIS)) - .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture()); - return notificationCaptor.getValue(); - } - - private TestTelecomCall getTestCall(boolean canPull) { - TestTelecomCall testCall = TestTelecomCall.createInstance( - "1", - Uri.parse("tel:650-555-1212"), /* handle */ - TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */ - "Joe", /* callerDisplayName */ - TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */ - new PhoneAccountHandle(new ComponentName("test", "class"), - "handle"), /* accountHandle */ - canPull ? CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL : 0, /* capabilities */ - CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL, /* properties */ - null, /* disconnectCause */ - 0, /* connectTimeMillis */ - null, /* GatewayInfo */ - VideoProfile.STATE_AUDIO_ONLY, /* videoState */ - null, /* statusHints */ - null, /* extras */ - null /* intentExtras */); - return testCall; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/InCallContactInteractionsTest.java b/InCallUI/tests/src/com/android/incallui/InCallContactInteractionsTest.java deleted file mode 100644 index 625cda44837b93739edaca596a8b50afde39f7eb..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/InCallContactInteractionsTest.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.location.Address; -import android.test.AndroidTestCase; -import android.util.Pair; - -import com.android.incallui.InCallContactInteractions.BusinessContextInfo; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; -import java.util.Locale; - -/** - * Tests for InCallContactInteractions class methods for formatting info for display. - * - * NOTE: tests assume system settings are set to 12hr time format and US locale. This means that - * the output of InCallContactInteractions methods are compared against strings in 12hr time format - * and US locale address formatting unless otherwise specified. - */ -public class InCallContactInteractionsTest extends AndroidTestCase { - private InCallContactInteractions mInCallContactInteractions; - private static final float TEST_DISTANCE = (float) 1234.56; - - @Override - protected void setUp() { - mInCallContactInteractions = new InCallContactInteractions(mContext, true /* isBusiness */); - } - - public void testIsOpenNow_NowMatchesOpenTime() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(8), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testIsOpenNow_ClosingAfterMidnight() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(10), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHourAndDaysFromToday(1, 1)))) - .heading); - } - - public void testIsOpenNow_Open24Hours() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(10), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHourAndDaysFromToday(8, 1)))) - .heading); - } - - public void testIsOpenNow_AfterMiddayBreak() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(13), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)))) - .heading); - } - - public void testIsClosedNow_DuringMiddayBreak() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(11), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)))) - .heading); - } - - public void testIsClosedNow_BeforeOpen() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(6), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testIsClosedNow_NowMatchesClosedTime() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(20), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testIsClosedNow_AfterClosed() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testOpeningHours_SingleOpenRangeWhileOpen() { - assertEquals("8:00 AM - 8:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(12), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .detail); - } - - public void testOpeningHours_TwoOpenRangesWhileOpen() { - assertEquals("8:00 AM - 10:00 AM, 12:00 PM - 3:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(12), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)))) - .detail); - } - - public void testOpeningHours_AfterClosedNoTomorrow() { - assertEquals("Closed today at 8:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .detail); - } - - public void testOpeningHours_NotOpenTodayOpenTomorrow() { - assertEquals("Opens tomorrow at 8:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - Arrays.asList( - Pair.create( - getTestCalendarWithHourAndDaysFromToday(8, 1), - getTestCalendarWithHourAndDaysFromToday(10, 1)))) - .detail); - } - - public void testMultipleOpenRanges_BeforeOpen() { - assertEquals("Opens today at 8:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(7), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpenRanges_DuringFirstRange() { - assertEquals("Closes at 10:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(9), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpenRanges_BeforeMiddleRange() { - assertEquals("Opens today at 12:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(11), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpeningHours_DuringLastRange() { - assertEquals("Closes at 9:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(19), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpeningHours_AfterClose() { - assertEquals("Opens tomorrow at 8:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(22), - getMultipleOpeningHours()) - .detail); - } - - public void testNotOpenTodayOrTomorrow() { - assertEquals(null, - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - new ArrayList>())); - } - - public void testLocationInfo_ForUS() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.US, - getAddressForTest(), - TEST_DISTANCE); - assertEquals("0.8 mi away", info.heading); - assertEquals("Test address, Test locality", info.detail); - } - - public void testLocationInfo_ForNotUS() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.CANADA, - getAddressForTest(), - TEST_DISTANCE); - assertEquals("1.2 km away", info.heading); - assertEquals("Test address, Test locality", info.detail); - } - - public void testLocationInfo_NoLocality() { - Address address = getAddressForTest(); - address.setLocality(null); - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.CANADA, - address, - TEST_DISTANCE); - assertEquals("1.2 km away", info.heading); - assertEquals("Test address", info.detail); - } - - public void testLocationInfo_NoAddress() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.CANADA, - null, - TEST_DISTANCE); - assertEquals(null, info); - } - - public void testLocationInfo_NoDistance() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.US, - getAddressForTest(), - DistanceHelper.DISTANCE_NOT_FOUND); - assertEquals(null, info.heading); - } - - private Address getAddressForTest() { - Address address = new Address(Locale.US); - address.setAddressLine(0, "Test address"); - address.setLocality("Test locality"); - return address; - } - - private Calendar getTestCalendarWithHour(int hour) { - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar; - } - - private Calendar getTestCalendarWithHourAndDaysFromToday(int hour, int daysFromToday) { - Calendar calendar = getTestCalendarWithHour(hour); - calendar.add(Calendar.DATE, daysFromToday); - return calendar; - } - - private List> getMultipleOpeningHours() { - return Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)), - Pair.create( - getTestCalendarWithHour(17), - getTestCalendarWithHour(21)), - Pair.create( - getTestCalendarWithHourAndDaysFromToday(8, 1), - getTestCalendarWithHourAndDaysFromToday(10, 1)), - Pair.create( - getTestCalendarWithHourAndDaysFromToday(12, 1), - getTestCalendarWithHourAndDaysFromToday(8, 1))); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java b/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java deleted file mode 100644 index f0f08ab6807b94e8f92f926b0e79a1892065589f..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.Intent; -import android.telecom.PhoneAccountHandle; -import android.telephony.TelephonyManager; -import android.test.InstrumentationTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.incallui.InCallPresenter.InCallState; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class InCallPresenterTest extends InstrumentationTestCase { - private MockCallListWrapper mCallList; - @Mock private InCallActivity mInCallActivity; - @Mock private AudioModeProvider mAudioModeProvider; - @Mock private StatusBarNotifier mStatusBarNotifier; - @Mock private ExternalCallNotifier mExternalCallNotifier; - @Mock private ContactInfoCache mContactInfoCache; - @Mock private ProximitySensor mProximitySensor; - - InCallPresenter mInCallPresenter; - @Mock private Context mContext; - @Mock private TelephonyManager mTelephonyManager; - - @Override - protected void setUp() throws Exception { - super.setUp(); - System.setProperty("dexmaker.dexcache", - getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(this); - mCallList = new MockCallListWrapper(); - - when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager); - - mInCallPresenter = InCallPresenter.getInstance(); - mInCallPresenter.setUp(mContext, mCallList.getCallList(), new ExternalCallList(), - mAudioModeProvider, mStatusBarNotifier, mExternalCallNotifier, mContactInfoCache, - mProximitySensor); - } - - @Override - protected void tearDown() throws Exception { - // The tear down method needs to run in the main thread since there is an explicit check - // inside TelecomAdapter.getInstance(). - getInstrumentation().runOnMainSync(new Runnable() { - @Override - public void run() { - mInCallPresenter.unsetActivity(mInCallActivity); - mInCallPresenter.tearDown(); - InCallPresenter.setInstance(null); - } - }); - } - - public void testOnActivitySet_finishesActivityWhenNoCalls() { - mInCallPresenter.setActivity(mInCallActivity); - - verify(mInCallActivity).finish(); - } - - public void testOnCallListChange_sendsNotificationWhenInCall() { - mCallList.setHasCall(Call.State.INCOMING, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verify(mStatusBarNotifier).updateNotification(InCallState.INCOMING, - mCallList.getCallList()); - verifyInCallActivityNotStarted(); - } - - /** - * This behavior is required to ensure that the screen is turned on again by the restarting - * activity. - */ - public void testOnCallListChange_handlesCallWaitingWhenScreenOffShouldRestartActivity() { - mCallList.setHasCall(Call.State.ACTIVE, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - mInCallPresenter.setActivity(mInCallActivity); - - // Pretend that there is a call waiting and the screen is off - when(mInCallActivity.isDestroyed()).thenReturn(false); - when(mInCallActivity.isFinishing()).thenReturn(false); - when(mProximitySensor.isScreenReallyOff()).thenReturn(true); - mCallList.setHasCall(Call.State.INCOMING, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - verify(mInCallActivity).finish(); - } - - /** - * Verifies that the PENDING_OUTGOING -> IN_CALL transition brings up InCallActivity so - * that it can display an error dialog. - */ - public void testOnCallListChange_pendingOutgoingToInCallTransitionShowsUiForErrorDialog() { - mCallList.setHasCall(Call.State.CONNECTING, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - mCallList.setHasCall(Call.State.CONNECTING, false); - mCallList.setHasCall(Call.State.ACTIVE, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verify(mContext).startActivity(InCallPresenter.getInstance().getInCallIntent(false, false)); - verifyIncomingCallNotificationNotSent(); - } - - /** - * Verifies that if there is a call in the SELECT_PHONE_ACCOUNT state, InCallActivity is displayed - * to display the account picker. - */ - public void testOnCallListChange_noAccountProvidedForCallShowsUiForAccountPicker() { - mCallList.setHasCall(Call.State.SELECT_PHONE_ACCOUNT, true); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verify(mContext).startActivity(InCallPresenter.getInstance().getInCallIntent(false, false)); - verifyIncomingCallNotificationNotSent(); - } - - /** - * Verifies that for an expected call state change (e.g. NO_CALLS -> PENDING_OUTGOING), - * InCallActivity is not displayed. - */ - public void testOnCallListChange_noCallsToPendingOutgoingDoesNotShowUi() { - mCallList.setHasCall(Call.State.CONNECTING, true); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verifyInCallActivityNotStarted(); - verifyIncomingCallNotificationNotSent(); - } - - public void testOnCallListChange_LastCallDisconnectedNoCallsLeftFinishesUi() { - mCallList.setHasCall(Call.State.DISCONNECTED, true); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - mInCallPresenter.setActivity(mInCallActivity); - - verify(mInCallActivity, never()).finish(); - - // Last remaining disconnected call is removed from CallList, activity should shut down. - mCallList.setHasCall(Call.State.DISCONNECTED, false); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - verify(mInCallActivity).finish(); - } - - - //TODO - public void testCircularReveal_startsCircularRevealForOutgoingCalls() { - - } - - public void testCircularReveal_waitTillCircularRevealSentBeforeShowingCallCard() { - } - - public void testHangupOngoingCall_disconnectsCallCorrectly() { - } - - public void testAnswerIncomingCall() { - } - - public void testDeclineIncomingCall() { - } - - private void verifyInCallActivityNotStarted() { - verify(mContext, never()).startActivity(Mockito.any(Intent.class)); - } - - private void verifyIncomingCallNotificationNotSent() { - verify(mStatusBarNotifier, never()).updateNotification(Mockito.any(InCallState.class), - Mockito.any(CallList.class)); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/LatencyReportTest.java b/InCallUI/tests/src/com/android/incallui/LatencyReportTest.java deleted file mode 100644 index 9d8a5131b02ac91164ccfdae48a4a6d086148f76..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/LatencyReportTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static com.android.incallui.LatencyReport.INVALID_TIME; - -import android.os.Bundle; -import android.telecom.Connection; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import java.util.ArrayList; -import java.util.Arrays; - -public class LatencyReportTest extends AndroidTestCase { - public void testEmptyInit() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getCreatedTimeMillis()); - assertEquals(INVALID_TIME, report.getTelecomRoutingStartTimeMillis()); - assertEquals(INVALID_TIME, report.getTelecomRoutingEndTimeMillis()); - assertTrue(report.getCallAddedTimeMillis() > 0); - } - - public void testCallBlocking() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getCallBlockingTimeMillis()); - report.onCallBlockingDone(); - assertTrue(report.getCallBlockingTimeMillis() > 0); - } - - public void testNotificationShown() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getCallNotificationTimeMillis()); - report.onNotificationShown(); - assertTrue(report.getCallNotificationTimeMillis() > 0); - } - - public void testInCallUiShown() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getInCallUiShownTimeMillis()); - report.onInCallUiShown(false); - assertTrue(report.getInCallUiShownTimeMillis() > 0); - assertFalse(report.getDidDisplayHeadsUpNotification()); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/MockCallListWrapper.java b/InCallUI/tests/src/com/android/incallui/MockCallListWrapper.java deleted file mode 100644 index 369c4303f9ae27a4508104aa3ef108002556c758..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/MockCallListWrapper.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import android.telecom.PhoneAccountHandle; - -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.util.HashSet; - -/** - * Provides an instance of a mock CallList, and provides utility methods to put the CallList into - * various states (e.g. incoming call, active call, call waiting). - */ -public class MockCallListWrapper { - private CallList mCallList; - private HashSet mCallSet = new HashSet<>(); - - public MockCallListWrapper() { - mCallList = Mockito.mock(CallList.class); - mCallList = spy(new CallList()); - when(mCallList.getFirstCallWithState(anyInt())).thenAnswer(new Answer() { - @Override - public Call answer(InvocationOnMock i) throws Throwable { - Object[] args = i.getArguments(); - final int state = (int) args[0]; - if (mCallSet.contains(state)) { - return getMockCall(state); - } else { - return null; - } - } - }); - } - - public CallList getCallList() { - return mCallList; - } - - public void setHasCall(int state, boolean hasCall) { - if (hasCall) { - mCallSet.add(state); - } else { - mCallSet.remove(state); - } - } - - private static Call getMockCall(int state) { - return getMockCall(state, state != Call.State.SELECT_PHONE_ACCOUNT); - } - - private static Call getMockCall(int state, boolean hasAccountHandle) { - final Call call = Mockito.mock(Call.class); - when(call.getState()).thenReturn(Integer.valueOf(state)); - if (hasAccountHandle) { - when(call.getAccountHandle()).thenReturn(new PhoneAccountHandle(null, null)); - } - return call; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ProximitySensorTest.java b/InCallUI/tests/src/com/android/incallui/ProximitySensorTest.java deleted file mode 100644 index 1c8f34721438fc6bbd1576fb65aef1c21735630f..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/ProximitySensorTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.test.InstrumentationTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.incallui.InCallPresenter.InCallState; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class ProximitySensorTest extends InstrumentationTestCase { - @Mock private AccelerometerListener mAccelerometerListener; - private MockCallListWrapper mCallList; - - @Override - protected void setUp() throws Exception { - super.setUp(); - System.setProperty("dexmaker.dexcache", - getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(this); - mCallList = new MockCallListWrapper(); - } - - public void testAccelerometerBehaviorOnDisplayChange() { - final ProximitySensor proximitySensor = - new ProximitySensor( - getInstrumentation().getContext(), - new AudioModeProvider(), - mAccelerometerListener); - verify(mAccelerometerListener, never()).enable(anyBoolean()); - proximitySensor.onStateChange(null, InCallState.OUTGOING, mCallList.getCallList()); - verify(mAccelerometerListener).enable(true); - verify(mAccelerometerListener, never()).enable(false); - - proximitySensor.onDisplayStateChanged(false); - verify(mAccelerometerListener).enable(true); - verify(mAccelerometerListener).enable(false); - - proximitySensor.onDisplayStateChanged(true); - verify(mAccelerometerListener, times(2)).enable(true); - verify(mAccelerometerListener).enable(false); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/StatusBarNotifierTest.java b/InCallUI/tests/src/com/android/incallui/StatusBarNotifierTest.java deleted file mode 100644 index 4c55ddcc0495add2223c479855e3a5d3de3b629d..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/StatusBarNotifierTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class StatusBarNotifierTest extends AndroidTestCase { - - private static final String NAME_PRIMARY = "Full Name"; - private static final String NAME_ALTERNATIVE = "Name, Full"; - private static final String LOCATION = "US"; - private static final String NUMBER = "8006459001"; - - @Mock private Call mCall; - @Mock private ContactsPreferences mContactsPreferences; - private ContactCacheEntry mUnlockedContactInfo; - private ContactCacheEntry mLockedContactInfo; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - - Mockito.when(mContactsPreferences.getDisplayOrder()) - .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY); - - // Unlocked all contact info is available - mUnlockedContactInfo = new ContactCacheEntry(); - mUnlockedContactInfo.namePrimary = NAME_PRIMARY; - mUnlockedContactInfo.nameAlternative = NAME_ALTERNATIVE; - mUnlockedContactInfo.location = LOCATION; - mUnlockedContactInfo.number = NUMBER; - - // Locked only number and location are available - mLockedContactInfo = new ContactCacheEntry(); - mLockedContactInfo .location = LOCATION; - mLockedContactInfo .number = NUMBER; - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - ContactsPreferencesFactory.setTestInstance(null); - } - - public void testGetContentTitle_ConferenceCall() { - ContactsPreferencesFactory.setTestInstance(null); - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - - Mockito.when(mCall.isConferenceCall()).thenReturn(true); - Mockito.when(mCall.hasProperty(Mockito.anyInt())).thenReturn(false); - - assertEquals(mContext.getResources().getString(R.string.card_title_conf_call), - statusBarNotifier.getContentTitle(null, mCall)); - } - - public void testGetContentTitle_Unlocked() { - ContactsPreferencesFactory.setTestInstance(mContactsPreferences); - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - assertEquals(NAME_PRIMARY, statusBarNotifier.getContentTitle(mUnlockedContactInfo, mCall)); - } - - public void testGetContentTitle_Locked() { - ContactsPreferencesFactory.setTestInstance(null); - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - assertEquals(NUMBER, statusBarNotifier.getContentTitle(mLockedContactInfo, mCall)); - } - - public void testGetContentTitle_EmptyPreferredName() { - ContactCacheEntry contactCacheEntry = new ContactCacheEntry(); - contactCacheEntry.number = NUMBER; - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - assertEquals(NUMBER, statusBarNotifier.getContentTitle(contactCacheEntry, mCall)); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java b/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java deleted file mode 100644 index 48ac6e18fb92b8410e3347e8a68b1c0eb049fb04..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.telecom.DisconnectCause; -import android.telecom.GatewayInfo; -import android.telecom.PhoneAccountHandle; -import android.telecom.StatusHints; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Wrapper class which uses reflection to create instances of {@link android.telecom.Call} for use - * with unit testing. Since {@link android.telecom.Call} is final, it cannot be mocked using - * mockito, and since all setter methods are hidden, it is necessary to use reflection. In the - * future, it would be desirable to replace this if a different mocking solution is used. - */ -public class TestTelecomCall { - - private android.telecom.Call mCall; - - public static @Nullable TestTelecomCall createInstance(String callId, - Uri handle, - int handlePresentation, - String callerDisplayName, - int callerDisplayNamePresentation, - PhoneAccountHandle accountHandle, - int capabilities, - int properties, - DisconnectCause disconnectCause, - long connectTimeMillis, - GatewayInfo gatewayInfo, - int videoState, - StatusHints statusHints, - Bundle extras, - Bundle intentExtras) { - - try { - // Phone and InCall adapter are @hide, so we cannot refer to them directly. - Class phoneClass = Class.forName("android.telecom.Phone"); - Class incallAdapterClass = Class.forName("android.telecom.InCallAdapter"); - Class callClass = android.telecom.Call.class; - Constructor cons = callClass - .getDeclaredConstructor(phoneClass, String.class, incallAdapterClass); - cons.setAccessible(true); - - android.telecom.Call call = (android.telecom.Call) cons.newInstance(null, callId, null); - - // Create an instance of the call details. - Class callDetailsClass = android.telecom.Call.Details.class; - Constructor detailsCons = callDetailsClass.getDeclaredConstructor( - String.class, /* telecomCallId */ - Uri.class, /* handle */ - int.class, /* handlePresentation */ - String.class, /* callerDisplayName */ - int.class, /* callerDisplayNamePresentation */ - PhoneAccountHandle.class, /* accountHandle */ - int.class, /* capabilities */ - int.class, /* properties */ - DisconnectCause.class, /* disconnectCause */ - long.class, /* connectTimeMillis */ - GatewayInfo.class, /* gatewayInfo */ - int.class, /* videoState */ - StatusHints.class, /* statusHints */ - Bundle.class, /* extras */ - Bundle.class /* intentExtras */); - detailsCons.setAccessible(true); - - android.telecom.Call.Details details = (android.telecom.Call.Details) - detailsCons.newInstance(callId, handle, handlePresentation, callerDisplayName, - callerDisplayNamePresentation, accountHandle, capabilities, properties, - disconnectCause, connectTimeMillis, gatewayInfo, videoState, - statusHints, - extras, intentExtras); - - // Finally, set this as the details of the call. - Field detailsField = call.getClass().getDeclaredField("mDetails"); - detailsField.setAccessible(true); - detailsField.set(call, details); - - return new TestTelecomCall(call); - } catch (NoSuchMethodException nsm) { - return null; - } catch (ClassNotFoundException cnf) { - return null; - } catch (IllegalAccessException e) { - return null; - } catch (InstantiationException e) { - return null; - } catch (InvocationTargetException e) { - return null; - } catch (NoSuchFieldException e) { - return null; - } - } - - private TestTelecomCall(android.telecom.Call call) { - mCall = call; - } - - public android.telecom.Call getCall() { - return mCall; - } - - public void forceDetailsUpdate() { - Preconditions.checkNotNull(mCall); - - try { - Method method = mCall.getClass().getDeclaredMethod("fireDetailsChanged", - android.telecom.Call.Details.class); - method.setAccessible(true); - method.invoke(mCall, mCall.getDetails()); - } catch (NoSuchMethodException e) { - Log.e(this, "forceDetailsUpdate", e); - } catch (InvocationTargetException e) { - Log.e(this, "forceDetailsUpdate", e); - } catch (IllegalAccessException e) { - Log.e(this, "forceDetailsUpdate", e); - } - } - - public void setCapabilities(int capabilities) { - Preconditions.checkNotNull(mCall); - try { - Field field = mCall.getDetails().getClass().getDeclaredField("mCallCapabilities"); - field.setAccessible(true); - field.set(mCall.getDetails(), capabilities); - } catch (IllegalAccessException e) { - Log.e(this, "setProperties", e); - } catch (NoSuchFieldException e) { - Log.e(this, "setProperties", e); - } - } - - public void setCall(android.telecom.Call call) { - mCall = call; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java b/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java deleted file mode 100644 index 5717c9478ea12108ae7d05bffafab565d3ed2a77..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.async; - -import java.util.concurrent.Executors; - -import javax.annotation.concurrent.ThreadSafe; - -/** - * {@link PausableExecutor} for use in tests. It is intended to be used between one test thread - * and one prod thread. See {@link com.android.incallui.ringtone.InCallTonePlayerTest} for example - * usage. - */ -@ThreadSafe -public final class SingleProdThreadExecutor implements PausableExecutor { - - private int mMilestonesReached; - private int mMilestonesAcked; - private boolean mHasAckedAllMilestones; - - @Override - public synchronized void milestone() { - ++mMilestonesReached; - notify(); - while (!mHasAckedAllMilestones && mMilestonesReached > mMilestonesAcked) { - try { - wait(); - } catch (InterruptedException e) {} - } - } - - @Override - public synchronized void ackMilestoneForTesting() { - ++mMilestonesAcked; - notify(); - } - - @Override - public synchronized void ackAllMilestonesForTesting() { - mHasAckedAllMilestones = true; - notify(); - } - - @Override - public synchronized void awaitMilestoneForTesting() throws InterruptedException { - while (!mHasAckedAllMilestones && mMilestonesReached <= mMilestonesAcked) { - wait(); - } - } - - @Override - public synchronized void execute(Runnable command) { - Executors.newSingleThreadExecutor().execute(command); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ringtone/DialerRingtoneManagerTest.java b/InCallUI/tests/src/com/android/incallui/ringtone/DialerRingtoneManagerTest.java deleted file mode 100644 index 01db202725202563eac88faad9bf0f8118605984..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/ringtone/DialerRingtoneManagerTest.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.ringtone; - -import android.media.RingtoneManager; -import android.net.Uri; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import com.android.contacts.common.compat.CompatUtils; -import com.android.incallui.Call; -import com.android.incallui.Call.State; -import com.android.incallui.CallList; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@SmallTest -public class DialerRingtoneManagerTest extends AndroidTestCase { - - private static final Uri RINGTONE_URI = RingtoneManager - .getDefaultUri(RingtoneManager.TYPE_RINGTONE); - - @Mock private InCallTonePlayer mInCallTonePlayer; - @Mock private CallList mCallList; - @Mock private Call mCall; - private DialerRingtoneManager mRingtoneManagerEnabled; - private DialerRingtoneManager mRingtoneManagerDisabled; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - mRingtoneManagerEnabled = new DialerRingtoneManager(mInCallTonePlayer, mCallList); - mRingtoneManagerEnabled.setDialerRingingEnabledForTesting(true); - mRingtoneManagerDisabled = new DialerRingtoneManager(mInCallTonePlayer, mCallList); - mRingtoneManagerDisabled.setDialerRingingEnabledForTesting(false); - } - - public void testNullInCallTonePlayer() { - try { - new DialerRingtoneManager(null, mCallList); - fail(); - } catch (NullPointerException e) {} - } - - public void testNullCallList() { - try { - new DialerRingtoneManager(mInCallTonePlayer, null); - fail(); - } catch (NullPointerException e) {} - } - - public void testShouldPlayRingtone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(0, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N_NullUri() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.INCOMING, null)); - } - - public void testShouldPlayRingtone_N_Disabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerDisabled.shouldPlayRingtone(State.INCOMING, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N_NotIncoming() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.ACTIVE, RINGTONE_URI)); - } - - // Specific case for call waiting since that needs its own sound - public void testShouldPlayRingtone_N_CallWaitingByState() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.CALL_WAITING, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N_CallWaitingByActiveCall() { - if (!CompatUtils.isNCompatible()) { - return; - } - Mockito.when(mCallList.getActiveCall()).thenReturn(mCall); - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.INCOMING, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertTrue(mRingtoneManagerEnabled.shouldPlayRingtone(State.INCOMING, RINGTONE_URI)); - } - - public void testShouldPlayCallWaitingTone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(0)); - } - - public void testShouldPlayCallWaitingTone_N_Disabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerDisabled.shouldPlayCallWaitingTone(State.CALL_WAITING)); - } - - public void testShouldPlayCallWaitingTone_N_NotCallWaiting() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.ACTIVE)); - } - - // Specific case for incoming since it plays its own sound - public void testShouldPlayCallWaitingTone_N_Incoming() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.INCOMING)); - } - - public void testShouldPlayCallWaitingTone_N_AlreadyPlaying() { - if (!CompatUtils.isNCompatible()) { - return; - } - Mockito.when(mInCallTonePlayer.isPlayingTone()).thenReturn(true); - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.CALL_WAITING)); - } - - public void testShouldPlayCallWaitingTone_N_ByState() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertTrue(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.CALL_WAITING)); - } - - public void testShouldPlayCallWaitingTone_N_ByActiveCall() { - if (!CompatUtils.isNCompatible()) { - return; - } - Mockito.when(mCallList.getActiveCall()).thenReturn(mCall); - assertTrue(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.INCOMING)); - } - - public void testPlayCallWaitingTone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.playCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).play(Mockito.anyInt()); - } - - public void testPlayCallWaitingTone_N_NotEnabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerDisabled.playCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).play(Mockito.anyInt()); - } - - public void testPlayCallWaitingTone_N() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.playCallWaitingTone(); - Mockito.verify(mInCallTonePlayer).play(Mockito.anyInt()); - } - - public void testStopCallWaitingTone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.stopCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).stop(); - } - - public void testStopCallWaitingTone_N_NotEnabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerDisabled.stopCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).stop(); - } - - public void testStopCallWaitingTone_N() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.stopCallWaitingTone(); - Mockito.verify(mInCallTonePlayer).stop(); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java b/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java deleted file mode 100644 index bde5c50e4b67285d6b08bdaa042d30f2a2d1a0cc..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.ringtone; - -import android.media.AudioManager; -import android.media.ToneGenerator; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import com.android.incallui.async.PausableExecutor; -import com.android.incallui.async.SingleProdThreadExecutor; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@SmallTest -public class InCallTonePlayerTest extends AndroidTestCase { - - @Mock private ToneGeneratorFactory mToneGeneratorFactory; - @Mock private ToneGenerator mToneGenerator; - private InCallTonePlayer mInCallTonePlayer; - - /* - * InCallTonePlayer milestones: - * 1) After tone starts playing - * 2) After tone finishes waiting (could have timed out) - * 3) After cleaning up state to allow new tone to play - */ - private PausableExecutor mExecutor; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - Mockito.when(mToneGeneratorFactory.newInCallToneGenerator(Mockito.anyInt(), - Mockito.anyInt())).thenReturn(mToneGenerator); - mExecutor = new SingleProdThreadExecutor(); - mInCallTonePlayer = new InCallTonePlayer(mToneGeneratorFactory, mExecutor); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - // Stop any playing so the InCallTonePlayer isn't stuck waiting for the tone to complete - mInCallTonePlayer.stop(); - // Ack all milestones to ensure that the prod thread doesn't block forever - mExecutor.ackAllMilestonesForTesting(); - } - - public void testIsPlayingTone_False() { - assertFalse(mInCallTonePlayer.isPlayingTone()); - } - - public void testIsPlayingTone_True() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - - assertTrue(mInCallTonePlayer.isPlayingTone()); - } - - public void testPlay_InvalidTone() { - try { - mInCallTonePlayer.play(Integer.MIN_VALUE); - fail(); - } catch (IllegalArgumentException e) {} - } - - public void testPlay_CurrentlyPlaying() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - try { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - fail(); - } catch (IllegalStateException e) {} - } - - public void testPlay_VoiceCallStream() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - Mockito.verify(mToneGeneratorFactory).newInCallToneGenerator(AudioManager.STREAM_VOICE_CALL, - InCallTonePlayer.VOLUME_RELATIVE_HIGH_PRIORITY); - } - - public void testPlay_Single() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - - Mockito.verify(mToneGenerator).startTone(ToneGenerator.TONE_SUP_CALL_WAITING); - } - - public void testPlay_Consecutive() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - // Prevent waiting forever - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - - Mockito.verify(mToneGenerator, Mockito.times(2)) - .startTone(ToneGenerator.TONE_SUP_CALL_WAITING); - } - - public void testStop_NotPlaying() { - // No crash - mInCallTonePlayer.stop(); - } - - public void testStop() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - - assertFalse(mInCallTonePlayer.isPlayingTone()); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java b/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java deleted file mode 100644 index fb0b460b6fa22b09e9957f923737be1688e1b27f..0000000000000000000000000000000000000000 --- a/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - - -package com.android.incallui.spam; - -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.any; - -import android.content.ContentValues; -import android.content.Context; -import android.provider.CallLog; -import android.telecom.DisconnectCause; -import android.test.InstrumentationTestCase; - -import com.android.contacts.common.test.mocks.ContactsMockContext; -import com.android.contacts.common.test.mocks.MockContentProvider; -import com.android.dialer.calllog.CallLogAsyncTaskUtil; -import com.android.dialer.util.AsyncTaskExecutors; -import com.android.dialer.util.FakeAsyncTaskExecutor; -import com.android.dialer.util.TelecomUtil; -import com.android.incallui.Call; -import com.android.incallui.Call.CallHistoryStatus; -import com.android.incallui.Call.LogState; - -public class SpamCallListListenerTest extends InstrumentationTestCase { - private static final String NUMBER = "+18005657862"; - private static final int DURATION = 100; - - private TestSpamCallListListener mListener; - private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor; - private ContactsMockContext mContext; - - @Override - public void setUp() throws Exception { - super.setUp(); - mContext = new ContactsMockContext(getInstrumentation().getContext(), CallLog.AUTHORITY); - mListener = new TestSpamCallListListener(mContext); - mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation()); - AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory()); - } - - @Override - public void tearDown() throws Exception { - AsyncTaskExecutors.setFactoryForTest(null); - CallLogAsyncTaskUtil.resetForTest(); - super.tearDown(); - } - - public void testOutgoingCall() { - Call call = getMockCall(NUMBER, false, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_UnknownNumber() { - Call call = getMockCall(null, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_Rejected() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - public void testIncomingCall_HangUpLocal() { - Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - mListener.onDisconnect(call); - assertTrue(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_HangUpRemote() { - Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE); - mListener.onDisconnect(call); - assertTrue(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_NotInCallHistory_InContacts() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_LOCAL_CONTACT, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_InCallHistory_InContacts() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_PRESENT, - LogState.LOOKUP_LOCAL_CONTACT, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_InCallHistory_NotInContacts() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_NotInCallHistory_NotInContacts() throws Throwable { - Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - mListener.onDisconnect(call); - assertTrue(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_CheckCallHistory_NumberExists() throws Throwable { - final Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_UNKNOWN, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - expectCallLogQuery(NUMBER, true); - incomingCall(call); - verify(call).setCallHistoryStatus(eq(Call.CALL_HISTORY_STATUS_PRESENT)); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_CheckCallHistory_NumberNotExists() throws Throwable { - final Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_UNKNOWN, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - expectCallLogQuery(NUMBER, false); - incomingCall(call); - verify(call).setCallHistoryStatus(eq(Call.CALL_HISTORY_STATUS_NOT_PRESENT)); - assertTrue(mListener.mShowNotificationCalled); - } - - private void incomingCall(final Call call) throws Throwable { - runTestOnUiThread(new Runnable() { - @Override - public void run() { - mListener.onIncomingCall(call); - } - }); - getInstrumentation().waitForIdleSync(); - mFakeAsyncTaskExecutor.runTask(CallLogAsyncTaskUtil.Tasks.GET_NUMBER_IN_CALL_HISTORY); - mListener.onDisconnect(call); - } - - private void expectCallLogQuery(String number, boolean inCallHistory) { - MockContentProvider.Query query = mContext.getContactsProvider() - .expectQuery(TelecomUtil.getCallLogUri(mContext)) - .withSelection(CallLog.Calls.NUMBER + " = ?", number) - .withProjection(CallLog.Calls._ID) - .withAnySortOrder(); - ContentValues values = new ContentValues(); - values.put(CallLog.Calls.NUMBER, number); - if (inCallHistory) { - query.returnRow(values); - } else { - query.returnEmptyCursor(); - } - } - - private static Call getMockCall(String number, - boolean isIncoming, - int duration, - @CallHistoryStatus int callHistoryStatus, - int contactLookupResult, - int disconnectCause) { - Call call = mock(Call.class); - LogState logState = new LogState(); - logState.isIncoming = isIncoming; - logState.duration = duration; - logState.contactLookupResult = contactLookupResult; - when(call.getDisconnectCause()).thenReturn(new DisconnectCause(disconnectCause)); - when(call.getLogState()).thenReturn(logState); - when(call.getNumber()).thenReturn(number); - doCallRealMethod().when(call).setCallHistoryStatus(anyInt()); - when(call.getCallHistoryStatus()).thenCallRealMethod(); - call.setCallHistoryStatus(callHistoryStatus); - return call; - } - - private static class TestSpamCallListListener extends SpamCallListListener { - private boolean mShowNotificationCalled; - - public TestSpamCallListListener(Context context) { - super(context); - } - - void showNotification(String number) { - mShowNotificationCalled = true; - } - } -} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..c5b1efa7aac764ae6d8da63476a2d5cec02a6a5d --- /dev/null +++ b/LICENSE @@ -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/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..70d3011ddde8067ba10afe096677c125ff626fc4 Binary files /dev/null and b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..4068d5aa7a890ffae83f836e398c670a8112f336 Binary files /dev/null and b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..60d3c3a499eb852beb35fc5b69f4bb5e2b218450 Binary files /dev/null and b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..0524cf0537a0fa62de28307e62afec904e1b1161 Binary files /dev/null and b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..ec99ca6b8eaf03b695a81e4f5c7bf9b5131fc9fc Binary files /dev/null and b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..ba27ee76b1bbf422bf0a305517ee9cd732dfef69 Binary files /dev/null and b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..2b009a3da5a62b39d51ea524f1ba561430148459 Binary files /dev/null and b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc724899e10776bbe6feaf62fcb85e8ccac9c7c Binary files /dev/null and b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..4b111b1caf7a2a01e4c9fc1950b97a15c7ebc6be Binary files /dev/null and b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 0000000000000000000000000000000000000000..230be8ceb5f71e9da6daa29fc16e92b1a9ee725d Binary files /dev/null and b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..cd1972677699802e4ef9723ea50fcb284f9a2d9e Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..41541bb0d01963961a68459622330e9f2b714ac2 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ec2349ca837da9f2a9f15af5ff4406232e2e218c Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..398f0a938c2e92b9bf89b8250e7d35c8a4816a41 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..625b827c44e7d15ce385221dbea4c1733c5ea8f7 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..51456d3d5d39d2cba1f15ce7d0dec9446a021bbc Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..b7aba8072efcd302db5d7ecadc7df25375814f3d Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..0bdc56be6fd82a6b250287e5015ba078352d3118 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc5065155baeba719d76845d4398431c289cde0 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..497c88ca82b139d8523f62d272569b97777cdec7 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..c8e69dcebb98d43695027fcc7e39a339c84dda51 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..f8394dc4f28abf889e90c85fbb2658fc49f42184 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..0354d41ca9ace28087d6f9cb866e66cea9210612 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e802d90aeb092474fe4441d4904624e19e33aa19 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_36.png new file mode 100644 index 0000000000000000000000000000000000000000..4992b76b0934b5febf290dedac62285e1d8f987e Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_check_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..2c2ad771f72c8beaa5adbff66526893d8767990d Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ceb1a1eebf2b2cc9a008f42010e144f4dab968de Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..86bd673afc93c2e27f670547acb39914c17ff0fa Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..82710e72a51d83b567d45e9c8eedb85c5ef7f8f3 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_dialpad_white_36.png differ diff --git a/InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png b/assets/quantum/res/drawable-hdpi/quantum_ic_forward_white_24.png similarity index 100% rename from InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png rename to assets/quantum/res/drawable-hdpi/quantum_ic_forward_white_24.png diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..159bea7fd8a47a129f63ce2e1208003beb7e49a9 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3b97cac1474572ce9d30c2bb4146eea35234a4 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..9b8131124d7cb5a540f50e963b1940737574d5cd Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_group_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..cbdef2f3e9539b04521c75b4a599534ea56d40eb Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_group_white_36.png differ diff --git a/InCallUI/res/drawable-hdpi/ic_hd_24dp.png b/assets/quantum/res/drawable-hdpi/quantum_ic_hd_white_24.png similarity index 100% rename from InCallUI/res/drawable-hdpi/ic_hd_24dp.png rename to assets/quantum/res/drawable-hdpi/quantum_ic_hd_white_24.png diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e859c2f31a12e5f03a09408e6fa62042610fe349 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..f77f24767c74a59447611fc495261249cb45a4f4 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_message_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..57177b7c6fb1adb122b1171231a4214bdaa3b3e4 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..1755dbf3fab08d39326fa7a5963a9752a2a70aa2 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..203cb8a9ffca0035313e24692c44d3e2df42b4b2 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..8df91f236755061891e702f3860ea541bfdad458 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..1d024393aad17639ee338941a2ccf278129f45eb Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..10ae5a70c4fce44cfebe24f4d7d05861ec6c4cbc Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..c4a2229e94c965d954eda74042ec0bf0134f5c46 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b414cf5b6881d6ec172d2a7fbd73ada5bbf167ab Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f1defa6df89b5a7a68df6787a4ba799d3bd3b2 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_report_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_report_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..f0bb6f5bebfb0cc46cf98c4323c674d23d228bd6 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_report_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..f5518772a69e796883ca2fbf12f38c37c1b0ee78 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..5d4ad4b02074ad862b3085da735cbf84268720a9 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..8c3a0edaa381a8e37f51156843e9f53c81767bf3 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..21b4e4388d86b576f20c18f2316b608e7deec59d Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..029b077364213949366799a4a22bfcf6a99160e0 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..4366bb08273e6a0ffba684885ad1249da7a58807 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf5ac2085f43be7db35a99adef0618d0b26b417 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..f2e461a9f1d0782f3b471f74960aaf7abbdf9d18 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..abf478adaa42c582921abe059181c28ccb313c6f Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d83e0d50c3dd1aa384568f658f815b35819462a0 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..49562a6408e231ec706cfa08622c26caaf9392f4 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..23e8997f2b631c6a6db1f1d7a78bf2d5298e4f4c Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..49eb8fcc34d9093c8b631e48d4c2750db01aa65e Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..62d22bec87c03d0bda6670b82c58422e8a8bbc14 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_white_36.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..f5175576277d2a0f5939c65b3c2d0ac1c5e05c81 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..c0ab46f5db372a57c79447adc9ada920fa4327f2 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b8d4ce444b9e7fb0d8b634f8dafaef24b47cdc35 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..6c8174f3af7694b529152f9265558b6821fedb95 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..22a1140ae2a3d368b6e07ebc0b975e47245dad94 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..f2232c7db9d071660d7b903fe20c758db459239a Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..7933f42f0abdd7de4cb8686cce58fe5c8f19f8b9 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..b47cef666e3226960304fc0a5968dfd310763a58 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d858f18e6c2ef050c2d06f205059dc15416f2cde Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..3a25fbb866a157759a4c33cad2d9feda224b0e1e Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..4735a7d71186c6c3762b44d2e6686dc793d9e7cf Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..6a984c4f16f3a742c34071ff204ecb34907ea9fe Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..614ad49a3e4fb4c29193b38001841b2486038bcc Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6dd1a9644a25f4841e740653846cf467c9b8ab Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..4a9e2c24aad3a987694cd596dca35c06e8dccb98 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..907911055bd255db665bebd6df0e8ab1895649b6 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d409b544b7f62950a69d7d1ea58e97ef6c5ea546 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..92117e109d4b8904679272afdeaa61138a106959 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..0167ac8291295daf62b3eadd02196870ee64b9fe Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..aa7a91943077c4444d60283d9d8fc9980df1a346 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..4ef72eec99423c5d4f83227e34b24835a79f324f Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1fc3d7cae323f13ef542c927b89eb2c9a4e2a2 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..de635e034d8a61e6ba9ef6ce9289515b0ad18ac3 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..046372d0dfa4dbfe328e00e53f471e141b78a3dc Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..378272ffc15f451e392eb042f955e24c02630763 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..625b827c44e7d15ce385221dbea4c1733c5ea8f7 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..a2eb54bab110f4e434bac24abb96f5e20129febd Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..bd5748575fef5344a13fde180d29be69cbd713c1 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..77f9de5e3ccb30fb6e580454412c98e2f6c553c1 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e830522008b0a1b1f39fdde1156ff1bae3f955e5 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..be9fb226a53ce5ee4008cfafa0754f42284d51b3 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..f36dcd49699b19f8df8a78aa3bd8ecfd561a214f Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5494f282e68b180847a1c54e4052f6b89fb3aa Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..1c14c9c44592e95983dec13ca705ab99a6c54f21 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_36.png new file mode 100644 index 0000000000000000000000000000000000000000..e802d90aeb092474fe4441d4904624e19e33aa19 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_check_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..3b2b65d26291575f2741d223cdf80facb436dc20 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..af7f8288da6854204dcc4e6678b9053cd72032c4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..ceb1a1eebf2b2cc9a008f42010e144f4dab968de Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..9037f94e843b77bfd389f4405035d5a89ec017cc Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_dialpad_white_36.png differ diff --git a/InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png b/assets/quantum/res/drawable-mdpi/quantum_ic_forward_white_24.png similarity index 100% rename from InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png rename to assets/quantum/res/drawable-mdpi/quantum_ic_forward_white_24.png diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..364bad0b843bf6a17478979fb0e66915aa67d818 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..c150cb58db7456a60d4979766d022313cb8efa7e Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..4423c7ce990e5f02ba52de5f53659b75e907a8b0 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/res/drawable-hdpi/ic_people_24dp.png b/assets/quantum/res/drawable-mdpi/quantum_ic_group_white_36.png similarity index 100% rename from res/drawable-hdpi/ic_people_24dp.png rename to assets/quantum/res/drawable-mdpi/quantum_ic_group_white_36.png diff --git a/InCallUI/res/drawable-mdpi/ic_hd_24dp.png b/assets/quantum/res/drawable-mdpi/quantum_ic_hd_white_24.png similarity index 100% rename from InCallUI/res/drawable-mdpi/ic_hd_24dp.png rename to assets/quantum/res/drawable-mdpi/quantum_ic_hd_white_24.png diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..371efd38220c090ddecf112b100a772b32ee3ff4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..d25d3888e1a31b389b8d3d47da45045511e54a95 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_message_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..3072b75699814e04c8328548e87540aac102eba4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..da605a5a19a6840369f56899c68de52b65fff291 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..6fccf5d09f041a62f4c831bb7038d59216d91a88 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..1c3e8b98793de1aecf85422d398b42cb39bdb859 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/res/drawable-hdpi/ic_pause_24dp.png b/assets/quantum/res/drawable-mdpi/quantum_ic_pause_white_36.png similarity index 100% rename from res/drawable-hdpi/ic_pause_24dp.png rename to assets/quantum/res/drawable-mdpi/quantum_ic_pause_white_36.png diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..38e0a2882afccf81da8107628cdde589dd23fa0d Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..02ef4cdb0080f74e19c773d9b7627863b4a4dfb1 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d474bd577d00d2aa045685f38b1729e4b2c314e2 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..2642b9e09ec00be308649f62d9323f22ae2b6c6c Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_report_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_report_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..63ef736834c7bc09d9ab5649dbfbbb64d2eaa431 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_report_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..8bff465eb37d9517384edfec8016da5c94e562d6 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b58afb0b49f986eb5f42a7f80a97a5c97a94b9f1 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..9491f2d1afa2cab56027e61091f0881d25d8df35 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..c318983c7bb761eb86335daab91d8aa29aad70c4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..2bf8a169e14204b5b9914f52005c34489e835ee9 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..b67f6a9116b4933c2d6322e496e22cf7355f2bc4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d1cca6f0a0a84d01c22758d7a20046b090ee0cd1 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf5ac2085f43be7db35a99adef0618d0b26b417 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..1dafd4927617f77975c9f65736120872f7cfee7a Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d146209a5145962cfa3226918807fb663d2c7267 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..d83e0d50c3dd1aa384568f658f815b35819462a0 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..fa4755769265118f0be817ad295ab91f46e90bf7 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d6cea3667acb615398f1b2d9157d66034594067c Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/res/drawable-hdpi/ic_volume_up_24dp.png b/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_white_36.png similarity index 100% rename from res/drawable-hdpi/ic_volume_up_24dp.png rename to assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_white_36.png diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..832f5a36172308b2c53cefe5098f828b0b4eae53 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..4c6076df77463404d7163c9929bad5798576c49b Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..eea1bbf04b00cda6609939c4abacc4a5fecb881a Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..d5022d063e4355cc8fb30c2e079c842d7c9c973d Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..a4fe6889d159cac861cac4f885ae3ec28cd9ca44 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..e1831d7afd086dcfc741a496d058af3d0308da99 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..01daecf6567f5d26d11eee3ed02d30072cdc1118 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc5065155baeba719d76845d4398431c289cde0 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ef45e933a99b720cc5f6127e6da22bc2fa679244 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..be9fb226a53ce5ee4008cfafa0754f42284d51b3 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..777658e95515ca47c9852d00621e2e6d45abc5c7 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb24d1f292b06f87a54566d565e0872b5011945 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..8392b2a888469b0bc58aadbf2648e6bbd393a870 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..64a4944f7531ab9fb745fd34dd00c778cff1573f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_36.png new file mode 100644 index 0000000000000000000000000000000000000000..b26a2c05e3f261641734c91aef9322830ab10daf Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..d670618c7e96225f7756cb4c2743e7ebbf688cf8 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c7ffd0e795ba76ed3a062566c9016448795f7a Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..6b717e0dda8649aa3b5f1d6851ba0dd20cc4ea66 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..175000510d4d2010d38fe000b55e9121f01a6c1c Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_dialpad_white_36.png differ diff --git a/InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_forward_white_24.png similarity index 100% rename from InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png rename to assets/quantum/res/drawable-xhdpi/quantum_ic_forward_white_24.png diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..ef360fe40c758ab7e8d3e168e6c2ef013515646a Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..9b8131124d7cb5a540f50e963b1940737574d5cd Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..c1dcfb29024fc0eec6fb8d2135e295b5205f1323 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/res/drawable-xxhdpi/ic_people_24dp.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_group_white_36.png similarity index 100% rename from res/drawable-xxhdpi/ic_people_24dp.png rename to assets/quantum/res/drawable-xhdpi/quantum_ic_group_white_36.png diff --git a/InCallUI/res/drawable-xhdpi/ic_hd_24dp.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_hd_white_24.png similarity index 100% rename from InCallUI/res/drawable-xhdpi/ic_hd_24dp.png rename to assets/quantum/res/drawable-xhdpi/quantum_ic_hd_white_24.png diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..f7dbee156ba558ba6dbb0993fda637c525ffa56d Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..82db5427b7613252c64dec1dbb782f4d987a0cfa Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_message_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..763767b4f6f4e3c64cdaabcd7bfd2197363812d5 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..fa741be1c0e71d611d0b63c8f7d3c210be2eb581 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..7a15a9ea9e9125e04739214c0fad7c0226d5eca2 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ca927f3de51d3ca9a3e120a15f5014cddc525533 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/res/drawable-xxhdpi/ic_pause_24dp.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_pause_white_36.png similarity index 100% rename from res/drawable-xxhdpi/ic_pause_24dp.png rename to assets/quantum/res/drawable-xhdpi/quantum_ic_pause_white_36.png diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7c289d4971337ec3693780d13b26c146c58a5f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..4bd2898a83995e88bee0c8105c402a4f9f3a30de Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..2642b9e09ec00be308649f62d9323f22ae2b6c6c Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..2ffdb55f264ecd3610f90890f8202f93c00f72e1 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_48.png differ diff --git a/InCallUI/res/drawable-mdpi/ic_report_white_36dp.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_report_white_18.png similarity index 100% rename from InCallUI/res/drawable-mdpi/ic_report_white_36dp.png rename to assets/quantum/res/drawable-xhdpi/quantum_ic_report_white_18.png diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..fd36be8b96d80ceb9336bb2acebfdd42ca6f3cdc Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ef59e77678dbd3f5d866bca9058b6e90cb8d6098 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..698cd5d756f3b6031fcb2ca274e09a781a0cf0cc Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..bee95a1d4d77dbb0824cf10f831d6321b343224c Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7423f5f2c3f7e297a0849280539bae0533a4c7 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e719cdfb7e062d5adc218d37ef22e5db86e59f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..5d540589b4a4f9e22a8c4f1f292e12ec75ad443b Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..69565f2c75b69277ed31474e97954ea985a400c9 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..d83e0d50c3dd1aa384568f658f815b35819462a0 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..1b2583d34e8bafff26a20f89c9d7cacf4525617e Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..44c28e2f2830f927973beaa3a143ddfe439f20ed Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_36.png differ diff --git a/res/drawable-hdpi/ic_voicemail_24dp.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_voicemail_white_18.png similarity index 100% rename from res/drawable-hdpi/ic_voicemail_24dp.png rename to assets/quantum/res/drawable-xhdpi/quantum_ic_voicemail_white_18.png diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..a45093ff79e1ff08764467cd9cce9c1873499255 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/res/drawable-xxhdpi/ic_volume_up_24dp.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_white_36.png similarity index 100% rename from res/drawable-xxhdpi/ic_volume_up_24dp.png rename to assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_white_36.png diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..32a6d91ce8618ff42524d9e075451a13b2945f87 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..2609ae1341e844fa6b3fecad8d260a1709fc2867 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..99f57c12a8ad3786645f6d69a9e935e8d5eeb3e3 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..6842da6d0af5e839692fdae3e1ab70ae5c1de281 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e1831d7afd086dcfc741a496d058af3d0308da99 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..13ffc2ad75f087cc182d8794770507a7fcfd0a65 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..cefef6551b820325fc1bb129377d5cdb70dccd49 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..6f4dcea1f3cdf34726c7e881e97a8b40d985ce69 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..90ead2e4551b165530bd2430b3d69c34263c5c4e Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..c8e69dcebb98d43695027fcc7e39a339c84dda51 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e7aea72dad80db6724ac6e961b8d942a7dd03e Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..115cdb72b9bc2fc96df12a5963482c5169880678 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..087508fdb1144d070a2887b5bc9f5ead417b0450 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b26a2c05e3f261641734c91aef9322830ab10daf Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_36.png new file mode 100644 index 0000000000000000000000000000000000000000..1e6101fd972848740db88bfab5a0bb8bbff8e6af Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..bfd7b82aaa13a7789d0e2de69a9d42dbc4742a67 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..6b717e0dda8649aa3b5f1d6851ba0dd20cc4ea66 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..3b28ea4b0a824bdd22d27f8794caf964db5fd8ab Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..54ebbafaeb341b96e5b50a5745f929486f0ac01c Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_dialpad_white_36.png differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_forward_white_24.png similarity index 100% rename from InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png rename to assets/quantum/res/drawable-xxhdpi/quantum_ic_forward_white_24.png diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..b7f4133fd978de01cb1e62b660d402ec92e3e4da Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..ca9135b4919cc3c91ab6632475d2877fd13c1f7c Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a1b4d4f3c5213f8803300e4428968bc037098f Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_group_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..57f87aec0ba7fe0edf6746f9d5b97da08d0b5f09 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_group_white_36.png differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_hd_24dp.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_hd_white_24.png similarity index 100% rename from InCallUI/res/drawable-xxhdpi/ic_hd_24dp.png rename to assets/quantum/res/drawable-xxhdpi/quantum_ic_hd_white_24.png diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..de1739bf4f1aefc22b12b12b0ab4b31ba94ab00b Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d8b14c047b9c5227dead0c5797e36db8ff5a2b Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_message_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..0a79824b8ffdb6ef7da089b69866c9ecdd395c19 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..084bf3c9f4d780e5e79d6d64e9c352f02f82b207 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..585d38326ccd4971141326e376c58b3d8da1c99e Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..75469cd852958cb43add295e80b12f2ac1922e1d Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..a03bad27eddf5a828be03feaa79bf4de39ba5974 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..8f744f0391a2312f76d07c0668754fe83e346710 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..497479291e69adaac3e2ebfbcfd9a1d788872368 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f1defa6df89b5a7a68df6787a4ba799d3bd3b2 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe5c5ceb684831ab06bcc40d681890a3eeaa63c Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_48.png differ diff --git a/InCallUI/res/drawable-hdpi/ic_report_white_36dp.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_report_white_18.png similarity index 100% rename from InCallUI/res/drawable-hdpi/ic_report_white_36dp.png rename to assets/quantum/res/drawable-xxhdpi/quantum_ic_report_white_18.png diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..7caa9db4a4fe83f4939ea692629f2b4e20a68db0 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..0c5256413cf501ce0e6d05d6fd8c5df7b5f46424 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..140da28a8c97a4e09eb350446878cdd3c97bfe43 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..52b298e0db4035b10c5400ce7531dd057fbb65af Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..0e0f3c26fd520371fc4546cc83bee5e1c7517c62 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..8745f69ffc8b4b224b564871e1dce2bd87694240 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..69565f2c75b69277ed31474e97954ea985a400c9 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..ff84832956021c24249fcf8aa422ac09b0a2605f Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..49562a6408e231ec706cfa08622c26caaf9392f4 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..44c28e2f2830f927973beaa3a143ddfe439f20ed Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..839af26f8245769952a3ecd77aa76c1d77d8745d Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..d839f2167ce11c4434fe49f67588756efccd7d47 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..413b386524c487ef4c7da687a20c1a7478be0009 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..96c1f982fb1f2302c2bf193a0b7b3b107e7c8b73 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e27034d67874687a900f0f960c662e94cd633e2a Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..c19c19d2bd2709454a29d9140d5d0a1ff51302c4 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..1595be1697ec2ed02d668555b234fd2b36e42d98 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe7c23502e00baa93fd3477c7ea314b6d93fa74 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..8801d0ded431c21d4d6e3e092e56d540b699962f Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..c8099a1a15dda6c065cbcf3f5a51c3cf4f25fd25 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..9419ffbbc9282ca15912f8078f23cece12d5c8b8 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..90ead2e4551b165530bd2430b3d69c34263c5c4e Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b0e020573d37e8b4acac23fcd3e01cc39531b5e4 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..777658e95515ca47c9852d00621e2e6d45abc5c7 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..f2fe54bd511e7b6c0303ab3392f12d0fb20359bf Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..9f43e2073bc1b169259c7919d752a56dc9d87184 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..e3cac8e74762a07aaef920f1ea0b5935381f26bc Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..2f6d6386de9510fa6dd8c83cbb61a6f2e0fab9b2 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_36.png new file mode 100644 index 0000000000000000000000000000000000000000..5697dba95430eaee4bd539c22e3cb701a210f91e Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..23a197082a47c660dad770daeba573a95d75ea2b Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..39641921925f090e33df2767a4ee5e6d5911194f Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..4927bc242e23be272c9fd4be0f4da56a0e1c54d6 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..a53aeb1d339b7102b37c1884c79a9707dd284b20 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_dialpad_white_36.png differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_forward_white_24.png similarity index 100% rename from InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png rename to assets/quantum/res/drawable-xxxhdpi/quantum_ic_forward_white_24.png diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..b47b3f8bdbfbd9b67d208988c6856b0bdc3faefa Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a1b4d4f3c5213f8803300e4428968bc037098f Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9f18ae63d133b7b3214e6f6e9addc7a1696884 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_group_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..9ec120f5fe59764fa403f8a383cf1f22094e22eb Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_group_white_36.png differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_hd_24dp.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_hd_white_24.png similarity index 100% rename from InCallUI/res/drawable-xxxhdpi/ic_hd_24dp.png rename to assets/quantum/res/drawable-xxxhdpi/quantum_ic_hd_white_24.png diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e968fa7d12c1e085b26c3f0501c3cd584fde60c0 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..89b9910476f70957c233add97612f0fdf2facb43 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_message_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..fa7c17ac45923f50462b3b1f5e016348852fe206 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..90d0606a4590ef538301e37f1a0d0490cca327e7 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a10fbf67a8ded874cae95f4c17c937b364efd7 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..eb284e3838ab78a7be6a1e1007894dfc8978985d Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_pause_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea7e03e5dc315fcca0178a146ad2eb1b013c15a Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa2cca80cef9e50a0ebf4ec94b8f3f87c732520 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..8627f4276787a7a84ff723d84e3951d983c2b4c5 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..2ffdb55f264ecd3610f90890f8202f93c00f72e1 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..7d5091ded87b138183db10e24afabf288766a598 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_48.png differ diff --git a/InCallUI/res/drawable-xhdpi/ic_report_white_36dp.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_report_white_18.png similarity index 100% rename from InCallUI/res/drawable-xhdpi/ic_report_white_36dp.png rename to assets/quantum/res/drawable-xxxhdpi/quantum_ic_report_white_18.png diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b1a0bce48d4ff30c37635e9b59457dd0550fa2c6 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..9dfa888c15b52986f347a608c490897fecaca6d5 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..f8470b5dc00104545f84fe99a59e283cb12f1311 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..8cbb600a4b9e3429fe5e323899b504a80e8a4a85 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..854e7830139e7eb411f5a5f4321091e58441b83b Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 0000000000000000000000000000000000000000..6d703c6ae2541f6f34251717bc04379cb3c203c5 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..bf37b57f9c97e150142064fa0b6eabe4126bb44a Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..7a915c30dbf5c7ebb61fbd05c0c1981d3e46ba07 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..44c28e2f2830f927973beaa3a143ddfe439f20ed Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_18.png differ diff --git a/res/drawable-xxxhdpi/ic_videocam_24dp.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_24.png similarity index 100% rename from res/drawable-xxxhdpi/ic_videocam_24dp.png rename to assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_24.png diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..eff5923da4e05a303b8f3e50cd68df42f375cf56 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_36.png differ diff --git a/res/drawable-xxhdpi/ic_voicemail_24dp.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_voicemail_white_18.png similarity index 100% rename from res/drawable-xxhdpi/ic_voicemail_24dp.png rename to assets/quantum/res/drawable-xxxhdpi/quantum_ic_voicemail_white_18.png diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 0000000000000000000000000000000000000000..429dc02df002905271543ae7b7ccdba102fbc4ae Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..fd633b6cb4437452e127fd47c7f4ce89cd529fe9 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_white_36.png differ diff --git a/build-app.gradle b/build-app.gradle deleted file mode 100644 index 2ea437619c75717e3640815a771a0caebc46258c..0000000000000000000000000000000000000000 --- a/build-app.gradle +++ /dev/null @@ -1,39 +0,0 @@ -apply plugin: 'com.android.application' - -android { - defaultConfig { - minSdkVersion 23 - targetSdkVersion 23 - multiDexEnabled true - } - - sourceSets.main { - java.srcDirs = ['src', 'src-pre-N', 'InCallUI/src'] - manifest.srcFile 'AndroidManifest.xml' - res.srcDirs = ['res'] - } - - sourceSets.androidTest { - java.srcDirs = ['tests/src'] - res.srcDirs = ['test/res'] - } -} - -dependencies { - compile 'com.android.support:support-v4:23.1.+' - compile 'com.android.support:support-v13:23.1.+' - compile 'com.android.support:appcompat-v7:23.1.+' - compile 'com.android.support:cardview-v7:23.1.+' - compile 'com.android.support:design:23.1.+' - compile 'com.android.support:recyclerview-v7:23.1.+' - - compile project(':android-common') - compile project(':guava') - compile project(':libphonenumber') - compile project(':jsr305') - compile project(':vcard') - - compile project(':contactscommon') - compile project(':incallui') - compile project(':phonecommon') -} diff --git a/build-library.gradle b/build-library.gradle deleted file mode 100644 index a2394aac88696d23abb4fdfdb543b30cd589728c..0000000000000000000000000000000000000000 --- a/build-library.gradle +++ /dev/null @@ -1,39 +0,0 @@ -apply plugin: 'com.android.library' - -android { - defaultConfig { - minSdkVersion 23 - targetSdkVersion 23 - multiDexEnabled true - } - - sourceSets.main { - java.srcDirs = ['src', 'src-pre-N', 'InCallUI/src'] - manifest.srcFile 'AndroidManifest.xml' - res.srcDirs = ['res'] - } - - sourceSets.androidTest { - java.srcDirs = ['tests/src'] - res.srcDirs = ['test/res'] - } -} - -dependencies { - compile 'com.android.support:support-v4:23.1.+' - compile 'com.android.support:support-v13:23.1.+' - compile 'com.android.support:appcompat-v7:23.1.+' - compile 'com.android.support:cardview-v7:23.1.+' - compile 'com.android.support:design:23.1.+' - compile 'com.android.support:recyclerview-v7:23.1.+' - - compile project(':android-common') - compile project(':guava') - compile project(':libphonenumber') - compile project(':jsr305') - compile project(':vcard') - - compile project(':contactscommon') - compile project(':incallui') - compile project(':phonecommon') -} diff --git a/java/com/android/contacts/common/AndroidManifest.xml b/java/com/android/contacts/common/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..eae70cd30549a524a9139f180f8e353662f4f29f --- /dev/null +++ b/java/com/android/contacts/common/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/Bindings.java b/java/com/android/contacts/common/Bindings.java new file mode 100644 index 0000000000000000000000000000000000000000..29cf7950a53616e4ca821a5cfbe3d4ca5569bb8e --- /dev/null +++ b/java/com/android/contacts/common/Bindings.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import com.android.contacts.common.bindings.ContactsCommonBindings; +import com.android.contacts.common.bindings.ContactsCommonBindingsFactory; +import com.android.contacts.common.bindings.ContactsCommonBindingsStub; +import java.util.Objects; + +/** Accessor for the contacts common bindings. */ +public class Bindings { + + private static ContactsCommonBindings instance; + + private Bindings() {} + + public static ContactsCommonBindings get(Context context) { + Objects.requireNonNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof ContactsCommonBindingsFactory) { + instance = ((ContactsCommonBindingsFactory) application).newContactsCommonBindings(); + } + + if (instance == null) { + instance = new ContactsCommonBindingsStub(); + } + return instance; + } + + public static void setForTesting(ContactsCommonBindings testInstance) { + instance = testInstance; + } +} diff --git a/java/com/android/contacts/common/ClipboardUtils.java b/java/com/android/contacts/common/ClipboardUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..9345b0f9c193917737f7122049f629de83c90b7d --- /dev/null +++ b/java/com/android/contacts/common/ClipboardUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.text.TextUtils; +import android.widget.Toast; + +public class ClipboardUtils { + + private static final String TAG = "ClipboardUtils"; + + private ClipboardUtils() {} + + /** + * Copy a text to clipboard. + * + * @param context Context + * @param label Label to show to the user describing this clip. + * @param text Text to copy. + * @param showToast If {@code true}, a toast is shown to the user. + */ + public static void copyText( + Context context, CharSequence label, CharSequence text, boolean showToast) { + if (TextUtils.isEmpty(text)) { + return; + } + + ClipboardManager clipboardManager = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText(label == null ? "" : label, text); + clipboardManager.setPrimaryClip(clipData); + + if (showToast) { + String toastText = context.getString(R.string.toast_text_copied); + Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/java/com/android/contacts/common/Collapser.java b/java/com/android/contacts/common/Collapser.java new file mode 100644 index 0000000000000000000000000000000000000000..0b5c48bf260f8e25e020969787f332cabd789b01 --- /dev/null +++ b/java/com/android/contacts/common/Collapser.java @@ -0,0 +1,95 @@ +/* + * 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.contacts.common; + +import android.content.Context; +import java.util.Iterator; +import java.util.List; + +/** + * Class used for collapsing data items into groups of similar items. The data items that should be + * collapsible should implement the Collapsible interface. The class also contains a utility + * function that takes an ArrayList of items and returns a list of the same items collapsed into + * groups. + */ +public final class Collapser { + + /* + * The Collapser uses an n^2 algorithm so we don't want it to run on + * lists beyond a certain size. This specifies the maximum size to collapse. + */ + private static final int MAX_LISTSIZE_TO_COLLAPSE = 20; + + /* + * This utility class cannot be instantiated. + */ + private Collapser() {} + + /** + * Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed if + * {@link Collapsible#shouldCollapseWith(Object)} returns true, and are collapsed through the + * {@Link Collapsible#collapseWith(Object)} function implemented by the data item. + * + * @param list List of Objects of type > to be collapsed. + */ + public static > void collapseList(List list, Context context) { + + int listSize = list.size(); + // The algorithm below is n^2 so don't run on long lists + if (listSize > MAX_LISTSIZE_TO_COLLAPSE) { + return; + } + + for (int i = 0; i < listSize; i++) { + T iItem = list.get(i); + if (iItem != null) { + for (int j = i + 1; j < listSize; j++) { + T jItem = list.get(j); + if (jItem != null) { + if (iItem.shouldCollapseWith(jItem, context)) { + iItem.collapseWith(jItem); + list.set(j, null); + } else if (jItem.shouldCollapseWith(iItem, context)) { + jItem.collapseWith(iItem); + list.set(i, null); + break; + } + } + } + } + } + + // Remove the null items + Iterator itr = list.iterator(); + while (itr.hasNext()) { + if (itr.next() == null) { + itr.remove(); + } + } + } + + /* + * Interface implemented by data types that can be collapsed into groups of similar data. This + * can be used for example to collapse similar contact data items into a single item. + */ + public interface Collapsible { + + void collapseWith(T t); + + boolean shouldCollapseWith(T t, Context context); + } +} diff --git a/java/com/android/contacts/common/ContactPhotoManager.java b/java/com/android/contacts/common/ContactPhotoManager.java new file mode 100644 index 0000000000000000000000000000000000000000..8344710475431e806c46e7e6ecf1fa79c6232af4 --- /dev/null +++ b/java/com/android/contacts/common/ContactPhotoManager.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 com.android.contacts.common; + +import android.content.ComponentCallbacks2; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.Uri.Builder; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import com.android.contacts.common.lettertiles.LetterTileDrawable; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; + +/** Asynchronously loads contact photos and maintains a cache of photos. */ +public abstract class ContactPhotoManager implements ComponentCallbacks2 { + + /** Contact type constants used for default letter images */ + public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON; + + public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS; + public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL; + public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT; + /** Scale and offset default constants used for default letter images */ + public static final float SCALE_DEFAULT = 1.0f; + + public static final float OFFSET_DEFAULT = 0.0f; + public static final boolean IS_CIRCULAR_DEFAULT = false; + // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check. + // LINT.DoNotSubmitIf(true) + static final boolean DEBUG = false; + // LINT.DoNotSubmitIf(true) + static final boolean DEBUG_SIZES = false; + /** Uri-related constants used for default letter images */ + private static final String DISPLAY_NAME_PARAM_KEY = "display_name"; + + private static final String IDENTIFIER_PARAM_KEY = "identifier"; + private static final String CONTACT_TYPE_PARAM_KEY = "contact_type"; + private static final String SCALE_PARAM_KEY = "scale"; + private static final String OFFSET_PARAM_KEY = "offset"; + private static final String IS_CIRCULAR_PARAM_KEY = "is_circular"; + private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage"; + private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://"); + public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider(); + private static ContactPhotoManager sInstance; + + /** + * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile + * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri + * is not guaranteed to remain the same across application versions, so the actual uri should + * never be persisted in long-term storage and reused. + * + * @param request A {@link DefaultImageRequest} object with the fields configured to return a + * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link + * #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to + * request a default contact image, drawn as a letter tile using the parameters as configured + * in the provided {@link DefaultImageRequest} + */ + public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) { + final Builder builder = DEFAULT_IMAGE_URI.buildUpon(); + if (request != null) { + if (!TextUtils.isEmpty(request.displayName)) { + builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName); + } + if (!TextUtils.isEmpty(request.identifier)) { + builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier); + } + if (request.contactType != TYPE_DEFAULT) { + builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType)); + } + if (request.scale != SCALE_DEFAULT) { + builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale)); + } + if (request.offset != OFFSET_DEFAULT) { + builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset)); + } + if (request.isCircular != IS_CIRCULAR_DEFAULT) { + builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular)); + } + } + return builder.build(); + } + + /** + * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby + * Places can be identified as business photo URLs rather than URLs for personal contact photos. + * + * @param photoUrl The photo URL to modify. + * @return URL with the contact type parameter added and set to TYPE_BUSINESS. + */ + public static String appendBusinessContactType(String photoUrl) { + Uri uri = Uri.parse(photoUrl); + Builder builder = uri.buildUpon(); + builder.encodedFragment(String.valueOf(TYPE_BUSINESS)); + return builder.build().toString(); + } + + /** + * Removes the contact type information stored in the photo URI encoded fragment. + * + * @param photoUri The photo URI to remove the contact type from. + * @return The photo URI with contact type removed. + */ + public static Uri removeContactType(Uri photoUri) { + String encodedFragment = photoUri.getEncodedFragment(); + if (!TextUtils.isEmpty(encodedFragment)) { + Builder builder = photoUri.buildUpon(); + builder.encodedFragment(null); + return builder.build(); + } + return photoUri; + } + + /** + * Inspects a photo URI to determine if the photo URI represents a business. + * + * @param photoUri The URI to inspect. + * @return Whether the URI represents a business photo or not. + */ + public static boolean isBusinessContactUri(Uri photoUri) { + if (photoUri == null) { + return false; + } + + String encodedFragment = photoUri.getEncodedFragment(); + return !TextUtils.isEmpty(encodedFragment) + && encodedFragment.equals(String.valueOf(TYPE_BUSINESS)); + } + + protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) { + final DefaultImageRequest request = + new DefaultImageRequest( + uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY), + uri.getQueryParameter(IDENTIFIER_PARAM_KEY), + false); + try { + String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY); + if (!TextUtils.isEmpty(contactType)) { + request.contactType = Integer.valueOf(contactType); + } + + String scale = uri.getQueryParameter(SCALE_PARAM_KEY); + if (!TextUtils.isEmpty(scale)) { + request.scale = Float.valueOf(scale); + } + + String offset = uri.getQueryParameter(OFFSET_PARAM_KEY); + if (!TextUtils.isEmpty(offset)) { + request.offset = Float.valueOf(offset); + } + + String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY); + if (!TextUtils.isEmpty(isCircular)) { + request.isCircular = Boolean.valueOf(isCircular); + } + } catch (NumberFormatException e) { + LogUtil.w( + "ContactPhotoManager.getDefaultImageRequestFromUri", + "Invalid DefaultImageRequest image parameters provided, ignoring and using " + + "defaults."); + } + + return request; + } + + public static ContactPhotoManager getInstance(Context context) { + if (sInstance == null) { + Context applicationContext = context.getApplicationContext(); + sInstance = createContactPhotoManager(applicationContext); + applicationContext.registerComponentCallbacks(sInstance); + if (PermissionsUtil.hasContactsPermissions(context)) { + sInstance.preloadPhotosInBackground(); + } + } + return sInstance; + } + + public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { + return new ContactPhotoManagerImpl(context); + } + + @VisibleForTesting + public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) { + sInstance = photoManager; + } + + protected boolean isDefaultImageUri(Uri uri) { + return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme()); + } + + /** + * Load thumbnail image into the supplied image view. If the photo is already cached, it is + * displayed immediately. Otherwise a request is sent to load the photo from the database. + */ + public abstract void loadThumbnail( + ImageView view, + long photoId, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider); + + /** + * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest, + * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}. + */ + public final void loadThumbnail( + ImageView view, + long photoId, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest) { + loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); + } + + /** + * Load photo into the supplied image view. If the photo is already cached, it is displayed + * immediately. Otherwise a request is sent to load the photo from the location specified by the + * URI. + * + * @param view The target view + * @param photoUri The uri of the photo to load + * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is + * useful if the source image can be a lot bigger that the target, so that the decoding is + * done using efficient sampling. If requestedExtent is specified, no sampling of the image is + * performed + * @param darkTheme Whether the background is dark. This is used for default avatars + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer + * to an existing image) + */ + public abstract void loadPhoto( + ImageView view, + Uri photoUri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider); + + /** + * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest, + * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup + * keys. + * + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + */ + public final void loadPhoto( + ImageView view, + Uri photoUri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest) { + loadPhoto( + view, + photoUri, + requestedExtent, + darkTheme, + isCircular, + defaultImageRequest, + DEFAULT_AVATAR); + } + + /** + * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest, + * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is + * a thumbnail. + * + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + */ + public final void loadDirectoryPhoto( + ImageView view, + Uri photoUri, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest) { + loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); + } + + /** + * Remove photo from the supplied image view. This also cancels current pending load request + * inside this photo manager. + */ + public abstract void removePhoto(ImageView view); + + /** Cancels all pending requests to load photos asynchronously. */ + public abstract void cancelPendingRequests(View fragmentRootView); + + /** Temporarily stops loading photos from the database. */ + public abstract void pause(); + + /** Resumes loading photos from the database. */ + public abstract void resume(); + + /** + * Marks all cached photos for reloading. We can continue using cache but should also make sure + * the photos haven't changed in the background and notify the views if so. + */ + public abstract void refreshCache(); + + /** Initiates a background process that over time will fill up cache with preload photos. */ + public abstract void preloadPhotosInBackground(); + + // ComponentCallbacks2 + @Override + public void onConfigurationChanged(Configuration newConfig) {} + + // ComponentCallbacks2 + @Override + public void onLowMemory() {} + + // ComponentCallbacks2 + @Override + public void onTrimMemory(int level) {} + + /** + * Contains fields used to contain contact details and other user-defined settings that might be + * used by the ContactPhotoManager to generate a default contact image. This contact image takes + * the form of a letter or bitmap drawn on top of a colored tile. + */ + public static class DefaultImageRequest { + + /** + * Used to indicate that a drawable that represents a contact without any contact details should + * be returned. + */ + public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest(); + /** + * Used to indicate that a drawable that represents a business without a business photo should + * be returned. + */ + public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST = + new DefaultImageRequest(null, null, TYPE_BUSINESS, false); + /** + * Used to indicate that a circular drawable that represents a contact without any contact + * details should be returned. + */ + public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST = + new DefaultImageRequest(null, null, true); + /** + * Used to indicate that a circular drawable that represents a business without a business photo + * should be returned. + */ + public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST = + new DefaultImageRequest(null, null, TYPE_BUSINESS, true); + /** The contact's display name. The display name is used to */ + public String displayName; + /** + * A unique and deterministic string that can be used to identify this contact. This is usually + * the contact's lookup key, but other contact details can be used as well, especially for + * non-local or temporary contacts that might not have a lookup key. This is used to determine + * the color of the tile. + */ + public String identifier; + /** + * The type of this contact. This contact type may be used to decide the kind of image to use in + * the case where a unique letter cannot be generated from the contact's display name and + * identifier. See: {@link #TYPE_PERSON} {@link #TYPE_BUSINESS} {@link #TYPE_PERSON} {@link + * #TYPE_DEFAULT} + */ + public int contactType = TYPE_DEFAULT; + /** + * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of + * 0.0f to 2.0f). The default value is 1.0f. + */ + public float scale = SCALE_DEFAULT; + /** + * The amount to vertically offset the letter or image to within the tile. The provided offset + * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted + * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be + * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f, + * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn + * on, which means it will be drawn with the center of the letter starting at the bottom edge of + * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center + * of the tile. + */ + public float offset = OFFSET_DEFAULT; + /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */ + public boolean isCircular = false; + + public DefaultImageRequest() {} + + public DefaultImageRequest(String displayName, String identifier, boolean isCircular) { + this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); + } + + public DefaultImageRequest( + String displayName, String identifier, int contactType, boolean isCircular) { + this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); + } + + public DefaultImageRequest( + String displayName, + String identifier, + int contactType, + float scale, + float offset, + boolean isCircular) { + this.displayName = displayName; + this.identifier = identifier; + this.contactType = contactType; + this.scale = scale; + this.offset = offset; + this.isCircular = isCircular; + } + } + + public abstract static class DefaultImageProvider { + + /** + * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or + * height). If darkTheme is set, the avatar is one that looks better on dark background + * + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + */ + public abstract void applyDefaultImage( + ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest); + } + + /** + * A default image provider that applies a letter tile consisting of a colored background and a + * letter in the foreground as the default image for a contact. The color of the background and + * the type of letter is decided based on the contact's details. + */ + private static class LetterTileDefaultImageProvider extends DefaultImageProvider { + + public static Drawable getDefaultImageForContact( + Resources resources, DefaultImageRequest defaultImageRequest) { + final LetterTileDrawable drawable = new LetterTileDrawable(resources); + final int tileShape = + defaultImageRequest.isCircular + ? LetterTileDrawable.SHAPE_CIRCLE + : LetterTileDrawable.SHAPE_RECTANGLE; + if (defaultImageRequest != null) { + // If the contact identifier is null or empty, fallback to the + // displayName. In that case, use {@code null} for the contact's + // display name so that a default bitmap will be used instead of a + // letter + if (TextUtils.isEmpty(defaultImageRequest.identifier)) { + drawable.setCanonicalDialerLetterTileDetails( + null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType); + } else { + drawable.setCanonicalDialerLetterTileDetails( + defaultImageRequest.displayName, + defaultImageRequest.identifier, + tileShape, + defaultImageRequest.contactType); + } + drawable.setScale(defaultImageRequest.scale); + drawable.setOffset(defaultImageRequest.offset); + } + return drawable; + } + + @Override + public void applyDefaultImage( + ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) { + final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest); + view.setImageDrawable(drawable); + } + } +} + diff --git a/java/com/android/contacts/common/ContactPhotoManagerImpl.java b/java/com/android/contacts/common/ContactPhotoManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..2e6ff9fdc4de68360dc24221bd69e1d7e6acfdf1 --- /dev/null +++ b/java/com/android/contacts/common/ContactPhotoManagerImpl.java @@ -0,0 +1,1262 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.app.ActivityManager; +import android.content.ComponentCallbacks2; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.media.ThumbnailUtils; +import android.net.TrafficStats; +import android.net.Uri; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Message; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Contacts.Photo; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Directory; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import android.text.TextUtils; +import android.util.LruCache; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import com.android.contacts.common.util.BitmapUtil; +import com.android.contacts.common.util.TrafficStatsTags; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { + + private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; + + private static final int FADE_TRANSITION_DURATION = 200; + + /** + * Type of message sent by the UI thread to itself to indicate that some photos need to be loaded. + */ + private static final int MESSAGE_REQUEST_LOADING = 1; + + /** Type of message sent by the loader thread to indicate that some photos have been loaded. */ + private static final int MESSAGE_PHOTOS_LOADED = 2; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static final String[] COLUMNS = new String[] {Photo._ID, Photo.PHOTO}; + + /** + * Dummy object used to indicate that a bitmap for a given key could not be stored in the cache. + */ + private static final BitmapHolder BITMAP_UNAVAILABLE; + /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */ + private static final int HOLDER_CACHE_SIZE = 2000000; + /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */ + private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K + /** Height/width of a thumbnail image */ + private static int mThumbnailSize; + + static { + BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0); + BITMAP_UNAVAILABLE.bitmapRef = new SoftReference(null); + } + + private final Context mContext; + /** + * An LRU cache for bitmap holders. The cache contains bytes for photos just as they come from the + * database. Each holder has a soft reference to the actual bitmap. + */ + private final LruCache mBitmapHolderCache; + /** Cache size threshold at which bitmaps will not be preloaded. */ + private final int mBitmapHolderCacheRedZoneBytes; + /** + * Level 2 LRU cache for bitmaps. This is a smaller cache that holds the most recently used + * bitmaps to save time on decoding them from bytes (the bytes are stored in {@link + * #mBitmapHolderCache}. + */ + private final LruCache mBitmapCache; + /** + * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. The + * request may swapped out before the photo loading request is started. + */ + private final ConcurrentHashMap mPendingRequests = + new ConcurrentHashMap(); + /** Handler for messages sent to the UI thread. */ + private final Handler mMainThreadHandler = new Handler(this); + /** For debug: How many times we had to reload cached photo for a stale entry */ + private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger(); + /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */ + private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger(); + /** {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. */ + private volatile boolean mBitmapHolderCacheAllUnfresh = true; + /** Thread responsible for loading photos from the database. Created upon the first request. */ + private LoaderThread mLoaderThread; + /** A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */ + private boolean mLoadingRequested; + /** Flag indicating if the image loading is paused. */ + private boolean mPaused; + /** The user agent string to use when loading URI based photos. */ + private String mUserAgent; + + public ContactPhotoManagerImpl(Context context) { + mContext = context; + + final ActivityManager am = + ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)); + + final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f; + + final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE); + mBitmapCache = + new LruCache(bitmapCacheSize) { + @Override + protected int sizeOf(Object key, Bitmap value) { + return value.getByteCount(); + } + + @Override + protected void entryRemoved( + boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) { + if (DEBUG) { + dumpStats(); + } + } + }; + final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE); + mBitmapHolderCache = + new LruCache(holderCacheSize) { + @Override + protected int sizeOf(Object key, BitmapHolder value) { + return value.bytes != null ? value.bytes.length : 0; + } + + @Override + protected void entryRemoved( + boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) { + if (DEBUG) { + dumpStats(); + } + } + }; + mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75); + LogUtil.i( + "ContactPhotoManagerImpl.ContactPhotoManagerImpl", "cache adj: " + cacheSizeAdjustment); + if (DEBUG) { + LogUtil.d( + "ContactPhotoManagerImpl.ContactPhotoManagerImpl", + "Cache size: " + btk(mBitmapHolderCache.maxSize()) + " + " + btk(mBitmapCache.maxSize())); + } + + mThumbnailSize = + context.getResources().getDimensionPixelSize(R.dimen.contact_browser_list_item_photo_size); + + // Get a user agent string to use for URI photo requests. + mUserAgent = Bindings.get(context).getUserAgent(); + if (mUserAgent == null) { + mUserAgent = ""; + } + } + + /** Converts bytes to K bytes, rounding up. Used only for debug log. */ + private static String btk(int bytes) { + return ((bytes + 1023) / 1024) + "K"; + } + + private static final int safeDiv(int dividend, int divisor) { + return (divisor == 0) ? 0 : (dividend / divisor); + } + + private static boolean isChildView(View parent, View potentialChild) { + return potentialChild.getParent() != null + && (potentialChild.getParent() == parent + || (potentialChild.getParent() instanceof ViewGroup + && isChildView(parent, (ViewGroup) potentialChild.getParent()))); + } + + /** + * If necessary, decodes bytes stored in the holder to Bitmap. As long as the bitmap is held + * either by {@link #mBitmapCache} or by a soft reference in the holder, it will not be necessary + * to decode the bitmap. + */ + private static void inflateBitmap(BitmapHolder holder, int requestedExtent) { + final int sampleSize = + BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent); + byte[] bytes = holder.bytes; + if (bytes == null || bytes.length == 0) { + return; + } + + if (sampleSize == holder.decodedSampleSize) { + // Check the soft reference. If will be retained if the bitmap is also + // in the LRU cache, so we don't need to check the LRU cache explicitly. + if (holder.bitmapRef != null) { + holder.bitmap = holder.bitmapRef.get(); + if (holder.bitmap != null) { + return; + } + } + } + + try { + Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize); + + // TODO: As a temporary workaround while framework support is being added to + // clip non-square bitmaps into a perfect circle, manually crop the bitmap into + // into a square if it will be displayed as a thumbnail so that it can be cropped + // into a circle. + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + + // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just + // below twice the length of a thumbnail image due to the way we calculate the optimal + // sample size. + if (height != width && Math.min(height, width) <= mThumbnailSize * 2) { + final int dimension = Math.min(height, width); + bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension); + } + // make bitmap mutable and draw size onto it + if (DEBUG_SIZES) { + Bitmap original = bitmap; + bitmap = bitmap.copy(bitmap.getConfig(), true); + original.recycle(); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setTextSize(16); + paint.setColor(Color.BLUE); + paint.setStyle(Style.FILL); + canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint); + paint.setColor(Color.WHITE); + paint.setAntiAlias(true); + canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint); + } + + holder.decodedSampleSize = sampleSize; + holder.bitmap = bitmap; + holder.bitmapRef = new SoftReference(bitmap); + if (DEBUG) { + LogUtil.d( + "ContactPhotoManagerImpl.inflateBitmap", + "inflateBitmap " + + btk(bytes.length) + + " -> " + + bitmap.getWidth() + + "x" + + bitmap.getHeight() + + ", " + + btk(bitmap.getByteCount())); + } + } catch (OutOfMemoryError e) { + // Do nothing - the photo will appear to be missing + } + } + + /** Dump cache stats on logcat. */ + private void dumpStats() { + if (!DEBUG) { + return; + } + { + int numHolders = 0; + int rawBytes = 0; + int bitmapBytes = 0; + int numBitmaps = 0; + for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) { + numHolders++; + if (h.bytes != null) { + rawBytes += h.bytes.length; + } + Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null; + if (b != null) { + numBitmaps++; + bitmapBytes += b.getByteCount(); + } + } + LogUtil.d( + "ContactPhotoManagerImpl.dumpStats", + "L1: " + + btk(rawBytes) + + " + " + + btk(bitmapBytes) + + " = " + + btk(rawBytes + bitmapBytes) + + ", " + + numHolders + + " holders, " + + numBitmaps + + " bitmaps, avg: " + + btk(safeDiv(rawBytes, numHolders)) + + "," + + btk(safeDiv(bitmapBytes, numBitmaps))); + LogUtil.d( + "ContactPhotoManagerImpl.dumpStats", + "L1 Stats: " + + mBitmapHolderCache.toString() + + ", overwrite: fresh=" + + mFreshCacheOverwrite.get() + + " stale=" + + mStaleCacheOverwrite.get()); + } + + { + int numBitmaps = 0; + int bitmapBytes = 0; + for (Bitmap b : mBitmapCache.snapshot().values()) { + numBitmaps++; + bitmapBytes += b.getByteCount(); + } + LogUtil.d( + "ContactPhotoManagerImpl.dumpStats", + "L2: " + + btk(bitmapBytes) + + ", " + + numBitmaps + + " bitmaps" + + ", avg: " + + btk(safeDiv(bitmapBytes, numBitmaps))); + // We don't get from L2 cache, so L2 stats is meaningless. + } + } + + @Override + public void onTrimMemory(int level) { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.onTrimMemory", "onTrimMemory: " + level); + } + if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + // Clear the caches. Note all pending requests will be removed too. + clear(); + } + } + + @Override + public void preloadPhotosInBackground() { + ensureLoaderThread(); + mLoaderThread.requestPreloading(); + } + + @Override + public void loadThumbnail( + ImageView view, + long photoId, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider) { + if (photoId == 0) { + // No photo is needed + defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest); + mPendingRequests.remove(view); + } else { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.loadThumbnail", "loadPhoto request: " + photoId); + } + loadPhotoByIdOrUri( + view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, defaultProvider)); + } + } + + @Override + public void loadPhoto( + ImageView view, + Uri photoUri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider) { + if (photoUri == null) { + // No photo is needed + defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, defaultImageRequest); + mPendingRequests.remove(view); + } else { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.loadPhoto", "loadPhoto request: " + photoUri); + } + if (isDefaultImageUri(photoUri)) { + createAndApplyDefaultImageForUri( + view, photoUri, requestedExtent, darkTheme, isCircular, defaultProvider); + } else { + loadPhotoByIdOrUri( + view, + Request.createFromUri( + photoUri, requestedExtent, darkTheme, isCircular, defaultProvider)); + } + } + } + + private void createAndApplyDefaultImageForUri( + ImageView view, + Uri uri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageProvider defaultProvider) { + DefaultImageRequest request = getDefaultImageRequestFromUri(uri); + request.isCircular = isCircular; + defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request); + } + + private void loadPhotoByIdOrUri(ImageView view, Request request) { + boolean loaded = loadCachedPhoto(view, request, false); + if (loaded) { + mPendingRequests.remove(view); + } else { + mPendingRequests.put(view, request); + if (!mPaused) { + // Send a request to start loading photos + requestLoading(); + } + } + } + + @Override + public void removePhoto(ImageView view) { + view.setImageDrawable(null); + mPendingRequests.remove(view); + } + + /** + * Cancels pending requests to load photos asynchronously for views inside {@param + * fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests. + */ + @Override + public void cancelPendingRequests(View fragmentRootView) { + if (fragmentRootView == null) { + mPendingRequests.clear(); + return; + } + final Iterator> iterator = mPendingRequests.entrySet().iterator(); + while (iterator.hasNext()) { + final ImageView imageView = iterator.next().getKey(); + // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then + // we can safely remove its request. + if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) { + iterator.remove(); + } + } + } + + @Override + public void refreshCache() { + if (mBitmapHolderCacheAllUnfresh) { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache -- no fresh entries."); + } + return; + } + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache"); + } + mBitmapHolderCacheAllUnfresh = true; + for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { + if (holder != BITMAP_UNAVAILABLE) { + holder.fresh = false; + } + } + } + + /** + * Checks if the photo is present in cache. If so, sets the photo on the view. + * + * @return false if the photo needs to be (re)loaded from the provider. + */ + @UiThread + private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) { + BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); + if (holder == null) { + // The bitmap has not been loaded ==> show default avatar + request.applyDefaultImage(view, request.mIsCircular); + return false; + } + + if (holder.bytes == null) { + request.applyDefaultImage(view, request.mIsCircular); + return holder.fresh; + } + + Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get(); + if (cachedBitmap == null) { + request.applyDefaultImage(view, request.mIsCircular); + return false; + } + + final Drawable previousDrawable = view.getDrawable(); + if (fadeIn && previousDrawable != null) { + final Drawable[] layers = new Drawable[2]; + // Prevent cascade of TransitionDrawables. + if (previousDrawable instanceof TransitionDrawable) { + final TransitionDrawable previousTransitionDrawable = (TransitionDrawable) previousDrawable; + layers[0] = + previousTransitionDrawable.getDrawable( + previousTransitionDrawable.getNumberOfLayers() - 1); + } else { + layers[0] = previousDrawable; + } + layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request); + TransitionDrawable drawable = new TransitionDrawable(layers); + view.setImageDrawable(drawable); + drawable.startTransition(FADE_TRANSITION_DURATION); + } else { + view.setImageDrawable(getDrawableForBitmap(mContext.getResources(), cachedBitmap, request)); + } + + // Put the bitmap in the LRU cache. But only do this for images that are small enough + // (we require that at least six of those can be cached at the same time) + if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) { + mBitmapCache.put(request.getKey(), cachedBitmap); + } + + // Soften the reference + holder.bitmap = null; + + return holder.fresh; + } + + /** + * Given a bitmap, returns a drawable that is configured to display the bitmap based on the + * specified request. + */ + private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) { + if (request.mIsCircular) { + final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap); + drawable.setAntiAlias(true); + drawable.setCornerRadius(bitmap.getHeight() / 2); + return drawable; + } else { + return new BitmapDrawable(resources, bitmap); + } + } + + public void clear() { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.clear", "clear"); + } + mPendingRequests.clear(); + mBitmapHolderCache.evictAll(); + mBitmapCache.evictAll(); + } + + @Override + public void pause() { + mPaused = true; + } + + @Override + public void resume() { + mPaused = false; + if (DEBUG) { + dumpStats(); + } + if (!mPendingRequests.isEmpty()) { + requestLoading(); + } + } + + /** + * Sends a message to this thread itself to start loading images. If the current view contains + * multiple image views, all of those image views will get a chance to request their respective + * photos before any of those requests are executed. This allows us to load images in bulk. + */ + private void requestLoading() { + if (!mLoadingRequested) { + mLoadingRequested = true; + mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); + } + } + + /** Processes requests on the main thread. */ + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_REQUEST_LOADING: + { + mLoadingRequested = false; + if (!mPaused) { + ensureLoaderThread(); + mLoaderThread.requestLoading(); + } + return true; + } + + case MESSAGE_PHOTOS_LOADED: + { + if (!mPaused) { + processLoadedImages(); + } + if (DEBUG) { + dumpStats(); + } + return true; + } + } + return false; + } + + public void ensureLoaderThread() { + if (mLoaderThread == null) { + mLoaderThread = new LoaderThread(mContext.getContentResolver()); + mLoaderThread.start(); + } + } + + /** + * Goes over pending loading requests and displays loaded photos. If some of the photos still + * haven't been loaded, sends another request for image loading. + */ + private void processLoadedImages() { + final Iterator> iterator = mPendingRequests.entrySet().iterator(); + while (iterator.hasNext()) { + final Entry entry = iterator.next(); + // TODO: Temporarily disable contact photo fading in, until issues with + // RoundedBitmapDrawables overlapping the default image drawables are resolved. + final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false); + if (loaded) { + iterator.remove(); + } + } + + softenCache(); + + if (!mPendingRequests.isEmpty()) { + requestLoading(); + } + } + + /** + * Removes strong references to loaded bitmaps to allow them to be garbage collected if needed. + * Some of the bitmaps will still be retained by {@link #mBitmapCache}. + */ + private void softenCache() { + for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { + holder.bitmap = null; + } + } + + /** Stores the supplied bitmap in cache. */ + private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) { + if (DEBUG) { + BitmapHolder prev = mBitmapHolderCache.get(key); + if (prev != null && prev.bytes != null) { + LogUtil.d( + "ContactPhotoManagerImpl.cacheBitmap", + "overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); + if (prev.fresh) { + mFreshCacheOverwrite.incrementAndGet(); + } else { + mStaleCacheOverwrite.incrementAndGet(); + } + } + LogUtil.d( + "ContactPhotoManagerImpl.cacheBitmap", + "caching data: key=" + key + ", " + (bytes == null ? "" : btk(bytes.length))); + } + BitmapHolder holder = + new BitmapHolder(bytes, bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes)); + + // Unless this image is being preloaded, decode it right away while + // we are still on the background thread. + if (!preloading) { + inflateBitmap(holder, requestedExtent); + } + + if (bytes != null) { + mBitmapHolderCache.put(key, holder); + if (mBitmapHolderCache.get(key) != holder) { + LogUtil.w("ContactPhotoManagerImpl.cacheBitmap", "bitmap too big to fit in cache."); + mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); + } + } else { + mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); + } + + mBitmapHolderCacheAllUnfresh = false; + } + + /** + * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have + * already loaded + */ + private void obtainPhotoIdsAndUrisToLoad( + Set photoIds, Set photoIdsAsStrings, Set uris) { + photoIds.clear(); + photoIdsAsStrings.clear(); + uris.clear(); + + boolean jpegsDecoded = false; + + /* + * Since the call is made from the loader thread, the map could be + * changing during the iteration. That's not really a problem: + * ConcurrentHashMap will allow those changes to happen without throwing + * exceptions. Since we may miss some requests in the situation of + * concurrent change, we will need to check the map again once loading + * is complete. + */ + Iterator iterator = mPendingRequests.values().iterator(); + while (iterator.hasNext()) { + Request request = iterator.next(); + final BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); + if (holder == BITMAP_UNAVAILABLE) { + continue; + } + if (holder != null + && holder.bytes != null + && holder.fresh + && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) { + // This was previously loaded but we don't currently have the inflated Bitmap + inflateBitmap(holder, request.getRequestedExtent()); + jpegsDecoded = true; + } else { + if (holder == null || !holder.fresh) { + if (request.isUriRequest()) { + uris.add(request); + } else { + photoIds.add(request.getId()); + photoIdsAsStrings.add(String.valueOf(request.mId)); + } + } + } + } + + if (jpegsDecoded) { + mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); + } + } + + /** Maintains the state of a particular photo. */ + private static class BitmapHolder { + + final byte[] bytes; + final int originalSmallerExtent; + + volatile boolean fresh; + Bitmap bitmap; + Reference bitmapRef; + int decodedSampleSize; + + public BitmapHolder(byte[] bytes, int originalSmallerExtent) { + this.bytes = bytes; + this.fresh = true; + this.originalSmallerExtent = originalSmallerExtent; + } + } + + /** + * A holder for either a Uri or an id and a flag whether this was requested for the dark or light + * theme + */ + private static final class Request { + + private final long mId; + private final Uri mUri; + private final boolean mDarkTheme; + private final int mRequestedExtent; + private final DefaultImageProvider mDefaultProvider; + /** Whether or not the contact photo is to be displayed as a circle */ + private final boolean mIsCircular; + + private Request( + long id, + Uri uri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageProvider defaultProvider) { + mId = id; + mUri = uri; + mDarkTheme = darkTheme; + mIsCircular = isCircular; + mRequestedExtent = requestedExtent; + mDefaultProvider = defaultProvider; + } + + public static Request createFromThumbnailId( + long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) { + return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider); + } + + public static Request createFromUri( + Uri uri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageProvider defaultProvider) { + return new Request( + 0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, defaultProvider); + } + + public boolean isUriRequest() { + return mUri != null; + } + + public Uri getUri() { + return mUri; + } + + public long getId() { + return mId; + } + + public int getRequestedExtent() { + return mRequestedExtent; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (mId ^ (mId >>> 32)); + result = prime * result + mRequestedExtent; + result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Request that = (Request) obj; + if (mId != that.mId) { + return false; + } + if (mRequestedExtent != that.mRequestedExtent) { + return false; + } + if (!UriUtils.areEqual(mUri, that.mUri)) { + return false; + } + // Don't compare equality of mDarkTheme because it is only used in the default contact + // photo case. When the contact does have a photo, the contact photo is the same + // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue + // twice. + return true; + } + + public Object getKey() { + return mUri == null ? mId : mUri; + } + + /** + * Applies the default image to the current view. If the request is URI-based, looks for the + * contact type encoded fragment to determine if this is a request for a business photo, in + * which case we will load the default business photo. + * + * @param view The current image view to apply the image to. + * @param isCircular Whether the image is circular or not. + */ + public void applyDefaultImage(ImageView view, boolean isCircular) { + final DefaultImageRequest request; + + if (isCircular) { + request = + ContactPhotoManager.isBusinessContactUri(mUri) + ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST + : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST; + } else { + request = + ContactPhotoManager.isBusinessContactUri(mUri) + ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST + : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST; + } + mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request); + } + } + + /** The thread that performs loading of photos from the database. */ + private class LoaderThread extends HandlerThread implements Callback { + + private static final int BUFFER_SIZE = 1024 * 16; + private static final int MESSAGE_PRELOAD_PHOTOS = 0; + private static final int MESSAGE_LOAD_PHOTOS = 1; + + /** A pause between preload batches that yields to the UI thread. */ + private static final int PHOTO_PRELOAD_DELAY = 1000; + + /** Number of photos to preload per batch. */ + private static final int PRELOAD_BATCH = 25; + + /** + * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size + * of a photo is 4kb, then this number should be 2Mb/4kb = 500. + */ + private static final int MAX_PHOTOS_TO_PRELOAD = 100; + + private static final int PRELOAD_STATUS_NOT_STARTED = 0; + private static final int PRELOAD_STATUS_IN_PROGRESS = 1; + private static final int PRELOAD_STATUS_DONE = 2; + private final ContentResolver mResolver; + private final StringBuilder mStringBuilder = new StringBuilder(); + private final Set mPhotoIds = new HashSet<>(); + private final Set mPhotoIdsAsStrings = new HashSet<>(); + private final Set mPhotoUris = new HashSet<>(); + private final List mPreloadPhotoIds = new ArrayList<>(); + private Handler mLoaderThreadHandler; + private byte[] mBuffer; + private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED; + + public LoaderThread(ContentResolver resolver) { + super(LOADER_THREAD_NAME); + mResolver = resolver; + } + + public void ensureHandler() { + if (mLoaderThreadHandler == null) { + mLoaderThreadHandler = new Handler(getLooper(), this); + } + } + + /** + * Kicks off preloading of the next batch of photos on the background thread. Preloading will + * happen after a delay: we want to yield to the UI thread as much as possible. + * + *

If preloading is already complete, does nothing. + */ + public void requestPreloading() { + if (mPreloadStatus == PRELOAD_STATUS_DONE) { + return; + } + + ensureHandler(); + if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { + return; + } + + mLoaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); + } + + /** + * Sends a message to this thread to load requested photos. Cancels a preloading request, if + * any: we don't want preloading to impede loading of the photos we need to display now. + */ + public void requestLoading() { + ensureHandler(); + mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); + mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); + } + + /** + * Receives the above message, loads photos and then sends a message to the main thread to + * process them. + */ + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_PRELOAD_PHOTOS: + preloadPhotosInBackground(); + break; + case MESSAGE_LOAD_PHOTOS: + loadPhotosInBackground(); + break; + } + return true; + } + + /** + * The first time it is called, figures out which photos need to be preloaded. Each subsequent + * call preloads the next batch of photos and requests another cycle of preloading after a + * delay. The whole process ends when we either run out of photos to preload or fill up cache. + */ + @WorkerThread + private void preloadPhotosInBackground() { + if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { + return; + } + + if (mPreloadStatus == PRELOAD_STATUS_DONE) { + return; + } + + if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) { + queryPhotosForPreload(); + if (mPreloadPhotoIds.isEmpty()) { + mPreloadStatus = PRELOAD_STATUS_DONE; + } else { + mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS; + } + requestPreloading(); + return; + } + + if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) { + mPreloadStatus = PRELOAD_STATUS_DONE; + return; + } + + mPhotoIds.clear(); + mPhotoIdsAsStrings.clear(); + + int count = 0; + int preloadSize = mPreloadPhotoIds.size(); + while (preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) { + preloadSize--; + count++; + Long photoId = mPreloadPhotoIds.get(preloadSize); + mPhotoIds.add(photoId); + mPhotoIdsAsStrings.add(photoId.toString()); + mPreloadPhotoIds.remove(preloadSize); + } + + loadThumbnails(true); + + if (preloadSize == 0) { + mPreloadStatus = PRELOAD_STATUS_DONE; + } + + LogUtil.v( + "ContactPhotoManagerImpl.preloadPhotosInBackground", + "preloaded " + count + " photos. cached bytes: " + mBitmapHolderCache.size()); + + requestPreloading(); + } + + @WorkerThread + private void queryPhotosForPreload() { + Cursor cursor = null; + try { + Uri uri = + Contacts.CONTENT_URI + .buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) + .appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, String.valueOf(MAX_PHOTOS_TO_PRELOAD)) + .build(); + cursor = + mResolver.query( + uri, + new String[] {Contacts.PHOTO_ID}, + Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", + null, + Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); + + if (cursor != null) { + while (cursor.moveToNext()) { + // Insert them in reverse order, because we will be taking + // them from the end of the list for loading. + mPreloadPhotoIds.add(0, cursor.getLong(0)); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @WorkerThread + private void loadPhotosInBackground() { + if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { + return; + } + obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris); + loadThumbnails(false); + loadUriBasedPhotos(); + requestPreloading(); + } + + /** Loads thumbnail photos with ids */ + @WorkerThread + private void loadThumbnails(boolean preloading) { + if (mPhotoIds.isEmpty()) { + return; + } + + // Remove loaded photos from the preload queue: we don't want + // the preloading process to load them again. + if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) { + for (Long id : mPhotoIds) { + mPreloadPhotoIds.remove(id); + } + if (mPreloadPhotoIds.isEmpty()) { + mPreloadStatus = PRELOAD_STATUS_DONE; + } + } + + mStringBuilder.setLength(0); + mStringBuilder.append(Photo._ID + " IN("); + for (int i = 0; i < mPhotoIds.size(); i++) { + if (i != 0) { + mStringBuilder.append(','); + } + mStringBuilder.append('?'); + } + mStringBuilder.append(')'); + + Cursor cursor = null; + try { + if (DEBUG) { + LogUtil.d( + "ContactPhotoManagerImpl.loadThumbnails", + "loading " + TextUtils.join(",", mPhotoIdsAsStrings)); + } + cursor = + mResolver.query( + Data.CONTENT_URI, + COLUMNS, + mStringBuilder.toString(), + mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), + null); + + if (cursor != null) { + while (cursor.moveToNext()) { + Long id = cursor.getLong(0); + byte[] bytes = cursor.getBlob(1); + cacheBitmap(id, bytes, preloading, -1); + mPhotoIds.remove(id); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + // Remaining photos were not found in the contacts database (but might be in profile). + for (Long id : mPhotoIds) { + if (ContactsContract.isProfileId(id)) { + Cursor profileCursor = null; + try { + profileCursor = + mResolver.query( + ContentUris.withAppendedId(Data.CONTENT_URI, id), COLUMNS, null, null, null); + if (profileCursor != null && profileCursor.moveToFirst()) { + cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), preloading, -1); + } else { + // Couldn't load a photo this way either. + cacheBitmap(id, null, preloading, -1); + } + } finally { + if (profileCursor != null) { + profileCursor.close(); + } + } + } else { + // Not a profile photo and not found - mark the cache accordingly + cacheBitmap(id, null, preloading, -1); + } + } + + mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); + } + + /** + * Loads photos referenced with Uris. Those can be remote thumbnails (from directory searches), + * display photos etc + */ + @WorkerThread + private void loadUriBasedPhotos() { + for (Request uriRequest : mPhotoUris) { + // Keep the original URI and use this to key into the cache. Failure to do so will + // result in an image being continually reloaded into cache if the original URI + // has a contact type encodedFragment (eg nearby places business photo URLs). + Uri originalUri = uriRequest.getUri(); + + // Strip off the "contact type" we added to the URI to ensure it was identifiable as + // a business photo -- there is no need to pass this on to the server. + Uri uri = ContactPhotoManager.removeContactType(originalUri); + + if (mBuffer == null) { + mBuffer = new byte[BUFFER_SIZE]; + } + try { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.loadUriBasedPhotos", "loading " + uri); + } + final String scheme = uri.getScheme(); + InputStream is = null; + if (scheme.equals("http") || scheme.equals("https")) { + TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG); + final HttpURLConnection connection = + (HttpURLConnection) new URL(uri.toString()).openConnection(); + + // Include the user agent if it is specified. + if (!TextUtils.isEmpty(mUserAgent)) { + connection.setRequestProperty("User-Agent", mUserAgent); + } + try { + is = connection.getInputStream(); + } catch (IOException e) { + connection.disconnect(); + is = null; + } + TrafficStats.clearThreadStatsTag(); + } else { + is = mResolver.openInputStream(uri); + } + if (is != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + int size; + while ((size = is.read(mBuffer)) != -1) { + baos.write(mBuffer, 0, size); + } + } finally { + is.close(); + } + cacheBitmap(originalUri, baos.toByteArray(), false, uriRequest.getRequestedExtent()); + mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); + } else { + LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri); + cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); + } + } catch (final Exception | OutOfMemoryError ex) { + LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri, ex); + cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); + } + } + } + } +} diff --git a/java/com/android/contacts/common/ContactPresenceIconUtil.java b/java/com/android/contacts/common/ContactPresenceIconUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..eeaf652a82cca974b11b08017ce184e253ae5ba2 --- /dev/null +++ b/java/com/android/contacts/common/ContactPresenceIconUtil.java @@ -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. + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.provider.ContactsContract.StatusUpdates; + +/** Define the contact present show policy in Contacts */ +public class ContactPresenceIconUtil { + + /** + * Get the presence icon resource according the status. + * + * @return null means don't show the status icon. + */ + public static Drawable getPresenceIcon(Context context, int status) { + // We don't show the offline status in Contacts + switch (status) { + case StatusUpdates.AVAILABLE: + case StatusUpdates.IDLE: + case StatusUpdates.AWAY: + case StatusUpdates.DO_NOT_DISTURB: + case StatusUpdates.INVISIBLE: + return context.getResources().getDrawable(StatusUpdates.getPresenceIconResourceId(status)); + case StatusUpdates.OFFLINE: + // The undefined status is treated as OFFLINE in getPresenceIconResourceId(); + default: + return null; + } + } +} diff --git a/java/com/android/contacts/common/ContactStatusUtil.java b/java/com/android/contacts/common/ContactStatusUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..97d84c876627d25373547004ae1a7133f067aad6 --- /dev/null +++ b/java/com/android/contacts/common/ContactStatusUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.content.res.Resources; +import android.provider.ContactsContract.StatusUpdates; + +/** Provides static function to get default contact status message. */ +public class ContactStatusUtil { + + private static final String TAG = "ContactStatusUtil"; + + public static String getStatusString(Context context, int presence) { + Resources resources = context.getResources(); + switch (presence) { + case StatusUpdates.AVAILABLE: + return resources.getString(R.string.status_available); + case StatusUpdates.IDLE: + case StatusUpdates.AWAY: + return resources.getString(R.string.status_away); + case StatusUpdates.DO_NOT_DISTURB: + return resources.getString(R.string.status_busy); + case StatusUpdates.OFFLINE: + case StatusUpdates.INVISIBLE: + default: + return null; + } + } +} diff --git a/java/com/android/contacts/common/ContactTileLoaderFactory.java b/java/com/android/contacts/common/ContactTileLoaderFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..d71472ef85dfbdbbcda45a64fffb87bf6d648fe6 --- /dev/null +++ b/java/com/android/contacts/common/ContactTileLoaderFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.contacts.common; + +import android.content.Context; +import android.content.CursorLoader; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.support.annotation.VisibleForTesting; + +/** + * Used to create {@link CursorLoader} which finds contacts information from the strequents table. + * + *

Only returns contacts with phone numbers. + */ +public final class ContactTileLoaderFactory { + + /** + * The _ID field returned for strequent items actually contains data._id instead of contacts._id + * because the query is performed on the data table. In order to obtain the contact id for + * strequent items, use Phone.contact_id instead. + */ + @VisibleForTesting + public static final String[] COLUMNS_PHONE_ONLY = + new String[] { + Contacts._ID, + Contacts.DISPLAY_NAME_PRIMARY, + Contacts.STARRED, + Contacts.PHOTO_URI, + Contacts.LOOKUP_KEY, + Phone.NUMBER, + Phone.TYPE, + Phone.LABEL, + Phone.IS_SUPER_PRIMARY, + Contacts.PINNED, + Phone.CONTACT_ID, + Contacts.DISPLAY_NAME_ALTERNATIVE, + }; + + public static CursorLoader createStrequentPhoneOnlyLoader(Context context) { + Uri uri = + Contacts.CONTENT_STREQUENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") + .build(); + + return new CursorLoader(context, uri, COLUMNS_PHONE_ONLY, null, null, null); + } +} diff --git a/java/com/android/contacts/common/ContactsUtils.java b/java/com/android/contacts/common/ContactsUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..60af44b9afeaeb6b903295017a5e14fa650bdcd3 --- /dev/null +++ b/java/com/android/contacts/common/ContactsUtils.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.DisplayPhoto; +import android.support.annotation.IntDef; +import android.text.TextUtils; +import android.util.Pair; +import com.android.contacts.common.compat.ContactsCompat; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.model.AccountTypeManager; +import com.android.contacts.common.model.account.AccountWithDataSet; +import com.android.contacts.common.model.dataitem.ImDataItem; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +public class ContactsUtils { + + // Telecomm related schemes are in CallUtil + public static final String SCHEME_IMTO = "imto"; + public static final String SCHEME_MAILTO = "mailto"; + public static final String SCHEME_SMSTO = "smsto"; + public static final long USER_TYPE_CURRENT = 0; + public static final long USER_TYPE_WORK = 1; + private static final String TAG = "ContactsUtils"; + private static final int DEFAULT_THUMBNAIL_SIZE = 96; + private static int sThumbnailSize = -1; + + /** + * This looks up the provider name defined in ProviderNames from the predefined IM protocol id. + * This is used for interacting with the IM application. + * + * @param protocol the protocol ID + * @return the provider name the IM app uses for the given protocol, or null if no provider is + * defined for the given protocol + * @hide + */ + public static String lookupProviderNameFromId(int protocol) { + switch (protocol) { + case Im.PROTOCOL_GOOGLE_TALK: + return ProviderNames.GTALK; + case Im.PROTOCOL_AIM: + return ProviderNames.AIM; + case Im.PROTOCOL_MSN: + return ProviderNames.MSN; + case Im.PROTOCOL_YAHOO: + return ProviderNames.YAHOO; + case Im.PROTOCOL_ICQ: + return ProviderNames.ICQ; + case Im.PROTOCOL_JABBER: + return ProviderNames.JABBER; + case Im.PROTOCOL_SKYPE: + return ProviderNames.SKYPE; + case Im.PROTOCOL_QQ: + return ProviderNames.QQ; + } + return null; + } + + /** + * Test if the given {@link CharSequence} contains any graphic characters, first checking {@link + * TextUtils#isEmpty(CharSequence)} to handle null. + */ + public static boolean isGraphic(CharSequence str) { + return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str); + } + + /** Returns true if two objects are considered equal. Two null references are equal here. */ + public static boolean areObjectsEqual(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** Returns true if two {@link Intent}s are both null, or have the same action. */ + public static final boolean areIntentActionEqual(Intent a, Intent b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return TextUtils.equals(a.getAction(), b.getAction()); + } + + public static boolean areGroupWritableAccountsAvailable(Context context) { + final List accounts = + AccountTypeManager.getInstance(context).getGroupWritableAccounts(); + return !accounts.isEmpty(); + } + + /** + * Returns the size (width and height) of thumbnail pictures as configured in the provider. This + * can safely be called from the UI thread, as the provider can serve this without performing a + * database access + */ + public static int getThumbnailSize(Context context) { + if (sThumbnailSize == -1) { + final Cursor c = + context + .getContentResolver() + .query( + DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, + new String[] {DisplayPhoto.THUMBNAIL_MAX_DIM}, + null, + null, + null); + if (c != null) { + try { + if (c.moveToFirst()) { + sThumbnailSize = c.getInt(0); + } + } finally { + c.close(); + } + } + } + return sThumbnailSize != -1 ? sThumbnailSize : DEFAULT_THUMBNAIL_SIZE; + } + + private static Intent getCustomImIntent(ImDataItem im, int protocol) { + String host = im.getCustomProtocol(); + final String data = im.getData(); + if (TextUtils.isEmpty(data)) { + return null; + } + if (protocol != Im.PROTOCOL_CUSTOM) { + // Try bringing in a well-known host for specific protocols + host = ContactsUtils.lookupProviderNameFromId(protocol); + } + if (TextUtils.isEmpty(host)) { + return null; + } + final String authority = host.toLowerCase(); + final Uri imUri = + new Uri.Builder().scheme(SCHEME_IMTO).authority(authority).appendPath(data).build(); + final Intent intent = new Intent(Intent.ACTION_SENDTO, imUri); + return intent; + } + + /** + * Returns the proper Intent for an ImDatItem. If available, a secondary intent is stored in the + * second Pair slot + */ + public static Pair buildImIntent(Context context, ImDataItem im) { + Intent intent = null; + Intent secondaryIntent = null; + final boolean isEmail = im.isCreatedFromEmail(); + + if (!isEmail && !im.isProtocolValid()) { + return new Pair<>(null, null); + } + + final String data = im.getData(); + if (TextUtils.isEmpty(data)) { + return new Pair<>(null, null); + } + + final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); + + if (protocol == Im.PROTOCOL_GOOGLE_TALK) { + final int chatCapability = im.getChatCapability(); + if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { + intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); + secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); + } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { + // Allow Talking and Texting + intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); + secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); + } else { + intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); + } + } else { + // Build an IM Intent + intent = getCustomImIntent(im, protocol); + } + return new Pair<>(intent, secondaryIntent); + } + + /** + * Determine UserType from directory id and contact id. + * + *

3 types of query + * + *

1. 2 profile query: content://com.android.contacts/phone_lookup_enterprise/1234567890 + * personal and work contact are mixed into one cursor. no directory id. contact_id indicates if + * it's work contact + * + *

2. work local query: + * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000000 either + * directory_id or contact_id is enough to identify work contact + * + *

3. work remote query: + * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000003 + * contact_id is random. only directory_id is available + * + *

Summary: If directory_id is not null, always use directory_id to identify work contact. + * (which is the case here) Otherwise, use contact_id. + * + * @param directoryId directory id of ContactsProvider query + * @param contactId contact id + * @return UserType indicates the user type of the contact. A directory id or contact id larger + * than a thredshold indicates that the contact is stored in Work Profile, but not in current + * user. It's a contract by ContactsProvider and check by Contacts.isEnterpriseDirectoryId and + * Contacts.isEnterpriseContactId. Currently, only 2 kinds of users can be detected from the + * directoryId and contactId as ContactsProvider can only access current and work user's + * contacts + */ + public static @UserType long determineUserType(Long directoryId, Long contactId) { + // First check directory id + if (directoryId != null) { + return DirectoryCompat.isEnterpriseDirectoryId(directoryId) + ? USER_TYPE_WORK + : USER_TYPE_CURRENT; + } + // Only check contact id if directory id is null + if (contactId != null && contactId != 0L && ContactsCompat.isEnterpriseContactId(contactId)) { + return USER_TYPE_WORK; + } else { + return USER_TYPE_CURRENT; + } + } + + // TODO find a proper place for the canonical version of these + public interface ProviderNames { + + String YAHOO = "Yahoo"; + String GTALK = "GTalk"; + String MSN = "MSN"; + String ICQ = "ICQ"; + String AIM = "AIM"; + String XMPP = "XMPP"; + String JABBER = "JABBER"; + String SKYPE = "SKYPE"; + String QQ = "QQ"; + } + + /** + * UserType indicates the user type of the contact. If the contact is from Work User (Work Profile + * in Android Multi-User System), it's {@link #USER_TYPE_WORK}, otherwise, {@link + * #USER_TYPE_CURRENT}. Please note that current user can be in work profile, where the dialer is + * running inside Work Profile. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({USER_TYPE_CURRENT, USER_TYPE_WORK}) + public @interface UserType {} +} diff --git a/java/com/android/contacts/common/GeoUtil.java b/java/com/android/contacts/common/GeoUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..50b0cd9e37779ed86a894cff040dc0f7d4345521 --- /dev/null +++ b/java/com/android/contacts/common/GeoUtil.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common; + +import android.app.Application; +import android.content.Context; +import com.android.contacts.common.location.CountryDetector; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder; +import java.util.Locale; + +/** Static methods related to Geo. */ +public class GeoUtil { + + /** + * Returns the country code of the country the user is currently in. Before calling this method, + * make sure that {@link CountryDetector#initialize(Context)} has already been called in {@link + * Application#onCreate()}. + * + * @return The ISO 3166-1 two letters country code of the country the user is in. + */ + public static String getCurrentCountryIso(Context context) { + // The {@link CountryDetector} should never return null so this is safe to return as-is. + return CountryDetector.getInstance(context).getCurrentCountryIso(); + } + + public static String getGeocodedLocationFor(Context context, String phoneNumber) { + final PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance(); + final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + try { + final Phonenumber.PhoneNumber structuredPhoneNumber = + phoneNumberUtil.parse(phoneNumber, getCurrentCountryIso(context)); + final Locale locale = context.getResources().getConfiguration().locale; + return geocoder.getDescriptionForNumber(structuredPhoneNumber, locale); + } catch (NumberParseException e) { + return null; + } + } +} diff --git a/java/com/android/contacts/common/GroupMetaData.java b/java/com/android/contacts/common/GroupMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..b34f1d62991e463538b503fb7f587ffd009a43c3 --- /dev/null +++ b/java/com/android/contacts/common/GroupMetaData.java @@ -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 + */ +package com.android.contacts.common; + +/** + * Meta-data for a contact group. We load all groups associated with the contact's constituent + * accounts. + */ +public final class GroupMetaData { + + private String mAccountName; + private String mAccountType; + private String mDataSet; + private long mGroupId; + private String mTitle; + private boolean mDefaultGroup; + private boolean mFavorites; + + public GroupMetaData( + String accountName, + String accountType, + String dataSet, + long groupId, + String title, + boolean defaultGroup, + boolean favorites) { + this.mAccountName = accountName; + this.mAccountType = accountType; + this.mDataSet = dataSet; + this.mGroupId = groupId; + this.mTitle = title; + this.mDefaultGroup = defaultGroup; + this.mFavorites = favorites; + } + + public String getAccountName() { + return mAccountName; + } + + public String getAccountType() { + return mAccountType; + } + + public String getDataSet() { + return mDataSet; + } + + public long getGroupId() { + return mGroupId; + } + + public String getTitle() { + return mTitle; + } + + public boolean isDefaultGroup() { + return mDefaultGroup; + } + + public boolean isFavorites() { + return mFavorites; + } +} diff --git a/java/com/android/contacts/common/MoreContactUtils.java b/java/com/android/contacts/common/MoreContactUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..028f899715446f2fcfb19a010bfbf9374d82bcec --- /dev/null +++ b/java/com/android/contacts/common/MoreContactUtils.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.ContactsContract; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import com.android.contacts.common.model.account.AccountType; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; + +/** Shared static contact utility methods. */ +public class MoreContactUtils { + + private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT); + + /** + * Returns true if two data with mimetypes which represent values in contact entries are + * considered equal for collapsing in the GUI. For caller-id, use {@link + * android.telephony.PhoneNumberUtils#compare(android.content.Context, String, String)} instead + */ + public static boolean shouldCollapse( + CharSequence mimetype1, CharSequence data1, CharSequence mimetype2, CharSequence data2) { + // different mimetypes? don't collapse + if (!TextUtils.equals(mimetype1, mimetype2)) { + return false; + } + + // exact same string? good, bail out early + if (TextUtils.equals(data1, data2)) { + return true; + } + + // so if either is null, these two must be different + if (data1 == null || data2 == null) { + return false; + } + + // if this is not about phone numbers, we know this is not a match (of course, some + // mimetypes could have more sophisticated matching is the future, e.g. addresses) + if (!TextUtils.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, mimetype1)) { + return false; + } + + return shouldCollapsePhoneNumbers(data1.toString(), data2.toString()); + } + + // TODO: Move this to PhoneDataItem.shouldCollapse override + private static boolean shouldCollapsePhoneNumbers(String number1, String number2) { + // Work around to address b/20724444. We want to distinguish between #555, *555 and 555. + // This makes no attempt to distinguish between 555 and 55*5, since 55*5 is an improbable + // number. PhoneNumberUtil already distinguishes between 555 and 55#5. + if (number1.contains("#") != number2.contains("#") + || number1.contains("*") != number2.contains("*")) { + return false; + } + + // Now do the full phone number thing. split into parts, separated by waiting symbol + // and compare them individually + final String[] dataParts1 = number1.split(WAIT_SYMBOL_AS_STRING); + final String[] dataParts2 = number2.split(WAIT_SYMBOL_AS_STRING); + if (dataParts1.length != dataParts2.length) { + return false; + } + final PhoneNumberUtil util = PhoneNumberUtil.getInstance(); + for (int i = 0; i < dataParts1.length; i++) { + // Match phone numbers represented by keypad letters, in which case prefer the + // phone number with letters. + final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]); + final String dataPart2 = dataParts2[i]; + + // substrings equal? shortcut, don't parse + if (TextUtils.equals(dataPart1, dataPart2)) { + continue; + } + + // do a full parse of the numbers + final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2); + switch (result) { + case NOT_A_NUMBER: + // don't understand the numbers? let's play it safe + return false; + case NO_MATCH: + return false; + case EXACT_MATCH: + break; + case NSN_MATCH: + try { + // For NANP phone numbers, match when one has +1 and the other does not. + // In this case, prefer the +1 version. + if (util.parse(dataPart1, null).getCountryCode() == 1) { + // At this point, the numbers can be either case 1 or 2 below.... + // + // case 1) + // +14155551212 <--- country code 1 + // 14155551212 <--- 1 is trunk prefix, not country code + // + // and + // + // case 2) + // +14155551212 + // 4155551212 + // + // From b/7519057, case 2 needs to be equal. But also that bug, case 3 + // below should not be equal. + // + // case 3) + // 14155551212 + // 4155551212 + // + // So in order to make sure transitive equality is valid, case 1 cannot + // be equal. Otherwise, transitive equality breaks and the following + // would all be collapsed: + // 4155551212 | + // 14155551212 |----> +14155551212 + // +14155551212 | + // + // With transitive equality, the collapsed values should be: + // 4155551212 | 14155551212 + // 14155551212 |----> +14155551212 + // +14155551212 | + + // Distinguish between case 1 and 2 by checking for trunk prefix '1' + // at the start of number 2. + if (dataPart2.trim().charAt(0) == '1') { + // case 1 + return false; + } + break; + } + } catch (NumberParseException e) { + // This is the case where the first number does not have a country code. + // examples: + // (123) 456-7890 & 123-456-7890 (collapse) + // 0049 (8092) 1234 & +49/80921234 (unit test says do not collapse) + + // Check the second number. If it also does not have a country code, then + // we should collapse. If it has a country code, then it's a different + // number and we should not collapse (this conclusion is based on an + // existing unit test). + try { + util.parse(dataPart2, null); + } catch (NumberParseException e2) { + // Number 2 also does not have a country. Collapse. + break; + } + } + return false; + case SHORT_NSN_MATCH: + return false; + default: + throw new IllegalStateException("Unknown result value from phone number " + "library"); + } + } + return true; + } + + /** + * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates that + * are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to how the + * target {@link android.graphics.Rect} is calculated in {@link + * android.provider.ContactsContract.QuickContact#showQuickContact}. + */ + public static Rect getTargetRectFromView(View view) { + final int[] pos = new int[2]; + view.getLocationOnScreen(pos); + + final Rect rect = new Rect(); + rect.left = pos[0]; + rect.top = pos[1]; + rect.right = pos[0] + view.getWidth(); + rect.bottom = pos[1] + view.getHeight(); + return rect; + } + + /** + * Returns a header view based on the R.layout.list_separator, where the containing {@link + * android.widget.TextView} is set using the given textResourceId. + */ + public static TextView createHeaderView(Context context, int textResourceId) { + final TextView textView = (TextView) View.inflate(context, R.layout.list_separator, null); + textView.setText(context.getString(textResourceId)); + return textView; + } + + /** + * Set the top padding on the header view dynamically, based on whether the header is in the first + * row or not. + */ + public static void setHeaderViewBottomPadding( + Context context, TextView textView, boolean isFirstRow) { + final int topPadding; + if (isFirstRow) { + topPadding = + (int) + context + .getResources() + .getDimension(R.dimen.frequently_contacted_title_top_margin_when_first_row); + } else { + topPadding = + (int) context.getResources().getDimension(R.dimen.frequently_contacted_title_top_margin); + } + textView.setPaddingRelative( + textView.getPaddingStart(), + topPadding, + textView.getPaddingEnd(), + textView.getPaddingBottom()); + } + + /** + * Returns the intent to launch for the given invitable account type and contact lookup URI. This + * will return null if the account type is not invitable (i.e. there is no {@link + * AccountType#getInviteContactActivityClassName()} or {@link + * AccountType#syncAdapterPackageName}). + */ + public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) { + String syncAdapterPackageName = accountType.syncAdapterPackageName; + String className = accountType.getInviteContactActivityClassName(); + if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) { + return null; + } + Intent intent = new Intent(); + intent.setClassName(syncAdapterPackageName, className); + + intent.setAction(ContactsContract.Intents.INVITE_CONTACT); + + // Data is the lookup URI. + intent.setData(lookupUri); + return intent; + } +} diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindings.java b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java new file mode 100644 index 0000000000000000000000000000000000000000..44be53b3f4dd5b5a903df2f11f78ac8b18c1118f --- /dev/null +++ b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.bindings; + +import android.support.annotation.Nullable; + +/** Allows the container application to customize the contacts common library. */ +public interface ContactsCommonBindings { + + /** Builds a user agent string for the current application. */ + @Nullable + String getUserAgent(); +} diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..8958ad99736d76e7d3c707e1c7fbb82c86b24a34 --- /dev/null +++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.bindings; + +/** + * This interface should be implementated by the Application subclass. It allows the contacts common + * module to get references to the ContactsCommonBindings. + */ +public interface ContactsCommonBindingsFactory { + + ContactsCommonBindings newContactsCommonBindings(); +} diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java new file mode 100644 index 0000000000000000000000000000000000000000..f2e21b18e266559882234d5f35b1beb9bd37773a --- /dev/null +++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.bindings; + +import android.support.annotation.Nullable; + +/** Default implementation for contacts common bindings. */ +public class ContactsCommonBindingsStub implements ContactsCommonBindings { + + @Override + @Nullable + public String getUserAgent() { + return null; + } +} diff --git a/java/com/android/contacts/common/compat/CallCompat.java b/java/com/android/contacts/common/compat/CallCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..641f7b1bd4b0617142a9b7d967fe08eb7c123338 --- /dev/null +++ b/java/com/android/contacts/common/compat/CallCompat.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.telecom.Call; + +/** Compatibility utilities for android.telecom.Call */ +public class CallCompat { + + public static boolean canPullExternalCall(@NonNull android.telecom.Call call) { + return VERSION.SDK_INT >= VERSION_CODES.N_MR1 + && ((call.getDetails().getCallCapabilities() & Details.CAPABILITY_CAN_PULL_CALL) + == Details.CAPABILITY_CAN_PULL_CALL); + } + + /** android.telecom.Call.Details */ + public static class Details { + + public static final int PROPERTY_IS_EXTERNAL_CALL = Call.Details.PROPERTY_IS_EXTERNAL_CALL; + public static final int PROPERTY_ENTERPRISE_CALL = Call.Details.PROPERTY_ENTERPRISE_CALL; + public static final int CAPABILITY_CAN_PULL_CALL = Call.Details.CAPABILITY_CAN_PULL_CALL; + public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = + Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO; + + public static final String EXTRA_ANSWERING_DROPS_FOREGROUND_CALL = + "android.telecom.extra.ANSWERING_DROPS_FG_CALL"; + } +} diff --git a/java/com/android/contacts/common/compat/CallableCompat.java b/java/com/android/contacts/common/compat/CallableCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..5e86f518e054f330c42155fde5795718ce1c19ef --- /dev/null +++ b/java/com/android/contacts/common/compat/CallableCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract.CommonDataKinds.Callable; + +public class CallableCompat { + + // TODO: Use N APIs + private static final Uri ENTERPRISE_CONTENT_FILTER_URI = + Uri.withAppendedPath(Callable.CONTENT_URI, "filter_enterprise"); + + public static Uri getContentFilterUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return ENTERPRISE_CONTENT_FILTER_URI; + } + return Callable.CONTENT_FILTER_URI; + } +} diff --git a/java/com/android/contacts/common/compat/ContactsCompat.java b/java/com/android/contacts/common/compat/ContactsCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..39d0b55d376d1cc349b18540264b204c93613a21 --- /dev/null +++ b/java/com/android/contacts/common/compat/ContactsCompat.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import com.android.dialer.compat.CompatUtils; + +/** Compatibility class for {@link ContactsContract.Contacts} */ +public class ContactsCompat { + + // TODO: Use N APIs + private static final Uri ENTERPRISE_CONTENT_FILTER_URI = + Uri.withAppendedPath(Contacts.CONTENT_URI, "filter_enterprise"); + // Copied from ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE, which is hidden. + private static final long ENTERPRISE_CONTACT_ID_BASE = 1000000000; + + /** Not instantiable. */ + private ContactsCompat() {} + + public static Uri getContentUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return ENTERPRISE_CONTENT_FILTER_URI; + } + return Contacts.CONTENT_FILTER_URI; + } + + /** + * Return {@code true} if a contact ID is from the contacts provider on the enterprise profile. + */ + public static boolean isEnterpriseContactId(long contactId) { + if (CompatUtils.isLollipopCompatible()) { + return Contacts.isEnterpriseContactId(contactId); + } else { + // copied from ContactsContract.Contacts.isEnterpriseContactId + return (contactId >= ENTERPRISE_CONTACT_ID_BASE) + && (contactId < ContactsContract.Profile.MIN_ID); + } + } +} diff --git a/java/com/android/contacts/common/compat/DirectoryCompat.java b/java/com/android/contacts/common/compat/DirectoryCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..85f4a4202e1f8d1b62858b2194d7c90ac6cd465f --- /dev/null +++ b/java/com/android/contacts/common/compat/DirectoryCompat.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract.Directory; + +public class DirectoryCompat { + + public static Uri getContentUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return Directory.ENTERPRISE_CONTENT_URI; + } + return Directory.CONTENT_URI; + } + + public static boolean isInvisibleDirectory(long directoryId) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return (directoryId == Directory.LOCAL_INVISIBLE + || directoryId == Directory.ENTERPRISE_LOCAL_INVISIBLE); + } + return directoryId == Directory.LOCAL_INVISIBLE; + } + + public static boolean isRemoteDirectoryId(long directoryId) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return Directory.isRemoteDirectoryId(directoryId); + } + return !(directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE); + } + + public static boolean isEnterpriseDirectoryId(long directoryId) { + return VERSION.SDK_INT >= VERSION_CODES.N && Directory.isEnterpriseDirectoryId(directoryId); + } +} diff --git a/java/com/android/contacts/common/compat/PhoneAccountCompat.java b/java/com/android/contacts/common/compat/PhoneAccountCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..6a24ec033530efdc9eb2d848340aca08f9291347 --- /dev/null +++ b/java/com/android/contacts/common/compat/PhoneAccountCompat.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.contacts.common.compat; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.util.Log; +import com.android.dialer.compat.CompatUtils; + +/** Compatiblity class for {@link android.telecom.PhoneAccount} */ +public class PhoneAccountCompat { + + private static final String TAG = PhoneAccountCompat.class.getSimpleName(); + + /** + * Gets the {@link Icon} associated with the given {@link PhoneAccount} + * + * @param phoneAccount the PhoneAccount from which to retrieve the Icon + * @return the Icon, or null + */ + @Nullable + public static Icon getIcon(@Nullable PhoneAccount phoneAccount) { + if (phoneAccount == null) { + return null; + } + + if (CompatUtils.isMarshmallowCompatible()) { + return phoneAccount.getIcon(); + } + + return null; + } + + /** + * Builds and returns an icon {@code Drawable} to represent this {@code PhoneAccount} in a user + * interface. + * + * @param phoneAccount the PhoneAccount from which to build the icon. + * @param context A {@code Context} to use for loading Drawables. + * @return An icon for this PhoneAccount, or null + */ + @Nullable + public static Drawable createIconDrawable( + @Nullable PhoneAccount phoneAccount, @Nullable Context context) { + if (phoneAccount == null || context == null) { + return null; + } + + if (CompatUtils.isMarshmallowCompatible()) { + return createIconDrawableMarshmallow(phoneAccount, context); + } + + if (CompatUtils.isLollipopMr1Compatible()) { + return createIconDrawableLollipopMr1(phoneAccount, context); + } + return null; + } + + @Nullable + private static Drawable createIconDrawableMarshmallow( + PhoneAccount phoneAccount, Context context) { + Icon accountIcon = getIcon(phoneAccount); + if (accountIcon == null) { + return null; + } + return accountIcon.loadDrawable(context); + } + + @Nullable + private static Drawable createIconDrawableLollipopMr1( + PhoneAccount phoneAccount, Context context) { + try { + return (Drawable) + PhoneAccount.class + .getMethod("createIconDrawable", Context.class) + .invoke(phoneAccount, context); + } catch (ReflectiveOperationException e) { + return null; + } catch (Throwable t) { + Log.e( + TAG, + "Unexpected exception when attempting to call " + + "android.telecom.PhoneAccount#createIconDrawable", + t); + return null; + } + } +} diff --git a/java/com/android/contacts/common/compat/PhoneCompat.java b/java/com/android/contacts/common/compat/PhoneCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..31db7b5375fffe0571aeda65621fa16c85103b15 --- /dev/null +++ b/java/com/android/contacts/common/compat/PhoneCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract.CommonDataKinds.Phone; + +public class PhoneCompat { + + // TODO: Use N APIs + private static final Uri ENTERPRISE_CONTENT_FILTER_URI = + Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise"); + + public static Uri getContentFilterUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return ENTERPRISE_CONTENT_FILTER_URI; + } + return Phone.CONTENT_FILTER_URI; + } +} diff --git a/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..960b340d8ad01aa8babcaa2fad8d18f532890f0c --- /dev/null +++ b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.telephony.PhoneNumberUtils; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.style.TtsSpan; +import com.android.dialer.compat.CompatUtils; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; + +/** + * This class contains static utility methods extracted from PhoneNumberUtils, and the methods were + * added in API level 23. In this way, we could enable the corresponding functionality for pre-M + * devices. We need maintain this class and keep it synced with PhoneNumberUtils. Another thing to + * keep in mind is that we use com.google.i18n rather than com.android.i18n in here, so we need make + * sure the application behavior is preserved. + */ +public class PhoneNumberUtilsCompat { + + /** Not instantiable. */ + private PhoneNumberUtilsCompat() {} + + public static String normalizeNumber(String phoneNumber) { + if (CompatUtils.isLollipopCompatible()) { + return PhoneNumberUtils.normalizeNumber(phoneNumber); + } else { + return normalizeNumberInternal(phoneNumber); + } + } + + /** Implementation copied from {@link PhoneNumberUtils#normalizeNumber} */ + private static String normalizeNumberInternal(String phoneNumber) { + if (TextUtils.isEmpty(phoneNumber)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + int len = phoneNumber.length(); + for (int i = 0; i < len; i++) { + char c = phoneNumber.charAt(i); + // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) + int digit = Character.digit(c, 10); + if (digit != -1) { + sb.append(digit); + } else if (sb.length() == 0 && c == '+') { + sb.append(c); + } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber)); + } + } + return sb.toString(); + } + + public static String formatNumber( + String phoneNumber, String phoneNumberE164, String defaultCountryIso) { + if (CompatUtils.isLollipopCompatible()) { + return PhoneNumberUtils.formatNumber(phoneNumber, phoneNumberE164, defaultCountryIso); + } else { + // This method was deprecated in API level 21, so it's only used on pre-L SDKs. + return PhoneNumberUtils.formatNumber(phoneNumber); + } + } + + public static CharSequence createTtsSpannable(CharSequence phoneNumber) { + if (CompatUtils.isMarshmallowCompatible()) { + return PhoneNumberUtils.createTtsSpannable(phoneNumber); + } else { + return createTtsSpannableInternal(phoneNumber); + } + } + + public static TtsSpan createTtsSpan(String phoneNumber) { + if (CompatUtils.isMarshmallowCompatible()) { + return PhoneNumberUtils.createTtsSpan(phoneNumber); + } else if (CompatUtils.isLollipopCompatible()) { + return createTtsSpanLollipop(phoneNumber); + } else { + return null; + } + } + + /** Copied from {@link PhoneNumberUtils#createTtsSpannable} */ + private static CharSequence createTtsSpannableInternal(CharSequence phoneNumber) { + if (phoneNumber == null) { + return null; + } + Spannable spannable = Spannable.Factory.getInstance().newSpannable(phoneNumber); + addTtsSpanInternal(spannable, 0, spannable.length()); + return spannable; + } + + /** Compat method for addTtsSpan, see {@link PhoneNumberUtils#addTtsSpan} */ + public static void addTtsSpan(Spannable s, int start, int endExclusive) { + if (CompatUtils.isMarshmallowCompatible()) { + PhoneNumberUtils.addTtsSpan(s, start, endExclusive); + } else { + addTtsSpanInternal(s, start, endExclusive); + } + } + + /** Copied from {@link PhoneNumberUtils#addTtsSpan} */ + private static void addTtsSpanInternal(Spannable s, int start, int endExclusive) { + s.setSpan( + createTtsSpan(s.subSequence(start, endExclusive).toString()), + start, + endExclusive, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + /** Copied from {@link PhoneNumberUtils#createTtsSpan} */ + private static TtsSpan createTtsSpanLollipop(String phoneNumberString) { + if (phoneNumberString == null) { + return null; + } + + // Parse the phone number + final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + PhoneNumber phoneNumber = null; + try { + // Don't supply a defaultRegion so this fails for non-international numbers because + // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already + // present + phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null); + } catch (NumberParseException ignored) { + } + + // Build a telephone tts span + final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder(); + if (phoneNumber == null) { + // Strip separators otherwise TalkBack will be silent + // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel) + builder.setNumberParts(splitAtNonNumerics(phoneNumberString)); + } else { + if (phoneNumber.hasCountryCode()) { + builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode())); + } + builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber())); + } + return builder.build(); + } + + /** + * Split a phone number using spaces, ignoring anything that is not a digit + * + * @param number A {@code CharSequence} before splitting, e.g., "+20(123)-456#" + * @return A {@code String} after splitting, e.g., "20 123 456". + */ + private static String splitAtNonNumerics(CharSequence number) { + StringBuilder sb = new StringBuilder(number.length()); + for (int i = 0; i < number.length(); i++) { + sb.append(PhoneNumberUtils.isISODigit(number.charAt(i)) ? number.charAt(i) : " "); + } + // It is very important to remove extra spaces. At time of writing, any leading or trailing + // spaces, or any sequence of more than one space, will confuse TalkBack and cause the TTS + // span to be non-functional! + return sb.toString().replaceAll(" +", " ").trim(); + } +} diff --git a/java/com/android/contacts/common/compat/TelephonyManagerCompat.java b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..c8665af51ac5e28ea789d7da15f59507983ab1b1 --- /dev/null +++ b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import java.lang.reflect.InvocationTargetException; + +public class TelephonyManagerCompat { + + // TODO: Use public API for these constants when available + public static final String EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE = + "android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE"; + public static final String EVENT_HANDOVER_TO_WIFI_FAILED = + "android.telephony.event.EVENT_HANDOVER_TO_WIFI_FAILED"; + public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD"; + public static final String EVENT_CALL_REMOTELY_UNHELD = + "android.telecom.event.CALL_REMOTELY_UNHELD"; + + public static final String TELEPHONY_MANAGER_CLASS = "android.telephony.TelephonyManager"; + + /** + * @param telephonyManager The telephony manager instance to use for method calls. + * @return true if the current device is "voice capable". + *

"Voice capable" means that this device supports circuit-switched (i.e. voice) phone + * calls over the telephony network, and is allowed to display the in-call UI while a cellular + * voice call is active. This will be false on "data only" devices which can't make voice + * calls and don't support any in-call UI. + *

Note: the meaning of this flag is subtly different from the + * PackageManager.FEATURE_TELEPHONY system feature, which is available on any device with a + * telephony radio, even if the device is data-only. + */ + public static boolean isVoiceCapable(@Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return false; + } + if (CompatUtils.isLollipopMr1Compatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isVoiceCapable")) { + // isVoiceCapable was unhidden in L-MR1 + return telephonyManager.isVoiceCapable(); + } + final int phoneType = telephonyManager.getPhoneType(); + return phoneType == TelephonyManager.PHONE_TYPE_CDMA + || phoneType == TelephonyManager.PHONE_TYPE_GSM; + } + + /** + * Returns the number of phones available. Returns 1 for Single standby mode (Single SIM + * functionality) Returns 2 for Dual standby mode.(Dual SIM functionality) + * + *

Returns 1 if the method or telephonyManager is not available. + * + * @param telephonyManager The telephony manager instance to use for method calls. + */ + public static int getPhoneCount(@Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return 1; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getPhoneCount")) { + return telephonyManager.getPhoneCount(); + } + return 1; + } + + /** + * Returns the unique device ID of a subscription, for example, the IMEI for GSM and the MEID for + * CDMA phones. Return null if device ID is not available. + * + *

Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @param slotId of which deviceID is returned + */ + public static String getDeviceId(@Nullable TelephonyManager telephonyManager, int slotId) { + if (telephonyManager == null) { + return null; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getDeviceId", Integer.class)) { + return telephonyManager.getDeviceId(slotId); + } + return null; + } + + /** + * Whether the phone supports TTY mode. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @return {@code true} if the device supports TTY mode, and {@code false} otherwise. + */ + public static boolean isTtyModeSupported(@Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return false; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isTtyModeSupported")) { + return telephonyManager.isTtyModeSupported(); + } + return false; + } + + /** + * Whether the phone supports hearing aid compatibility. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @return {@code true} if the device supports hearing aid compatibility, and {@code false} + * otherwise. + */ + public static boolean isHearingAidCompatibilitySupported( + @Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return false; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELEPHONY_MANAGER_CLASS, "isHearingAidCompatibilitySupported")) { + return telephonyManager.isHearingAidCompatibilitySupported(); + } + return false; + } + + /** + * Returns the URI for the per-account voicemail ringtone set in Phone settings. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to + * retrieve the voicemail ringtone. + * @return The URI for the ringtone to play when receiving a voicemail from a specific + * PhoneAccount. + */ + @Nullable + public static Uri getVoicemailRingtoneUri( + TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) { + if (VERSION.SDK_INT < VERSION_CODES.N) { + return null; + } + return telephonyManager.getVoicemailRingtoneUri(accountHandle); + } + + /** + * Returns whether vibration is set for voicemail notification in Phone settings. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to + * retrieve the voicemail vibration setting. + * @return {@code true} if the vibration is set for this PhoneAccount, {@code false} otherwise. + */ + public static boolean isVoicemailVibrationEnabled( + TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) { + return VERSION.SDK_INT < VERSION_CODES.N + || telephonyManager.isVoicemailVibrationEnabled(accountHandle); + } + + /** + * This method uses a new system API to enable or disable visual voicemail. TODO: restrict + * to N MR1, not needed in future SDK. + */ + public static void setVisualVoicemailEnabled( + TelephonyManager telephonyManager, PhoneAccountHandle handle, boolean enabled) { + if (VERSION.SDK_INT < VERSION_CODES.N_MR1) { + Assert.fail("setVisualVoicemailEnabled called on pre-NMR1"); + } + try { + TelephonyManager.class + .getMethod("setVisualVoicemailEnabled", PhoneAccountHandle.class, boolean.class) + .invoke(telephonyManager, handle, enabled); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e); + } + } + + /** + * This method uses a new system API to check if visual voicemail is enabled TODO: restrict + * to N MR1, not needed in future SDK. + */ + public static boolean isVisualVoicemailEnabled( + TelephonyManager telephonyManager, PhoneAccountHandle handle) { + if (VERSION.SDK_INT < VERSION_CODES.N_MR1) { + Assert.fail("isVisualVoicemailEnabled called on pre-NMR1"); + } + try { + return (boolean) + TelephonyManager.class + .getMethod("isVisualVoicemailEnabled", PhoneAccountHandle.class) + .invoke(telephonyManager, handle); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e); + } + return false; + } +} diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..5687f6fbf06d53b7d6c83027d146d892f005379e --- /dev/null +++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.compat.telecom; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.dialer.compat.CompatUtils; +import java.util.ArrayList; +import java.util.List; + +/** Compatibility class for {@link android.telecom.TelecomManager}. */ +public class TelecomManagerCompat { + + public static final String TELECOM_MANAGER_CLASS = "android.telecom.TelecomManager"; + + // TODO: remove once this is available in android.telecom.Call + // b/33779976 + public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = + "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; + + /** + * Places a new outgoing call to the provided address using the system telecom service with the + * specified intent. + * + * @param activity {@link Activity} used to start another activity for the given intent + * @param telecomManager the {@link TelecomManager} used to place a call, if possible + * @param intent the intent for the call + */ + public static void placeCall( + @Nullable Activity activity, + @Nullable TelecomManager telecomManager, + @Nullable Intent intent) { + if (activity == null || telecomManager == null || intent == null) { + return; + } + if (CompatUtils.isMarshmallowCompatible()) { + telecomManager.placeCall(intent.getData(), intent.getExtras()); + return; + } + activity.startActivityForResult(intent, 0); + } + + /** + * Get the URI for running an adn query. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @param accountHandle The handle for the account to derive an adn query URI for or {@code null} + * to return a URI which will use the default account. + * @return The URI (with the content:// scheme) specific to the specified {@link PhoneAccount} for + * the the content retrieve. + */ + public static Uri getAdnUriForPhoneAccount( + @Nullable TelecomManager telecomManager, PhoneAccountHandle accountHandle) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getAdnUriForPhoneAccount", PhoneAccountHandle.class))) { + return telecomManager.getAdnUriForPhoneAccount(accountHandle); + } + return Uri.parse("content://icc/adn"); + } + + /** + * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone + * calls. The returned list includes only those accounts which have been explicitly enabled by the + * user. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @return A list of PhoneAccountHandle objects. + */ + public static List getCallCapablePhoneAccounts( + @Nullable TelecomManager telecomManager) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getCallCapablePhoneAccounts"))) { + return telecomManager.getCallCapablePhoneAccounts(); + } + return new ArrayList<>(); + } + + /** + * Used to determine the currently selected default dialer package. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @return package name for the default dialer package or null if no package has been selected as + * the default dialer. + */ + @Nullable + public static String getDefaultDialerPackage(@Nullable TelecomManager telecomManager) { + if (telecomManager != null && CompatUtils.isDefaultDialerCompatible()) { + return telecomManager.getDefaultDialerPackage(); + } + return null; + } + + /** + * Return the {@link PhoneAccount} which will be used to place outgoing calls to addresses with + * the specified {@code uriScheme}. This PhoneAccount will always be a member of the list which is + * returned from invoking {@link TelecomManager#getCallCapablePhoneAccounts()}. The specific + * account returned depends on the following priorities: + * + *

1. If the user-selected default PhoneAccount supports the specified scheme, it will be + * returned. 2. If there exists only one PhoneAccount that supports the specified scheme, it will + * be returned. + * + *

If no PhoneAccount fits the criteria above, this method will return {@code null}. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @param uriScheme The URI scheme. + * @return The {@link PhoneAccountHandle} corresponding to the account to be used. + */ + @Nullable + public static PhoneAccountHandle getDefaultOutgoingPhoneAccount( + @Nullable TelecomManager telecomManager, @Nullable String uriScheme) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getDefaultOutgoingPhoneAccount", String.class))) { + return telecomManager.getDefaultOutgoingPhoneAccount(uriScheme); + } + return null; + } + + /** + * Return the line 1 phone number for given phone account. + * + * @param telecomManager the {@link TelecomManager} to use in the event that {@link + * TelecomManager#getLine1Number(PhoneAccountHandle)} is available + * @param telephonyManager the {@link TelephonyManager} to use if TelecomManager#getLine1Number is + * unavailable + * @param phoneAccountHandle the phoneAccountHandle upon which to check the line one number + * @return the line one number + */ + @Nullable + public static String getLine1Number( + @Nullable TelecomManager telecomManager, + @Nullable TelephonyManager telephonyManager, + @Nullable PhoneAccountHandle phoneAccountHandle) { + if (telecomManager != null && CompatUtils.isMarshmallowCompatible()) { + return telecomManager.getLine1Number(phoneAccountHandle); + } + if (telephonyManager != null) { + return telephonyManager.getLine1Number(); + } + return null; + } + + /** + * Return whether a given phone number is the configured voicemail number for a particular phone + * account. + * + * @param telecomManager the {@link TelecomManager} to use for checking the number. + * @param accountHandle The handle for the account to check the voicemail number against + * @param number The number to look up. + */ + public static boolean isVoiceMailNumber( + @Nullable TelecomManager telecomManager, + @Nullable PhoneAccountHandle accountHandle, + @Nullable String number) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, + "isVoiceMailNumber", + PhoneAccountHandle.class, + String.class))) { + return telecomManager.isVoiceMailNumber(accountHandle, number); + } + return PhoneNumberUtils.isVoiceMailNumber(number); + } + + /** + * Return the {@link PhoneAccount} for a specified {@link PhoneAccountHandle}. Object includes + * resources which can be used in a user interface. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @param account The {@link PhoneAccountHandle}. + * @return The {@link PhoneAccount} object or null if it doesn't exist. + */ + @Nullable + public static PhoneAccount getPhoneAccount( + @Nullable TelecomManager telecomManager, @Nullable PhoneAccountHandle accountHandle) { + if (telecomManager != null + && (CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getPhoneAccount", PhoneAccountHandle.class))) { + return telecomManager.getPhoneAccount(accountHandle); + } + return null; + } + + /** + * Return the voicemail number for a given phone account. + * + * @param telecomManager The {@link TelecomManager} object to use for retrieving the voicemail + * number if accountHandle is specified. + * @param telephonyManager The {@link TelephonyManager} object to use for retrieving the voicemail + * number if accountHandle is null. + * @param accountHandle The handle for the phone account. + * @return The voicemail number for the phone account, and {@code null} if one has not been + * configured. + */ + @Nullable + public static String getVoiceMailNumber( + @Nullable TelecomManager telecomManager, + @Nullable TelephonyManager telephonyManager, + @Nullable PhoneAccountHandle accountHandle) { + if (telecomManager != null + && (CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getVoiceMailNumber", PhoneAccountHandle.class))) { + return telecomManager.getVoiceMailNumber(accountHandle); + } else if (telephonyManager != null) { + return telephonyManager.getVoiceMailNumber(); + } + return null; + } + + /** + * Processes the specified dial string as an MMI code. MMI codes are any sequence of characters + * entered into the dialpad that contain a "*" or "#". Some of these sequences launch special + * behavior through handled by Telephony. + * + * @param telecomManager The {@link TelecomManager} object to use for handling MMI. + * @param dialString The digits to dial. + * @return {@code true} if the digits were processed as an MMI code, {@code false} otherwise. + */ + public static boolean handleMmi( + @Nullable TelecomManager telecomManager, + @Nullable String dialString, + @Nullable PhoneAccountHandle accountHandle) { + if (telecomManager == null || TextUtils.isEmpty(dialString)) { + return false; + } + if (CompatUtils.isMarshmallowCompatible()) { + return telecomManager.handleMmi(dialString, accountHandle); + } + + Object handleMmiResult = + CompatUtils.invokeMethod( + telecomManager, + "handleMmi", + new Class[] {PhoneAccountHandle.class, String.class}, + new Object[] {accountHandle, dialString}); + if (handleMmiResult != null) { + return (boolean) handleMmiResult; + } + + return telecomManager.handleMmi(dialString); + } + + /** + * Silences the ringer if a ringing call exists. Noop if {@link TelecomManager#silenceRinger()} is + * unavailable. + * + * @param telecomManager the TelecomManager to use to silence the ringer. + */ + public static void silenceRinger(@Nullable TelecomManager telecomManager) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "silenceRinger"))) { + telecomManager.silenceRinger(); + } + } + + /** + * Returns the current SIM call manager. Apps must be prepared for this method to return null, + * indicating that there currently exists no registered SIM call manager. + * + * @param telecomManager the {@link TelecomManager} to use to fetch the SIM call manager. + * @return The phone account handle of the current sim call manager. + */ + @Nullable + public static PhoneAccountHandle getSimCallManager(TelecomManager telecomManager) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "getSimCallManager"))) { + return telecomManager.getSimCallManager(); + } + return null; + } +} diff --git a/java/com/android/contacts/common/database/ContactUpdateUtils.java b/java/com/android/contacts/common/database/ContactUpdateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1a9febc07e246a16c23ec92d85aad4d23a077ed7 --- /dev/null +++ b/java/com/android/contacts/common/database/ContactUpdateUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.database; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract; +import android.util.Log; + +/** Static methods to update contact information. */ +public class ContactUpdateUtils { + + private static final String TAG = ContactUpdateUtils.class.getSimpleName(); + + public static void setSuperPrimary(Context context, long dataId) { + if (dataId == -1) { + Log.e(TAG, "Invalid arguments for setSuperPrimary request"); + return; + } + + // Update the primary values in the data record. + ContentValues values = new ContentValues(2); + values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); + values.put(ContactsContract.Data.IS_PRIMARY, 1); + + context + .getContentResolver() + .update( + ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId), + values, + null, + null); + } +} diff --git a/java/com/android/contacts/common/database/EmptyCursor.java b/java/com/android/contacts/common/database/EmptyCursor.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b24cdf7eaca3c4b1cf94ba20a6b44accfb5b2b --- /dev/null +++ b/java/com/android/contacts/common/database/EmptyCursor.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.database; + +import android.database.AbstractCursor; +import android.database.CursorIndexOutOfBoundsException; + +/** + * A cursor that is empty. + * + *

If you want an empty cursor, this class is better than a MatrixCursor because it has less + * overhead. + */ +public final class EmptyCursor extends AbstractCursor { + + private String[] mColumns; + + public EmptyCursor(String[] columns) { + this.mColumns = columns; + } + + @Override + public int getCount() { + return 0; + } + + @Override + public String[] getColumnNames() { + return mColumns; + } + + @Override + public String getString(int column) { + throw cursorException(); + } + + @Override + public short getShort(int column) { + throw cursorException(); + } + + @Override + public int getInt(int column) { + throw cursorException(); + } + + @Override + public long getLong(int column) { + throw cursorException(); + } + + @Override + public float getFloat(int column) { + throw cursorException(); + } + + @Override + public double getDouble(int column) { + throw cursorException(); + } + + @Override + public boolean isNull(int column) { + throw cursorException(); + } + + private CursorIndexOutOfBoundsException cursorException() { + return new CursorIndexOutOfBoundsException("Operation not permitted on an empty cursor."); + } +} diff --git a/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..d5e61354a3fcf364283cdcddbab50b0fc8a5b2eb --- /dev/null +++ b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.database; + +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +/** + * An {@AsyncQueryHandler} that will never return a null cursor. + * + *

Instead, will return a {@link Cursor} with 0 records. + */ +public abstract class NoNullCursorAsyncQueryHandler extends AsyncQueryHandler { + + public NoNullCursorAsyncQueryHandler(ContentResolver cr) { + super(cr); + } + + @Override + public void startQuery( + int token, + Object cookie, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String orderBy) { + final CookieWithProjection projectionCookie = new CookieWithProjection(cookie, projection); + super.startQuery(token, projectionCookie, uri, projection, selection, selectionArgs, orderBy); + } + + @Override + protected final void onQueryComplete(int token, Object cookie, Cursor cursor) { + CookieWithProjection projectionCookie = (CookieWithProjection) cookie; + + super.onQueryComplete(token, projectionCookie.originalCookie, cursor); + + if (cursor == null) { + cursor = new EmptyCursor(projectionCookie.projection); + } + onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor); + } + + protected abstract void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor); + + /** Class to add projection to an existing cookie. */ + private static class CookieWithProjection { + + public final Object originalCookie; + public final String[] projection; + + public CookieWithProjection(Object cookie, String[] projection) { + this.originalCookie = cookie; + this.projection = projection; + } + } +} diff --git a/java/com/android/contacts/common/dialog/CallSubjectDialog.java b/java/com/android/contacts/common/dialog/CallSubjectDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..d2e3a23573c69085d0be5843df0302d145286cf7 --- /dev/null +++ b/java/com/android/contacts/common/dialog/CallSubjectDialog.java @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.dialog; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.telecom.TelecomManagerCompat; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.ViewUtil; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements a dialog which prompts for a call subject for an outgoing call. The dialog includes a + * pop up list of historical call subjects. + */ +public class CallSubjectDialog extends Activity { + + public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count"; + public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item"; + /** Activity intent argument bundle keys: */ + public static final String ARG_PHOTO_ID = "PHOTO_ID"; + + public static final String ARG_PHOTO_URI = "PHOTO_URI"; + public static final String ARG_CONTACT_URI = "CONTACT_URI"; + public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER"; + public static final String ARG_IS_BUSINESS = "IS_BUSINESS"; + public static final String ARG_NUMBER = "NUMBER"; + public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER"; + public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL"; + public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE"; + private static final int CALL_SUBJECT_LIMIT = 16; + private static final int CALL_SUBJECT_HISTORY_SIZE = 5; + private int mAnimationDuration; + private Charset mMessageEncoding; + private View mBackgroundView; + private View mDialogView; + private QuickContactBadge mContactPhoto; + private TextView mNameView; + private TextView mNumberView; + private EditText mCallSubjectView; + private TextView mCharacterLimitView; + private View mHistoryButton; + private View mSendAndCallButton; + private ListView mSubjectList; + + private int mLimit = CALL_SUBJECT_LIMIT; + /** Handles changes to the text in the subject box. Ensures the character limit is updated. */ + private final TextWatcher mTextWatcher = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // no-op + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + updateCharacterLimit(); + } + + @Override + public void afterTextChanged(Editable s) { + // no-op + } + }; + + private int mPhotoSize; + private SharedPreferences mPrefs; + private List mSubjectHistory; + /** Handles displaying the list of past call subjects. */ + private final View.OnClickListener mHistoryOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView); + showCallHistory(mSubjectList.getVisibility() == View.GONE); + } + }; + /** + * Handles auto-hiding the call history when user clicks in the call subject field to give it + * focus. + */ + private final View.OnClickListener mCallSubjectClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mSubjectList.getVisibility() == View.VISIBLE) { + showCallHistory(false); + } + } + }; + + private long mPhotoID; + private Uri mPhotoUri; + private Uri mContactUri; + private String mNameOrNumber; + private boolean mIsBusiness; + private String mNumber; + private String mDisplayNumber; + private String mNumberLabel; + private PhoneAccountHandle mPhoneAccountHandle; + /** Handles starting a call with a call subject specified. */ + private final View.OnClickListener mSendAndCallOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + String subject = mCallSubjectView.getText().toString(); + Intent intent = + new CallIntentBuilder(mNumber, CallInitiationType.Type.CALL_SUBJECT_DIALOG) + .setPhoneAccountHandle(mPhoneAccountHandle) + .setCallSubject(subject) + .build(); + + TelecomManagerCompat.placeCall( + CallSubjectDialog.this, + (TelecomManager) getSystemService(Context.TELECOM_SERVICE), + intent); + + mSubjectHistory.add(subject); + saveSubjectHistory(mSubjectHistory); + finish(); + } + }; + /** Click listener which handles user clicks outside of the dialog. */ + private View.OnClickListener mBackgroundListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }; + /** + * Item click listener which handles user clicks on the items in the list view. Dismisses the + * activity, returning the subject to the caller and closing the activity with the {@link + * Activity#RESULT_OK} result code. + */ + private AdapterView.OnItemClickListener mItemClickListener = + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView arg0, View view, int position, long arg3) { + mCallSubjectView.setText(mSubjectHistory.get(position)); + showCallHistory(false); + } + }; + + /** + * Show the call subject dialog given a phone number to dial (e.g. from the dialpad). + * + * @param activity The activity. + * @param number The number to dial. + */ + public static void start(Activity activity, String number) { + start( + activity, + -1 /* photoId */, + null /* photoUri */, + null /* contactUri */, + number /* nameOrNumber */, + false /* isBusiness */, + number /* number */, + null /* displayNumber */, + null /* numberLabel */, + null /* phoneAccountHandle */); + } + + /** + * Creates a call subject dialog. + * + * @param activity The current activity. + * @param photoId The photo ID (used to populate contact photo). + * @param photoUri The photo Uri (used to populate contact photo). + * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo). + * @param nameOrNumber The name or number of the callee. + * @param isBusiness {@code true} if a business is being called (used for contact photo). + * @param number The raw number to dial. + * @param displayNumber The number to dial, formatted for display. + * @param numberLabel The label for the number (if from a contact). + * @param phoneAccountHandle The phone account handle. + */ + public static void start( + Activity activity, + long photoId, + Uri photoUri, + Uri contactUri, + String nameOrNumber, + boolean isBusiness, + String number, + String displayNumber, + String numberLabel, + PhoneAccountHandle phoneAccountHandle) { + Bundle arguments = new Bundle(); + arguments.putLong(ARG_PHOTO_ID, photoId); + arguments.putParcelable(ARG_PHOTO_URI, photoUri); + arguments.putParcelable(ARG_CONTACT_URI, contactUri); + arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber); + arguments.putBoolean(ARG_IS_BUSINESS, isBusiness); + arguments.putString(ARG_NUMBER, number); + arguments.putString(ARG_DISPLAY_NUMBER, displayNumber); + arguments.putString(ARG_NUMBER_LABEL, numberLabel); + arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); + start(activity, arguments); + } + + /** + * Shows the call subject dialog given a Bundle containing all the arguments required to display + * the dialog (e.g. from Quick Contacts). + * + * @param activity The activity. + * @param arguments The arguments bundle. + */ + public static void start(Activity activity, Bundle arguments) { + Intent intent = new Intent(activity, CallSubjectDialog.class); + intent.putExtras(arguments); + activity.startActivity(intent); + } + + /** + * Loads the subject history from shared preferences. + * + * @param prefs Shared preferences. + * @return List of subject history strings. + */ + public static List loadSubjectHistory(SharedPreferences prefs) { + int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0); + List subjects = new ArrayList(historySize); + + for (int ix = 0; ix < historySize; ix++) { + String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null); + if (!TextUtils.isEmpty(historyItem)) { + subjects.add(historyItem); + } + } + + return subjects; + } + + /** + * Creates the dialog, inflating the layout and populating it with the name and phone number. + * + * @param savedInstanceState The last saved instance state of the Fragment, or null if this is a + * freshly created Fragment. + * @return Dialog instance. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mPhotoSize = + getResources().getDimensionPixelSize(R.dimen.call_subject_dialog_contact_photo_size); + readArguments(); + loadConfiguration(); + mSubjectHistory = loadSubjectHistory(mPrefs); + + setContentView(R.layout.dialog_call_subject); + getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + mBackgroundView = findViewById(R.id.call_subject_dialog); + mBackgroundView.setOnClickListener(mBackgroundListener); + mDialogView = findViewById(R.id.dialog_view); + mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo); + mNameView = (TextView) findViewById(R.id.name); + mNumberView = (TextView) findViewById(R.id.number); + mCallSubjectView = (EditText) findViewById(R.id.call_subject); + mCallSubjectView.addTextChangedListener(mTextWatcher); + mCallSubjectView.setOnClickListener(mCallSubjectClickListener); + InputFilter[] filters = new InputFilter[1]; + filters[0] = new InputFilter.LengthFilter(mLimit); + mCallSubjectView.setFilters(filters); + mCharacterLimitView = (TextView) findViewById(R.id.character_limit); + mHistoryButton = findViewById(R.id.history_button); + mHistoryButton.setOnClickListener(mHistoryOnClickListener); + mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE); + mSendAndCallButton = findViewById(R.id.send_and_call_button); + mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener); + mSubjectList = (ListView) findViewById(R.id.subject_list); + mSubjectList.setOnItemClickListener(mItemClickListener); + mSubjectList.setVisibility(View.GONE); + + updateContactInfo(); + updateCharacterLimit(); + } + + /** Populates the contact info fields based on the current contact information. */ + private void updateContactInfo() { + if (mContactUri != null) { + setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness); + } else { + mContactPhoto.setVisibility(View.GONE); + } + mNameView.setText(mNameOrNumber); + if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) { + mNumberView.setVisibility(View.VISIBLE); + mNumberView.setText( + getString(R.string.call_subject_type_and_number, mNumberLabel, mDisplayNumber)); + } else { + mNumberView.setVisibility(View.GONE); + mNumberView.setText(null); + } + } + + /** Reads arguments from the fragment arguments and populates the necessary instance variables. */ + private void readArguments() { + Bundle arguments = getIntent().getExtras(); + if (arguments == null) { + LogUtil.e("CallSubjectDialog.readArguments", "arguments cannot be null"); + return; + } + mPhotoID = arguments.getLong(ARG_PHOTO_ID); + mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI); + mContactUri = arguments.getParcelable(ARG_CONTACT_URI); + mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER); + mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS); + mNumber = arguments.getString(ARG_NUMBER); + mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER); + mNumberLabel = arguments.getString(ARG_NUMBER_LABEL); + mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE); + } + + /** + * Updates the character limit display, coloring the text RED when the limit is reached or + * exceeded. + */ + private void updateCharacterLimit() { + String subjectText = mCallSubjectView.getText().toString(); + final int length; + + // If a message encoding is specified, use that to count bytes in the message. + if (mMessageEncoding != null) { + length = subjectText.getBytes(mMessageEncoding).length; + } else { + // No message encoding specified, so just count characters entered. + length = subjectText.length(); + } + + mCharacterLimitView.setText(getString(R.string.call_subject_limit, length, mLimit)); + if (length >= mLimit) { + mCharacterLimitView.setTextColor( + getResources().getColor(R.color.call_subject_limit_exceeded)); + } else { + mCharacterLimitView.setTextColor( + getResources().getColor(R.color.dialer_secondary_text_color)); + } + } + + /** Sets the photo on the quick contact photo. */ + private void setPhoto( + long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) { + mContactPhoto.assignContactUri(contactUri); + if (CompatUtils.isLollipopCompatible()) { + mContactPhoto.setOverlay(null); + } + + int contactType; + if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } else { + contactType = ContactPhotoManager.TYPE_DEFAULT; + } + + String lookupKey = null; + if (contactUri != null) { + lookupKey = UriUtils.getLookupKeyFromUri(contactUri); + } + + ContactPhotoManager.DefaultImageRequest request = + new ContactPhotoManager.DefaultImageRequest( + displayName, lookupKey, contactType, true /* isCircular */); + + if (photoId == 0 && photoUri != null) { + ContactPhotoManager.getInstance(this) + .loadPhoto( + mContactPhoto, + photoUri, + mPhotoSize, + false /* darkTheme */, + true /* isCircular */, + request); + } else { + ContactPhotoManager.getInstance(this) + .loadThumbnail( + mContactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request); + } + } + + /** + * Saves the subject history list to shared prefs, removing older items so that there are only + * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most. + * + * @param history The history. + */ + private void saveSubjectHistory(List history) { + // Remove oldest subject(s). + while (history.size() > CALL_SUBJECT_HISTORY_SIZE) { + history.remove(0); + } + + SharedPreferences.Editor editor = mPrefs.edit(); + int historyCount = 0; + for (String subject : history) { + if (!TextUtils.isEmpty(subject)) { + editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount, subject); + historyCount++; + } + } + editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount); + editor.apply(); + } + + /** Hide software keyboard for the given {@link View}. */ + public void hideSoftKeyboard(Context context, View view) { + InputMethodManager imm = + (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + /** + * Hides or shows the call history list. + * + * @param show {@code true} if the call history should be shown, {@code false} otherwise. + */ + private void showCallHistory(final boolean show) { + // Bail early if the visibility has not changed. + if ((show && mSubjectList.getVisibility() == View.VISIBLE) + || (!show && mSubjectList.getVisibility() == View.GONE)) { + return; + } + + final int dialogStartingBottom = mDialogView.getBottom(); + if (show) { + // Showing the subject list; bind the list of history items to the list and show it. + ArrayAdapter adapter = + new ArrayAdapter( + CallSubjectDialog.this, R.layout.call_subject_history_list_item, mSubjectHistory); + mSubjectList.setAdapter(adapter); + mSubjectList.setVisibility(View.VISIBLE); + } else { + // Hiding the subject list. + mSubjectList.setVisibility(View.GONE); + } + + // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout + // states. + ViewUtil.doOnPreDraw( + mBackgroundView, + true, + new Runnable() { + @Override + public void run() { + // Determine the amount the dialog has shifted due to the relayout. + int shiftAmount = dialogStartingBottom - mDialogView.getBottom(); + + // If the dialog needs to be shifted, do that now. + if (shiftAmount != 0) { + // Start animation in translated state and animate to translationY 0. + mDialogView.setTranslationY(shiftAmount); + mDialogView + .animate() + .translationY(0) + .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) + .setDuration(mAnimationDuration) + .start(); + } + + if (show) { + // Show the subject list. + mSubjectList.setTranslationY(mSubjectList.getHeight()); + + mSubjectList + .animate() + .translationY(0) + .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) + .setDuration(mAnimationDuration) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + } + + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mSubjectList.setVisibility(View.VISIBLE); + } + }) + .start(); + } else { + // Hide the subject list. + mSubjectList.setTranslationY(0); + + mSubjectList + .animate() + .translationY(mSubjectList.getHeight()) + .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) + .setDuration(mAnimationDuration) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mSubjectList.setVisibility(View.GONE); + } + + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + } + }) + .start(); + } + } + }); + } + + /** + * Loads the message encoding and maximum message length from the phone account extras for the + * current phone account. + */ + private void loadConfiguration() { + // Only attempt to load configuration from the phone account extras if the SDK is N or + // later. If we've got a prior SDK the default encoding and message length will suffice. + if (VERSION.SDK_INT < VERSION_CODES.N) { + return; + } + + if (mPhoneAccountHandle == null) { + return; + } + + TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); + final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle); + + Bundle phoneAccountExtras = account.getExtras(); + if (phoneAccountExtras == null) { + return; + } + + // Get limit, if provided; otherwise default to existing value. + mLimit = phoneAccountExtras.getInt(PhoneAccount.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit); + + // Get charset; default to none (e.g. count characters 1:1). + String charsetName = + phoneAccountExtras.getString(PhoneAccount.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING); + + if (!TextUtils.isEmpty(charsetName)) { + try { + mMessageEncoding = Charset.forName(charsetName); + } catch (java.nio.charset.UnsupportedCharsetException uce) { + // Character set was invalid; log warning and fallback to none. + LogUtil.e("CallSubjectDialog.loadConfiguration", "invalid charset: " + charsetName); + mMessageEncoding = null; + } + } else { + // No character set specified, so count characters 1:1. + mMessageEncoding = null; + } + } +} diff --git a/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..e96496cdaf2496f4ba126bb024e4d539ae97dec2 --- /dev/null +++ b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.ContactsContract; +import com.android.contacts.common.R; +import com.android.dialer.util.PermissionsUtil; + +/** Dialog that clears the frequently contacted list after confirming with the user. */ +public class ClearFrequentsDialog extends DialogFragment { + + /** Preferred way to show this dialog */ + public static void show(FragmentManager fragmentManager) { + ClearFrequentsDialog dialog = new ClearFrequentsDialog(); + dialog.show(fragmentManager, "clearFrequents"); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity().getApplicationContext(); + final ContentResolver resolver = getActivity().getContentResolver(); + final OnClickListener okListener = + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (!PermissionsUtil.hasContactsPermissions(context)) { + return; + } + + final ProgressDialog progressDialog = + ProgressDialog.show( + getContext(), + getString(R.string.clearFrequentsProgress_title), + null, + true, + true); + + final AsyncTask task = + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + resolver.delete( + ContactsContract.DataUsageFeedback.DELETE_USAGE_URI, null, null); + return null; + } + + @Override + protected void onPostExecute(Void result) { + progressDialog.dismiss(); + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + }; + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.clearFrequentsConfirmation_title) + .setMessage(R.string.clearFrequentsConfirmation) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, okListener) + .setCancelable(true) + .create(); + } +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java new file mode 100644 index 0000000000000000000000000000000000000000..2607ad19a21bf42cc40c04753b4c43ab6242a9ee --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.content.Context; +import com.android.contacts.common.list.DirectoryPartition; +import java.util.List; + +/** An interface for adding extended phone directories. */ +public interface PhoneDirectoryExtender { + /** + * Return a list of extended directories to add. May return null if no directories are to be + * added. + */ + List getExtendedDirectories(Context context); +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java new file mode 100644 index 0000000000000000000000000000000000000000..84649f1ed3ed639ec78d20029065a1b4f316ec18 --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.content.Context; +import android.support.annotation.NonNull; +import com.android.dialer.common.Assert; + +/** Accessor for the phone directory extender singleton. */ +public final class PhoneDirectoryExtenderAccessor { + + private static PhoneDirectoryExtender instance; + + private PhoneDirectoryExtenderAccessor() {} + + @NonNull + public static PhoneDirectoryExtender get(@NonNull Context context) { + Assert.isNotNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof PhoneDirectoryExtenderFactory) { + instance = ((PhoneDirectoryExtenderFactory) application).newPhoneDirectoryExtender(); + } + + if (instance == null) { + instance = new PhoneDirectoryExtenderStub(); + } + return instance; + } +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..9750ee30039204f697cfae0ca14b297275d6bec4 --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.support.annotation.NonNull; + +/** + * This interface should be implemented by the Application subclass. It allows the contacts module + * to get references to the PhoneDirectoryExtender. + */ +public interface PhoneDirectoryExtenderFactory { + + @NonNull + PhoneDirectoryExtender newPhoneDirectoryExtender(); +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java new file mode 100644 index 0000000000000000000000000000000000000000..95f9715336838601f184042d2037317cc8957d04 --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.content.Context; +import com.android.contacts.common.list.DirectoryPartition; +import java.util.Collections; +import java.util.List; + +/** No-op implementation for phone directory extender. */ +class PhoneDirectoryExtenderStub implements PhoneDirectoryExtender { + + @Override + public List getExtendedDirectories(Context context) { + return Collections.emptyList(); + } +} diff --git a/java/com/android/contacts/common/format/FormatUtils.java b/java/com/android/contacts/common/format/FormatUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..727c15b83032f4bcb0c8d5cf17101bc30e1ca821 --- /dev/null +++ b/java/com/android/contacts/common/format/FormatUtils.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.format; + +import android.database.CharArrayBuffer; +import android.graphics.Typeface; +import android.support.annotation.VisibleForTesting; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import java.util.Arrays; + +/** Assorted utility methods related to text formatting in Contacts. */ +public class FormatUtils { + + /** + * Finds the earliest point in buffer1 at which the first part of buffer2 matches. For example, + * overlapPoint("abcd", "cdef") == 2. + */ + public static int overlapPoint(CharArrayBuffer buffer1, CharArrayBuffer buffer2) { + if (buffer1 == null || buffer2 == null) { + return -1; + } + return overlapPoint( + Arrays.copyOfRange(buffer1.data, 0, buffer1.sizeCopied), + Arrays.copyOfRange(buffer2.data, 0, buffer2.sizeCopied)); + } + + /** + * Finds the earliest point in string1 at which the first part of string2 matches. For example, + * overlapPoint("abcd", "cdef") == 2. + */ + @VisibleForTesting + public static int overlapPoint(String string1, String string2) { + if (string1 == null || string2 == null) { + return -1; + } + return overlapPoint(string1.toCharArray(), string2.toCharArray()); + } + + /** + * Finds the earliest point in array1 at which the first part of array2 matches. For example, + * overlapPoint("abcd", "cdef") == 2. + */ + public static int overlapPoint(char[] array1, char[] array2) { + if (array1 == null || array2 == null) { + return -1; + } + int count1 = array1.length; + int count2 = array2.length; + + // Ignore matching tails of the two arrays. + while (count1 > 0 && count2 > 0 && array1[count1 - 1] == array2[count2 - 1]) { + count1--; + count2--; + } + + int size = count2; + for (int i = 0; i < count1; i++) { + if (i + size > count1) { + size = count1 - i; + } + int j; + for (j = 0; j < size; j++) { + if (array1[i + j] != array2[j]) { + break; + } + } + if (j == size) { + return i; + } + } + + return -1; + } + + /** + * Applies the given style to a range of the input CharSequence. + * + * @param style The style to apply (see the style constants in {@link Typeface}). + * @param input The CharSequence to style. + * @param start Starting index of the range to style (will be clamped to be a minimum of 0). + * @param end Ending index of the range to style (will be clamped to a maximum of the input + * length). + * @param flags Bitmask for configuring behavior of the span. See {@link android.text.Spanned}. + * @return The styled CharSequence. + */ + public static CharSequence applyStyleToSpan( + int style, CharSequence input, int start, int end, int flags) { + // Enforce bounds of the char sequence. + start = Math.max(0, start); + end = Math.min(input.length(), end); + SpannableString text = new SpannableString(input); + text.setSpan(new StyleSpan(style), start, end, flags); + return text; + } + + @VisibleForTesting + public static void copyToCharArrayBuffer(String text, CharArrayBuffer buffer) { + if (text != null) { + char[] data = buffer.data; + if (data == null || data.length < text.length()) { + buffer.data = text.toCharArray(); + } else { + text.getChars(0, text.length(), data, 0); + } + buffer.sizeCopied = text.length(); + } else { + buffer.sizeCopied = 0; + } + } + + /** Returns a String that represents the content of the given {@link CharArrayBuffer}. */ + @VisibleForTesting + public static String charArrayBufferToString(CharArrayBuffer buffer) { + return new String(buffer.data, 0, buffer.sizeCopied); + } + + /** + * Finds the index of the first word that starts with the given prefix. + * + *

If not found, returns -1. + * + * @param text the text in which to search for the prefix + * @param prefix the text to find, in upper case letters + */ + public static int indexOfWordPrefix(CharSequence text, String prefix) { + if (prefix == null || text == null) { + return -1; + } + + int textLength = text.length(); + int prefixLength = prefix.length(); + + if (prefixLength == 0 || textLength < prefixLength) { + return -1; + } + + int i = 0; + while (i < textLength) { + // Skip non-word characters + while (i < textLength && !Character.isLetterOrDigit(text.charAt(i))) { + i++; + } + + if (i + prefixLength > textLength) { + return -1; + } + + // Compare the prefixes + int j; + for (j = 0; j < prefixLength; j++) { + if (Character.toUpperCase(text.charAt(i + j)) != prefix.charAt(j)) { + break; + } + } + if (j == prefixLength) { + return i; + } + + // Skip this word + while (i < textLength && Character.isLetterOrDigit(text.charAt(i))) { + i++; + } + } + + return -1; + } +} diff --git a/java/com/android/contacts/common/format/TextHighlighter.java b/java/com/android/contacts/common/format/TextHighlighter.java new file mode 100644 index 0000000000000000000000000000000000000000..30c03fdf33ae97bb83888ab35ecfe438ba3f1093 --- /dev/null +++ b/java/com/android/contacts/common/format/TextHighlighter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.format; + +import android.text.SpannableString; +import android.text.style.CharacterStyle; +import android.text.style.StyleSpan; +import android.widget.TextView; + +/** Highlights the text in a text field. */ +public class TextHighlighter { + + private static final boolean DEBUG = false; + private final String TAG = TextHighlighter.class.getSimpleName(); + private int mTextStyle; + + private CharacterStyle mTextStyleSpan; + + public TextHighlighter(int textStyle) { + mTextStyle = textStyle; + mTextStyleSpan = getStyleSpan(); + } + + /** + * Sets the text on the given text view, highlighting the word that matches the given prefix. + * + * @param view the view on which to set the text + * @param text the string to use as the text + * @param prefix the prefix to look for + */ + public void setPrefixText(TextView view, String text, String prefix) { + view.setText(applyPrefixHighlight(text, prefix)); + } + + private CharacterStyle getStyleSpan() { + return new StyleSpan(mTextStyle); + } + + /** + * Applies highlight span to the text. + * + * @param text Text sequence to be highlighted. + * @param start Start position of the highlight sequence. + * @param end End position of the highlight sequence. + */ + public void applyMaskingHighlight(SpannableString text, int start, int end) { + /** Sets text color of the masked locations to be highlighted. */ + text.setSpan(getStyleSpan(), start, end, 0); + } + + /** + * Returns a CharSequence which highlights the given prefix if found in the given text. + * + * @param text the text to which to apply the highlight + * @param prefix the prefix to look for + */ + public CharSequence applyPrefixHighlight(CharSequence text, String prefix) { + if (prefix == null) { + return text; + } + + // Skip non-word characters at the beginning of prefix. + int prefixStart = 0; + while (prefixStart < prefix.length() + && !Character.isLetterOrDigit(prefix.charAt(prefixStart))) { + prefixStart++; + } + final String trimmedPrefix = prefix.substring(prefixStart); + + int index = FormatUtils.indexOfWordPrefix(text, trimmedPrefix); + if (index != -1) { + final SpannableString result = new SpannableString(text); + result.setSpan(mTextStyleSpan, index, index + trimmedPrefix.length(), 0 /* flags */); + return result; + } else { + return text; + } + } +} diff --git a/java/com/android/contacts/common/format/testing/SpannedTestUtils.java b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..293d9d5adf57f6c2341913e9b82aab3ada6aad4e --- /dev/null +++ b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.format.testing; + +import android.test.suitebuilder.annotation.SmallTest; +import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.widget.TextView; +import junit.framework.Assert; + +/** Utility class to check the value of spanned text in text views. */ +@SmallTest +public class SpannedTestUtils { + + /** + * Checks that the text contained in the text view matches the given HTML text. + * + * @param expectedHtmlText the expected text to be in the text view + * @param textView the text view from which to get the text + */ + public static void checkHtmlText(String expectedHtmlText, TextView textView) { + String actualHtmlText = Html.toHtml((Spanned) textView.getText()); + if (TextUtils.isEmpty(expectedHtmlText)) { + // If the text is empty, it does not add the

bits to it. + Assert.assertEquals("", actualHtmlText); + } else { + Assert.assertEquals("

" + expectedHtmlText + "

\n", actualHtmlText); + } + } + + /** + * Assert span exists in the correct location. + * + * @param seq The spannable string to check. + * @param start The starting index. + * @param end The ending index. + */ + public static void assertPrefixSpan(CharSequence seq, int start, int end) { + Assert.assertTrue(seq instanceof Spanned); + Spanned spannable = (Spanned) seq; + + if (start > 0) { + Assert.assertEquals(0, getNumForegroundColorSpansBetween(spannable, 0, start - 1)); + } + Assert.assertEquals(1, getNumForegroundColorSpansBetween(spannable, start, end)); + Assert.assertEquals( + 0, getNumForegroundColorSpansBetween(spannable, end + 1, spannable.length() - 1)); + } + + private static int getNumForegroundColorSpansBetween(Spanned value, int start, int end) { + return value.getSpans(start, end, StyleSpan.class).length; + } + + /** + * Asserts that the given character sequence is not a Spanned object and text is correct. + * + * @param seq The sequence to check. + * @param expected The expected text. + */ + public static void assertNotSpanned(CharSequence seq, String expected) { + Assert.assertFalse(seq instanceof Spanned); + Assert.assertEquals(expected, seq); + } + + public static int getNextTransition(SpannableString seq, int start) { + return seq.nextSpanTransition(start, seq.length(), StyleSpan.class); + } +} diff --git a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java new file mode 100644 index 0000000000000000000000000000000000000000..7e1839c1e59c6c6e6a4c0e8303879e812ee63210 --- /dev/null +++ b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.lettertiles; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import com.android.contacts.common.R; +import com.android.dialer.common.Assert; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A drawable that encapsulates all the functionality needed to display a letter tile to represent a + * contact image. + */ +public class LetterTileDrawable extends Drawable { + + /** + * ContactType indicates the avatar type of the contact. For a person or for the default when no + * name is provided, it is {@link #TYPE_DEFAULT}, otherwise, for a business it is {@link + * #TYPE_BUSINESS}, and voicemail contacts should use {@link #TYPE_VOICEMAIL}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL}) + public @interface ContactType {} + + /** Contact type constants */ + public static final int TYPE_PERSON = 1; + public static final int TYPE_BUSINESS = 2; + public static final int TYPE_VOICEMAIL = 3; + @ContactType public static final int TYPE_DEFAULT = TYPE_PERSON; + + /** + * Shape indicates the letter tile shape. It can be either a {@link #SHAPE_CIRCLE}, otherwise, it + * is a {@link #SHAPE_RECTANGLE}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SHAPE_CIRCLE, SHAPE_RECTANGLE}) + public @interface Shape {} + + /** Shape constants */ + public static final int SHAPE_CIRCLE = 1; + + public static final int SHAPE_RECTANGLE = 2; + + /** 54% opacity */ + private static final int ALPHA = 138; + + /** Reusable components to avoid new allocations */ + private static final Paint sPaint = new Paint(); + + private static final Rect sRect = new Rect(); + private static final char[] sFirstChar = new char[1]; + /** Letter tile */ + private static TypedArray sColors; + + private static int sDefaultColor; + private static int sTileFontColor; + private static float sLetterToTileRatio; + private static Bitmap sDefaultPersonAvatar; + private static Bitmap sDefaultBusinessAvatar; + private static Bitmap sDefaultVoicemailAvatar; + private static final String TAG = LetterTileDrawable.class.getSimpleName(); + private final Paint mPaint; + private int mContactType = TYPE_DEFAULT; + private float mScale = 1.0f; + private float mOffset = 0.0f; + private boolean mIsCircle = false; + + private int mColor; + private Character mLetter = null; + + private boolean mAvatarWasVoicemailOrBusiness = false; + private String mDisplayName; + + public LetterTileDrawable(final Resources res) { + if (sColors == null) { + sColors = res.obtainTypedArray(R.array.letter_tile_colors); + sDefaultColor = res.getColor(R.color.letter_tile_default_color); + sTileFontColor = res.getColor(R.color.letter_tile_font_color); + sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1); + sDefaultPersonAvatar = + BitmapFactory.decodeResource( + res, R.drawable.product_logo_avatar_anonymous_white_color_120); + sDefaultBusinessAvatar = + BitmapFactory.decodeResource(res, R.drawable.ic_business_white_120dp); + sDefaultVoicemailAvatar = BitmapFactory.decodeResource(res, R.drawable.ic_voicemail_avatar); + sPaint.setTypeface( + Typeface.create(res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL)); + sPaint.setTextAlign(Align.CENTER); + sPaint.setAntiAlias(true); + } + mPaint = new Paint(); + mPaint.setFilterBitmap(true); + mPaint.setDither(true); + mColor = sDefaultColor; + } + + private static Bitmap getBitmapForContactType(int contactType) { + switch (contactType) { + case TYPE_BUSINESS: + return sDefaultBusinessAvatar; + case TYPE_VOICEMAIL: + return sDefaultVoicemailAvatar; + case TYPE_PERSON: + default: + return sDefaultPersonAvatar; + } + } + + private static boolean isEnglishLetter(final char c) { + return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'); + } + + @Override + public void draw(final Canvas canvas) { + final Rect bounds = getBounds(); + if (!isVisible() || bounds.isEmpty()) { + return; + } + // Draw letter tile. + drawLetterTile(canvas); + } + + /** + * Draw the bitmap onto the canvas at the current bounds taking into account the current scale. + */ + private void drawBitmap( + final Bitmap bitmap, final int width, final int height, final Canvas canvas) { + // The bitmap should be drawn in the middle of the canvas without changing its width to + // height ratio. + final Rect destRect = copyBounds(); + + // Crop the destination bounds into a square, scaled and offset as appropriate + final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2); + + destRect.set( + destRect.centerX() - halfLength, + (int) (destRect.centerY() - halfLength + mOffset * destRect.height()), + destRect.centerX() + halfLength, + (int) (destRect.centerY() + halfLength + mOffset * destRect.height())); + + // Source rectangle remains the entire bounds of the source bitmap. + sRect.set(0, 0, width, height); + + sPaint.setTextAlign(Align.CENTER); + sPaint.setAntiAlias(true); + sPaint.setAlpha(ALPHA); + + canvas.drawBitmap(bitmap, sRect, destRect, sPaint); + } + + private void drawLetterTile(final Canvas canvas) { + // Draw background color. + sPaint.setColor(mColor); + + sPaint.setAlpha(mPaint.getAlpha()); + final Rect bounds = getBounds(); + final int minDimension = Math.min(bounds.width(), bounds.height()); + + if (mIsCircle) { + canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint); + } else { + canvas.drawRect(bounds, sPaint); + } + + // Draw letter/digit only if the first character is an english letter or there's a override + + if (mLetter != null) { + // Draw letter or digit. + sFirstChar[0] = mLetter; + + // Scale text by canvas bounds and user selected scaling factor + sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension); + sPaint.getTextBounds(sFirstChar, 0, 1, sRect); + sPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL)); + sPaint.setColor(sTileFontColor); + sPaint.setAlpha(ALPHA); + + // Draw the letter in the canvas, vertically shifted up or down by the user-defined + // offset + canvas.drawText( + sFirstChar, + 0, + 1, + bounds.centerX(), + bounds.centerY() + mOffset * bounds.height() - sRect.exactCenterY(), + sPaint); + } else { + // Draw the default image if there is no letter/digit to be drawn + final Bitmap bitmap = getBitmapForContactType(mContactType); + drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(), canvas); + } + } + + public int getColor() { + return mColor; + } + + public LetterTileDrawable setColor(int color) { + mColor = color; + return this; + } + + /** Returns a deterministic color based on the provided contact identifier string. */ + private int pickColor(final String identifier) { + if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) { + return sDefaultColor; + } + // String.hashCode() implementation is not supposed to change across java versions, so + // this should guarantee the same email address always maps to the same color. + // The email should already have been normalized by the ContactRequest. + final int color = Math.abs(identifier.hashCode()) % sColors.length(); + return sColors.getColor(color, sDefaultColor); + } + + @Override + public void setAlpha(final int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(final ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return android.graphics.PixelFormat.OPAQUE; + } + + @Override + public void getOutline(Outline outline) { + if (mIsCircle) { + outline.setOval(getBounds()); + } else { + outline.setRect(getBounds()); + } + + outline.setAlpha(1); + } + + /** + * Scale the drawn letter tile to a ratio of its default size + * + * @param scale The ratio the letter tile should be scaled to as a percentage of its default size, + * from a scale of 0 to 2.0f. The default is 1.0f. + */ + public LetterTileDrawable setScale(float scale) { + mScale = scale; + return this; + } + + /** + * Assigns the vertical offset of the position of the letter tile to the ContactDrawable + * + * @param offset The provided offset must be within the range of -0.5f to 0.5f. If set to -0.5f, + * the letter will be shifted upwards by 0.5 times the height of the canvas it is being drawn + * on, which means it will be drawn with the center of the letter starting at the top edge of + * the canvas. If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of + * the canvas it is being drawn on, which means it will be drawn with the center of the letter + * starting at the bottom edge of the canvas. The default is 0.0f. + */ + public LetterTileDrawable setOffset(float offset) { + Assert.checkArgument(offset >= -0.5f && offset <= 0.5f); + mOffset = offset; + return this; + } + + public LetterTileDrawable setLetter(Character letter) { + mLetter = letter; + return this; + } + + public Character getLetter() { + return this.mLetter; + } + + private LetterTileDrawable setLetterAndColorFromContactDetails( + final String displayName, final String identifier) { + if (displayName != null && displayName.length() > 0 && isEnglishLetter(displayName.charAt(0))) { + mLetter = Character.toUpperCase(displayName.charAt(0)); + } else { + mLetter = null; + } + mColor = pickColor(identifier); + return this; + } + + public LetterTileDrawable setContactType(@ContactType int contactType) { + mContactType = contactType; + return this; + } + + @ContactType + public int getContactType() { + return this.mContactType; + } + + public LetterTileDrawable setIsCircular(boolean isCircle) { + mIsCircle = isCircle; + return this; + } + + /** + * Creates a canonical letter tile for use across dialer fragments. + * + * @param displayName The display name to produce the letter in the tile. Null values or numbers + * yield no letter. + * @param identifierForTileColor The string used to produce the tile color. + * @param shape The shape of the tile. + * @param contactType The type of contact, e.g. TYPE_VOICEMAIL. + * @return this + */ + public LetterTileDrawable setCanonicalDialerLetterTileDetails( + @Nullable final String displayName, + @Nullable final String identifierForTileColor, + @Shape final int shape, + final int contactType) { + setContactType(contactType); + /** + * During hangup, we lose the call state for special types of contacts, like voicemail. To help + * callers avoid extraneous LetterTileDrawable allocations, we keep track of the special case + * until we encounter a new display name. + */ + if (contactType == TYPE_VOICEMAIL || contactType == TYPE_BUSINESS) { + this.mAvatarWasVoicemailOrBusiness = true; + } else if (displayName != null && !displayName.equals(mDisplayName)) { + this.mAvatarWasVoicemailOrBusiness = false; + } + this.mDisplayName = displayName; + if (shape == SHAPE_CIRCLE) { + this.setIsCircular(true); + } else { + this.setIsCircular(false); + } + + /** + * To preserve style, we don't use contactType to set the tile icon. In the future, when all + * callers surface this detail, we can use this to better style the tile icon. + */ + if (mAvatarWasVoicemailOrBusiness) { + this.setLetterAndColorFromContactDetails(null, displayName); + return this; + } else { + if (identifierForTileColor != null) { + this.setLetterAndColorFromContactDetails(displayName, identifierForTileColor); + return this; + } else { + this.setLetterAndColorFromContactDetails(displayName, displayName); + return this; + } + } + } +} diff --git a/java/com/android/contacts/common/list/AutoScrollListView.java b/java/com/android/contacts/common/list/AutoScrollListView.java new file mode 100644 index 0000000000000000000000000000000000000000..601abf5283233874e841fe87c9352ccc9562df29 --- /dev/null +++ b/java/com/android/contacts/common/list/AutoScrollListView.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.ListView; + +/** + * A ListView that can be asked to scroll (smoothly or otherwise) to a specific position. This class + * takes advantage of similar functionality that exists in {@link ListView} and enhances it. + */ +public class AutoScrollListView extends ListView { + + /** Position the element at about 1/3 of the list height */ + private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f; + + private int mRequestedScrollPosition = -1; + private boolean mSmoothScrollRequested; + + public AutoScrollListView(Context context) { + super(context); + } + + public AutoScrollListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Brings the specified position to view by optionally performing a jump-scroll maneuver: first it + * jumps to some position near the one requested and then does a smooth scroll to the requested + * position. This creates an impression of full smooth scrolling without actually traversing the + * entire list. If smooth scrolling is not requested, instantly positions the requested item at a + * preferred offset. + */ + public void requestPositionToScreen(int position, boolean smoothScroll) { + mRequestedScrollPosition = position; + mSmoothScrollRequested = smoothScroll; + requestLayout(); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + if (mRequestedScrollPosition == -1) { + return; + } + + final int position = mRequestedScrollPosition; + mRequestedScrollPosition = -1; + + int firstPosition = getFirstVisiblePosition() + 1; + int lastPosition = getLastVisiblePosition(); + if (position >= firstPosition && position <= lastPosition) { + return; // Already on screen + } + + final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP); + if (!mSmoothScrollRequested) { + setSelectionFromTop(position, offset); + + // Since we have changed the scrolling position, we need to redo child layout + // Calling "requestLayout" in the middle of a layout pass has no effect, + // so we call layoutChildren explicitly + super.layoutChildren(); + + } else { + // We will first position the list a couple of screens before or after + // the new selection and then scroll smoothly to it. + int twoScreens = (lastPosition - firstPosition) * 2; + int preliminaryPosition; + if (position < firstPosition) { + preliminaryPosition = position + twoScreens; + if (preliminaryPosition >= getCount()) { + preliminaryPosition = getCount() - 1; + } + if (preliminaryPosition < firstPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } else { + preliminaryPosition = position - twoScreens; + if (preliminaryPosition < 0) { + preliminaryPosition = 0; + } + if (preliminaryPosition > lastPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } + + smoothScrollToPositionFromTop(position, offset); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + // Workaround for b/31160338 and b/32778636. + if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N + || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) { + layoutChildren(); + } + } +} diff --git a/java/com/android/contacts/common/list/ContactEntry.java b/java/com/android/contacts/common/list/ContactEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..e33165e45f04966a0798f7ee099339a93c1aca61 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactEntry.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.net.Uri; +import android.provider.ContactsContract.PinnedPositions; +import android.text.TextUtils; +import com.android.contacts.common.preference.ContactsPreferences; + +/** Class to hold contact information */ +public class ContactEntry { + + public static final ContactEntry BLANK_ENTRY = new ContactEntry(); + private static final int UNSET_DISPLAY_ORDER_PREFERENCE = -1; + /** Primary name for a Contact */ + public String namePrimary; + /** Alternative name for a Contact, e.g. last name first */ + public String nameAlternative; + /** + * The user's preference on name display order, last name first or first time first. {@see + * ContactsPreferences} + */ + public int nameDisplayOrder = UNSET_DISPLAY_ORDER_PREFERENCE; + + public String phoneLabel; + public String phoneNumber; + public Uri photoUri; + public Uri lookupUri; + public String lookupKey; + public long id; + public int pinned = PinnedPositions.UNPINNED; + public boolean isFavorite = false; + public boolean isDefaultNumber = false; + + public String getPreferredDisplayName() { + if (nameDisplayOrder == UNSET_DISPLAY_ORDER_PREFERENCE + || nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY + || TextUtils.isEmpty(nameAlternative)) { + return namePrimary; + } + return nameAlternative; + } +} diff --git a/java/com/android/contacts/common/list/ContactEntryListAdapter.java b/java/com/android/contacts/common/list/ContactEntryListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..18bbae3824813a21fe80958a5baef2cd84ad05a3 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactEntryListAdapter.java @@ -0,0 +1,742 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.CursorLoader; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.QuickContactBadge; +import android.widget.SectionIndexer; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.util.SearchUtil; +import com.android.dialer.compat.CompatUtils; +import java.util.HashSet; + +/** + * Common base class for various contact-related lists, e.g. contact list, phone number list etc. + */ +public abstract class ContactEntryListAdapter extends IndexerListAdapter { + + /** + * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be included in the + * search. + */ + public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false; + + private static final String TAG = "ContactEntryListAdapter"; + private int mDisplayOrder; + private int mSortOrder; + + private boolean mDisplayPhotos; + private boolean mCircularPhotos = true; + private boolean mQuickContactEnabled; + private boolean mAdjustSelectionBoundsEnabled; + + /** The root view of the fragment that this adapter is associated with. */ + private View mFragmentRootView; + + private ContactPhotoManager mPhotoLoader; + + private String mQueryString; + private String mUpperCaseQueryString; + private boolean mSearchMode; + private int mDirectorySearchMode; + private int mDirectoryResultLimit = Integer.MAX_VALUE; + + private boolean mEmptyListEnabled = true; + + private boolean mSelectionVisible; + + private ContactListFilter mFilter; + private boolean mDarkTheme = false; + + /** Resource used to provide header-text for default filter. */ + private CharSequence mDefaultFilterHeaderText; + + public ContactEntryListAdapter(Context context) { + super(context); + setDefaultFilterHeaderText(R.string.local_search_label); + addPartitions(); + } + + /** + * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of image + * loading requests that get cancelled on cursor changes. + */ + protected void setFragmentRootView(View fragmentRootView) { + mFragmentRootView = fragmentRootView; + } + + protected void setDefaultFilterHeaderText(int resourceId) { + mDefaultFilterHeaderText = getContext().getResources().getText(resourceId); + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + final ContactListItemView view = new ContactListItemView(context, null); + view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); + view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); + return view; + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + final ContactListItemView view = (ContactListItemView) itemView; + view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); + bindWorkProfileIcon(view, partition); + } + + @Override + protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) { + return new ContactListPinnedHeaderView(context, null, parent); + } + + @Override + protected void setPinnedSectionTitle(View pinnedHeaderView, String title) { + ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title); + } + + protected void addPartitions() { + addPartition(createDefaultDirectoryPartition()); + } + + protected DirectoryPartition createDefaultDirectoryPartition() { + DirectoryPartition partition = new DirectoryPartition(true, true); + partition.setDirectoryId(Directory.DEFAULT); + partition.setDirectoryType(getContext().getString(R.string.contactsList)); + partition.setPriorityDirectory(true); + partition.setPhotoSupported(true); + partition.setLabel(mDefaultFilterHeaderText.toString()); + return partition; + } + + /** + * Remove all directories after the default directory. This is typically used when contacts list + * screens are asked to exit the search mode and thus need to remove all remote directory results + * for the search. + * + *

This code assumes that the default directory and directories before that should not be + * deleted (e.g. Join screen has "suggested contacts" directory before the default director, and + * we should not remove the directory). + */ + public void removeDirectoriesAfterDefault() { + final int partitionCount = getPartitionCount(); + for (int i = partitionCount - 1; i >= 0; i--) { + final Partition partition = getPartition(i); + if ((partition instanceof DirectoryPartition) + && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) { + break; + } else { + removePartition(i); + } + } + } + + protected int getPartitionByDirectoryId(long id) { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + if (((DirectoryPartition) partition).getDirectoryId() == id) { + return i; + } + } + } + return -1; + } + + protected DirectoryPartition getDirectoryById(long id) { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + final DirectoryPartition directoryPartition = (DirectoryPartition) partition; + if (directoryPartition.getDirectoryId() == id) { + return directoryPartition; + } + } + } + return null; + } + + public abstract void configureLoader(CursorLoader loader, long directoryId); + + /** Marks all partitions as "loading" */ + public void onDataReload() { + boolean notify = false; + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + if (!directoryPartition.isLoading()) { + notify = true; + } + directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); + } + } + if (notify) { + notifyDataSetChanged(); + } + } + + @Override + public void clearPartitions() { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); + } + } + super.clearPartitions(); + } + + public boolean isSearchMode() { + return mSearchMode; + } + + public void setSearchMode(boolean flag) { + mSearchMode = flag; + } + + public String getQueryString() { + return mQueryString; + } + + public void setQueryString(String queryString) { + mQueryString = queryString; + if (TextUtils.isEmpty(queryString)) { + mUpperCaseQueryString = null; + } else { + mUpperCaseQueryString = SearchUtil.cleanStartAndEndOfSearchQuery(queryString.toUpperCase()); + } + } + + public String getUpperCaseQueryString() { + return mUpperCaseQueryString; + } + + public int getDirectorySearchMode() { + return mDirectorySearchMode; + } + + public void setDirectorySearchMode(int mode) { + mDirectorySearchMode = mode; + } + + public int getDirectoryResultLimit() { + return mDirectoryResultLimit; + } + + public void setDirectoryResultLimit(int limit) { + this.mDirectoryResultLimit = limit; + } + + public int getDirectoryResultLimit(DirectoryPartition directoryPartition) { + final int limit = directoryPartition.getResultLimit(); + return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit; + } + + public int getContactNameDisplayOrder() { + return mDisplayOrder; + } + + public void setContactNameDisplayOrder(int displayOrder) { + mDisplayOrder = displayOrder; + } + + public int getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(int sortOrder) { + mSortOrder = sortOrder; + } + + protected ContactPhotoManager getPhotoLoader() { + return mPhotoLoader; + } + + public void setPhotoLoader(ContactPhotoManager photoLoader) { + mPhotoLoader = photoLoader; + } + + public boolean getDisplayPhotos() { + return mDisplayPhotos; + } + + public void setDisplayPhotos(boolean displayPhotos) { + mDisplayPhotos = displayPhotos; + } + + public boolean getCircularPhotos() { + return mCircularPhotos; + } + + public boolean isSelectionVisible() { + return mSelectionVisible; + } + + public void setSelectionVisible(boolean flag) { + this.mSelectionVisible = flag; + } + + public boolean isQuickContactEnabled() { + return mQuickContactEnabled; + } + + public void setQuickContactEnabled(boolean quickContactEnabled) { + mQuickContactEnabled = quickContactEnabled; + } + + public boolean isAdjustSelectionBoundsEnabled() { + return mAdjustSelectionBoundsEnabled; + } + + public void setAdjustSelectionBoundsEnabled(boolean enabled) { + mAdjustSelectionBoundsEnabled = enabled; + } + + public void setProfileExists(boolean exists) { + // Stick the "ME" header for the profile + if (exists) { + setSectionHeader(R.string.user_profile_contacts_list_header, /* # of ME */ 1); + } + } + + private void setSectionHeader(int resId, int numberOfItems) { + SectionIndexer indexer = getIndexer(); + if (indexer != null) { + ((ContactsSectionIndexer) indexer) + .setProfileAndFavoritesHeader(getContext().getString(resId), numberOfItems); + } + } + + public void setDarkTheme(boolean value) { + mDarkTheme = value; + } + + /** Updates partitions according to the directory meta-data contained in the supplied cursor. */ + public void changeDirectories(Cursor cursor) { + if (cursor.getCount() == 0) { + // Directory table must have at least local directory, without which this adapter will + // enter very weird state. + Log.e( + TAG, + "Directory search loader returned an empty cursor, which implies we have " + + "no directory entries.", + new RuntimeException()); + return; + } + HashSet directoryIds = new HashSet(); + + int idColumnIndex = cursor.getColumnIndex(Directory._ID); + int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE); + int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME); + int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT); + + // TODO preserve the order of partition to match those of the cursor + // Phase I: add new directories + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + long id = cursor.getLong(idColumnIndex); + directoryIds.add(id); + if (getPartitionByDirectoryId(id) == -1) { + DirectoryPartition partition = new DirectoryPartition(false, true); + partition.setDirectoryId(id); + if (DirectoryCompat.isRemoteDirectoryId(id)) { + if (DirectoryCompat.isEnterpriseDirectoryId(id)) { + partition.setLabel(mContext.getString(R.string.directory_search_label_work)); + } else { + partition.setLabel(mContext.getString(R.string.directory_search_label)); + } + } else { + if (DirectoryCompat.isEnterpriseDirectoryId(id)) { + partition.setLabel(mContext.getString(R.string.list_filter_phones_work)); + } else { + partition.setLabel(mDefaultFilterHeaderText.toString()); + } + } + partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex)); + partition.setDisplayName(cursor.getString(displayNameColumnIndex)); + int photoSupport = cursor.getInt(photoSupportColumnIndex); + partition.setPhotoSupported( + photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY + || photoSupport == Directory.PHOTO_SUPPORT_FULL); + addPartition(partition); + } + } + + // Phase II: remove deleted directories + int count = getPartitionCount(); + for (int i = count; --i >= 0; ) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + long id = ((DirectoryPartition) partition).getDirectoryId(); + if (!directoryIds.contains(id)) { + removePartition(i); + } + } + } + + invalidate(); + notifyDataSetChanged(); + } + + @Override + public void changeCursor(int partitionIndex, Cursor cursor) { + if (partitionIndex >= getPartitionCount()) { + // There is no partition for this data + return; + } + + Partition partition = getPartition(partitionIndex); + if (partition instanceof DirectoryPartition) { + ((DirectoryPartition) partition).setStatus(DirectoryPartition.STATUS_LOADED); + } + + if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) { + mPhotoLoader.refreshCache(); + } + + super.changeCursor(partitionIndex, cursor); + + if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { + updateIndexer(cursor); + } + + // When the cursor changes, cancel any pending asynchronous photo loads. + mPhotoLoader.cancelPendingRequests(mFragmentRootView); + } + + public void changeCursor(Cursor cursor) { + changeCursor(0, cursor); + } + + /** Updates the indexer, which is used to produce section headers. */ + private void updateIndexer(Cursor cursor) { + if (cursor == null || cursor.isClosed()) { + setIndexer(null); + return; + } + + Bundle bundle = cursor.getExtras(); + if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) + && bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) { + String[] sections = bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); + int[] counts = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); + + if (getExtraStartingSection()) { + // Insert an additional unnamed section at the top of the list. + String[] allSections = new String[sections.length + 1]; + int[] allCounts = new int[counts.length + 1]; + for (int i = 0; i < sections.length; i++) { + allSections[i + 1] = sections[i]; + allCounts[i + 1] = counts[i]; + } + allCounts[0] = 1; + allSections[0] = ""; + setIndexer(new ContactsSectionIndexer(allSections, allCounts)); + } else { + setIndexer(new ContactsSectionIndexer(sections, counts)); + } + } else { + setIndexer(null); + } + } + + protected boolean getExtraStartingSection() { + return false; + } + + @Override + public int getViewTypeCount() { + // We need a separate view type for each item type, plus another one for + // each type with header, plus one for "other". + return getItemViewTypeCount() * 2 + 1; + } + + @Override + public int getItemViewType(int partitionIndex, int position) { + int type = super.getItemViewType(partitionIndex, position); + if (!isUserProfile(position) + && isSectionHeaderDisplayEnabled() + && partitionIndex == getIndexedPartition()) { + Placement placement = getItemPlacementInSection(position); + return placement.firstInSection ? type : getItemViewTypeCount() + type; + } else { + return type; + } + } + + @Override + public boolean isEmpty() { + // TODO + // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) { + // return true; + // } + + if (!mEmptyListEnabled) { + return false; + } else if (isSearchMode()) { + return TextUtils.isEmpty(getQueryString()); + } else { + return super.isEmpty(); + } + } + + public boolean isLoading() { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition && ((DirectoryPartition) partition).isLoading()) { + return true; + } + } + return false; + } + + /** Changes visibility parameters for the default directory partition. */ + public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) { + int defaultPartitionIndex = -1; + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition + && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) { + defaultPartitionIndex = i; + break; + } + } + if (defaultPartitionIndex != -1) { + setShowIfEmpty(defaultPartitionIndex, showIfEmpty); + setHasHeader(defaultPartitionIndex, hasHeader); + } + } + + @Override + protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + View view = inflater.inflate(R.layout.directory_header, parent, false); + if (!getPinnedPartitionHeadersEnabled()) { + // If the headers are unpinned, there is no need for their background + // color to be non-transparent. Setting this transparent reduces maintenance for + // non-pinned headers. We don't need to bother synchronizing the activity's + // background color with the header background color. + view.setBackground(null); + } + return view; + } + + protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) { + final Partition partition = getPartition(partitionId); + if (partition instanceof DirectoryPartition) { + final DirectoryPartition directoryPartition = (DirectoryPartition) partition; + final long directoryId = directoryPartition.getDirectoryId(); + final long userType = ContactsUtils.determineUserType(directoryId, null); + view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK); + } + } + + @Override + protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) { + Partition partition = getPartition(partitionIndex); + if (!(partition instanceof DirectoryPartition)) { + return; + } + + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + long directoryId = directoryPartition.getDirectoryId(); + TextView labelTextView = (TextView) view.findViewById(R.id.label); + TextView displayNameTextView = (TextView) view.findViewById(R.id.display_name); + labelTextView.setText(directoryPartition.getLabel()); + if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) { + displayNameTextView.setText(null); + } else { + String directoryName = directoryPartition.getDisplayName(); + String displayName = + !TextUtils.isEmpty(directoryName) ? directoryName : directoryPartition.getDirectoryType(); + displayNameTextView.setText(displayName); + } + + final Resources res = getContext().getResources(); + final int headerPaddingTop = + partitionIndex == 1 && getPartition(0).isEmpty() + ? 0 + : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding); + // There should be no extra padding at the top of the first directory header + view.setPaddingRelative( + view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), view.getPaddingBottom()); + } + + /** Checks whether the contact entry at the given position represents the user's profile. */ + protected boolean isUserProfile(int position) { + // The profile only ever appears in the first position if it is present. So if the position + // is anything beyond 0, it can't be the profile. + boolean isUserProfile = false; + if (position == 0) { + int partition = getPartitionForPosition(position); + if (partition >= 0) { + // Save the old cursor position - the call to getItem() may modify the cursor + // position. + int offset = getCursor(partition).getPosition(); + Cursor cursor = (Cursor) getItem(position); + if (cursor != null) { + int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE); + if (profileColumnIndex != -1) { + isUserProfile = cursor.getInt(profileColumnIndex) == 1; + } + // Restore the old cursor position. + cursor.moveToPosition(offset); + } + } + } + return isUserProfile; + } + + public boolean isPhotoSupported(int partitionIndex) { + Partition partition = getPartition(partitionIndex); + if (partition instanceof DirectoryPartition) { + return ((DirectoryPartition) partition).isPhotoSupported(); + } + return true; + } + + /** Returns the currently selected filter. */ + public ContactListFilter getFilter() { + return mFilter; + } + + public void setFilter(ContactListFilter filter) { + mFilter = filter; + } + + // TODO: move sharable logic (bindXX() methods) to here with extra arguments + + /** + * Loads the photo for the quick contact view and assigns the contact uri. + * + * @param photoIdColumn Index of the photo id column + * @param photoUriColumn Index of the photo uri column. Optional: Can be -1 + * @param contactIdColumn Index of the contact id column + * @param lookUpKeyColumn Index of the lookup key column + * @param displayNameColumn Index of the display name column + */ + protected void bindQuickContact( + final ContactListItemView view, + int partitionIndex, + Cursor cursor, + int photoIdColumn, + int photoUriColumn, + int contactIdColumn, + int lookUpKeyColumn, + int displayNameColumn) { + long photoId = 0; + if (!cursor.isNull(photoIdColumn)) { + photoId = cursor.getLong(photoIdColumn); + } + + QuickContactBadge quickContact = view.getQuickContact(); + quickContact.assignContactUri( + getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn)); + if (CompatUtils.hasPrioritizedMimeType()) { + // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume + // that only Dialer will use this QuickContact badge. This means prioritizing the phone + // mimetype here is reasonable. + quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + + if (photoId != 0 || photoUriColumn == -1) { + getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, null); + } else { + final String photoUriString = cursor.getString(photoUriColumn); + final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); + DefaultImageRequest request = null; + if (photoUri == null) { + request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, lookUpKeyColumn); + } + getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, request); + } + } + + @Override + public boolean hasStableIds() { + // Whenever bindViewId() is called, the values passed into setId() are stable or + // stable-ish. For example, when one contact is modified we don't expect a second + // contact's Contact._ID values to change. + return true; + } + + protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) { + // Set a semi-stable id, so that talkback won't get confused when the list gets + // refreshed. There is little harm in inserting the same ID twice. + long contactId = cursor.getLong(idColumn); + view.setId((int) (contactId % Integer.MAX_VALUE)); + } + + protected Uri getContactUri( + int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) { + long contactId = cursor.getLong(contactIdColumn); + String lookupKey = cursor.getString(lookUpKeyColumn); + long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId(); + Uri uri = Contacts.getLookupUri(contactId, lookupKey); + if (uri != null && directoryId != Directory.DEFAULT) { + uri = + uri.buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) + .build(); + } + return uri; + } + + /** + * Retrieves the lookup key and display name from a cursor, and returns a {@link + * DefaultImageRequest} containing these contact details + * + * @param cursor Contacts cursor positioned at the current row to retrieve contact details for + * @param displayNameColumn Column index of the display name + * @param lookupKeyColumn Column index of the lookup key + * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the + * display name and lookup key of the contact. + */ + public DefaultImageRequest getDefaultImageRequestFromCursor( + Cursor cursor, int displayNameColumn, int lookupKeyColumn) { + final String displayName = cursor.getString(displayNameColumn); + final String lookupKey = cursor.getString(lookupKeyColumn); + return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos); + } +} diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..a8d9b55ba28d6023d9d181ce4e2c271ea5ce2e22 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java @@ -0,0 +1,862 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.app.Activity; +import android.app.Fragment; +import android.app.LoaderManager; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Parcelable; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnFocusChangeListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ListView; +import com.android.common.widget.CompositeCursorAdapter.Partition; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.ContactListViewUtils; +import java.util.Locale; + +/** Common base class for various contact-related list fragments. */ +public abstract class ContactEntryListFragment extends Fragment + implements OnItemClickListener, + OnScrollListener, + OnFocusChangeListener, + OnTouchListener, + OnItemLongClickListener, + LoaderCallbacks { + private static final String TAG = "ContactEntryListFragment"; + private static final String KEY_LIST_STATE = "liststate"; + private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled"; + private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled"; + private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled"; + private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = "adjustSelectionBoundsEnabled"; + private static final String KEY_INCLUDE_PROFILE = "includeProfile"; + private static final String KEY_SEARCH_MODE = "searchMode"; + private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled"; + private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition"; + private static final String KEY_QUERY_STRING = "queryString"; + private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode"; + private static final String KEY_SELECTION_VISIBLE = "selectionVisible"; + private static final String KEY_DARK_THEME = "darkTheme"; + private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility"; + private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit"; + + private static final String DIRECTORY_ID_ARG_KEY = "directoryId"; + + private static final int DIRECTORY_LOADER_ID = -1; + + private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300; + private static final int DIRECTORY_SEARCH_MESSAGE = 1; + + private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20; + private static final int STATUS_NOT_LOADED = 0; + private static final int STATUS_LOADING = 1; + private static final int STATUS_LOADED = 2; + protected boolean mUserProfileExists; + private boolean mSectionHeaderDisplayEnabled; + private boolean mPhotoLoaderEnabled; + private boolean mQuickContactEnabled = true; + private boolean mAdjustSelectionBoundsEnabled = true; + private boolean mIncludeProfile; + private boolean mSearchMode; + private boolean mVisibleScrollbarEnabled; + private boolean mShowEmptyListForEmptyQuery; + private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition(); + private String mQueryString; + private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE; + private boolean mSelectionVisible; + private boolean mLegacyCompatibility; + private boolean mEnabled = true; + private T mAdapter; + private View mView; + private ListView mListView; + /** Used to save the scrolling state of the list when the fragment is not recreated. */ + private int mListViewTopIndex; + + private int mListViewTopOffset; + /** Used for keeping track of the scroll state of the list. */ + private Parcelable mListState; + + private int mDisplayOrder; + private int mSortOrder; + private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT; + private ContactPhotoManager mPhotoManager; + private ContactsPreferences mContactsPrefs; + private boolean mForceLoad; + private boolean mDarkTheme; + private int mDirectoryListStatus = STATUS_NOT_LOADED; + + /** + * Indicates whether we are doing the initial complete load of data (false) or a refresh caused by + * a change notification (true) + */ + private boolean mLoadPriorityDirectoriesOnly; + + private Context mContext; + + private LoaderManager mLoaderManager; + + private Handler mDelayedDirectorySearchHandler = + new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == DIRECTORY_SEARCH_MESSAGE) { + loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj); + } + } + }; + private ContactsPreferences.ChangeListener mPreferencesChangeListener = + new ContactsPreferences.ChangeListener() { + @Override + public void onChange() { + loadPreferences(); + reloadData(); + } + }; + + protected abstract View inflateView(LayoutInflater inflater, ViewGroup container); + + protected abstract T createListAdapter(); + + /** + * @param position Please note that the position is already adjusted for header views, so "0" + * means the first list item below header views. + */ + protected abstract void onItemClick(int position, long id); + + /** + * @param position Please note that the position is already adjusted for header views, so "0" + * means the first list item below header views. + */ + protected boolean onItemLongClick(int position, long id) { + return false; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + setContext(activity); + setLoaderManager(super.getLoaderManager()); + } + + @Override + public Context getContext() { + return mContext; + } + + /** Sets a context for the fragment in the unit test environment. */ + public void setContext(Context context) { + mContext = context; + configurePhotoLoader(); + } + + public void setEnabled(boolean enabled) { + if (mEnabled != enabled) { + mEnabled = enabled; + if (mAdapter != null) { + if (mEnabled) { + reloadData(); + } else { + mAdapter.clearPartitions(); + } + } + } + } + + @Override + public LoaderManager getLoaderManager() { + return mLoaderManager; + } + + /** Overrides a loader manager for use in unit tests. */ + public void setLoaderManager(LoaderManager loaderManager) { + mLoaderManager = loaderManager; + } + + public T getAdapter() { + return mAdapter; + } + + @Override + public View getView() { + return mView; + } + + public ListView getListView() { + return mListView; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled); + outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled); + outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled); + outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled); + outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile); + outState.putBoolean(KEY_SEARCH_MODE, mSearchMode); + outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled); + outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition); + outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode); + outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible); + outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility); + outState.putString(KEY_QUERY_STRING, mQueryString); + outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit); + outState.putBoolean(KEY_DARK_THEME, mDarkTheme); + + if (mListView != null) { + outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState()); + } + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + restoreSavedState(savedState); + mAdapter = createListAdapter(); + mContactsPrefs = new ContactsPreferences(mContext); + } + + public void restoreSavedState(Bundle savedState) { + if (savedState == null) { + return; + } + + mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED); + mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED); + mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED); + mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED); + mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE); + mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE); + mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED); + mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION); + mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE); + mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE); + mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY); + mQueryString = savedState.getString(KEY_QUERY_STRING); + mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT); + mDarkTheme = savedState.getBoolean(KEY_DARK_THEME); + + // Retrieve list state. This will be applied in onLoadFinished + mListState = savedState.getParcelable(KEY_LIST_STATE); + } + + @Override + public void onStart() { + super.onStart(); + + mContactsPrefs.registerChangeListener(mPreferencesChangeListener); + + mForceLoad = loadPreferences(); + + mDirectoryListStatus = STATUS_NOT_LOADED; + mLoadPriorityDirectoriesOnly = true; + + startLoading(); + } + + protected void startLoading() { + if (mAdapter == null) { + // The method was called before the fragment was started + return; + } + + configureAdapter(); + int partitionCount = mAdapter.getPartitionCount(); + for (int i = 0; i < partitionCount; i++) { + Partition partition = mAdapter.getPartition(i); + if (partition instanceof DirectoryPartition) { + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) { + if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) { + startLoadingDirectoryPartition(i); + } + } + } else { + getLoaderManager().initLoader(i, null, this); + } + } + + // Next time this method is called, we should start loading non-priority directories + mLoadPriorityDirectoriesOnly = false; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == DIRECTORY_LOADER_ID) { + DirectoryListLoader loader = new DirectoryListLoader(mContext); + loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode()); + loader.setLocalInvisibleDirectoryEnabled( + ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED); + return loader; + } else { + CursorLoader loader = createCursorLoader(mContext); + long directoryId = + args != null && args.containsKey(DIRECTORY_ID_ARG_KEY) + ? args.getLong(DIRECTORY_ID_ARG_KEY) + : Directory.DEFAULT; + mAdapter.configureLoader(loader, directoryId); + return loader; + } + } + + public CursorLoader createCursorLoader(Context context) { + return new CursorLoader(context, null, null, null, null, null) { + @Override + protected Cursor onLoadInBackground() { + try { + return super.onLoadInBackground(); + } catch (RuntimeException e) { + // We don't even know what the projection should be, so no point trying to + // return an empty MatrixCursor with the correct projection here. + Log.w(TAG, "RuntimeException while trying to query ContactsProvider."); + return null; + } + } + }; + } + + private void startLoadingDirectoryPartition(int partitionIndex) { + DirectoryPartition partition = (DirectoryPartition) mAdapter.getPartition(partitionIndex); + partition.setStatus(DirectoryPartition.STATUS_LOADING); + long directoryId = partition.getDirectoryId(); + if (mForceLoad) { + if (directoryId == Directory.DEFAULT) { + loadDirectoryPartition(partitionIndex, partition); + } else { + loadDirectoryPartitionDelayed(partitionIndex, partition); + } + } else { + Bundle args = new Bundle(); + args.putLong(DIRECTORY_ID_ARG_KEY, directoryId); + getLoaderManager().initLoader(partitionIndex, args, this); + } + } + + /** + * Queues up a delayed request to search the specified directory. Since directory search will + * likely introduce a lot of network traffic, we want to wait for a pause in the user's typing + * before sending a directory request. + */ + private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) { + mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition); + Message msg = + mDelayedDirectorySearchHandler.obtainMessage( + DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition); + mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS); + } + + /** Loads the directory partition. */ + protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) { + Bundle args = new Bundle(); + args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId()); + getLoaderManager().restartLoader(partitionIndex, args, this); + } + + /** Cancels all queued directory loading requests. */ + private void removePendingDirectorySearchRequests() { + mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (!mEnabled) { + return; + } + + int loaderId = loader.getId(); + if (loaderId == DIRECTORY_LOADER_ID) { + mDirectoryListStatus = STATUS_LOADED; + mAdapter.changeDirectories(data); + startLoading(); + } else { + onPartitionLoaded(loaderId, data); + if (isSearchMode()) { + int directorySearchMode = getDirectorySearchMode(); + if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) { + if (mDirectoryListStatus == STATUS_NOT_LOADED) { + mDirectoryListStatus = STATUS_LOADING; + getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this); + } else { + startLoading(); + } + } + } else { + mDirectoryListStatus = STATUS_NOT_LOADED; + getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); + } + } + } + + @Override + public void onLoaderReset(Loader loader) {} + + protected void onPartitionLoaded(int partitionIndex, Cursor data) { + if (partitionIndex >= mAdapter.getPartitionCount()) { + // When we get unsolicited data, ignore it. This could happen + // when we are switching from search mode to the default mode. + return; + } + + mAdapter.changeCursor(partitionIndex, data); + setProfileHeader(); + + if (!isLoading()) { + completeRestoreInstanceState(); + } + } + + public boolean isLoading() { + if (mAdapter != null && mAdapter.isLoading()) { + return true; + } + + return isLoadingDirectoryList(); + + } + + public boolean isLoadingDirectoryList() { + return isSearchMode() + && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE + && (mDirectoryListStatus == STATUS_NOT_LOADED || mDirectoryListStatus == STATUS_LOADING); + } + + @Override + public void onStop() { + super.onStop(); + mContactsPrefs.unregisterChangeListener(); + mAdapter.clearPartitions(); + } + + protected void reloadData() { + removePendingDirectorySearchRequests(); + mAdapter.onDataReload(); + mLoadPriorityDirectoriesOnly = true; + mForceLoad = true; + startLoading(); + } + + /** + * Shows a view at the top of the list with a pseudo local profile prompting the user to add a + * local profile. Default implementation does nothing. + */ + protected void setProfileHeader() { + mUserProfileExists = false; + } + + /** Provides logic that dismisses this fragment. The default implementation does nothing. */ + protected void finish() {} + + public boolean isSectionHeaderDisplayEnabled() { + return mSectionHeaderDisplayEnabled; + } + + public void setSectionHeaderDisplayEnabled(boolean flag) { + if (mSectionHeaderDisplayEnabled != flag) { + mSectionHeaderDisplayEnabled = flag; + if (mAdapter != null) { + mAdapter.setSectionHeaderDisplayEnabled(flag); + } + configureVerticalScrollbar(); + } + } + + public boolean isVisibleScrollbarEnabled() { + return mVisibleScrollbarEnabled; + } + + public void setVisibleScrollbarEnabled(boolean flag) { + if (mVisibleScrollbarEnabled != flag) { + mVisibleScrollbarEnabled = flag; + configureVerticalScrollbar(); + } + } + + private void configureVerticalScrollbar() { + boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled(); + + if (mListView != null) { + mListView.setFastScrollEnabled(hasScrollbar); + mListView.setFastScrollAlwaysVisible(hasScrollbar); + mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition); + mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); + } + } + + public boolean isPhotoLoaderEnabled() { + return mPhotoLoaderEnabled; + } + + public void setPhotoLoaderEnabled(boolean flag) { + mPhotoLoaderEnabled = flag; + configurePhotoLoader(); + } + + public void setQuickContactEnabled(boolean flag) { + this.mQuickContactEnabled = flag; + } + + public void setAdjustSelectionBoundsEnabled(boolean flag) { + mAdjustSelectionBoundsEnabled = flag; + } + + public final boolean isSearchMode() { + return mSearchMode; + } + + /** + * Enter/exit search mode. This is method is tightly related to the current query, and should only + * be called by {@link #setQueryString}. + * + *

Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it. + */ + protected void setSearchMode(boolean flag) { + if (mSearchMode != flag) { + mSearchMode = flag; + setSectionHeaderDisplayEnabled(!mSearchMode); + + if (!flag) { + mDirectoryListStatus = STATUS_NOT_LOADED; + getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); + } + + if (mAdapter != null) { + mAdapter.setSearchMode(flag); + + mAdapter.clearPartitions(); + if (!flag) { + // If we are switching from search to regular display, remove all directory + // partitions after default one, assuming they are remote directories which + // should be cleaned up on exiting the search mode. + mAdapter.removeDirectoriesAfterDefault(); + } + mAdapter.configureDefaultPartition(false, flag); + } + + if (mListView != null) { + mListView.setFastScrollEnabled(!flag); + } + } + } + + public final String getQueryString() { + return mQueryString; + } + + public void setQueryString(String queryString) { + if (!TextUtils.equals(mQueryString, queryString)) { + if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) { + if (TextUtils.isEmpty(mQueryString)) { + // Restore the adapter if the query used to be empty. + mListView.setAdapter(mAdapter); + } else if (TextUtils.isEmpty(queryString)) { + // Instantly clear the list view if the new query is empty. + mListView.setAdapter(null); + } + } + + mQueryString = queryString; + setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery); + + if (mAdapter != null) { + mAdapter.setQueryString(queryString); + reloadData(); + } + } + } + + public void setShowEmptyListForNullQuery(boolean show) { + mShowEmptyListForEmptyQuery = show; + } + + public boolean getShowEmptyListForNullQuery() { + return mShowEmptyListForEmptyQuery; + } + + public int getDirectoryLoaderId() { + return DIRECTORY_LOADER_ID; + } + + public int getDirectorySearchMode() { + return mDirectorySearchMode; + } + + public void setDirectorySearchMode(int mode) { + mDirectorySearchMode = mode; + } + + protected int getContactNameDisplayOrder() { + return mDisplayOrder; + } + + protected void setContactNameDisplayOrder(int displayOrder) { + mDisplayOrder = displayOrder; + if (mAdapter != null) { + mAdapter.setContactNameDisplayOrder(displayOrder); + } + } + + public int getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(int sortOrder) { + mSortOrder = sortOrder; + if (mAdapter != null) { + mAdapter.setSortOrder(sortOrder); + } + } + + public void setDirectoryResultLimit(int limit) { + mDirectoryResultLimit = limit; + } + + protected boolean loadPreferences() { + boolean changed = false; + if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) { + setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder()); + changed = true; + } + + if (getSortOrder() != mContactsPrefs.getSortOrder()) { + setSortOrder(mContactsPrefs.getSortOrder()); + changed = true; + } + + return changed; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + onCreateView(inflater, container); + + boolean searchMode = isSearchMode(); + mAdapter.setSearchMode(searchMode); + mAdapter.configureDefaultPartition(false, searchMode); + mAdapter.setPhotoLoader(mPhotoManager); + mListView.setAdapter(mAdapter); + + if (!isSearchMode()) { + mListView.setFocusableInTouchMode(true); + mListView.requestFocus(); + } + + return mView; + } + + protected void onCreateView(LayoutInflater inflater, ViewGroup container) { + mView = inflateView(inflater, container); + + mListView = (ListView) mView.findViewById(android.R.id.list); + if (mListView == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + "'android.R.id.list'"); + } + + View emptyView = mView.findViewById(android.R.id.empty); + if (emptyView != null) { + mListView.setEmptyView(emptyView); + } + + mListView.setOnItemClickListener(this); + mListView.setOnItemLongClickListener(this); + mListView.setOnFocusChangeListener(this); + mListView.setOnTouchListener(this); + mListView.setFastScrollEnabled(!isSearchMode()); + + // Tell list view to not show dividers. We'll do it ourself so that we can *not* show + // them when an A-Z headers is visible. + mListView.setDividerHeight(0); + + // We manually save/restore the listview state + mListView.setSaveEnabled(false); + + configureVerticalScrollbar(); + configurePhotoLoader(); + + getAdapter().setFragmentRootView(getView()); + + ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView); + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + if (getActivity() != null && getView() != null && !hidden) { + // If the padding was last applied when in a hidden state, it may have been applied + // incorrectly. Therefore we need to reapply it. + ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView()); + } + } + + protected void configurePhotoLoader() { + if (isPhotoLoaderEnabled() && mContext != null) { + if (mPhotoManager == null) { + mPhotoManager = ContactPhotoManager.getInstance(mContext); + } + if (mListView != null) { + mListView.setOnScrollListener(this); + } + if (mAdapter != null) { + mAdapter.setPhotoLoader(mPhotoManager); + } + } + } + + protected void configureAdapter() { + if (mAdapter == null) { + return; + } + + mAdapter.setQuickContactEnabled(mQuickContactEnabled); + mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled); + mAdapter.setQueryString(mQueryString); + mAdapter.setDirectorySearchMode(mDirectorySearchMode); + mAdapter.setPinnedPartitionHeadersEnabled(false); + mAdapter.setContactNameDisplayOrder(mDisplayOrder); + mAdapter.setSortOrder(mSortOrder); + mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled); + mAdapter.setSelectionVisible(mSelectionVisible); + mAdapter.setDirectoryResultLimit(mDirectoryResultLimit); + mAdapter.setDarkTheme(mDarkTheme); + } + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { + mPhotoManager.pause(); + } else if (isPhotoLoaderEnabled()) { + mPhotoManager.resume(); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + hideSoftKeyboard(); + + int adjPosition = position - mListView.getHeaderViewsCount(); + if (adjPosition >= 0) { + onItemClick(adjPosition, id); + } + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + int adjPosition = position - mListView.getHeaderViewsCount(); + + if (adjPosition >= 0) { + return onItemLongClick(adjPosition, id); + } + return false; + } + + private void hideSoftKeyboard() { + // Hide soft keyboard, if visible + InputMethodManager inputMethodManager = + (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0); + } + + /** Dismisses the soft keyboard when the list takes focus. */ + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (view == mListView && hasFocus) { + hideSoftKeyboard(); + } + } + + /** Dismisses the soft keyboard when the list is touched. */ + @Override + public boolean onTouch(View view, MotionEvent event) { + if (view == mListView) { + hideSoftKeyboard(); + } + return false; + } + + @Override + public void onPause() { + // Save the scrolling state of the list view + mListViewTopIndex = mListView.getFirstVisiblePosition(); + View v = mListView.getChildAt(0); + mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop()); + + super.onPause(); + removePendingDirectorySearchRequests(); + } + + @Override + public void onResume() { + super.onResume(); + // Restore the selection of the list view. See b/19982820. + // This has to be done manually because if the list view has its emptyView set, + // the scrolling state will be reset when clearPartitions() is called on the adapter. + mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset); + } + + /** Restore the list state after the adapter is populated. */ + protected void completeRestoreInstanceState() { + if (mListState != null) { + mListView.onRestoreInstanceState(mListState); + mListState = null; + } + } + + public void setDarkTheme(boolean value) { + mDarkTheme = value; + if (mAdapter != null) { + mAdapter.setDarkTheme(value); + } + } + + private int getDefaultVerticalScrollbarPosition() { + final Locale locale = Locale.getDefault(); + final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); + switch (layoutDirection) { + case View.LAYOUT_DIRECTION_RTL: + return View.SCROLLBAR_POSITION_LEFT; + case View.LAYOUT_DIRECTION_LTR: + default: + return View.SCROLLBAR_POSITION_RIGHT; + } + } +} diff --git a/java/com/android/contacts/common/list/ContactListAdapter.java b/java/com/android/contacts/common/list/ContactListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..6cd31181147999c6a1952f24fffea0b82f871d44 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListAdapter.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.SearchSnippets; +import android.view.ViewGroup; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.R; +import com.android.contacts.common.preference.ContactsPreferences; + +/** + * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. Also + * includes support for including the {@link ContactsContract.Profile} record in the list. + */ +public abstract class ContactListAdapter extends ContactEntryListAdapter { + + private CharSequence mUnknownNameText; + + public ContactListAdapter(Context context) { + super(context); + + mUnknownNameText = context.getText(R.string.missing_name); + } + + protected static Uri buildSectionIndexerUri(Uri uri) { + return uri.buildUpon().appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build(); + } + + public Uri getContactUri(int partitionIndex, Cursor cursor) { + long contactId = cursor.getLong(ContactQuery.CONTACT_ID); + String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY); + Uri uri = Contacts.getLookupUri(contactId, lookupKey); + long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId(); + if (uri != null && directoryId != Directory.DEFAULT) { + uri = + uri.buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) + .build(); + } + return uri; + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + ContactListItemView view = super.newView(context, partition, cursor, position, parent); + view.setUnknownNameText(mUnknownNameText); + view.setQuickContactEnabled(isQuickContactEnabled()); + view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); + view.setActivatedStateSupported(isSelectionVisible()); + return view; + } + + protected void bindSectionHeaderAndDivider( + ContactListItemView view, int position, Cursor cursor) { + view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); + if (isSectionHeaderDisplayEnabled()) { + Placement placement = getItemPlacementInSection(position); + view.setSectionHeader(placement.sectionHeader); + } else { + view.setSectionHeader(null); + } + } + + protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) { + if (!isPhotoSupported(partitionIndex)) { + view.removePhotoView(); + return; + } + + // Set the photo, if available + long photoId = 0; + if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) { + photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID); + } + + if (photoId != 0) { + getPhotoLoader() + .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null); + } else { + final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI); + final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); + DefaultImageRequest request = null; + if (photoUri == null) { + request = + getDefaultImageRequestFromCursor( + cursor, ContactQuery.CONTACT_DISPLAY_NAME, ContactQuery.CONTACT_LOOKUP_KEY); + } + getPhotoLoader() + .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request); + } + } + + protected void bindNameAndViewId(final ContactListItemView view, Cursor cursor) { + view.showDisplayName(cursor, ContactQuery.CONTACT_DISPLAY_NAME); + // Note: we don't show phonetic any more (See issue 5265330) + + bindViewId(view, cursor, ContactQuery.CONTACT_ID); + } + + protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) { + view.showPresenceAndStatusMessage( + cursor, ContactQuery.CONTACT_PRESENCE_STATUS, ContactQuery.CONTACT_CONTACT_STATUS); + } + + protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) { + view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET); + } + + @Override + public void changeCursor(int partitionIndex, Cursor cursor) { + super.changeCursor(partitionIndex, cursor); + + if (cursor == null || !cursor.moveToFirst()) { + return; + } + + // hasProfile tells whether the first row is a profile + final boolean hasProfile = cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1; + + // Add ME profile on top of favorites + cursor.moveToFirst(); + setProfileExists(hasProfile); + } + + /** @return Projection useful for children. */ + protected final String[] getProjection(boolean forSearch) { + final int sortOrder = getContactNameDisplayOrder(); + if (forSearch) { + if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + return ContactQuery.FILTER_PROJECTION_PRIMARY; + } else { + return ContactQuery.FILTER_PROJECTION_ALTERNATIVE; + } + } else { + if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + return ContactQuery.CONTACT_PROJECTION_PRIMARY; + } else { + return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE; + } + } + } + + protected static class ContactQuery { + + public static final int CONTACT_ID = 0; + public static final int CONTACT_DISPLAY_NAME = 1; + public static final int CONTACT_PRESENCE_STATUS = 2; + public static final int CONTACT_CONTACT_STATUS = 3; + public static final int CONTACT_PHOTO_ID = 4; + public static final int CONTACT_PHOTO_URI = 5; + public static final int CONTACT_LOOKUP_KEY = 6; + public static final int CONTACT_IS_USER_PROFILE = 7; + public static final int CONTACT_PHONETIC_NAME = 8; + public static final int CONTACT_STARRED = 9; + public static final int CONTACT_SNIPPET = 10; + private static final String[] CONTACT_PROJECTION_PRIMARY = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_PRIMARY, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + }; + private static final String[] CONTACT_PROJECTION_ALTERNATIVE = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_ALTERNATIVE, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + }; + private static final String[] FILTER_PROJECTION_PRIMARY = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_PRIMARY, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + SearchSnippets.SNIPPET, // 10 + }; + private static final String[] FILTER_PROJECTION_ALTERNATIVE = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_ALTERNATIVE, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + SearchSnippets.SNIPPET, // 10 + }; + } +} diff --git a/java/com/android/contacts/common/list/ContactListFilter.java b/java/com/android/contacts/common/list/ContactListFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..1a03bb64c445485d1839b7099f48cee421f555b5 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListFilter.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; + +/** Contact list filter parameters. */ +public final class ContactListFilter implements Comparable, Parcelable { + + public static final int FILTER_TYPE_DEFAULT = -1; + public static final int FILTER_TYPE_ALL_ACCOUNTS = -2; + public static final int FILTER_TYPE_CUSTOM = -3; + public static final int FILTER_TYPE_STARRED = -4; + public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5; + public static final int FILTER_TYPE_SINGLE_CONTACT = -6; + + public static final int FILTER_TYPE_ACCOUNT = 0; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ContactListFilter createFromParcel(Parcel source) { + int filterType = source.readInt(); + String accountName = source.readString(); + String accountType = source.readString(); + String dataSet = source.readString(); + return new ContactListFilter(filterType, accountType, accountName, dataSet, null); + } + + @Override + public ContactListFilter[] newArray(int size) { + return new ContactListFilter[size]; + } + }; + /** + * Obsolete filter which had been used in Honeycomb. This may be stored in {@link + * SharedPreferences}, but should be replaced with ALL filter when it is found. + * + *

TODO: "group" filter and relevant variables are all obsolete. Remove them. + */ + private static final int FILTER_TYPE_GROUP = 1; + + private static final String KEY_FILTER_TYPE = "filter.type"; + private static final String KEY_ACCOUNT_NAME = "filter.accountName"; + private static final String KEY_ACCOUNT_TYPE = "filter.accountType"; + private static final String KEY_DATA_SET = "filter.dataSet"; + public final int filterType; + public final String accountType; + public final String accountName; + public final String dataSet; + public final Drawable icon; + private String mId; + + public ContactListFilter( + int filterType, String accountType, String accountName, String dataSet, Drawable icon) { + this.filterType = filterType; + this.accountType = accountType; + this.accountName = accountName; + this.dataSet = dataSet; + this.icon = icon; + } + + public static ContactListFilter createFilterWithType(int filterType) { + return new ContactListFilter(filterType, null, null, null, null); + } + + public static ContactListFilter createAccountFilter( + String accountType, String accountName, String dataSet, Drawable icon) { + return new ContactListFilter( + ContactListFilter.FILTER_TYPE_ACCOUNT, accountType, accountName, dataSet, icon); + } + + /** + * Store the given {@link ContactListFilter} to preferences. If the requested filter is of type + * {@link #FILTER_TYPE_SINGLE_CONTACT} then do not save it to preferences because it is a + * temporary state. + */ + public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) { + if (filter != null && filter.filterType == FILTER_TYPE_SINGLE_CONTACT) { + return; + } + prefs + .edit() + .putInt(KEY_FILTER_TYPE, filter == null ? FILTER_TYPE_DEFAULT : filter.filterType) + .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName) + .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType) + .putString(KEY_DATA_SET, filter == null ? null : filter.dataSet) + .apply(); + } + + /** + * Try to obtain ContactListFilter object saved in SharedPreference. If there's no info there, + * return ALL filter instead. + */ + public static ContactListFilter restoreDefaultPreferences(SharedPreferences prefs) { + ContactListFilter filter = restoreFromPreferences(prefs); + if (filter == null) { + filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS); + } + // "Group" filter is obsolete and thus is not exposed anymore. The "single contact mode" + // should also not be stored in preferences anymore since it is a temporary state. + if (filter.filterType == FILTER_TYPE_GROUP || filter.filterType == FILTER_TYPE_SINGLE_CONTACT) { + filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS); + } + return filter; + } + + private static ContactListFilter restoreFromPreferences(SharedPreferences prefs) { + int filterType = prefs.getInt(KEY_FILTER_TYPE, FILTER_TYPE_DEFAULT); + if (filterType == FILTER_TYPE_DEFAULT) { + return null; + } + + String accountName = prefs.getString(KEY_ACCOUNT_NAME, null); + String accountType = prefs.getString(KEY_ACCOUNT_TYPE, null); + String dataSet = prefs.getString(KEY_DATA_SET, null); + return new ContactListFilter(filterType, accountType, accountName, dataSet, null); + } + + public static final String filterTypeToString(int filterType) { + switch (filterType) { + case FILTER_TYPE_DEFAULT: + return "FILTER_TYPE_DEFAULT"; + case FILTER_TYPE_ALL_ACCOUNTS: + return "FILTER_TYPE_ALL_ACCOUNTS"; + case FILTER_TYPE_CUSTOM: + return "FILTER_TYPE_CUSTOM"; + case FILTER_TYPE_STARRED: + return "FILTER_TYPE_STARRED"; + case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY"; + case FILTER_TYPE_SINGLE_CONTACT: + return "FILTER_TYPE_SINGLE_CONTACT"; + case FILTER_TYPE_ACCOUNT: + return "FILTER_TYPE_ACCOUNT"; + default: + return "(unknown)"; + } + } + + /** Returns true if this filter is based on data and may become invalid over time. */ + public boolean isValidationRequired() { + return filterType == FILTER_TYPE_ACCOUNT; + } + + @Override + public String toString() { + switch (filterType) { + case FILTER_TYPE_DEFAULT: + return "default"; + case FILTER_TYPE_ALL_ACCOUNTS: + return "all_accounts"; + case FILTER_TYPE_CUSTOM: + return "custom"; + case FILTER_TYPE_STARRED: + return "starred"; + case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + return "with_phones"; + case FILTER_TYPE_SINGLE_CONTACT: + return "single"; + case FILTER_TYPE_ACCOUNT: + return "account: " + + accountType + + (dataSet != null ? "/" + dataSet : "") + + " " + + accountName; + } + return super.toString(); + } + + @Override + public int compareTo(ContactListFilter another) { + int res = accountName.compareTo(another.accountName); + if (res != 0) { + return res; + } + + res = accountType.compareTo(another.accountType); + if (res != 0) { + return res; + } + + return filterType - another.filterType; + } + + @Override + public int hashCode() { + int code = filterType; + if (accountType != null) { + code = code * 31 + accountType.hashCode(); + code = code * 31 + accountName.hashCode(); + } + if (dataSet != null) { + code = code * 31 + dataSet.hashCode(); + } + return code; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof ContactListFilter)) { + return false; + } + + ContactListFilter otherFilter = (ContactListFilter) other; + return filterType == otherFilter.filterType + && TextUtils.equals(accountName, otherFilter.accountName) + && TextUtils.equals(accountType, otherFilter.accountType) + && TextUtils.equals(dataSet, otherFilter.dataSet); + + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(filterType); + dest.writeString(accountName); + dest.writeString(accountType); + dest.writeString(dataSet); + } + + @Override + public int describeContents() { + return 0; + } + + /** Returns a string that can be used as a stable persistent identifier for this filter. */ + public String getId() { + if (mId == null) { + StringBuilder sb = new StringBuilder(); + sb.append(filterType); + if (accountType != null) { + sb.append('-').append(accountType); + } + if (dataSet != null) { + sb.append('/').append(dataSet); + } + if (accountName != null) { + sb.append('-').append(accountName.replace('-', '_')); + } + mId = sb.toString(); + } + return mId; + } + + /** + * Adds the account query parameters to the given {@code uriBuilder}. + * + * @throws IllegalStateException if the filter type is not {@link #FILTER_TYPE_ACCOUNT}. + */ + public Uri.Builder addAccountQueryParameterToUrl(Uri.Builder uriBuilder) { + if (filterType != FILTER_TYPE_ACCOUNT) { + throw new IllegalStateException("filterType must be FILTER_TYPE_ACCOUNT"); + } + uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName); + uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType); + if (!TextUtils.isEmpty(dataSet)) { + uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet); + } + return uriBuilder; + } + + public String toDebugString() { + final StringBuilder builder = new StringBuilder(); + builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")"); + if (filterType == FILTER_TYPE_ACCOUNT) { + builder + .append(", accountType: " + accountType) + .append(", accountName: " + accountName) + .append(", dataSet: " + dataSet); + } + builder.append(", icon: " + icon + "]"); + return builder.toString(); + } +} diff --git a/java/com/android/contacts/common/list/ContactListFilterController.java b/java/com/android/contacts/common/list/ContactListFilterController.java new file mode 100644 index 0000000000000000000000000000000000000000..d2168f3f25488432e63f4206e6b5f294f0a3d5fb --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListFilterController.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import com.android.contacts.common.model.AccountTypeManager; +import com.android.contacts.common.model.account.AccountWithDataSet; +import java.util.ArrayList; +import java.util.List; + +/** Manages {@link ContactListFilter}. All methods must be called from UI thread. */ +public abstract class ContactListFilterController { + + // singleton to cache the filter controller + private static ContactListFilterControllerImpl sFilterController = null; + + public static ContactListFilterController getInstance(Context context) { + // We may need to synchronize this in the future if background task will call this. + if (sFilterController == null) { + sFilterController = new ContactListFilterControllerImpl(context); + } + return sFilterController; + } + + public abstract void addListener(ContactListFilterListener listener); + + public abstract void removeListener(ContactListFilterListener listener); + + /** Return the currently-active filter. */ + public abstract ContactListFilter getFilter(); + + /** + * @param filter the filter + * @param persistent True when the given filter should be saved soon. False when the filter should + * not be saved. The latter case may happen when some Intent requires a certain type of UI + * (e.g. single contact) temporarily. + */ + public abstract void setContactListFilter(ContactListFilter filter, boolean persistent); + + public abstract void selectCustomFilter(); + + /** + * Checks if the current filter is valid and reset the filter if not. It may happen when an + * account is removed while the filter points to the account with {@link + * ContactListFilter#FILTER_TYPE_ACCOUNT} type, for example. It may also happen if the current + * filter is {@link ContactListFilter#FILTER_TYPE_SINGLE_CONTACT}, in which case, we should switch + * to the last saved filter in {@link SharedPreferences}. + */ + public abstract void checkFilterValidity(boolean notifyListeners); + + public interface ContactListFilterListener { + + void onContactListFilterChanged(); + } +} + +/** + * Stores the {@link ContactListFilter} selected by the user and saves it to {@link + * SharedPreferences} if necessary. + */ +class ContactListFilterControllerImpl extends ContactListFilterController { + + private final Context mContext; + private final List mListeners = + new ArrayList(); + private ContactListFilter mFilter; + + public ContactListFilterControllerImpl(Context context) { + mContext = context; + mFilter = ContactListFilter.restoreDefaultPreferences(getSharedPreferences()); + checkFilterValidity(true /* notify listeners */); + } + + @Override + public void addListener(ContactListFilterListener listener) { + mListeners.add(listener); + } + + @Override + public void removeListener(ContactListFilterListener listener) { + mListeners.remove(listener); + } + + @Override + public ContactListFilter getFilter() { + return mFilter; + } + + private SharedPreferences getSharedPreferences() { + return PreferenceManager.getDefaultSharedPreferences(mContext); + } + + @Override + public void setContactListFilter(ContactListFilter filter, boolean persistent) { + setContactListFilter(filter, persistent, true); + } + + private void setContactListFilter( + ContactListFilter filter, boolean persistent, boolean notifyListeners) { + if (!filter.equals(mFilter)) { + mFilter = filter; + if (persistent) { + ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter); + } + if (notifyListeners && !mListeners.isEmpty()) { + notifyContactListFilterChanged(); + } + } + } + + @Override + public void selectCustomFilter() { + setContactListFilter( + ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_CUSTOM), true); + } + + private void notifyContactListFilterChanged() { + for (ContactListFilterListener listener : mListeners) { + listener.onContactListFilterChanged(); + } + } + + @Override + public void checkFilterValidity(boolean notifyListeners) { + if (mFilter == null) { + return; + } + + switch (mFilter.filterType) { + case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: + setContactListFilter( + ContactListFilter.restoreDefaultPreferences(getSharedPreferences()), + false, + notifyListeners); + break; + case ContactListFilter.FILTER_TYPE_ACCOUNT: + if (!filterAccountExists()) { + // The current account filter points to invalid account. Use "all" filter + // instead. + setContactListFilter( + ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), + true, + notifyListeners); + } + } + } + + /** @return true if the Account for the current filter exists. */ + private boolean filterAccountExists() { + final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext); + final AccountWithDataSet filterAccount = + new AccountWithDataSet(mFilter.accountName, mFilter.accountType, mFilter.dataSet); + return accountTypeManager.contains(filterAccount, false); + } +} diff --git a/java/com/android/contacts/common/list/ContactListItemView.java b/java/com/android/contacts/common/list/ContactListItemView.java new file mode 100644 index 0000000000000000000000000000000000000000..76842483a9feac47d6dcd6a80445ce46a43d25b7 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListItemView.java @@ -0,0 +1,1513 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.SearchSnippets; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.SelectionBoundsAdjuster; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.contacts.common.ContactPresenceIconUtil; +import com.android.contacts.common.ContactStatusUtil; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.format.TextHighlighter; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.contacts.common.util.SearchUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.ViewUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A custom view for an item in the contact list. The view contains the contact's photo, a set of + * text views (for name, status, etc...) and icons for presence and call. The view uses no XML file + * for layout and all the measurements and layouts are done in the onMeasure and onLayout methods. + * + *

The layout puts the contact's photo on the right side of the view, the call icon (if present) + * to the left of the photo, the text lines are aligned to the left and the presence icon (if + * present) is set to the left of the status line. + * + *

The layout also supports a header (used as a header of a group of contacts) that is above the + * contact's data and a divider between contact view. + */ +public class ContactListItemView extends ViewGroup implements SelectionBoundsAdjuster { + private static final Pattern SPLIT_PATTERN = + Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); + static final char SNIPPET_START_MATCH = '['; + static final char SNIPPET_END_MATCH = ']'; + /** A helper used to highlight a prefix in a text field. */ + private final TextHighlighter mTextHighlighter; + // Style values for layout and appearance + // The initialized values are defaults if none is provided through xml. + private int mPreferredHeight = 0; + private int mGapBetweenImageAndText = 0; + private int mGapBetweenLabelAndData = 0; + private int mPresenceIconMargin = 4; + private int mPresenceIconSize = 16; + private int mTextIndent = 0; + private int mTextOffsetTop; + private int mNameTextViewTextSize; + private int mHeaderWidth; + private Drawable mActivatedBackgroundDrawable; + private int mVideoCallIconSize = 32; + private int mVideoCallIconMargin = 16; + // Set in onLayout. Represent left and right position of the View on the screen. + private int mLeftOffset; + private int mRightOffset; + /** Used with {@link #mLabelView}, specifying the width ratio between label and data. */ + private int mLabelViewWidthWeight = 3; + /** Used with {@link #mDataView}, specifying the width ratio between label and data. */ + private int mDataViewWidthWeight = 5; + + private ArrayList mNameHighlightSequence; + private ArrayList mNumberHighlightSequence; + // Highlighting prefix for names. + private String mHighlightedPrefix; + /** Used to notify listeners when a video call icon is clicked. */ + private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener; + /** Indicates whether to show the "video call" icon, used to initiate a video call. */ + private boolean mShowVideoCallIcon = false; + /** Indicates whether the view should leave room for the "video call" icon. */ + private boolean mSupportVideoCallIcon = false; + + private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */); + // Header layout data + private TextView mHeaderTextView; + private boolean mIsSectionHeaderEnabled; + // The views inside the contact view + private boolean mQuickContactEnabled = true; + private QuickContactBadge mQuickContact; + private ImageView mPhotoView; + private TextView mNameTextView; + private TextView mLabelView; + private TextView mDataView; + private TextView mSnippetView; + private TextView mStatusView; + private ImageView mPresenceIcon; + private ImageView mVideoCallIcon; + private ImageView mWorkProfileIcon; + private ColorStateList mSecondaryTextColor; + private int mDefaultPhotoViewSize = 0; + /** + * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding + * to align other data in this View. + */ + private int mPhotoViewWidth; + /** + * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding. + */ + private int mPhotoViewHeight; + /** + * Only effective when {@link #mPhotoView} is null. When true all the Views on the right side of + * the photo should have horizontal padding on those left assuming there is a photo. + */ + private boolean mKeepHorizontalPaddingForPhotoView; + /** Only effective when {@link #mPhotoView} is null. */ + private boolean mKeepVerticalPaddingForPhotoView; + /** + * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used. + * False indicates those values should be updated before being used in position calculation. + */ + private boolean mPhotoViewWidthAndHeightAreReady = false; + + private int mNameTextViewHeight; + private int mNameTextViewTextColor = Color.BLACK; + private int mPhoneticNameTextViewHeight; + private int mLabelViewHeight; + private int mDataViewHeight; + private int mSnippetTextViewHeight; + private int mStatusTextViewHeight; + private int mCheckBoxWidth; + // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the + // same row. + private int mLabelAndDataViewMaxHeight; + private boolean mActivatedStateSupported; + private boolean mAdjustSelectionBoundsEnabled = true; + private Rect mBoundsWithoutHeader = new Rect(); + private CharSequence mUnknownNameText; + private int mPosition; + + public ContactListItemView(Context context) { + super(context); + + mTextHighlighter = new TextHighlighter(Typeface.BOLD); + mNameHighlightSequence = new ArrayList(); + mNumberHighlightSequence = new ArrayList(); + } + + public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) { + this(context, attrs); + + mSupportVideoCallIcon = supportVideoCallIcon; + } + + public ContactListItemView(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a; + + if (R.styleable.ContactListItemView != null) { + // Read all style values + a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); + mPreferredHeight = + a.getDimensionPixelSize( + R.styleable.ContactListItemView_list_item_height, mPreferredHeight); + mActivatedBackgroundDrawable = + a.getDrawable(R.styleable.ContactListItemView_activated_background); + + mGapBetweenImageAndText = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_gap_between_image_and_text, + mGapBetweenImageAndText); + mGapBetweenLabelAndData = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_gap_between_label_and_data, + mGapBetweenLabelAndData); + mPresenceIconMargin = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_presence_icon_margin, mPresenceIconMargin); + mPresenceIconSize = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize); + mDefaultPhotoViewSize = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize); + mTextIndent = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_text_indent, mTextIndent); + mTextOffsetTop = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop); + mDataViewWidthWeight = + a.getInteger( + R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight); + mLabelViewWidthWeight = + a.getInteger( + R.styleable.ContactListItemView_list_item_label_width_weight, mLabelViewWidthWeight); + mNameTextViewTextColor = + a.getColor( + R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor); + mNameTextViewTextSize = + (int) + a.getDimension( + R.styleable.ContactListItemView_list_item_name_text_size, + (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size)); + mVideoCallIconSize = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_video_call_icon_size, mVideoCallIconSize); + mVideoCallIconMargin = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_video_call_icon_margin, + mVideoCallIconMargin); + + setPaddingRelative( + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_left, 0), + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_top, 0), + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_right, 0), + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_bottom, 0)); + + a.recycle(); + } + + mTextHighlighter = new TextHighlighter(Typeface.BOLD); + + if (R.styleable.Theme != null) { + a = getContext().obtainStyledAttributes(R.styleable.Theme); + mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary); + a.recycle(); + } + + mHeaderWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); + + if (mActivatedBackgroundDrawable != null) { + mActivatedBackgroundDrawable.setCallback(this); + } + + mNameHighlightSequence = new ArrayList(); + mNumberHighlightSequence = new ArrayList(); + + setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); + } + + public static final PhotoPosition getDefaultPhotoPosition(boolean opposite) { + final Locale locale = Locale.getDefault(); + final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); + switch (layoutDirection) { + case View.LAYOUT_DIRECTION_RTL: + return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT); + case View.LAYOUT_DIRECTION_LTR: + default: + return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT); + } + } + + /** + * Helper method for splitting a string into tokens. The lists passed in are populated with the + * tokens and offsets into the content of each token. The tokenization function parses e-mail + * addresses as a single token; otherwise it splits on any non-alphanumeric character. + * + * @param content Content to split. + * @return List of token strings. + */ + private static List split(String content) { + final Matcher matcher = SPLIT_PATTERN.matcher(content); + final ArrayList tokens = new ArrayList<>(); + while (matcher.find()) { + tokens.add(matcher.group()); + } + return tokens; + } + + public void setUnknownNameText(CharSequence unknownNameText) { + mUnknownNameText = unknownNameText; + } + + public void setQuickContactEnabled(boolean flag) { + mQuickContactEnabled = flag; + } + + /** + * Sets whether the video calling icon is shown. For the video calling icon to be shown, {@link + * #mSupportVideoCallIcon} must be {@code true}. + * + * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false} + * otherwise. + * @param listener Listener to notify when the video calling icon is clicked. + * @param position The position in the adapater of the video calling icon. + */ + public void setShowVideoCallIcon( + boolean showVideoCallIcon, PhoneNumberListAdapter.Listener listener, int position) { + mShowVideoCallIcon = showVideoCallIcon; + mPhoneNumberListAdapterListener = listener; + mPosition = position; + + if (mShowVideoCallIcon) { + if (mVideoCallIcon == null) { + mVideoCallIcon = new ImageView(getContext()); + addView(mVideoCallIcon); + } + mVideoCallIcon.setContentDescription( + getContext().getString(R.string.description_search_video_call)); + mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call); + mVideoCallIcon.setScaleType(ScaleType.CENTER); + mVideoCallIcon.setVisibility(View.VISIBLE); + mVideoCallIcon.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + // Inform the adapter that the video calling icon was clicked. + if (mPhoneNumberListAdapterListener != null) { + mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition); + } + } + }); + } else { + if (mVideoCallIcon != null) { + mVideoCallIcon.setVisibility(View.GONE); + } + } + } + + /** + * Sets whether the view supports a video calling icon. This is independent of whether the view is + * actually showing an icon. Support for the video calling icon ensures that the layout leaves + * space for the video icon, should it be shown. + * + * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false} + * otherwise. + */ + public void setSupportVideoCallIcon(boolean supportVideoCallIcon) { + mSupportVideoCallIcon = supportVideoCallIcon; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // We will match parent's width and wrap content vertically, but make sure + // height is no less than listPreferredItemHeight. + final int specWidth = resolveSize(0, widthMeasureSpec); + final int preferredHeight = mPreferredHeight; + + mNameTextViewHeight = 0; + mPhoneticNameTextViewHeight = 0; + mLabelViewHeight = 0; + mDataViewHeight = 0; + mLabelAndDataViewMaxHeight = 0; + mSnippetTextViewHeight = 0; + mStatusTextViewHeight = 0; + mCheckBoxWidth = 0; + + ensurePhotoViewSize(); + + // Width each TextView is able to use. + int effectiveWidth; + // All the other Views will honor the photo, so available width for them may be shrunk. + if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) { + effectiveWidth = + specWidth + - getPaddingLeft() + - getPaddingRight() + - (mPhotoViewWidth + mGapBetweenImageAndText); + } else { + effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight(); + } + + if (mIsSectionHeaderEnabled) { + effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText; + } + + if (mSupportVideoCallIcon) { + effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin); + } + + // Go over all visible text views and measure actual width of each of them. + // Also calculate their heights to get the total height for this entire view. + + if (isVisible(mNameTextView)) { + // Calculate width for name text - this parallels similar measurement in onLayout. + int nameTextWidth = effectiveWidth; + if (mPhotoPosition != PhotoPosition.LEFT) { + nameTextWidth -= mTextIndent; + } + mNameTextView.measure( + MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mNameTextViewHeight = mNameTextView.getMeasuredHeight(); + } + + // If both data (phone number/email address) and label (type like "MOBILE") are quite long, + // we should ellipsize both using appropriate ratio. + final int dataWidth; + final int labelWidth; + if (isVisible(mDataView)) { + if (isVisible(mLabelView)) { + final int totalWidth = effectiveWidth - mGapBetweenLabelAndData; + dataWidth = + ((totalWidth * mDataViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight)); + labelWidth = + ((totalWidth * mLabelViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight)); + } else { + dataWidth = effectiveWidth; + labelWidth = 0; + } + } else { + dataWidth = 0; + if (isVisible(mLabelView)) { + labelWidth = effectiveWidth; + } else { + labelWidth = 0; + } + } + + if (isVisible(mDataView)) { + mDataView.measure( + MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mDataViewHeight = mDataView.getMeasuredHeight(); + } + + if (isVisible(mLabelView)) { + mLabelView.measure( + MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mLabelViewHeight = mLabelView.getMeasuredHeight(); + } + mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight); + + if (isVisible(mSnippetView)) { + mSnippetView.measure( + MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mSnippetTextViewHeight = mSnippetView.getMeasuredHeight(); + } + + // Status view height is the biggest of the text view and the presence icon + if (isVisible(mPresenceIcon)) { + mPresenceIcon.measure( + MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY)); + mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight(); + } + + if (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) { + mVideoCallIcon.measure( + MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY)); + } + + if (isVisible(mWorkProfileIcon)) { + mWorkProfileIcon.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mNameTextViewHeight = Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight()); + } + + if (isVisible(mStatusView)) { + // Presence and status are in a same row, so status will be affected by icon size. + final int statusWidth; + if (isVisible(mPresenceIcon)) { + statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() - mPresenceIconMargin); + } else { + statusWidth = effectiveWidth; + } + mStatusView.measure( + MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mStatusTextViewHeight = Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight()); + } + + // Calculate height including padding. + int height = + (mNameTextViewHeight + + mPhoneticNameTextViewHeight + + mLabelAndDataViewMaxHeight + + mSnippetTextViewHeight + + mStatusTextViewHeight); + + // Make sure the height is at least as high as the photo + height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop()); + + // Make sure height is at least the preferred height + height = Math.max(height, preferredHeight); + + // Measure the header if it is visible. + if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) { + mHeaderTextView.measure( + MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + + setMeasuredDimension(specWidth, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int height = bottom - top; + final int width = right - left; + + // Determine the vertical bounds by laying out the header first. + int topBound = 0; + int bottomBound = height; + int leftBound = getPaddingLeft(); + int rightBound = width - getPaddingRight(); + + final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this); + + // Put the section header on the left side of the contact view. + if (mIsSectionHeaderEnabled) { + // Align the text view all the way left, to be consistent with Contacts. + if (isLayoutRtl) { + rightBound = width; + } else { + leftBound = 0; + } + if (mHeaderTextView != null) { + int headerHeight = mHeaderTextView.getMeasuredHeight(); + int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop; + + mHeaderTextView.layout( + isLayoutRtl ? rightBound - mHeaderWidth : leftBound, + headerTopBound, + isLayoutRtl ? rightBound : leftBound + mHeaderWidth, + headerTopBound + headerHeight); + } + if (isLayoutRtl) { + rightBound -= mHeaderWidth; + } else { + leftBound += mHeaderWidth; + } + } + + mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound); + mLeftOffset = left + leftBound; + mRightOffset = left + rightBound; + if (mIsSectionHeaderEnabled) { + if (isLayoutRtl) { + rightBound -= mGapBetweenImageAndText; + } else { + leftBound += mGapBetweenImageAndText; + } + } + + if (mActivatedStateSupported && isActivated()) { + mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); + } + + final View photoView = mQuickContact != null ? mQuickContact : mPhotoView; + if (mPhotoPosition == PhotoPosition.LEFT) { + // Photo is the left most view. All the other Views should on the right of the photo. + if (photoView != null) { + // Center the photo vertically + final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; + photoView.layout( + leftBound, photoTop, leftBound + mPhotoViewWidth, photoTop + mPhotoViewHeight); + leftBound += mPhotoViewWidth + mGapBetweenImageAndText; + } else if (mKeepHorizontalPaddingForPhotoView) { + // Draw nothing but keep the padding. + leftBound += mPhotoViewWidth + mGapBetweenImageAndText; + } + } else { + // Photo is the right most view. Right bound should be adjusted that way. + if (photoView != null) { + // Center the photo vertically + final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; + photoView.layout( + rightBound - mPhotoViewWidth, photoTop, rightBound, photoTop + mPhotoViewHeight); + rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); + } else if (mKeepHorizontalPaddingForPhotoView) { + // Draw nothing but keep the padding. + rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); + } + + // Add indent between left-most padding and texts. + leftBound += mTextIndent; + } + + if (mSupportVideoCallIcon) { + // Place the video call button at the end of the list (e.g. take into account RTL mode). + if (isVisible(mVideoCallIcon)) { + // Center the video icon vertically + final int videoIconTop = topBound + (bottomBound - topBound - mVideoCallIconSize) / 2; + + if (!isLayoutRtl) { + // When photo is on left, video icon is placed on the right edge. + mVideoCallIcon.layout( + rightBound - mVideoCallIconSize, + videoIconTop, + rightBound, + videoIconTop + mVideoCallIconSize); + } else { + // When photo is on right, video icon is placed on the left edge. + mVideoCallIcon.layout( + leftBound, + videoIconTop, + leftBound + mVideoCallIconSize, + videoIconTop + mVideoCallIconSize); + } + } + + if (mPhotoPosition == PhotoPosition.LEFT) { + rightBound -= (mVideoCallIconSize + mVideoCallIconMargin); + } else { + leftBound += mVideoCallIconSize + mVideoCallIconMargin; + } + } + + // Center text vertically, then apply the top offset. + final int totalTextHeight = + mNameTextViewHeight + + mPhoneticNameTextViewHeight + + mLabelAndDataViewMaxHeight + + mSnippetTextViewHeight + + mStatusTextViewHeight; + int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop; + + // Work Profile icon align top + int workProfileIconWidth = 0; + if (isVisible(mWorkProfileIcon)) { + workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth(); + final int distanceFromEnd = mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0; + if (mPhotoPosition == PhotoPosition.LEFT) { + // When photo is on left, label is placed on the right edge of the list item. + mWorkProfileIcon.layout( + rightBound - workProfileIconWidth - distanceFromEnd, + textTopBound, + rightBound - distanceFromEnd, + textTopBound + mNameTextViewHeight); + } else { + // When photo is on right, label is placed on the left of data view. + mWorkProfileIcon.layout( + leftBound + distanceFromEnd, + textTopBound, + leftBound + workProfileIconWidth + distanceFromEnd, + textTopBound + mNameTextViewHeight); + } + } + + // Layout all text view and presence icon + // Put name TextView first + if (isVisible(mNameTextView)) { + final int distanceFromEnd = + workProfileIconWidth + + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0); + if (mPhotoPosition == PhotoPosition.LEFT) { + mNameTextView.layout( + leftBound, + textTopBound, + rightBound - distanceFromEnd, + textTopBound + mNameTextViewHeight); + } else { + mNameTextView.layout( + leftBound + distanceFromEnd, + textTopBound, + rightBound, + textTopBound + mNameTextViewHeight); + } + } + + if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) { + textTopBound += mNameTextViewHeight; + } + + // Presence and status + if (isLayoutRtl) { + int statusRightBound = rightBound; + if (isVisible(mPresenceIcon)) { + int iconWidth = mPresenceIcon.getMeasuredWidth(); + mPresenceIcon.layout( + rightBound - iconWidth, textTopBound, rightBound, textTopBound + mStatusTextViewHeight); + statusRightBound -= (iconWidth + mPresenceIconMargin); + } + + if (isVisible(mStatusView)) { + mStatusView.layout( + leftBound, textTopBound, statusRightBound, textTopBound + mStatusTextViewHeight); + } + } else { + int statusLeftBound = leftBound; + if (isVisible(mPresenceIcon)) { + int iconWidth = mPresenceIcon.getMeasuredWidth(); + mPresenceIcon.layout( + leftBound, textTopBound, leftBound + iconWidth, textTopBound + mStatusTextViewHeight); + statusLeftBound += (iconWidth + mPresenceIconMargin); + } + + if (isVisible(mStatusView)) { + mStatusView.layout( + statusLeftBound, textTopBound, rightBound, textTopBound + mStatusTextViewHeight); + } + } + + if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { + textTopBound += mStatusTextViewHeight; + } + + // Rest of text views + int dataLeftBound = leftBound; + + // Label and Data align bottom. + if (isVisible(mLabelView)) { + if (!isLayoutRtl) { + mLabelView.layout( + dataLeftBound, + textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData; + } else { + dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); + mLabelView.layout( + rightBound - mLabelView.getMeasuredWidth(), + textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData); + } + } + + if (isVisible(mDataView)) { + if (!isLayoutRtl) { + mDataView.layout( + dataLeftBound, + textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + } else { + mDataView.layout( + rightBound - mDataView.getMeasuredWidth(), + textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + } + } + if (isVisible(mLabelView) || isVisible(mDataView)) { + textTopBound += mLabelAndDataViewMaxHeight; + } + + if (isVisible(mSnippetView)) { + mSnippetView.layout( + leftBound, textTopBound, rightBound, textTopBound + mSnippetTextViewHeight); + } + } + + @Override + public void adjustListItemSelectionBounds(Rect bounds) { + if (mAdjustSelectionBoundsEnabled) { + bounds.top += mBoundsWithoutHeader.top; + bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); + bounds.left = mBoundsWithoutHeader.left; + bounds.right = mBoundsWithoutHeader.right; + } + } + + protected boolean isVisible(View view) { + return view != null && view.getVisibility() == View.VISIBLE; + } + + /** Extracts width and height from the style */ + private void ensurePhotoViewSize() { + if (!mPhotoViewWidthAndHeightAreReady) { + mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); + if (!mQuickContactEnabled && mPhotoView == null) { + if (!mKeepHorizontalPaddingForPhotoView) { + mPhotoViewWidth = 0; + } + if (!mKeepVerticalPaddingForPhotoView) { + mPhotoViewHeight = 0; + } + } + + mPhotoViewWidthAndHeightAreReady = true; + } + } + + protected int getDefaultPhotoViewSize() { + return mDefaultPhotoViewSize; + } + + /** + * Gets a LayoutParam that corresponds to the default photo size. + * + * @return A new LayoutParam. + */ + private LayoutParams getDefaultPhotoLayoutParams() { + LayoutParams params = generateDefaultLayoutParams(); + params.width = getDefaultPhotoViewSize(); + params.height = params.width; + return params; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mActivatedStateSupported) { + mActivatedBackgroundDrawable.setState(getDrawableState()); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); + } + + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + if (mActivatedStateSupported) { + mActivatedBackgroundDrawable.jumpToCurrentState(); + } + } + + @Override + public void dispatchDraw(Canvas canvas) { + if (mActivatedStateSupported && isActivated()) { + mActivatedBackgroundDrawable.draw(canvas); + } + + super.dispatchDraw(canvas); + } + + /** Sets section header or makes it invisible if the title is null. */ + public void setSectionHeader(String title) { + if (!TextUtils.isEmpty(title)) { + if (mHeaderTextView == null) { + mHeaderTextView = new TextView(getContext()); + mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle); + mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); + addView(mHeaderTextView); + } + setMarqueeText(mHeaderTextView, title); + mHeaderTextView.setVisibility(View.VISIBLE); + mHeaderTextView.setAllCaps(true); + } else if (mHeaderTextView != null) { + mHeaderTextView.setVisibility(View.GONE); + } + } + + public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) { + mIsSectionHeaderEnabled = isSectionHeaderEnabled; + } + + /** Returns the quick contact badge, creating it if necessary. */ + public QuickContactBadge getQuickContact() { + if (!mQuickContactEnabled) { + throw new IllegalStateException("QuickContact is disabled for this view"); + } + if (mQuickContact == null) { + mQuickContact = new QuickContactBadge(getContext()); + if (CompatUtils.isLollipopCompatible()) { + mQuickContact.setOverlay(null); + } + mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams()); + if (mNameTextView != null) { + mQuickContact.setContentDescription( + getContext() + .getString(R.string.description_quick_contact_for, mNameTextView.getText())); + } + + addView(mQuickContact); + mPhotoViewWidthAndHeightAreReady = false; + } + return mQuickContact; + } + + /** Returns the photo view, creating it if necessary. */ + public ImageView getPhotoView() { + if (mPhotoView == null) { + mPhotoView = new ImageView(getContext()); + mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams()); + // Quick contact style used above will set a background - remove it + mPhotoView.setBackground(null); + addView(mPhotoView); + mPhotoViewWidthAndHeightAreReady = false; + } + return mPhotoView; + } + + /** Removes the photo view. */ + public void removePhotoView() { + removePhotoView(false, true); + } + + /** + * Removes the photo view. + * + * @param keepHorizontalPadding True means data on the right side will have padding on left, + * pretending there is still a photo view. + * @param keepVerticalPadding True means the View will have some height enough for accommodating a + * photo view. + */ + public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { + mPhotoViewWidthAndHeightAreReady = false; + mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; + mKeepVerticalPaddingForPhotoView = keepVerticalPadding; + if (mPhotoView != null) { + removeView(mPhotoView); + mPhotoView = null; + } + if (mQuickContact != null) { + removeView(mQuickContact); + mQuickContact = null; + } + } + + /** + * Sets a word prefix that will be highlighted if encountered in fields like name and search + * snippet. This will disable the mask highlighting for names. + * + *

NOTE: must be all upper-case + */ + public void setHighlightedPrefix(String upperCasePrefix) { + mHighlightedPrefix = upperCasePrefix; + } + + /** Clears previously set highlight sequences for the view. */ + public void clearHighlightSequences() { + mNameHighlightSequence.clear(); + mNumberHighlightSequence.clear(); + mHighlightedPrefix = null; + } + + /** + * Adds a highlight sequence to the name highlighter. + * + * @param start The start position of the highlight sequence. + * @param end The end position of the highlight sequence. + */ + public void addNameHighlightSequence(int start, int end) { + mNameHighlightSequence.add(new HighlightSequence(start, end)); + } + + /** + * Adds a highlight sequence to the number highlighter. + * + * @param start The start position of the highlight sequence. + * @param end The end position of the highlight sequence. + */ + public void addNumberHighlightSequence(int start, int end) { + mNumberHighlightSequence.add(new HighlightSequence(start, end)); + } + + /** Returns the text view for the contact name, creating it if necessary. */ + public TextView getNameTextView() { + if (mNameTextView == null) { + mNameTextView = new TextView(getContext()); + mNameTextView.setSingleLine(true); + mNameTextView.setEllipsize(getTextEllipsis()); + mNameTextView.setTextColor(mNameTextViewTextColor); + mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize); + // Manually call setActivated() since this view may be added after the first + // setActivated() call toward this whole item view. + mNameTextView.setActivated(isActivated()); + mNameTextView.setGravity(Gravity.CENTER_VERTICAL); + mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + mNameTextView.setId(R.id.cliv_name_textview); + if (CompatUtils.isLollipopCompatible()) { + mNameTextView.setElegantTextHeight(false); + } + addView(mNameTextView); + } + return mNameTextView; + } + + /** Adds or updates a text view for the data label. */ + public void setLabel(CharSequence text) { + if (TextUtils.isEmpty(text)) { + if (mLabelView != null) { + mLabelView.setVisibility(View.GONE); + } + } else { + getLabelView(); + setMarqueeText(mLabelView, text); + mLabelView.setVisibility(VISIBLE); + } + } + + /** Returns the text view for the data label, creating it if necessary. */ + public TextView getLabelView() { + if (mLabelView == null) { + mLabelView = new TextView(getContext()); + mLabelView.setLayoutParams( + new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + mLabelView.setSingleLine(true); + mLabelView.setEllipsize(getTextEllipsis()); + mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); + if (mPhotoPosition == PhotoPosition.LEFT) { + mLabelView.setAllCaps(true); + } else { + mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); + } + mLabelView.setActivated(isActivated()); + mLabelView.setId(R.id.cliv_label_textview); + addView(mLabelView); + } + return mLabelView; + } + + /** + * Sets phone number for a list item. This takes care of number highlighting if the highlight mask + * exists. + */ + public void setPhoneNumber(String text) { + if (text == null) { + if (mDataView != null) { + mDataView.setVisibility(View.GONE); + } + } else { + getDataView(); + + // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to + // mDataView. Make sure that determination of the highlight sequences are done only + // after number formatting. + + // Sets phone number texts for display after highlighting it, if applicable. + // CharSequence textToSet = text; + final SpannableString textToSet = new SpannableString(text); + + if (mNumberHighlightSequence.size() != 0) { + final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0); + mTextHighlighter.applyMaskingHighlight( + textToSet, highlightSequence.start, highlightSequence.end); + } + + setMarqueeText(mDataView, textToSet); + mDataView.setVisibility(VISIBLE); + + // We have a phone number as "mDataView" so make it always LTR and VIEW_START + mDataView.setTextDirection(View.TEXT_DIRECTION_LTR); + mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + } + } + + private void setMarqueeText(TextView textView, CharSequence text) { + if (getTextEllipsis() == TruncateAt.MARQUEE) { + // To show MARQUEE correctly (with END effect during non-active state), we need + // to build Spanned with MARQUEE in addition to TextView's ellipsize setting. + final SpannableString spannable = new SpannableString(text); + spannable.setSpan( + TruncateAt.MARQUEE, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + textView.setText(spannable); + } else { + textView.setText(text); + } + } + + /** Returns the text view for the data text, creating it if necessary. */ + public TextView getDataView() { + if (mDataView == null) { + mDataView = new TextView(getContext()); + mDataView.setSingleLine(true); + mDataView.setEllipsize(getTextEllipsis()); + mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); + mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + mDataView.setActivated(isActivated()); + mDataView.setId(R.id.cliv_data_view); + if (CompatUtils.isLollipopCompatible()) { + mDataView.setElegantTextHeight(false); + } + addView(mDataView); + } + return mDataView; + } + + /** Adds or updates a text view for the search snippet. */ + public void setSnippet(String text) { + if (TextUtils.isEmpty(text)) { + if (mSnippetView != null) { + mSnippetView.setVisibility(View.GONE); + } + } else { + mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); + mSnippetView.setVisibility(VISIBLE); + if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { + // Give the text-to-speech engine a hint that it's a phone number + mSnippetView.setContentDescription(PhoneNumberUtilsCompat.createTtsSpannable(text)); + } else { + mSnippetView.setContentDescription(null); + } + } + } + + /** Returns the text view for the search snippet, creating it if necessary. */ + public TextView getSnippetView() { + if (mSnippetView == null) { + mSnippetView = new TextView(getContext()); + mSnippetView.setSingleLine(true); + mSnippetView.setEllipsize(getTextEllipsis()); + mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); + mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + mSnippetView.setActivated(isActivated()); + addView(mSnippetView); + } + return mSnippetView; + } + + /** Returns the text view for the status, creating it if necessary. */ + public TextView getStatusView() { + if (mStatusView == null) { + mStatusView = new TextView(getContext()); + mStatusView.setSingleLine(true); + mStatusView.setEllipsize(getTextEllipsis()); + mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); + mStatusView.setTextColor(mSecondaryTextColor); + mStatusView.setActivated(isActivated()); + mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + addView(mStatusView); + } + return mStatusView; + } + + /** Adds or updates a text view for the status. */ + public void setStatus(CharSequence text) { + if (TextUtils.isEmpty(text)) { + if (mStatusView != null) { + mStatusView.setVisibility(View.GONE); + } + } else { + getStatusView(); + setMarqueeText(mStatusView, text); + mStatusView.setVisibility(VISIBLE); + } + } + + /** Adds or updates the presence icon view. */ + public void setPresence(Drawable icon) { + if (icon != null) { + if (mPresenceIcon == null) { + mPresenceIcon = new ImageView(getContext()); + addView(mPresenceIcon); + } + mPresenceIcon.setImageDrawable(icon); + mPresenceIcon.setScaleType(ScaleType.CENTER); + mPresenceIcon.setVisibility(View.VISIBLE); + } else { + if (mPresenceIcon != null) { + mPresenceIcon.setVisibility(View.GONE); + } + } + } + + /** + * Set to display work profile icon or not + * + * @param enabled set to display work profile icon or not + */ + public void setWorkProfileIconEnabled(boolean enabled) { + if (mWorkProfileIcon != null) { + mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE); + } else if (enabled) { + mWorkProfileIcon = new ImageView(getContext()); + addView(mWorkProfileIcon); + mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile); + mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE); + mWorkProfileIcon.setVisibility(View.VISIBLE); + } + } + + private TruncateAt getTextEllipsis() { + return TruncateAt.MARQUEE; + } + + public void showDisplayName(Cursor cursor, int nameColumnIndex) { + CharSequence name = cursor.getString(nameColumnIndex); + setDisplayName(name); + + // Since the quick contact content description is derived from the display name and there is + // no guarantee that when the quick contact is initialized the display name is already set, + // do it here too. + if (mQuickContact != null) { + mQuickContact.setContentDescription( + getContext().getString(R.string.description_quick_contact_for, mNameTextView.getText())); + } + } + + public void setDisplayName(CharSequence name) { + if (!TextUtils.isEmpty(name)) { + // Chooses the available highlighting method for highlighting. + if (mHighlightedPrefix != null) { + name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix); + } else if (mNameHighlightSequence.size() != 0) { + final SpannableString spannableName = new SpannableString(name); + for (HighlightSequence highlightSequence : mNameHighlightSequence) { + mTextHighlighter.applyMaskingHighlight( + spannableName, highlightSequence.start, highlightSequence.end); + } + name = spannableName; + } + } else { + name = mUnknownNameText; + } + setMarqueeText(getNameTextView(), name); + + if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { + // Give the text-to-speech engine a hint that it's a phone number + mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR); + mNameTextView.setContentDescription( + PhoneNumberUtilsCompat.createTtsSpannable(name.toString())); + } else { + // Remove span tags of highlighting for talkback to avoid reading highlighting and rest + // of the name into two separate parts. + mNameTextView.setContentDescription(name.toString()); + } + } + + public void hideDisplayName() { + if (mNameTextView != null) { + removeView(mNameTextView); + mNameTextView = null; + } + } + + /** Sets the proper icon (star or presence or nothing) and/or status message. */ + public void showPresenceAndStatusMessage( + Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex) { + Drawable icon = null; + int presence = 0; + if (!cursor.isNull(presenceColumnIndex)) { + presence = cursor.getInt(presenceColumnIndex); + icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); + } + setPresence(icon); + + String statusMessage = null; + if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { + statusMessage = cursor.getString(contactStatusColumnIndex); + } + // If there is no status message from the contact, but there was a presence value, then use + // the default status message string + if (statusMessage == null && presence != 0) { + statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); + } + setStatus(statusMessage); + } + + /** Shows search snippet. */ + public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { + if (cursor.getColumnCount() <= summarySnippetColumnIndex + || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) { + setSnippet(null); + return; + } + + String snippet = cursor.getString(summarySnippetColumnIndex); + + // Do client side snippeting if provider didn't do it + final Bundle extras = cursor.getExtras(); + if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { + + final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY); + + String displayName = null; + int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); + if (displayNameIndex >= 0) { + displayName = cursor.getString(displayNameIndex); + } + + snippet = updateSnippet(snippet, query, displayName); + + } else { + if (snippet != null) { + int from = 0; + int to = snippet.length(); + int start = snippet.indexOf(SNIPPET_START_MATCH); + if (start == -1) { + snippet = null; + } else { + int firstNl = snippet.lastIndexOf('\n', start); + if (firstNl != -1) { + from = firstNl + 1; + } + int end = snippet.lastIndexOf(SNIPPET_END_MATCH); + if (end != -1) { + int lastNl = snippet.indexOf('\n', end); + if (lastNl != -1) { + to = lastNl; + } + } + + StringBuilder sb = new StringBuilder(); + for (int i = from; i < to; i++) { + char c = snippet.charAt(i); + if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) { + sb.append(c); + } + } + snippet = sb.toString(); + } + } + } + + setSnippet(snippet); + } + + /** + * Used for deferred snippets from the database. The contents come back as large strings which + * need to be extracted for display. + * + * @param snippet The snippet from the database. + * @param query The search query substring. + * @param displayName The contact display name. + * @return The proper snippet to display. + */ + private String updateSnippet(String snippet, String query, String displayName) { + + if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) { + return null; + } + query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase()); + + // If the display name already contains the query term, return empty - snippets should + // not be needed in that case. + if (!TextUtils.isEmpty(displayName)) { + final String lowerDisplayName = displayName.toLowerCase(); + final List nameTokens = split(lowerDisplayName); + for (String nameToken : nameTokens) { + if (nameToken.startsWith(query)) { + return null; + } + } + } + + // The snippet may contain multiple data lines. + // Show the first line that matches the query. + final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query); + + if (matched != null && matched.line != null) { + // Tokenize for long strings since the match may be at the end of it. + // Skip this part for short strings since the whole string will be displayed. + // Most contact strings are short so the snippetize method will be called infrequently. + final int lengthThreshold = + getResources().getInteger(R.integer.snippet_length_before_tokenize); + if (matched.line.length() > lengthThreshold) { + return snippetize(matched.line, matched.startIndex, lengthThreshold); + } else { + return matched.line; + } + } + + // No match found. + return null; + } + + private String snippetize(String line, int matchIndex, int maxLength) { + // Show up to maxLength characters. But we only show full tokens so show the last full token + // up to maxLength characters. So as many starting tokens as possible before trying ending + // tokens. + int remainingLength = maxLength; + int tempRemainingLength = remainingLength; + + // Start the end token after the matched query. + int index = matchIndex; + int endTokenIndex = index; + + // Find the match token first. + while (index < line.length()) { + if (!Character.isLetterOrDigit(line.charAt(index))) { + endTokenIndex = index; + remainingLength = tempRemainingLength; + break; + } + tempRemainingLength--; + index++; + } + + // Find as much content before the match. + index = matchIndex - 1; + tempRemainingLength = remainingLength; + int startTokenIndex = matchIndex; + while (index > -1 && tempRemainingLength > 0) { + if (!Character.isLetterOrDigit(line.charAt(index))) { + startTokenIndex = index; + remainingLength = tempRemainingLength; + } + tempRemainingLength--; + index--; + } + + index = endTokenIndex; + tempRemainingLength = remainingLength; + // Find remaining content at after match. + while (index < line.length() && tempRemainingLength > 0) { + if (!Character.isLetterOrDigit(line.charAt(index))) { + endTokenIndex = index; + } + tempRemainingLength--; + index++; + } + // Append ellipse if there is content before or after. + final StringBuilder sb = new StringBuilder(); + if (startTokenIndex > 0) { + sb.append("..."); + } + sb.append(line.substring(startTokenIndex, endTokenIndex)); + if (endTokenIndex < line.length()) { + sb.append("..."); + } + return sb.toString(); + } + + public void setActivatedStateSupported(boolean flag) { + this.mActivatedStateSupported = flag; + } + + public void setAdjustSelectionBoundsEnabled(boolean enabled) { + mAdjustSelectionBoundsEnabled = enabled; + } + + @Override + public void requestLayout() { + // We will assume that once measured this will not need to resize + // itself, so there is no need to pass the layout request to the parent + // view (ListView). + forceLayout(); + } + + public void setPhotoPosition(PhotoPosition photoPosition) { + mPhotoPosition = photoPosition; + } + + /** + * Set drawable resources directly for the drawable resource of the photo view. + * + * @param drawableId Id of drawable resource. + */ + public void setDrawableResource(int drawableId) { + ImageView photo = getPhotoView(); + photo.setScaleType(ImageView.ScaleType.CENTER); + final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId); + final int iconColor = ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color); + if (CompatUtils.isLollipopCompatible()) { + photo.setImageDrawable(drawable); + photo.setImageTintList(ColorStateList.valueOf(iconColor)); + } else { + final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate(); + DrawableCompat.setTint(drawableWrapper, iconColor); + photo.setImageDrawable(drawableWrapper); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + // If the touch event's coordinates are not within the view's header, then delegate + // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume + // and ignore the touch event. + if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) { + return super.onTouchEvent(event); + } else { + return true; + } + } + + private final boolean pointIsInView(float localX, float localY) { + return localX >= mLeftOffset + && localX < mRightOffset + && localY >= 0 + && localY < (getBottom() - getTop()); + } + + /** + * Where to put contact photo. This affects the other Views' layout or look-and-feel. + * + *

TODO: replace enum with int constants + */ + public enum PhotoPosition { + LEFT, + RIGHT + } + + protected static class HighlightSequence { + + private final int start; + private final int end; + + HighlightSequence(int start, int end) { + this.start = start; + this.end = end; + } + } +} diff --git a/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java new file mode 100644 index 0000000000000000000000000000000000000000..1f3e2bfe33f83108290ade9b7b4714e84170c858 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout.LayoutParams; +import android.widget.TextView; +import com.android.contacts.common.R; + +/** A custom view for the pinned section header shown at the top of the contact list. */ +public class ContactListPinnedHeaderView extends TextView { + + public ContactListPinnedHeaderView(Context context, AttributeSet attrs, View parent) { + super(context, attrs); + + if (R.styleable.ContactListItemView == null) { + return; + } + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); + int backgroundColor = + a.getColor(R.styleable.ContactListItemView_list_item_background_color, Color.WHITE); + int textOffsetTop = + a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_text_offset_top, 0); + int paddingStartOffset = + a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_padding_left, 0); + int textWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); + int widthIncludingPadding = paddingStartOffset + textWidth; + a.recycle(); + + setBackgroundColor(backgroundColor); + setTextAppearance(getContext(), R.style.SectionHeaderStyle); + setLayoutParams(new LayoutParams(textWidth, LayoutParams.WRAP_CONTENT)); + setLayoutDirection(parent.getLayoutDirection()); + setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); + + // Apply text top offset. Multiply by two, because we are implementing this by padding for a + // vertically centered view, rather than adjusting the position directly via a layout. + setPaddingRelative( + 0, getPaddingTop() + (textOffsetTop * 2), getPaddingEnd(), getPaddingBottom()); + } + + /** Sets section header or makes it invisible if the title is null. */ + public void setSectionHeaderTitle(String title) { + if (!TextUtils.isEmpty(title)) { + setText(title); + } else { + setVisibility(View.GONE); + } + } +} diff --git a/java/com/android/contacts/common/list/ContactTileView.java b/java/com/android/contacts/common/list/ContactTileView.java new file mode 100644 index 0000000000000000000000000000000000000000..9273b0583f3d149b9cd460a4ca92e10be679a910 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactTileView.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.graphics.Rect; +import android.net.Uri; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.R; + +/** A ContactTile displays a contact's picture and name */ +public abstract class ContactTileView extends FrameLayout { + + private static final String TAG = ContactTileView.class.getSimpleName(); + protected Listener mListener; + private Uri mLookupUri; + private ImageView mPhoto; + private TextView mName; + private ContactPhotoManager mPhotoManager = null; + + public ContactTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mName = (TextView) findViewById(R.id.contact_tile_name); + mPhoto = (ImageView) findViewById(R.id.contact_tile_image); + + OnClickListener listener = createClickListener(); + setOnClickListener(listener); + } + + protected OnClickListener createClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mListener == null) { + return; + } + mListener.onContactSelected( + getLookupUri(), MoreContactUtils.getTargetRectFromView(ContactTileView.this)); + } + }; + } + + public void setPhotoManager(ContactPhotoManager photoManager) { + mPhotoManager = photoManager; + } + + /** + * Populates the data members to be displayed from the fields in {@link + * com.android.contacts.common.list.ContactEntry} + */ + public void loadFromContact(ContactEntry entry) { + + if (entry != null) { + mName.setText(getNameForView(entry)); + mLookupUri = entry.lookupUri; + + setVisibility(View.VISIBLE); + + if (mPhotoManager != null) { + DefaultImageRequest request = getDefaultImageRequest(entry.namePrimary, entry.lookupKey); + configureViewForImage(entry.photoUri == null); + if (mPhoto != null) { + mPhotoManager.loadPhoto( + mPhoto, + entry.photoUri, + getApproximateImageSize(), + isDarkTheme(), + isContactPhotoCircular(), + request); + + + } + } else { + Log.w(TAG, "contactPhotoManager not set"); + } + } else { + setVisibility(View.INVISIBLE); + } + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public Uri getLookupUri() { + return mLookupUri; + } + + /** + * Returns the string that should actually be displayed as the contact's name. Subclasses can + * override this to return formatted versions of the name - i.e. first name only. + */ + protected String getNameForView(ContactEntry contactEntry) { + return contactEntry.namePrimary; + } + + /** + * Implemented by subclasses to estimate the size of the picture. This can return -1 if only a + * thumbnail is shown anyway + */ + protected abstract int getApproximateImageSize(); + + protected abstract boolean isDarkTheme(); + + /** + * Implemented by subclasses to reconfigure the view's layout and subviews, based on whether or + * not the contact has a user-defined photo. + * + * @param isDefaultImage True if the contact does not have a user-defined contact photo (which + * means a default contact image will be applied by the {@link ContactPhotoManager} + */ + protected void configureViewForImage(boolean isDefaultImage) { + // No-op by default. + } + + /** + * Implemented by subclasses to allow them to return a {@link DefaultImageRequest} with the + * various image parameters defined to match their own layouts. + * + * @param displayName The display name of the contact + * @param lookupKey The lookup key of the contact + * @return A {@link DefaultImageRequest} object with each field configured by the subclass as + * desired, or {@code null}. + */ + protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) { + return new DefaultImageRequest(displayName, lookupKey, isContactPhotoCircular()); + } + + /** + * Whether contact photo should be displayed as a circular image. Implemented by subclasses so + * they can change which drawables to fetch. + */ + protected boolean isContactPhotoCircular() { + return true; + } + + public interface Listener { + + /** Notification that the contact was selected; no specific action is dictated. */ + void onContactSelected(Uri contactLookupUri, Rect viewRect); + + /** Notification that the specified number is to be called. */ + void onCallNumberDirectly(String phoneNumber); + } +} diff --git a/java/com/android/contacts/common/list/ContactsSectionIndexer.java b/java/com/android/contacts/common/list/ContactsSectionIndexer.java new file mode 100644 index 0000000000000000000000000000000000000000..3f0f2b7ee911de192dc3f1957bf5058f0adf5e27 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactsSectionIndexer.java @@ -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. + */ + +package com.android.contacts.common.list; + +import android.text.TextUtils; +import android.widget.SectionIndexer; +import java.util.Arrays; + +/** + * A section indexer that is configured with precomputed section titles and their respective counts. + */ +public class ContactsSectionIndexer implements SectionIndexer { + + private static final String BLANK_HEADER_STRING = " "; + private String[] mSections; + private int[] mPositions; + private int mCount; + + /** + * Constructor. + * + * @param sections a non-null array + * @param counts a non-null array of the same size as sections + */ + public ContactsSectionIndexer(String[] sections, int[] counts) { + if (sections == null || counts == null) { + throw new NullPointerException(); + } + + if (sections.length != counts.length) { + throw new IllegalArgumentException( + "The sections and counts arrays must have the same length"); + } + + // TODO process sections/counts based on current locale and/or specific section titles + + this.mSections = sections; + mPositions = new int[counts.length]; + int position = 0; + for (int i = 0; i < counts.length; i++) { + if (TextUtils.isEmpty(mSections[i])) { + mSections[i] = BLANK_HEADER_STRING; + } else if (!mSections[i].equals(BLANK_HEADER_STRING)) { + mSections[i] = mSections[i].trim(); + } + + mPositions[i] = position; + position += counts[i]; + } + mCount = position; + } + + public Object[] getSections() { + return mSections; + } + + public int getPositionForSection(int section) { + if (section < 0 || section >= mSections.length) { + return -1; + } + + return mPositions[section]; + } + + public int getSectionForPosition(int position) { + if (position < 0 || position >= mCount) { + return -1; + } + + int index = Arrays.binarySearch(mPositions, position); + + /* + * Consider this example: section positions are 0, 3, 5; the supplied + * position is 4. The section corresponding to position 4 starts at + * position 3, so the expected return value is 1. Binary search will not + * find 4 in the array and thus will return -insertPosition-1, i.e. -3. + * To get from that number to the expected value of 1 we need to negate + * and subtract 2. + */ + return index >= 0 ? index : -index - 2; + } + + public void setProfileAndFavoritesHeader(String header, int numberOfItemsToAdd) { + if (mSections != null) { + // Don't do anything if the header is already set properly. + if (mSections.length > 0 && header.equals(mSections[0])) { + return; + } + + // Since the section indexer isn't aware of the profile at the top, we need to add a + // special section at the top for it and shift everything else down. + String[] tempSections = new String[mSections.length + 1]; + int[] tempPositions = new int[mPositions.length + 1]; + tempSections[0] = header; + tempPositions[0] = 0; + for (int i = 1; i <= mPositions.length; i++) { + tempSections[i] = mSections[i - 1]; + tempPositions[i] = mPositions[i - 1] + numberOfItemsToAdd; + } + mSections = tempSections; + mPositions = tempPositions; + mCount = mCount + numberOfItemsToAdd; + } + } +} diff --git a/java/com/android/contacts/common/list/DefaultContactListAdapter.java b/java/com/android/contacts/common/list/DefaultContactListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..7bcae0e0e2cafe3c630649be0057e34b76d2744f --- /dev/null +++ b/java/com/android/contacts/common/list/DefaultContactListAdapter.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.CursorLoader; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.SearchSnippets; +import android.text.TextUtils; +import android.view.View; +import com.android.contacts.common.compat.ContactsCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import java.util.ArrayList; +import java.util.List; + +/** A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. */ +public class DefaultContactListAdapter extends ContactListAdapter { + + public DefaultContactListAdapter(Context context) { + super(context); + } + + @Override + public void configureLoader(CursorLoader loader, long directoryId) { + String sortOrder = null; + if (isSearchMode()) { + String query = getQueryString(); + if (query == null) { + query = ""; + } + query = query.trim(); + if (TextUtils.isEmpty(query)) { + // Regardless of the directory, we don't want anything returned, + // so let's just send a "nothing" query to the local directory. + loader.setUri(Contacts.CONTENT_URI); + loader.setProjection(getProjection(false)); + loader.setSelection("0"); + } else { + final Builder builder = ContactsCompat.getContentUri().buildUpon(); + appendSearchParameters(builder, query, directoryId); + loader.setUri(builder.build()); + loader.setProjection(getProjection(true)); + } + } else { + final ContactListFilter filter = getFilter(); + configureUri(loader, directoryId, filter); + loader.setProjection(getProjection(false)); + configureSelection(loader, directoryId, filter); + } + + if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { + if (sortOrder == null) { + sortOrder = Contacts.SORT_KEY_PRIMARY; + } else { + sortOrder += ", " + Contacts.SORT_KEY_PRIMARY; + } + } else { + if (sortOrder == null) { + sortOrder = Contacts.SORT_KEY_ALTERNATIVE; + } else { + sortOrder += ", " + Contacts.SORT_KEY_ALTERNATIVE; + } + } + loader.setSortOrder(sortOrder); + } + + private void appendSearchParameters(Builder builder, String query, long directoryId) { + builder.appendPath(query); // Builder will encode the query + builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); + if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) { + builder.appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, + String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); + } + builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1"); + } + + protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) { + Uri uri = Contacts.CONTENT_URI; + + if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) { + uri = ContactListAdapter.buildSectionIndexerUri(uri); + } + + // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT + if (filter != null + && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM + && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { + final Uri.Builder builder = uri.buildUpon(); + builder.appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); + if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + filter.addAccountQueryParameterToUrl(builder); + } + uri = builder.build(); + } + + loader.setUri(uri); + } + + private void configureSelection(CursorLoader loader, long directoryId, ContactListFilter filter) { + if (filter == null) { + return; + } + + if (directoryId != Directory.DEFAULT) { + return; + } + + StringBuilder selection = new StringBuilder(); + List selectionArgs = new ArrayList(); + + switch (filter.filterType) { + case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: + { + // We have already added directory=0 to the URI, which takes care of this + // filter + break; + } + case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: + { + // We have already added the lookup key to the URI, which takes care of this + // filter + break; + } + case ContactListFilter.FILTER_TYPE_STARRED: + { + selection.append(Contacts.STARRED + "!=0"); + break; + } + case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + { + selection.append(Contacts.HAS_PHONE_NUMBER + "=1"); + break; + } + case ContactListFilter.FILTER_TYPE_CUSTOM: + { + selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); + if (isCustomFilterForPhoneNumbersOnly()) { + selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); + } + break; + } + case ContactListFilter.FILTER_TYPE_ACCOUNT: + { + // We use query parameters for account filter, so no selection to add here. + break; + } + } + loader.setSelection(selection.toString()); + loader.setSelectionArgs(selectionArgs.toArray(new String[0])); + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + final ContactListItemView view = (ContactListItemView) itemView; + + view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); + + bindSectionHeaderAndDivider(view, position, cursor); + + if (isQuickContactEnabled()) { + bindQuickContact( + view, + partition, + cursor, + ContactQuery.CONTACT_PHOTO_ID, + ContactQuery.CONTACT_PHOTO_URI, + ContactQuery.CONTACT_ID, + ContactQuery.CONTACT_LOOKUP_KEY, + ContactQuery.CONTACT_DISPLAY_NAME); + } else { + if (getDisplayPhotos()) { + bindPhoto(view, partition, cursor); + } + } + + bindNameAndViewId(view, cursor); + bindPresenceAndStatusMessage(view, cursor); + + if (isSearchMode()) { + bindSearchSnippet(view, cursor); + } else { + view.setSnippet(null); + } + } + + private boolean isCustomFilterForPhoneNumbersOnly() { + // TODO: this flag should not be stored in shared prefs. It needs to be in the db. + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + return prefs.getBoolean( + ContactsPreferences.PREF_DISPLAY_ONLY_PHONES, + ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT); + } +} diff --git a/java/com/android/contacts/common/list/DirectoryListLoader.java b/java/com/android/contacts/common/list/DirectoryListLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..48b098c076006fd94c39cd1ccd1d89a0335b7ec1 --- /dev/null +++ b/java/com/android/contacts/common/list/DirectoryListLoader.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Handler; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.DirectoryCompat; + +/** A specialized loader for the list of directories, see {@link Directory}. */ +public class DirectoryListLoader extends AsyncTaskLoader { + + public static final int SEARCH_MODE_NONE = 0; + public static final int SEARCH_MODE_DEFAULT = 1; + public static final int SEARCH_MODE_CONTACT_SHORTCUT = 2; + public static final int SEARCH_MODE_DATA_SHORTCUT = 3; + // This is a virtual column created for a MatrixCursor. + public static final String DIRECTORY_TYPE = "directoryType"; + private static final String TAG = "ContactEntryListAdapter"; + private static final String[] RESULT_PROJECTION = { + Directory._ID, DIRECTORY_TYPE, Directory.DISPLAY_NAME, Directory.PHOTO_SUPPORT, + }; + private final ContentObserver mObserver = + new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + forceLoad(); + } + }; + private int mDirectorySearchMode; + private boolean mLocalInvisibleDirectoryEnabled; + private MatrixCursor mDefaultDirectoryList; + + public DirectoryListLoader(Context context) { + super(context); + } + + public void setDirectorySearchMode(int mode) { + mDirectorySearchMode = mode; + } + + /** + * A flag that indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be + * included in the results. + */ + public void setLocalInvisibleDirectoryEnabled(boolean flag) { + this.mLocalInvisibleDirectoryEnabled = flag; + } + + @Override + protected void onStartLoading() { + getContext().getContentResolver().registerContentObserver(DirectoryQuery.URI, false, mObserver); + forceLoad(); + } + + @Override + protected void onStopLoading() { + getContext().getContentResolver().unregisterContentObserver(mObserver); + } + + @Override + public Cursor loadInBackground() { + if (mDirectorySearchMode == SEARCH_MODE_NONE) { + return getDefaultDirectories(); + } + + MatrixCursor result = new MatrixCursor(RESULT_PROJECTION); + Context context = getContext(); + PackageManager pm = context.getPackageManager(); + String selection; + switch (mDirectorySearchMode) { + case SEARCH_MODE_DEFAULT: + selection = null; + break; + + case SEARCH_MODE_CONTACT_SHORTCUT: + selection = Directory.SHORTCUT_SUPPORT + "=" + Directory.SHORTCUT_SUPPORT_FULL; + break; + + case SEARCH_MODE_DATA_SHORTCUT: + selection = + Directory.SHORTCUT_SUPPORT + + " IN (" + + Directory.SHORTCUT_SUPPORT_FULL + + ", " + + Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY + + ")"; + break; + + default: + throw new RuntimeException("Unsupported directory search mode: " + mDirectorySearchMode); + } + Cursor cursor = null; + try { + cursor = + context + .getContentResolver() + .query( + DirectoryQuery.URI, + DirectoryQuery.PROJECTION, + selection, + null, + DirectoryQuery.ORDER_BY); + + if (cursor == null) { + return result; + } + + while (cursor.moveToNext()) { + long directoryId = cursor.getLong(DirectoryQuery.ID); + if (!mLocalInvisibleDirectoryEnabled && DirectoryCompat.isInvisibleDirectory(directoryId)) { + continue; + } + String directoryType = null; + + String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); + int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); + if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) { + try { + directoryType = pm.getResourcesForApplication(packageName).getString(typeResourceId); + } catch (Exception e) { + Log.e(TAG, "Cannot obtain directory type from package: " + packageName); + } + } + String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); + int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT); + result.addRow(new Object[] {directoryId, directoryType, displayName, photoSupport}); + } + } catch (RuntimeException e) { + Log.w(TAG, "Runtime Exception when querying directory"); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + private Cursor getDefaultDirectories() { + if (mDefaultDirectoryList == null) { + mDefaultDirectoryList = new MatrixCursor(RESULT_PROJECTION); + mDefaultDirectoryList.addRow( + new Object[] {Directory.DEFAULT, getContext().getString(R.string.contactsList), null}); + mDefaultDirectoryList.addRow( + new Object[] { + Directory.LOCAL_INVISIBLE, + getContext().getString(R.string.local_invisible_directory), + null + }); + } + return mDefaultDirectoryList; + } + + @Override + protected void onReset() { + stopLoading(); + } + + private static final class DirectoryQuery { + + public static final Uri URI = DirectoryCompat.getContentUri(); + public static final String ORDER_BY = Directory._ID; + + public static final String[] PROJECTION = { + Directory._ID, + Directory.PACKAGE_NAME, + Directory.TYPE_RESOURCE_ID, + Directory.DISPLAY_NAME, + Directory.PHOTO_SUPPORT, + }; + + public static final int ID = 0; + public static final int PACKAGE_NAME = 1; + public static final int TYPE_RESOURCE_ID = 2; + public static final int DISPLAY_NAME = 3; + public static final int PHOTO_SUPPORT = 4; + } +} diff --git a/java/com/android/contacts/common/list/DirectoryPartition.java b/java/com/android/contacts/common/list/DirectoryPartition.java new file mode 100644 index 0000000000000000000000000000000000000000..26b851041db8716febfb76014a4ffb461dc322a4 --- /dev/null +++ b/java/com/android/contacts/common/list/DirectoryPartition.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.provider.ContactsContract.Directory; +import com.android.common.widget.CompositeCursorAdapter; + +/** Model object for a {@link Directory} row. */ +public final class DirectoryPartition extends CompositeCursorAdapter.Partition { + + public static final int STATUS_NOT_LOADED = 0; + public static final int STATUS_LOADING = 1; + public static final int STATUS_LOADED = 2; + + public static final int RESULT_LIMIT_DEFAULT = -1; + + private long mDirectoryId; + private String mContentUri; + private String mDirectoryType; + private String mDisplayName; + private int mStatus; + private boolean mPriorityDirectory; + private boolean mPhotoSupported; + private int mResultLimit = RESULT_LIMIT_DEFAULT; + private boolean mDisplayNumber = true; + + private String mLabel; + + public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) { + super(showIfEmpty, hasHeader); + } + + /** Directory ID, see {@link Directory}. */ + public long getDirectoryId() { + return mDirectoryId; + } + + public void setDirectoryId(long directoryId) { + this.mDirectoryId = directoryId; + } + + /** + * Directory type resolved from {@link Directory#PACKAGE_NAME} and {@link + * Directory#TYPE_RESOURCE_ID}; + */ + public String getDirectoryType() { + return mDirectoryType; + } + + public void setDirectoryType(String directoryType) { + this.mDirectoryType = directoryType; + } + + /** See {@link Directory#DISPLAY_NAME}. */ + public String getDisplayName() { + return mDisplayName; + } + + public void setDisplayName(String displayName) { + this.mDisplayName = displayName; + } + + public int getStatus() { + return mStatus; + } + + public void setStatus(int status) { + mStatus = status; + } + + public boolean isLoading() { + return mStatus == STATUS_NOT_LOADED || mStatus == STATUS_LOADING; + } + + /** Returns true if this directory should be loaded before non-priority directories. */ + public boolean isPriorityDirectory() { + return mPriorityDirectory; + } + + public void setPriorityDirectory(boolean priorityDirectory) { + mPriorityDirectory = priorityDirectory; + } + + /** Returns true if this directory supports photos. */ + public boolean isPhotoSupported() { + return mPhotoSupported; + } + + public void setPhotoSupported(boolean flag) { + this.mPhotoSupported = flag; + } + + /** + * Max number of results for this directory. Defaults to {@link #RESULT_LIMIT_DEFAULT} which + * implies using the adapter's {@link + * com.android.contacts.common.list.ContactListAdapter#getDirectoryResultLimit()} + */ + public int getResultLimit() { + return mResultLimit; + } + + public void setResultLimit(int resultLimit) { + mResultLimit = resultLimit; + } + + /** + * Used by extended directories to specify a custom content URI. Extended directories MUST have a + * content URI + */ + public String getContentUri() { + return mContentUri; + } + + public void setContentUri(String contentUri) { + mContentUri = contentUri; + } + + /** A label to display in the header next to the display name. */ + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + mLabel = label; + } + + @Override + public String toString() { + return "DirectoryPartition{" + + "mDirectoryId=" + + mDirectoryId + + ", mContentUri='" + + mContentUri + + '\'' + + ", mDirectoryType='" + + mDirectoryType + + '\'' + + ", mDisplayName='" + + mDisplayName + + '\'' + + ", mStatus=" + + mStatus + + ", mPriorityDirectory=" + + mPriorityDirectory + + ", mPhotoSupported=" + + mPhotoSupported + + ", mResultLimit=" + + mResultLimit + + ", mLabel='" + + mLabel + + '\'' + + '}'; + } + + /** + * Whether or not to display the phone number in app that have that option - Dialer. If false, + * Phone Label should be used instead of Phone Number. + */ + public boolean isDisplayNumber() { + return mDisplayNumber; + } + + public void setDisplayNumber(boolean displayNumber) { + mDisplayNumber = displayNumber; + } +} diff --git a/java/com/android/contacts/common/list/IndexerListAdapter.java b/java/com/android/contacts/common/list/IndexerListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..2289f6e596717fcdf57e4ac50e49f726e326bdf5 --- /dev/null +++ b/java/com/android/contacts/common/list/IndexerListAdapter.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import android.widget.SectionIndexer; + +/** A list adapter that supports section indexer and a pinned header. */ +public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer { + + protected Context mContext; + private SectionIndexer mIndexer; + private int mIndexedPartition = 0; + private boolean mSectionHeaderDisplayEnabled; + private View mHeader; + private Placement mPlacementCache = new Placement(); + + /** Constructor. */ + public IndexerListAdapter(Context context) { + super(context); + mContext = context; + } + + /** + * Creates a section header view that will be pinned at the top of the list as the user scrolls. + */ + protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent); + + /** Sets the title in the pinned header as the user scrolls. */ + protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title); + + public boolean isSectionHeaderDisplayEnabled() { + return mSectionHeaderDisplayEnabled; + } + + public void setSectionHeaderDisplayEnabled(boolean flag) { + this.mSectionHeaderDisplayEnabled = flag; + } + + public int getIndexedPartition() { + return mIndexedPartition; + } + + public void setIndexedPartition(int partition) { + this.mIndexedPartition = partition; + } + + public SectionIndexer getIndexer() { + return mIndexer; + } + + public void setIndexer(SectionIndexer indexer) { + mIndexer = indexer; + mPlacementCache.invalidate(); + } + + public Object[] getSections() { + if (mIndexer == null) { + return new String[] {" "}; + } else { + return mIndexer.getSections(); + } + } + + /** @return relative position of the section in the indexed partition */ + public int getPositionForSection(int sectionIndex) { + if (mIndexer == null) { + return -1; + } + + return mIndexer.getPositionForSection(sectionIndex); + } + + /** @param position relative position in the indexed partition */ + public int getSectionForPosition(int position) { + if (mIndexer == null) { + return -1; + } + + return mIndexer.getSectionForPosition(position); + } + + @Override + public int getPinnedHeaderCount() { + if (isSectionHeaderDisplayEnabled()) { + return super.getPinnedHeaderCount() + 1; + } else { + return super.getPinnedHeaderCount(); + } + } + + @Override + public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) { + if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) { + if (mHeader == null) { + mHeader = createPinnedSectionHeaderView(mContext, parent); + } + return mHeader; + } else { + return super.getPinnedHeaderView(viewIndex, convertView, parent); + } + } + + @Override + public void configurePinnedHeaders(PinnedHeaderListView listView) { + super.configurePinnedHeaders(listView); + + if (!isSectionHeaderDisplayEnabled()) { + return; + } + + int index = getPinnedHeaderCount() - 1; + if (mIndexer == null || getCount() == 0) { + listView.setHeaderInvisible(index, false); + } else { + int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight()); + int position = listPosition - listView.getHeaderViewsCount(); + + int section = -1; + int partition = getPartitionForPosition(position); + if (partition == mIndexedPartition) { + int offset = getOffsetInPartition(position); + if (offset != -1) { + section = getSectionForPosition(offset); + } + } + + if (section == -1) { + listView.setHeaderInvisible(index, false); + } else { + View topChild = listView.getChildAt(listPosition); + if (topChild != null) { + // Match the pinned header's height to the height of the list item. + mHeader.setMinimumHeight(topChild.getMeasuredHeight()); + } + setPinnedSectionTitle(mHeader, (String) mIndexer.getSections()[section]); + + // Compute the item position where the current partition begins + int partitionStart = getPositionForPartition(mIndexedPartition); + if (hasHeader(mIndexedPartition)) { + partitionStart++; + } + + // Compute the item position where the next section begins + int nextSectionPosition = partitionStart + getPositionForSection(section + 1); + boolean isLastInSection = position == nextSectionPosition - 1; + listView.setFadingHeader(index, listPosition, isLastInSection); + } + } + } + + /** + * Computes the item's placement within its section and populates the {@code placement} object + * accordingly. Please note that the returned object is volatile and should be copied if the + * result needs to be used later. + */ + public Placement getItemPlacementInSection(int position) { + if (mPlacementCache.position == position) { + return mPlacementCache; + } + + mPlacementCache.position = position; + if (isSectionHeaderDisplayEnabled()) { + int section = getSectionForPosition(position); + if (section != -1 && getPositionForSection(section) == position) { + mPlacementCache.firstInSection = true; + mPlacementCache.sectionHeader = (String) getSections()[section]; + } else { + mPlacementCache.firstInSection = false; + mPlacementCache.sectionHeader = null; + } + + mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position); + } else { + mPlacementCache.firstInSection = false; + mPlacementCache.lastInSection = false; + mPlacementCache.sectionHeader = null; + } + return mPlacementCache; + } + + /** + * An item view is displayed differently depending on whether it is placed at the beginning, + * middle or end of a section. It also needs to know the section header when it is at the + * beginning of a section. This object captures all this configuration. + */ + public static final class Placement { + + public boolean firstInSection; + public boolean lastInSection; + public String sectionHeader; + private int position = ListView.INVALID_POSITION; + + public void invalidate() { + position = ListView.INVALID_POSITION; + } + } +} diff --git a/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java new file mode 100644 index 0000000000000000000000000000000000000000..89bd889e6377309f1ed173ccff64783e63124d61 --- /dev/null +++ b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.app.ActionBar; +import android.net.Uri; +import com.android.dialer.callintent.nano.CallSpecificAppData; + +/** Action callbacks that can be sent by a phone number picker. */ +public interface OnPhoneNumberPickerActionListener { + + int CALL_INITIATION_UNKNOWN = 0; + + /** Returns the selected phone number uri to the requester. */ + void onPickDataUri(Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData); + + /** + * Returns the specified phone number to the requester. May call the specified phone number, + * either as an audio or video call. + */ + void onPickPhoneNumber( + String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData); + + /** Called when home menu in {@link ActionBar} is clicked by the user. */ + void onHomeInActionBarSelected(); +} diff --git a/java/com/android/contacts/common/list/PhoneNumberListAdapter.java b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..c7b24229f93c113f5a28a3bd5d4adcf941f5343e --- /dev/null +++ b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.CursorLoader; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Callable; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.CallableCompat; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.compat.PhoneCompat; +import com.android.contacts.common.extensions.PhoneDirectoryExtenderAccessor; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.Constants; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.CallUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and {@link + * SipAddress#CONTENT_ITEM_TYPE}. + * + *

By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} + * is called with "true", this adapter starts handling SIP addresses too, by using {@link Callable} + * API instead of {@link Phone}. + */ +public class PhoneNumberListAdapter extends ContactEntryListAdapter { + + private static final String TAG = PhoneNumberListAdapter.class.getSimpleName(); + private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000"; + // A list of extended directories to add to the directories from the database + private final List mExtendedDirectories; + private final CharSequence mUnknownNameText; + // Extended directories will have ID's that are higher than any of the id's from the database, + // so that we can identify them and set them up properly. If no extended directories + // exist, this will be Long.MAX_VALUE + private long mFirstExtendedDirectoryId = Long.MAX_VALUE; + private ContactListItemView.PhotoPosition mPhotoPosition; + private boolean mUseCallableUri; + private Listener mListener; + private boolean mIsVideoEnabled; + private boolean mIsPresenceEnabled; + + public PhoneNumberListAdapter(Context context) { + super(context); + setDefaultFilterHeaderText(R.string.list_filter_phones); + mUnknownNameText = context.getText(android.R.string.unknownName); + + mExtendedDirectories = + PhoneDirectoryExtenderAccessor.get(mContext).getExtendedDirectories(mContext); + + int videoCapabilities = CallUtil.getVideoCallingAvailability(context); + mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0; + mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0; + } + + @Override + public void configureLoader(CursorLoader loader, long directoryId) { + String query = getQueryString(); + if (query == null) { + query = ""; + } + if (isExtendedDirectory(directoryId)) { + final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId); + final String contentUri = directory.getContentUri(); + if (contentUri == null) { + throw new IllegalStateException("Extended directory must have a content URL: " + directory); + } + final Builder builder = Uri.parse(contentUri).buildUpon(); + builder.appendPath(query); + builder.appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, String.valueOf(getDirectoryResultLimit(directory))); + loader.setUri(builder.build()); + loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); + } else { + final boolean isRemoteDirectoryQuery = DirectoryCompat.isRemoteDirectoryId(directoryId); + final Builder builder; + if (isSearchMode()) { + final Uri baseUri; + if (isRemoteDirectoryQuery) { + baseUri = PhoneCompat.getContentFilterUri(); + } else if (mUseCallableUri) { + baseUri = CallableCompat.getContentFilterUri(); + } else { + baseUri = PhoneCompat.getContentFilterUri(); + } + builder = baseUri.buildUpon(); + builder.appendPath(query); // Builder will encode the query + builder.appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); + if (isRemoteDirectoryQuery) { + builder.appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, + String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); + } + } else { + Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI; + builder = + baseUri + .buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); + if (isSectionHeaderDisplayEnabled()) { + builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true"); + } + applyFilter(loader, builder, directoryId, getFilter()); + } + + // Ignore invalid phone numbers that are too long. These can potentially cause freezes + // in the UI and there is no reason to display them. + final String prevSelection = loader.getSelection(); + final String newSelection; + if (!TextUtils.isEmpty(prevSelection)) { + newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE; + } else { + newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE; + } + loader.setSelection(newSelection); + + // Remove duplicates when it is possible. + builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true"); + loader.setUri(builder.build()); + + // TODO a projection that includes the search snippet + if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); + } else { + loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE); + } + + if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { + loader.setSortOrder(Phone.SORT_KEY_PRIMARY); + } else { + loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE); + } + } + } + + protected boolean isExtendedDirectory(long directoryId) { + return directoryId >= mFirstExtendedDirectoryId; + } + + private DirectoryPartition getExtendedDirectoryFromId(long directoryId) { + final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId); + return mExtendedDirectories.get(directoryIndex); + } + + /** + * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code + * filter}. + */ + private void applyFilter( + CursorLoader loader, Uri.Builder uriBuilder, long directoryId, ContactListFilter filter) { + if (filter == null || directoryId != Directory.DEFAULT) { + return; + } + + final StringBuilder selection = new StringBuilder(); + final List selectionArgs = new ArrayList(); + + switch (filter.filterType) { + case ContactListFilter.FILTER_TYPE_CUSTOM: + { + selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); + selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); + break; + } + case ContactListFilter.FILTER_TYPE_ACCOUNT: + { + filter.addAccountQueryParameterToUrl(uriBuilder); + break; + } + case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: + case ContactListFilter.FILTER_TYPE_DEFAULT: + break; // No selection needed. + case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + break; // This adapter is always "phone only", so no selection needed either. + default: + Log.w( + TAG, + "Unsupported filter type came " + + "(type: " + + filter.filterType + + ", toString: " + + filter + + ")" + + " showing all contacts."); + // No selection. + break; + } + loader.setSelection(selection.toString()); + loader.setSelectionArgs(selectionArgs.toArray(new String[0])); + } + + public String getPhoneNumber(int position) { + final Cursor item = (Cursor) getItem(position); + return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null; + } + + /** + * Retrieves the lookup key for the given cursor position. + * + * @param position The cursor position. + * @return The lookup key. + */ + public String getLookupKey(int position) { + final Cursor item = (Cursor) getItem(position); + return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null; + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + ContactListItemView view = super.newView(context, partition, cursor, position, parent); + view.setUnknownNameText(mUnknownNameText); + view.setQuickContactEnabled(isQuickContactEnabled()); + view.setPhotoPosition(mPhotoPosition); + return view; + } + + protected void setHighlight(ContactListItemView view, Cursor cursor) { + view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + ContactListItemView view = (ContactListItemView) itemView; + + setHighlight(view, cursor); + + // Look at elements before and after this position, checking if contact IDs are same. + // If they have one same contact ID, it means they can be grouped. + // + // In one group, only the first entry will show its photo and its name, and the other + // entries in the group show just their data (e.g. phone number, email address). + cursor.moveToPosition(position); + boolean isFirstEntry = true; + final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID); + if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) { + final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID); + if (currentContactId == previousContactId) { + isFirstEntry = false; + } + } + cursor.moveToPosition(position); + + bindViewId(view, cursor, PhoneQuery.PHONE_ID); + + bindSectionHeaderAndDivider(view, position); + if (isFirstEntry) { + bindName(view, cursor); + if (isQuickContactEnabled()) { + bindQuickContact( + view, + partition, + cursor, + PhoneQuery.PHOTO_ID, + PhoneQuery.PHOTO_URI, + PhoneQuery.CONTACT_ID, + PhoneQuery.LOOKUP_KEY, + PhoneQuery.DISPLAY_NAME); + } else { + if (getDisplayPhotos()) { + bindPhoto(view, partition, cursor); + } + } + } else { + unbindName(view); + + view.removePhotoView(true, false); + } + + final DirectoryPartition directory = (DirectoryPartition) getPartition(partition); + + // If the first partition does not have a header, then all subsequent partitions' + // getPositionForPartition returns an index off by 1. + int partitionOffset = 0; + if (partition > 0 && !getPartition(0).getHasHeader()) { + partitionOffset = 1; + } + position += getPositionForPartition(partition) + partitionOffset; + + bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position); + } + + protected void bindPhoneNumber( + ContactListItemView view, Cursor cursor, boolean displayNumber, int position) { + CharSequence label = null; + if (displayNumber && !cursor.isNull(PhoneQuery.PHONE_TYPE)) { + final int type = cursor.getInt(PhoneQuery.PHONE_TYPE); + final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL); + + // TODO cache + label = Phone.getTypeLabel(getContext().getResources(), type, customLabel); + } + view.setLabel(label); + final String text; + if (displayNumber) { + text = cursor.getString(PhoneQuery.PHONE_NUMBER); + } else { + // Display phone label. If that's null, display geocoded location for the number + final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL); + if (phoneLabel != null) { + text = phoneLabel; + } else { + final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER); + text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber); + } + } + view.setPhoneNumber(text); + + if (CompatUtils.isVideoCompatible()) { + // Determine if carrier presence indicates the number supports video calling. + int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE); + boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; + + boolean isVideoIconShown = mIsVideoEnabled && (!mIsPresenceEnabled || isPresent); + view.setShowVideoCallIcon(isVideoIconShown, mListener, position); + } + } + + protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) { + if (isSectionHeaderDisplayEnabled()) { + Placement placement = getItemPlacementInSection(position); + view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null); + } else { + view.setSectionHeader(null); + } + } + + protected void bindName(final ContactListItemView view, Cursor cursor) { + view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME); + // Note: we don't show phonetic names any more (see issue 5265330) + } + + protected void unbindName(final ContactListItemView view) { + view.hideDisplayName(); + } + + @Override + protected void bindWorkProfileIcon(final ContactListItemView view, int partition) { + final DirectoryPartition directory = (DirectoryPartition) getPartition(partition); + final long directoryId = directory.getDirectoryId(); + final long userType = ContactsUtils.determineUserType(directoryId, null); + // Work directory must not be a extended directory. An extended directory is custom + // directory in the app, but not a directory provided by framework. So it can't be + // USER_TYPE_WORK. + view.setWorkProfileIconEnabled( + !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK); + } + + protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) { + if (!isPhotoSupported(partitionIndex)) { + view.removePhotoView(); + return; + } + + long photoId = 0; + if (!cursor.isNull(PhoneQuery.PHOTO_ID)) { + photoId = cursor.getLong(PhoneQuery.PHOTO_ID); + } + + if (photoId != 0) { + getPhotoLoader() + .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null); + } else { + final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI); + final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); + + DefaultImageRequest request = null; + if (photoUri == null) { + final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME); + final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY); + request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos()); + } + getPhotoLoader() + .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request); + } + } + + public ContactListItemView.PhotoPosition getPhotoPosition() { + return mPhotoPosition; + } + + public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { + mPhotoPosition = photoPosition; + } + + public void setUseCallableUri(boolean useCallableUri) { + mUseCallableUri = useCallableUri; + } + + /** + * Override base implementation to inject extended directories between local & remote directories. + * This is done in the following steps: 1. Call base implementation to add directories from the + * cursor. 2. Iterate all base directories and establish the following information: a. The highest + * directory id so that we can assign unused id's to the extended directories. b. The index of the + * last non-remote directory. This is where we will insert extended directories. 3. Iterate the + * extended directories and for each one, assign an ID and insert it in the proper location. + */ + @Override + public void changeDirectories(Cursor cursor) { + super.changeDirectories(cursor); + if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) { + return; + } + final int numExtendedDirectories = mExtendedDirectories.size(); + if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) { + // already added all directories; + return; + } + // + mFirstExtendedDirectoryId = Long.MAX_VALUE; + if (numExtendedDirectories > 0) { + // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's + // "special" ID. + long maxId = Directory.LOCAL_INVISIBLE; + int insertIndex = 0; + for (int i = 0, n = getPartitionCount(); i < n; i++) { + final DirectoryPartition partition = (DirectoryPartition) getPartition(i); + final long id = partition.getDirectoryId(); + if (id > maxId) { + maxId = id; + } + if (!DirectoryCompat.isRemoteDirectoryId(id)) { + // assuming remote directories come after local, we will end up with the index + // where we should insert extended directories. This also works if there are no + // remote directories at all. + insertIndex = i + 1; + } + } + // Extended directories ID's cannot collide with base directories + mFirstExtendedDirectoryId = maxId + 1; + for (int i = 0; i < numExtendedDirectories; i++) { + final long id = mFirstExtendedDirectoryId + i; + final DirectoryPartition directory = mExtendedDirectories.get(i); + if (getPartitionByDirectoryId(id) == -1) { + addPartition(insertIndex, directory); + directory.setDirectoryId(id); + } + } + } + } + + @Override + protected Uri getContactUri( + int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) { + final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex); + final long directoryId = directory.getDirectoryId(); + if (!isExtendedDirectory(directoryId)) { + return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn); + } + return Contacts.CONTENT_LOOKUP_URI + .buildUpon() + .appendPath(Constants.LOOKUP_URI_ENCODED) + .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel()) + .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) + .encodedFragment(cursor.getString(lookUpKeyColumn)) + .build(); + } + + public Listener getListener() { + return mListener; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public interface Listener { + + void onVideoCallIconClicked(int position); + } + + public static class PhoneQuery { + + /** + * Optional key used as part of a JSON lookup key to specify an analytics category associated + * with the row. + */ + public static final String ANALYTICS_CATEGORY = "analytics_category"; + + /** + * Optional key used as part of a JSON lookup key to specify an analytics action associated with + * the row. + */ + public static final String ANALYTICS_ACTION = "analytics_action"; + + /** + * Optional key used as part of a JSON lookup key to specify an analytics value associated with + * the row. + */ + public static final String ANALYTICS_VALUE = "analytics_value"; + + public static final String[] PROJECTION_PRIMARY_INTERNAL = + new String[] { + Phone._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Phone.CONTACT_ID, // 4 + Phone.LOOKUP_KEY, // 5 + Phone.PHOTO_ID, // 6 + Phone.DISPLAY_NAME_PRIMARY, // 7 + Phone.PHOTO_THUMBNAIL_URI, // 8 + }; + + public static final String[] PROJECTION_PRIMARY; + public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = + new String[] { + Phone._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Phone.CONTACT_ID, // 4 + Phone.LOOKUP_KEY, // 5 + Phone.PHOTO_ID, // 6 + Phone.DISPLAY_NAME_ALTERNATIVE, // 7 + Phone.PHOTO_THUMBNAIL_URI, // 8 + }; + public static final String[] PROJECTION_ALTERNATIVE; + public static final int PHONE_ID = 0; + public static final int PHONE_TYPE = 1; + public static final int PHONE_LABEL = 2; + public static final int PHONE_NUMBER = 3; + public static final int CONTACT_ID = 4; + public static final int LOOKUP_KEY = 5; + public static final int PHOTO_ID = 6; + public static final int DISPLAY_NAME = 7; + public static final int PHOTO_URI = 8; + public static final int CARRIER_PRESENCE = 9; + + static { + final List projectionList = + new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL)); + if (CompatUtils.isMarshmallowCompatible()) { + projectionList.add(Phone.CARRIER_PRESENCE); // 9 + } + PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]); + } + + static { + final List projectionList = + new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL)); + if (CompatUtils.isMarshmallowCompatible()) { + projectionList.add(Phone.CARRIER_PRESENCE); // 9 + } + PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]); + } + } +} diff --git a/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..4ae81529bc99b06900d10381e87b1d008251420a --- /dev/null +++ b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.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 com.android.contacts.common.list; + +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.ArraySet; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.R; +import com.android.contacts.common.util.AccountFilterUtil; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.Logger; +import java.util.Set; +import org.json.JSONException; +import org.json.JSONObject; + +/** Fragment containing a phone number list for picking. */ +public class PhoneNumberPickerFragment extends ContactEntryListFragment + implements PhoneNumberListAdapter.Listener { + + private static final String KEY_FILTER = "filter"; + private OnPhoneNumberPickerActionListener mListener; + private ContactListFilter mFilter; + private View mAccountFilterHeader; + /** + * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE. + */ + private View mPaddingView; + /** true if the loader has started at least once. */ + private boolean mLoaderStarted; + + private boolean mUseCallableUri; + + private ContactListItemView.PhotoPosition mPhotoPosition = + ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */); + + private final Set mLoadFinishedListeners = + new ArraySet(); + + private CursorReranker mCursorReranker; + + public PhoneNumberPickerFragment() { + setQuickContactEnabled(false); + setPhotoLoaderEnabled(true); + setSectionHeaderDisplayEnabled(true); + setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); + + // Show nothing instead of letting caller Activity show something. + setHasOptionsMenu(true); + } + + /** + * Handles a click on the video call icon for a row in the list. + * + * @param position The position in the list where the click ocurred. + */ + @Override + public void onVideoCallIconClicked(int position) { + callNumber(position, true /* isVideoCall */); + } + + public void setDirectorySearchEnabled(boolean flag) { + setDirectorySearchMode( + flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE); + } + + public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) { + this.mListener = listener; + } + + public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() { + return mListener; + } + + @Override + protected void onCreateView(LayoutInflater inflater, ViewGroup container) { + super.onCreateView(inflater, container); + + View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false); + mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding); + getListView().addHeaderView(paddingView); + + mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container); + updateFilterHeaderView(); + + setVisibleScrollbarEnabled(getVisibleScrollbarEnabled()); + } + + protected boolean getVisibleScrollbarEnabled() { + return true; + } + + @Override + protected void setSearchMode(boolean flag) { + super.setSearchMode(flag); + updateFilterHeaderView(); + } + + private void updateFilterHeaderView() { + final ContactListFilter filter = getFilter(); + if (mAccountFilterHeader == null || filter == null) { + return; + } + final boolean shouldShowHeader = + !isSearchMode() + && AccountFilterUtil.updateAccountFilterTitleForPhone( + mAccountFilterHeader, filter, false); + if (shouldShowHeader) { + mPaddingView.setVisibility(View.GONE); + mAccountFilterHeader.setVisibility(View.VISIBLE); + } else { + mPaddingView.setVisibility(View.VISIBLE); + mAccountFilterHeader.setVisibility(View.GONE); + } + } + + @Override + public void restoreSavedState(Bundle savedState) { + super.restoreSavedState(savedState); + + if (savedState == null) { + return; + } + + mFilter = savedState.getParcelable(KEY_FILTER); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(KEY_FILTER, mFilter); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() + if (mListener != null) { + mListener.onHomeInActionBarSelected(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onItemClick(int position, long id) { + callNumber(position, false /* isVideoCall */); + } + + /** + * Initiates a call to the number at the specified position. + * + * @param position The position. + * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false} + * otherwise. + */ + private void callNumber(int position, boolean isVideoCall) { + final String number = getPhoneNumber(position); + if (!TextUtils.isEmpty(number)) { + cacheContactInfo(position); + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = getCallInitiationType(true /* isRemoteDirectory */); + callSpecificAppData.positionOfSelectedSearchResult = position; + callSpecificAppData.charactersInSearchString = + getQueryString() == null ? 0 : getQueryString().length(); + mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData); + } else { + LogUtil.i( + "PhoneNumberPickerFragment.callNumber", + "item at %d was clicked before adapter is ready, ignoring", + position); + } + + // Get the lookup key and track any analytics + final String lookupKey = getLookupKey(position); + if (!TextUtils.isEmpty(lookupKey)) { + maybeTrackAnalytics(lookupKey); + } + } + + protected void cacheContactInfo(int position) { + // Not implemented. Hook for child classes + } + + protected String getPhoneNumber(int position) { + final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); + return adapter.getPhoneNumber(position); + } + + protected String getLookupKey(int position) { + final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); + return adapter.getLookupKey(position); + } + + @Override + protected void startLoading() { + mLoaderStarted = true; + super.startLoading(); + } + + @Override + @MainThread + public void onLoadFinished(Loader loader, Cursor data) { + Assert.isMainThread(); + // TODO: define and verify behavior for "Nearby places", corp directories, + // and dividers listed in UI between these categories + if (mCursorReranker != null + && data != null + && !data.isClosed() + && data.getCount() > 0 + && loader.getId() != -1) { // skip invalid directory ID of -1 + data = mCursorReranker.rerankCursor(data); + } + super.onLoadFinished(loader, data); + + // disable scroll bar if there is no data + setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0); + + if (data != null) { + notifyListeners(); + } + } + + /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */ + public interface CursorReranker { + @MainThread + Cursor rerankCursor(Cursor data); + } + + @MainThread + public void setReranker(@Nullable CursorReranker reranker) { + Assert.isMainThread(); + mCursorReranker = reranker; + } + + /** Listener that is notified when cursor has finished loading data. */ + public interface OnLoadFinishedListener { + void onLoadFinished(); + } + + @MainThread + public void addOnLoadFinishedListener(OnLoadFinishedListener listener) { + Assert.isMainThread(); + mLoadFinishedListeners.add(listener); + } + + @MainThread + public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) { + Assert.isMainThread(); + mLoadFinishedListeners.remove(listener); + } + + @MainThread + protected void notifyListeners() { + Assert.isMainThread(); + for (OnLoadFinishedListener listener : mLoadFinishedListeners) { + listener.onLoadFinished(); + } + } + + @MainThread + @Override + public void onDetach() { + Assert.isMainThread(); + mLoadFinishedListeners.clear(); + super.onDetach(); + } + + public void setUseCallableUri(boolean useCallableUri) { + mUseCallableUri = useCallableUri; + } + + public boolean usesCallableUri() { + return mUseCallableUri; + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity()); + adapter.setDisplayPhotos(true); + adapter.setUseCallableUri(mUseCallableUri); + return adapter; + } + + @Override + protected void configureAdapter() { + super.configureAdapter(); + + final ContactEntryListAdapter adapter = getAdapter(); + if (adapter == null) { + return; + } + + if (!isSearchMode() && mFilter != null) { + adapter.setFilter(mFilter); + } + + setPhotoPosition(adapter); + } + + protected void setPhotoPosition(ContactEntryListAdapter adapter) { + ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition); + } + + @Override + protected View inflateView(LayoutInflater inflater, ViewGroup container) { + return inflater.inflate(R.layout.contact_list_content, null); + } + + public ContactListFilter getFilter() { + return mFilter; + } + + public void setFilter(ContactListFilter filter) { + if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) { + return; + } + + mFilter = filter; + if (mLoaderStarted) { + reloadData(); + } + updateFilterHeaderView(); + } + + public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { + mPhotoPosition = photoPosition; + + final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); + if (adapter != null) { + adapter.setPhotoPosition(photoPosition); + } + } + + /** + * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number + * not in the local contacts database + */ + protected int getCallInitiationType(boolean isRemoteDirectory) { + return OnPhoneNumberPickerActionListener.CALL_INITIATION_UNKNOWN; + } + + /** + * Where a lookup key contains analytic event information, logs the associated analytics event. + * + * @param lookupKey The lookup key JSON object. + */ + private void maybeTrackAnalytics(String lookupKey) { + try { + JSONObject json = new JSONObject(lookupKey); + + String analyticsCategory = + json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY); + String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION); + String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE); + + if (TextUtils.isEmpty(analyticsCategory) + || TextUtils.isEmpty(analyticsAction) + || TextUtils.isEmpty(analyticsValue)) { + return; + } + + // Assume that the analytic value being tracked could be a float value, but just cast + // to a long so that the analytic server can handle it. + long value; + try { + float floatValue = Float.parseFloat(analyticsValue); + value = (long) floatValue; + } catch (NumberFormatException nfe) { + return; + } + + Logger.get(getActivity()) + .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value); + } catch (JSONException e) { + // Not an error; just a lookup key that doesn't have the right information. + } + } +} diff --git a/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..0bdcef084f9a0881c131266365e172103b429a85 --- /dev/null +++ b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java @@ -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. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import com.android.common.widget.CompositeCursorAdapter; + +/** A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. */ +public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter + implements PinnedHeaderListView.PinnedHeaderAdapter { + + public static final int PARTITION_HEADER_TYPE = 0; + + private boolean mPinnedPartitionHeadersEnabled; + private boolean[] mHeaderVisibility; + + public PinnedHeaderListAdapter(Context context) { + super(context); + } + + public boolean getPinnedPartitionHeadersEnabled() { + return mPinnedPartitionHeadersEnabled; + } + + public void setPinnedPartitionHeadersEnabled(boolean flag) { + this.mPinnedPartitionHeadersEnabled = flag; + } + + @Override + public int getPinnedHeaderCount() { + if (mPinnedPartitionHeadersEnabled) { + return getPartitionCount(); + } else { + return 0; + } + } + + protected boolean isPinnedPartitionHeaderVisible(int partition) { + return getPinnedPartitionHeadersEnabled() + && hasHeader(partition) + && !isPartitionEmpty(partition); + } + + /** The default implementation creates the same type of view as a normal partition header. */ + @Override + public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) { + if (hasHeader(partition)) { + View view = null; + if (convertView != null) { + Integer headerType = (Integer) convertView.getTag(); + if (headerType != null && headerType == PARTITION_HEADER_TYPE) { + view = convertView; + } + } + if (view == null) { + view = newHeaderView(getContext(), partition, null, parent); + view.setTag(PARTITION_HEADER_TYPE); + view.setFocusable(false); + view.setEnabled(false); + } + bindHeaderView(view, partition, getCursor(partition)); + view.setLayoutDirection(parent.getLayoutDirection()); + return view; + } else { + return null; + } + } + + @Override + public void configurePinnedHeaders(PinnedHeaderListView listView) { + if (!getPinnedPartitionHeadersEnabled()) { + return; + } + + int size = getPartitionCount(); + + // Cache visibility bits, because we will need them several times later on + if (mHeaderVisibility == null || mHeaderVisibility.length != size) { + mHeaderVisibility = new boolean[size]; + } + for (int i = 0; i < size; i++) { + boolean visible = isPinnedPartitionHeaderVisible(i); + mHeaderVisibility[i] = visible; + if (!visible) { + listView.setHeaderInvisible(i, true); + } + } + + int headerViewsCount = listView.getHeaderViewsCount(); + + // Starting at the top, find and pin headers for partitions preceding the visible one(s) + int maxTopHeader = -1; + int topHeaderHeight = 0; + for (int i = 0; i < size; i++) { + if (mHeaderVisibility[i]) { + int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount; + int partition = getPartitionForPosition(position); + if (i > partition) { + break; + } + + listView.setHeaderPinnedAtTop(i, topHeaderHeight, false); + topHeaderHeight += listView.getPinnedHeaderHeight(i); + maxTopHeader = i; + } + } + + // Starting at the bottom, find and pin headers for partitions following the visible one(s) + int maxBottomHeader = size; + int bottomHeaderHeight = 0; + int listHeight = listView.getHeight(); + for (int i = size; --i > maxTopHeader; ) { + if (mHeaderVisibility[i]) { + int position = listView.getPositionAt(listHeight - bottomHeaderHeight) - headerViewsCount; + if (position < 0) { + break; + } + + int partition = getPartitionForPosition(position - 1); + if (partition == -1 || i <= partition) { + break; + } + + int height = listView.getPinnedHeaderHeight(i); + bottomHeaderHeight += height; + + listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, false); + maxBottomHeader = i; + } + } + + // Headers in between the top-pinned and bottom-pinned should be hidden + for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) { + if (mHeaderVisibility[i]) { + listView.setHeaderInvisible(i, isPartitionEmpty(i)); + } + } + } + + @Override + public int getScrollPositionForHeader(int viewIndex) { + return getPositionForPartition(viewIndex); + } +} diff --git a/java/com/android/contacts/common/list/PinnedHeaderListView.java b/java/com/android/contacts/common/list/PinnedHeaderListView.java new file mode 100644 index 0000000000000000000000000000000000000000..33c68b68c4352463338aa0b0f4534fb5e0b95736 --- /dev/null +++ b/java/com/android/contacts/common/list/PinnedHeaderListView.java @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ListAdapter; +import com.android.dialer.util.ViewUtil; + +/** + * A ListView that maintains a header pinned at the top of the list. The pinned header can be pushed + * up and dissolved as needed. + */ +public class PinnedHeaderListView extends AutoScrollListView + implements OnScrollListener, OnItemSelectedListener { + + private static final int MAX_ALPHA = 255; + private static final int TOP = 0; + private static final int BOTTOM = 1; + private static final int FADING = 2; + private static final int DEFAULT_ANIMATION_DURATION = 20; + private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100; + private PinnedHeaderAdapter mAdapter; + private int mSize; + private PinnedHeader[] mHeaders; + private RectF mBounds = new RectF(); + private OnScrollListener mOnScrollListener; + private OnItemSelectedListener mOnItemSelectedListener; + private int mScrollState; + private boolean mScrollToSectionOnHeaderTouch = false; + private boolean mHeaderTouched = false; + private int mAnimationDuration = DEFAULT_ANIMATION_DURATION; + private boolean mAnimating; + private long mAnimationTargetTime; + private int mHeaderPaddingStart; + private int mHeaderWidth; + + public PinnedHeaderListView(Context context) { + this(context, null, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + super.setOnScrollListener(this); + super.setOnItemSelectedListener(this); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mHeaderPaddingStart = getPaddingStart(); + mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd(); + } + + @Override + public void setAdapter(ListAdapter adapter) { + mAdapter = (PinnedHeaderAdapter) adapter; + super.setAdapter(adapter); + } + + @Override + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListener = onScrollListener; + super.setOnScrollListener(this); + } + + @Override + public void setOnItemSelectedListener(OnItemSelectedListener listener) { + mOnItemSelectedListener = listener; + super.setOnItemSelectedListener(this); + } + + public void setScrollToSectionOnHeaderTouch(boolean value) { + mScrollToSectionOnHeaderTouch = value; + } + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mAdapter != null) { + int count = mAdapter.getPinnedHeaderCount(); + if (count != mSize) { + mSize = count; + if (mHeaders == null) { + mHeaders = new PinnedHeader[mSize]; + } else if (mHeaders.length < mSize) { + PinnedHeader[] headers = mHeaders; + mHeaders = new PinnedHeader[mSize]; + System.arraycopy(headers, 0, mHeaders, 0, headers.length); + } + } + + for (int i = 0; i < mSize; i++) { + if (mHeaders[i] == null) { + mHeaders[i] = new PinnedHeader(); + } + mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this); + } + + mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration; + mAdapter.configurePinnedHeaders(this); + invalidateIfAnimating(); + } + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount); + } + } + + @Override + protected float getTopFadingEdgeStrength() { + // Disable vertical fading at the top when the pinned header is present + return mSize > 0 ? 0 : super.getTopFadingEdgeStrength(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mScrollState = scrollState; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(this, scrollState); + } + } + + /** + * Ensures that the selected item is positioned below the top-pinned headers and above the + * bottom-pinned ones. + */ + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + int height = getHeight(); + + int windowTop = 0; + int windowBottom = height; + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + if (header.state == TOP) { + windowTop = header.y + header.height; + } else if (header.state == BOTTOM) { + windowBottom = header.y; + break; + } + } + } + + View selectedView = getSelectedView(); + if (selectedView != null) { + if (selectedView.getTop() < windowTop) { + setSelectionFromTop(position, windowTop); + } else if (selectedView.getBottom() > windowBottom) { + setSelectionFromTop(position, windowBottom - selectedView.getHeight()); + } + } + + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onItemSelected(parent, view, position, id); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onNothingSelected(parent); + } + } + + public int getPinnedHeaderHeight(int viewIndex) { + ensurePinnedHeaderLayout(viewIndex); + return mHeaders[viewIndex].view.getHeight(); + } + + /** + * Set header to be pinned at the top. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.y = y; + header.state = TOP; + + // TODO perhaps we should animate at the top as well + header.animating = false; + } + + /** + * Set header to be pinned at the bottom. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.state = BOTTOM; + if (header.animating) { + header.targetTime = mAnimationTargetTime; + header.sourceY = header.y; + header.targetY = y; + } else if (animate && (header.y != y || !header.visible)) { + if (header.visible) { + header.sourceY = header.y; + } else { + header.visible = true; + header.sourceY = y + header.height; + } + header.animating = true; + header.targetVisible = true; + header.targetTime = mAnimationTargetTime; + header.targetY = y; + } else { + header.visible = true; + header.y = y; + } + } + + /** + * Set header to be pinned at the top of the first visible item. + * + * @param viewIndex index of the header view + * @param position is position of the header in pixels. + */ + public void setFadingHeader(int viewIndex, int position, boolean fade) { + ensurePinnedHeaderLayout(viewIndex); + + View child = getChildAt(position - getFirstVisiblePosition()); + if (child == null) { + return; + } + + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.state = FADING; + header.alpha = MAX_ALPHA; + header.animating = false; + + int top = getTotalTopPinnedHeaderHeight(); + header.y = top; + if (fade) { + int bottom = child.getBottom() - top; + int headerHeight = header.height; + if (bottom < headerHeight) { + int portion = bottom - headerHeight; + header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight; + header.y = top + portion; + } + } + } + + /** + * Makes header invisible. + * + * @param viewIndex index of the header view + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderInvisible(int viewIndex, boolean animate) { + PinnedHeader header = mHeaders[viewIndex]; + if (header.visible && (animate || header.animating) && header.state == BOTTOM) { + header.sourceY = header.y; + if (!header.animating) { + header.visible = true; + header.targetY = getBottom() + header.height; + } + header.animating = true; + header.targetTime = mAnimationTargetTime; + header.targetVisible = false; + } else { + header.visible = false; + } + } + + private void ensurePinnedHeaderLayout(int viewIndex) { + View view = mHeaders[viewIndex].view; + if (view.isLayoutRequested()) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + int widthSpec; + int heightSpec; + + if (layoutParams != null && layoutParams.width > 0) { + widthSpec = View.MeasureSpec.makeMeasureSpec(layoutParams.width, View.MeasureSpec.EXACTLY); + } else { + widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY); + } + + if (layoutParams != null && layoutParams.height > 0) { + heightSpec = + View.MeasureSpec.makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + int height = view.getMeasuredHeight(); + mHeaders[viewIndex].height = height; + view.layout(0, 0, view.getMeasuredWidth(), height); + } + } + + /** Returns the sum of heights of headers pinned to the top. */ + public int getTotalTopPinnedHeaderHeight() { + for (int i = mSize; --i >= 0; ) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == TOP) { + return header.y + header.height; + } + } + return 0; + } + + /** Returns the list item position at the specified y coordinate. */ + public int getPositionAt(int y) { + do { + int position = pointToPosition(getPaddingLeft() + 1, y); + if (position != -1) { + return position; + } + // If position == -1, we must have hit a separator. Let's examine + // a nearby pixel + y--; + } while (y > 0); + return 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mHeaderTouched = false; + if (super.onInterceptTouchEvent(ev)) { + return true; + } + + if (mScrollState == SCROLL_STATE_IDLE) { + final int y = (int) ev.getY(); + final int x = (int) ev.getX(); + for (int i = mSize; --i >= 0; ) { + PinnedHeader header = mHeaders[i]; + // For RTL layouts, this also takes into account that the scrollbar is on the left + // side. + final int padding = getPaddingLeft(); + if (header.visible + && header.y <= y + && header.y + header.height > y + && x >= padding + && padding + header.view.getWidth() >= x) { + mHeaderTouched = true; + if (mScrollToSectionOnHeaderTouch && ev.getAction() == MotionEvent.ACTION_DOWN) { + return smoothScrollToPartition(i); + } else { + return true; + } + } + } + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mHeaderTouched) { + if (ev.getAction() == MotionEvent.ACTION_UP) { + mHeaderTouched = false; + } + return true; + } + return super.onTouchEvent(ev); + } + + private boolean smoothScrollToPartition(int partition) { + if (mAdapter == null) { + return false; + } + final int position = mAdapter.getScrollPositionForHeader(partition); + if (position == -1) { + return false; + } + + int offset = 0; + for (int i = 0; i < partition; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + offset += header.height; + } + } + smoothScrollToPositionFromTop( + position + getHeaderViewsCount(), offset, DEFAULT_SMOOTH_SCROLL_DURATION); + return true; + } + + private void invalidateIfAnimating() { + mAnimating = false; + for (int i = 0; i < mSize; i++) { + if (mHeaders[i].animating) { + mAnimating = true; + invalidate(); + return; + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + long currentTime = mAnimating ? System.currentTimeMillis() : 0; + + int top = 0; + int bottom = getBottom(); + boolean hasVisibleHeaders = false; + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + hasVisibleHeaders = true; + if (header.state == BOTTOM && header.y < bottom) { + bottom = header.y; + } else if (header.state == TOP || header.state == FADING) { + int newTop = header.y + header.height; + if (newTop > top) { + top = newTop; + } + } + } + } + + if (hasVisibleHeaders) { + canvas.save(); + } + + super.dispatchDraw(canvas); + + if (hasVisibleHeaders) { + canvas.restore(); + + // If the first item is visible and if it has a positive top that is greater than the + // first header's assigned y-value, use that for the first header's y value. This way, + // the header inherits any padding applied to the list view. + if (mSize > 0 && getFirstVisiblePosition() == 0) { + View firstChild = getChildAt(0); + PinnedHeader firstHeader = mHeaders[0]; + + if (firstHeader != null) { + int firstHeaderTop = firstChild != null ? firstChild.getTop() : 0; + firstHeader.y = Math.max(firstHeader.y, firstHeaderTop); + } + } + + // First draw top headers, then the bottom ones to handle the Z axis correctly + for (int i = mSize; --i >= 0; ) { + PinnedHeader header = mHeaders[i]; + if (header.visible && (header.state == TOP || header.state == FADING)) { + drawHeader(canvas, header, currentTime); + } + } + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == BOTTOM) { + drawHeader(canvas, header, currentTime); + } + } + } + + invalidateIfAnimating(); + } + + private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) { + if (header.animating) { + int timeLeft = (int) (header.targetTime - currentTime); + if (timeLeft <= 0) { + header.y = header.targetY; + header.visible = header.targetVisible; + header.animating = false; + } else { + header.y = + header.targetY + (header.sourceY - header.targetY) * timeLeft / mAnimationDuration; + } + } + if (header.visible) { + View view = header.view; + int saveCount = canvas.save(); + int translateX = + ViewUtil.isViewLayoutRtl(this) + ? getWidth() - mHeaderPaddingStart - view.getWidth() + : mHeaderPaddingStart; + canvas.translate(translateX, header.y); + if (header.state == FADING) { + mBounds.set(0, 0, view.getWidth(), view.getHeight()); + canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG); + } + view.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + /** Adapter interface. The list adapter must implement this interface. */ + public interface PinnedHeaderAdapter { + + /** Returns the overall number of pinned headers, visible or not. */ + int getPinnedHeaderCount(); + + /** Creates or updates the pinned header view. */ + View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent); + + /** + * Configures the pinned headers to match the visible list items. The adapter should call {@link + * PinnedHeaderListView#setHeaderPinnedAtTop}, {@link + * PinnedHeaderListView#setHeaderPinnedAtBottom}, {@link PinnedHeaderListView#setFadingHeader} + * or {@link PinnedHeaderListView#setHeaderInvisible}, for each header that needs to change its + * position or visibility. + */ + void configurePinnedHeaders(PinnedHeaderListView listView); + + /** + * Returns the list position to scroll to if the pinned header is touched. Return -1 if the list + * does not need to be scrolled. + */ + int getScrollPositionForHeader(int viewIndex); + } + + private static final class PinnedHeader { + + View view; + boolean visible; + int y; + int height; + int alpha; + int state; + + boolean animating; + boolean targetVisible; + int sourceY; + int targetY; + long targetTime; + } +} diff --git a/java/com/android/contacts/common/list/ViewPagerTabStrip.java b/java/com/android/contacts/common/list/ViewPagerTabStrip.java new file mode 100644 index 0000000000000000000000000000000000000000..969a6d342e0b186d9dcaafa68df7034f5ea3848e --- /dev/null +++ b/java/com/android/contacts/common/list/ViewPagerTabStrip.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import com.android.contacts.common.R; + +public class ViewPagerTabStrip extends LinearLayout { + + private final Paint mSelectedUnderlinePaint; + private int mSelectedUnderlineThickness; + private int mIndexForSelection; + private float mSelectionOffset; + + public ViewPagerTabStrip(Context context) { + this(context, null); + } + + public ViewPagerTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + final Resources res = context.getResources(); + + mSelectedUnderlineThickness = res.getDimensionPixelSize(R.dimen.tab_selected_underline_height); + int underlineColor = res.getColor(R.color.tab_selected_underline_color); + int backgroundColor = res.getColor(R.color.contactscommon_actionbar_background_color); + + mSelectedUnderlinePaint = new Paint(); + mSelectedUnderlinePaint.setColor(underlineColor); + + setBackgroundColor(backgroundColor); + setWillNotDraw(false); + } + + /** + * Notifies this view that view pager has been scrolled. We save the tab index and selection + * offset for interpolating the position and width of selection underline. + */ + void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mIndexForSelection = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + int childCount = getChildCount(); + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mIndexForSelection); + + if (selectedTitle == null) { + // The view pager's tab count changed but we weren't notified yet. Ignore this draw + // pass, when we get a new selection we will update and draw the selection strip in + // the correct place. + return; + } + int selectedLeft = selectedTitle.getLeft(); + int selectedRight = selectedTitle.getRight(); + final boolean isRtl = isRtl(); + final boolean hasNextTab = + isRtl ? mIndexForSelection > 0 : (mIndexForSelection < (getChildCount() - 1)); + if ((mSelectionOffset > 0.0f) && hasNextTab) { + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mIndexForSelection + (isRtl ? -1 : 1)); + int nextLeft = nextTitle.getLeft(); + int nextRight = nextTitle.getRight(); + + selectedLeft = + (int) (mSelectionOffset * nextLeft + (1.0f - mSelectionOffset) * selectedLeft); + selectedRight = + (int) (mSelectionOffset * nextRight + (1.0f - mSelectionOffset) * selectedRight); + } + + int height = getHeight(); + canvas.drawRect( + selectedLeft, + height - mSelectedUnderlineThickness, + selectedRight, + height, + mSelectedUnderlinePaint); + } + } + + private boolean isRtl() { + return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } +} diff --git a/java/com/android/contacts/common/list/ViewPagerTabs.java b/java/com/android/contacts/common/list/ViewPagerTabs.java new file mode 100644 index 0000000000000000000000000000000000000000..34f623ef416501d8a11964f1feee4ec71aa8a6f2 --- /dev/null +++ b/java/com/android/contacts/common/list/ViewPagerTabs.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Outline; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.R; +import com.android.dialer.compat.CompatUtils; + +/** + * Lightweight implementation of ViewPager tabs. This looks similar to traditional actionBar tabs, + * but allows for the view containing the tabs to be placed anywhere on screen. Text-related + * attributes can also be assigned in XML - these will get propogated to the child TextViews + * automatically. + */ +public class ViewPagerTabs extends HorizontalScrollView implements ViewPager.OnPageChangeListener { + + private static final ViewOutlineProvider VIEW_BOUNDS_OUTLINE_PROVIDER; + private static final int TAB_SIDE_PADDING_IN_DPS = 10; + // TODO: This should use in the future + private static final int[] ATTRS = + new int[] { + android.R.attr.textSize, + android.R.attr.textStyle, + android.R.attr.textColor, + android.R.attr.textAllCaps + }; + + static { + if (CompatUtils.isLollipopCompatible()) { + VIEW_BOUNDS_OUTLINE_PROVIDER = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRect(0, 0, view.getWidth(), view.getHeight()); + } + }; + } else { + VIEW_BOUNDS_OUTLINE_PROVIDER = null; + } + } + + /** + * Linearlayout that will contain the TextViews serving as tabs. This is the only child of the + * parent HorizontalScrollView. + */ + final int mTextStyle; + + final ColorStateList mTextColor; + final int mTextSize; + final boolean mTextAllCaps; + ViewPager mPager; + int mPrevSelected = -1; + int mSidePadding; + private ViewPagerTabStrip mTabStrip; + private int[] mTabIcons; + // For displaying the unread count next to the tab icon. + private int[] mUnreadCounts; + + public ViewPagerTabs(Context context) { + this(context, null); + } + + public ViewPagerTabs(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ViewPagerTabs(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setFillViewport(true); + + mSidePadding = (int) (getResources().getDisplayMetrics().density * TAB_SIDE_PADDING_IN_DPS); + + final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + mTextSize = a.getDimensionPixelSize(0, 0); + mTextStyle = a.getInt(1, 0); + mTextColor = a.getColorStateList(2); + mTextAllCaps = a.getBoolean(3, false); + + mTabStrip = new ViewPagerTabStrip(context); + addView( + mTabStrip, + new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + a.recycle(); + + if (CompatUtils.isLollipopCompatible()) { + // enable shadow casting from view bounds + setOutlineProvider(VIEW_BOUNDS_OUTLINE_PROVIDER); + } + } + + public void setViewPager(ViewPager viewPager) { + mPager = viewPager; + addTabs(mPager.getAdapter()); + } + + /** + * Set the tab icons and initialize an array for unread counts the same length as the icon array. + * + * @param tabIcons An array representing the tab icons in order. + */ + public void configureTabIcons(int[] tabIcons) { + mTabIcons = tabIcons; + mUnreadCounts = new int[tabIcons.length]; + } + + public void setUnreadCount(int count, int position) { + if (mUnreadCounts == null || position >= mUnreadCounts.length) { + return; + } + mUnreadCounts[position] = count; + } + + private void addTabs(PagerAdapter adapter) { + mTabStrip.removeAllViews(); + + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + addTab(adapter.getPageTitle(i), i); + } + } + + private void addTab(CharSequence tabTitle, final int position) { + View tabView; + if (mTabIcons != null && position < mTabIcons.length) { + View layout = LayoutInflater.from(getContext()).inflate(R.layout.unread_count_tab, null); + View iconView = layout.findViewById(R.id.icon); + iconView.setBackgroundResource(mTabIcons[position]); + iconView.setContentDescription(tabTitle); + TextView textView = (TextView) layout.findViewById(R.id.count); + if (mUnreadCounts != null && mUnreadCounts[position] > 0) { + textView.setText(Integer.toString(mUnreadCounts[position])); + textView.setVisibility(View.VISIBLE); + iconView.setContentDescription( + getResources() + .getQuantityString( + R.plurals.tab_title_with_unread_items, + mUnreadCounts[position], + tabTitle.toString(), + mUnreadCounts[position])); + } else { + textView.setVisibility(View.INVISIBLE); + iconView.setContentDescription(getResources().getString(R.string.tab_title, tabTitle)); + } + tabView = layout; + } else { + final TextView textView = new TextView(getContext()); + textView.setText(tabTitle); + textView.setBackgroundResource(R.drawable.view_pager_tab_background); + + // Assign various text appearance related attributes to child views. + if (mTextStyle > 0) { + textView.setTypeface(textView.getTypeface(), mTextStyle); + } + if (mTextSize > 0) { + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + } + if (mTextColor != null) { + textView.setTextColor(mTextColor); + } + textView.setAllCaps(mTextAllCaps); + textView.setGravity(Gravity.CENTER); + + tabView = textView; + } + + tabView.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + mPager.setCurrentItem(getRtlPosition(position)); + } + }); + + tabView.setOnLongClickListener(new OnTabLongClickListener(position)); + + tabView.setPadding(mSidePadding, 0, mSidePadding, 0); + + mTabStrip.addView( + tabView, + position, + new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1)); + + // Default to the first child being selected + if (position == 0) { + mPrevSelected = 0; + tabView.setSelected(true); + } + } + + /** + * Remove a tab at a certain index. + * + * @param index The index of the tab view we wish to remove. + */ + public void removeTab(int index) { + View view = mTabStrip.getChildAt(index); + if (view != null) { + mTabStrip.removeView(view); + } + } + + /** + * Refresh a tab at a certain index by removing it and reconstructing it. + * + * @param index The index of the tab view we wish to update. + */ + public void updateTab(int index) { + removeTab(index); + + if (index < mPager.getAdapter().getCount()) { + addTab(mPager.getAdapter().getPageTitle(index), index); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + position = getRtlPosition(position); + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageSelected(int position) { + position = getRtlPosition(position); + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + if (mPrevSelected >= 0 && mPrevSelected < tabStripChildCount) { + mTabStrip.getChildAt(mPrevSelected).setSelected(false); + } + final View selectedChild = mTabStrip.getChildAt(position); + selectedChild.setSelected(true); + + // Update scroll position + final int scrollPos = selectedChild.getLeft() - (getWidth() - selectedChild.getWidth()) / 2; + smoothScrollTo(scrollPos, 0); + mPrevSelected = position; + } + + @Override + public void onPageScrollStateChanged(int state) {} + + private int getRtlPosition(int position) { + if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + return mTabStrip.getChildCount() - 1 - position; + } + return position; + } + + /** Simulates actionbar tab behavior by showing a toast with the tab title when long clicked. */ + private class OnTabLongClickListener implements OnLongClickListener { + + final int mPosition; + + public OnTabLongClickListener(int position) { + mPosition = position; + } + + @Override + public boolean onLongClick(View v) { + final int[] screenPos = new int[2]; + getLocationOnScreen(screenPos); + + final Context context = getContext(); + final int width = getWidth(); + final int height = getHeight(); + final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; + + Toast toast = + Toast.makeText(context, mPager.getAdapter().getPageTitle(mPosition), Toast.LENGTH_SHORT); + + // Show the toast under the tab + toast.setGravity( + Gravity.TOP | Gravity.CENTER_HORIZONTAL, + (screenPos[0] + width / 2) - screenWidth / 2, + screenPos[1] + height); + + toast.show(); + return true; + } + } +} diff --git a/java/com/android/contacts/common/location/CountryDetector.java b/java/com/android/contacts/common/location/CountryDetector.java new file mode 100644 index 0000000000000000000000000000000000000000..7d9e42b38e6b2743099c63504439a04b8ba0cee4 --- /dev/null +++ b/java/com/android/contacts/common/location/CountryDetector.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.location; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationManager; +import android.preference.PreferenceManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import com.android.dialer.util.PermissionsUtil; +import java.util.Locale; + +/** + * This class is used to detect the country where the user is. It is a simplified version of the + * country detector service in the framework. The sources of country location are queried in the + * following order of reliability: + * + *

    + *
  • Mobile network + *
  • Location manager + *
  • SIM's country + *
  • User's default locale + *
+ * + * As far as possible this class tries to replicate the behavior of the system's country detector + * service: 1) Order in priority of sources of country location 2) Mobile network information + * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of + * 24 hours in the system) 4) Location updates only uses the {@link + * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully + * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the + * fallback never happens without a reboot) 6) Location is not used if the device does not implement + * a {@link android.location.Geocoder} + */ +public class CountryDetector { + + public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated"; + public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country"; + private static final String TAG = "CountryDetector"; + // Wait 12 hours between updates + private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12; + // Minimum distance before an update is triggered, in meters. We don't need this to be too + // exact because all we care about is what country the user is in. + private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000; + private static CountryDetector sInstance; + private final TelephonyManager mTelephonyManager; + private final LocationManager mLocationManager; + private final LocaleProvider mLocaleProvider; + // Used as a default country code when all the sources of country data have failed in the + // exceedingly rare event that the device does not have a default locale set for some reason. + private static final String DEFAULT_COUNTRY_ISO = "US"; + private final Context mContext; + + private CountryDetector(Context context) { + this( + context, + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), + (LocationManager) context.getSystemService(Context.LOCATION_SERVICE), + new LocaleProvider()); + } + + private CountryDetector( + Context context, + TelephonyManager telephonyManager, + LocationManager locationManager, + LocaleProvider localeProvider) { + mTelephonyManager = telephonyManager; + mLocationManager = locationManager; + mLocaleProvider = localeProvider; + mContext = context; + + registerForLocationUpdates(context, mLocationManager); + } + + public static void registerForLocationUpdates(Context context, LocationManager locationManager) { + if (!PermissionsUtil.hasLocationPermissions(context)) { + Log.w(TAG, "No location permissions, not registering for location updates."); + return; + } + + if (!Geocoder.isPresent()) { + // Certain devices do not have an implementation of a geocoder - in that case there is + // no point trying to get location updates because we cannot retrieve the country based + // on the location anyway. + return; + } + final Intent activeIntent = new Intent(context, LocationChangedReceiver.class); + final PendingIntent pendingIntent = + PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + locationManager.requestLocationUpdates( + LocationManager.PASSIVE_PROVIDER, + TIME_BETWEEN_UPDATES_MS, + DISTANCE_BETWEEN_UPDATES_METERS, + pendingIntent); + } + + /** + * Returns the instance of the country detector. {@link #initialize(Context)} must have been + * called previously. + * + * @return the initialized country detector. + */ + public static synchronized CountryDetector getInstance(Context context) { + if (sInstance == null) { + sInstance = new CountryDetector(context.getApplicationContext()); + } + return sInstance; + } + + /** Factory method for {@link CountryDetector} that allows the caller to provide mock objects. */ + public CountryDetector getInstanceForTest( + Context context, + TelephonyManager telephonyManager, + LocationManager locationManager, + LocaleProvider localeProvider, + Geocoder geocoder) { + return new CountryDetector(context, telephonyManager, locationManager, localeProvider); + } + + public String getCurrentCountryIso() { + String result = null; + if (isNetworkCountryCodeAvailable()) { + result = getNetworkBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = getLocationBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = getSimBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = getLocaleBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = DEFAULT_COUNTRY_ISO; + } + return result.toUpperCase(Locale.US); + } + + /** @return the country code of the current telephony network the user is connected to. */ + private String getNetworkBasedCountryIso() { + return mTelephonyManager.getNetworkCountryIso(); + } + + /** @return the geocoded country code detected by the {@link LocationManager}. */ + private String getLocationBasedCountryIso() { + if (!Geocoder.isPresent() || !PermissionsUtil.hasLocationPermissions(mContext)) { + return null; + } + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(mContext); + return sharedPreferences.getString(KEY_PREFERENCE_CURRENT_COUNTRY, null); + } + + /** @return the country code of the SIM card currently inserted in the device. */ + private String getSimBasedCountryIso() { + return mTelephonyManager.getSimCountryIso(); + } + + /** @return the country code of the user's currently selected locale. */ + private String getLocaleBasedCountryIso() { + Locale defaultLocale = mLocaleProvider.getDefaultLocale(); + if (defaultLocale != null) { + return defaultLocale.getCountry(); + } + return null; + } + + private boolean isNetworkCountryCodeAvailable() { + // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code. + // In this case, we want to ignore the value returned and fallback to location instead. + return mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM; + } + + /** + * Class that can be used to return the user's default locale. This is in its own class so that it + * can be mocked out. + */ + public static class LocaleProvider { + + public Locale getDefaultLocale() { + return Locale.getDefault(); + } + } + + public static class LocationChangedReceiver extends BroadcastReceiver { + + @Override + public void onReceive(final Context context, Intent intent) { + if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) { + return; + } + + final Location location = + (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); + + UpdateCountryService.updateCountry(context, location); + } + } +} diff --git a/java/com/android/contacts/common/location/UpdateCountryService.java b/java/com/android/contacts/common/location/UpdateCountryService.java new file mode 100644 index 0000000000000000000000000000000000000000..f23e09e2050bc1563b8718174d4e79812f468b2b --- /dev/null +++ b/java/com/android/contacts/common/location/UpdateCountryService.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.location; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.preference.PreferenceManager; +import android.util.Log; +import java.io.IOException; +import java.util.List; + +/** + * Service used to perform asynchronous geocoding from within a broadcast receiver. Given a {@link + * Location}, convert it into a country code, and save it in shared preferences. + */ +public class UpdateCountryService extends IntentService { + + private static final String TAG = UpdateCountryService.class.getSimpleName(); + + private static final String ACTION_UPDATE_COUNTRY = "saveCountry"; + + private static final String KEY_INTENT_LOCATION = "location"; + + public UpdateCountryService() { + super(TAG); + } + + public static void updateCountry(Context context, Location location) { + final Intent serviceIntent = new Intent(context, UpdateCountryService.class); + serviceIntent.setAction(ACTION_UPDATE_COUNTRY); + serviceIntent.putExtra(UpdateCountryService.KEY_INTENT_LOCATION, location); + context.startService(serviceIntent); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null) { + Log.d(TAG, "onHandleIntent: could not handle null intent"); + return; + } + if (ACTION_UPDATE_COUNTRY.equals(intent.getAction())) { + final Location location = intent.getParcelableExtra(KEY_INTENT_LOCATION); + final String country = getCountryFromLocation(getApplicationContext(), location); + + if (country == null) { + return; + } + + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + final Editor editor = prefs.edit(); + editor.putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis()); + editor.putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country); + editor.commit(); + } + } + + /** + * Given a {@link Location}, return a country code. + * + * @return the ISO 3166-1 two letter country code + */ + private String getCountryFromLocation(Context context, Location location) { + final Geocoder geocoder = new Geocoder(context); + String country = null; + try { + double latitude = location.getLatitude(); + // Latitude has to be between 90 and -90 (latitude of north and south poles wrt equator) + if (latitude <= 90 && latitude >= -90) { + final List
addresses = + geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); + if (addresses != null && addresses.size() > 0) { + country = addresses.get(0).getCountryCode(); + } + } else { + Log.w(TAG, "Invalid latitude"); + } + } catch (IOException e) { + Log.w(TAG, "Exception occurred when getting geocoded country from location"); + } + return country; + } +} diff --git a/java/com/android/contacts/common/model/AccountTypeManager.java b/java/com/android/contacts/common/model/AccountTypeManager.java new file mode 100644 index 0000000000000000000000000000000000000000..f225ff6ac6bb2660602c0fb8ce2abf78e20a3d6e --- /dev/null +++ b/java/com/android/contacts/common/model/AccountTypeManager.java @@ -0,0 +1,813 @@ +/* + * 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.contacts.common.model; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorDescription; +import android.accounts.OnAccountsUpdateListener; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SyncAdapterType; +import android.content.SyncStatusObserver; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.provider.ContactsContract; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.TimingLogger; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.list.ContactListFilterController; +import com.android.contacts.common.model.account.AccountType; +import com.android.contacts.common.model.account.AccountTypeWithDataSet; +import com.android.contacts.common.model.account.AccountWithDataSet; +import com.android.contacts.common.model.account.ExchangeAccountType; +import com.android.contacts.common.model.account.ExternalAccountType; +import com.android.contacts.common.model.account.FallbackAccountType; +import com.android.contacts.common.model.account.GoogleAccountType; +import com.android.contacts.common.model.account.SamsungAccountType; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.Constants; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Singleton holder for all parsed {@link AccountType} available on the system, typically filled + * through {@link PackageManager} queries. + */ +public abstract class AccountTypeManager { + + static final String TAG = "AccountTypeManager"; + + private static final Object mInitializationLock = new Object(); + private static AccountTypeManager mAccountTypeManager; + + /** + * Requests the singleton instance of {@link AccountTypeManager} with data bound from the + * available authenticators. This method can safely be called from the UI thread. + */ + public static AccountTypeManager getInstance(Context context) { + synchronized (mInitializationLock) { + if (mAccountTypeManager == null) { + context = context.getApplicationContext(); + mAccountTypeManager = new AccountTypeManagerImpl(context); + } + } + return mAccountTypeManager; + } + + /** + * Set the instance of account type manager. This is only for and should only be used by unit + * tests. While having this method is not ideal, it's simpler than the alternative of holding this + * as a service in the ContactsApplication context class. + * + * @param mockManager The mock AccountTypeManager. + */ + public static void setInstanceForTest(AccountTypeManager mockManager) { + synchronized (mInitializationLock) { + mAccountTypeManager = mockManager; + } + } + + /** + * Returns the list of all accounts (if contactWritableOnly is false) or just the list of contact + * writable accounts (if contactWritableOnly is true). + */ + // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts() + public abstract List getAccounts(boolean contactWritableOnly); + + /** Returns the list of accounts that are group writable. */ + public abstract List getGroupWritableAccounts(); + + public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet); + + public final AccountType getAccountType(String accountType, String dataSet) { + return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet)); + } + + public final AccountType getAccountTypeForAccount(AccountWithDataSet account) { + if (account != null) { + return getAccountType(account.getAccountTypeWithDataSet()); + } + return getAccountType(null, null); + } + + /** + * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which + * support the "invite" feature and have one or more account. + *

This is a filtered down and more "usable" list compared to {@link + * #getAllInvitableAccountTypes}, where usable is defined as: (1) making sure that the app + * that contributed the account type is not disabled (in order to avoid presenting the user + * with an option that does nothing), and (2) that there is at least one raw contact with that + * account type in the database (assuming that the user probably doesn't use that account + * type). + *

Warning: Don't use on the UI thread because this can scan the database. + */ + public abstract Map getUsableInvitableAccountTypes(); + + /** + * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link + * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching + * {@link FallbackAccountType}. + */ + public DataKind getKindOrFallback(AccountType type, String mimeType) { + return type == null ? null : type.getKindForMimetype(mimeType); + } + + /** + * Returns all registered {@link AccountType}s, including extension ones. + * + * @param contactWritableOnly if true, it only returns ones that support writing contacts. + */ + public abstract List getAccountTypes(boolean contactWritableOnly); + + /** + * @param contactWritableOnly if true, it only returns ones that support writing contacts. + * @return true when this instance contains the given account. + */ + public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) { + for (AccountWithDataSet account_2 : getAccounts(false)) { + if (account.equals(account_2)) { + return true; + } + } + return false; + } +} + +class AccountTypeManagerImpl extends AccountTypeManager + implements OnAccountsUpdateListener, SyncStatusObserver { + + private static final Map + EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP = + Collections.unmodifiableMap(new HashMap()); + + /** + * A sample contact URI used to test whether any activities will respond to an invitable intent + * with the given URI as the intent data. This doesn't need to be specific to a real contact + * because an app that intercepts the intent should probably do so for all types of contact URIs. + */ + private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(1, "xxx"); + + private static final int MESSAGE_LOAD_DATA = 0; + private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1; + private static final Comparator ACCOUNT_COMPARATOR = + new Comparator() { + @Override + public int compare(AccountWithDataSet a, AccountWithDataSet b) { + if (Objects.equals(a.name, b.name) + && Objects.equals(a.type, b.type) + && Objects.equals(a.dataSet, b.dataSet)) { + return 0; + } else if (b.name == null || b.type == null) { + return -1; + } else if (a.name == null || a.type == null) { + return 1; + } else { + int diff = a.name.compareTo(b.name); + if (diff != 0) { + return diff; + } + diff = a.type.compareTo(b.type); + if (diff != 0) { + return diff; + } + + // Accounts without data sets get sorted before those that have them. + if (a.dataSet != null) { + return b.dataSet == null ? 1 : a.dataSet.compareTo(b.dataSet); + } else { + return -1; + } + } + } + }; + private final InvitableAccountTypeCache mInvitableAccountTypeCache; + /** + * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been + * initialized. False otherwise. + */ + private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false); + /** + * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. False + * otherwise. + */ + private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false); + + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private Context mContext; + private final Runnable mCheckFilterValidityRunnable = + new Runnable() { + @Override + public void run() { + ContactListFilterController.getInstance(mContext).checkFilterValidity(true); + } + }; + private AccountManager mAccountManager; + private AccountType mFallbackAccountType; + private List mAccounts = new ArrayList<>(); + private List mContactWritableAccounts = new ArrayList<>(); + private List mGroupWritableAccounts = new ArrayList<>(); + private Map mAccountTypesWithDataSets = new ArrayMap<>(); + private Map mInvitableAccountTypes = + EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; + private HandlerThread mListenerThread; + private Handler mListenerHandler; + private BroadcastReceiver mBroadcastReceiver = + new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent); + mListenerHandler.sendMessage(msg); + } + }; + /* A latch that ensures that asynchronous initialization completes before data is used */ + private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1); + + /** Internal constructor that only performs initial parsing. */ + public AccountTypeManagerImpl(Context context) { + mContext = context; + mFallbackAccountType = new FallbackAccountType(context); + + mAccountManager = AccountManager.get(mContext); + + mListenerThread = new HandlerThread("AccountChangeListener"); + mListenerThread.start(); + mListenerHandler = + new Handler(mListenerThread.getLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_LOAD_DATA: + loadAccountsInBackground(); + break; + case MESSAGE_PROCESS_BROADCAST_INTENT: + processBroadcastIntent((Intent) msg.obj); + break; + } + } + }; + + mInvitableAccountTypeCache = new InvitableAccountTypeCache(); + + // Request updates when packages or accounts change + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + mContext.registerReceiver(mBroadcastReceiver, filter); + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiver(mBroadcastReceiver, sdFilter); + + // Request updates when locale is changed so that the order of each field will + // be able to be changed on the locale change. + filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); + mContext.registerReceiver(mBroadcastReceiver, filter); + + mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false); + + ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this); + + mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); + } + + /** + * Find a specific {@link AuthenticatorDescription} in the provided list that matches the given + * account type. + */ + protected static AuthenticatorDescription findAuthenticator( + AuthenticatorDescription[] auths, String accountType) { + for (AuthenticatorDescription auth : auths) { + if (accountType.equals(auth.type)) { + return auth; + } + } + return null; + } + + /** + * Return all {@link AccountType}s with at least one account which supports "invite", i.e. its + * {@link AccountType#getInviteContactActivityClassName()} is not empty. + */ + @VisibleForTesting + static Map findAllInvitableAccountTypes( + Context context, + Collection accounts, + Map accountTypesByTypeAndDataSet) { + Map result = new ArrayMap<>(); + for (AccountWithDataSet account : accounts) { + AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet(); + AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet); + if (type == null) { + continue; // just in case + } + if (result.containsKey(accountTypeWithDataSet)) { + continue; + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d( + TAG, + "Type " + + accountTypeWithDataSet + + " inviteClass=" + + type.getInviteContactActivityClassName()); + } + if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) { + result.put(accountTypeWithDataSet, type); + } + } + return Collections.unmodifiableMap(result); + } + + @Override + public void onStatusChanged(int which) { + mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); + } + + public void processBroadcastIntent(Intent intent) { + mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); + } + + /* This notification will arrive on the background thread */ + public void onAccountsUpdated(Account[] accounts) { + // Refresh to catch any changed accounts + loadAccountsInBackground(); + } + + /** + * Returns instantly if accounts and account types have already been loaded. Otherwise waits for + * the background thread to complete the loading. + */ + void ensureAccountsLoaded() { + CountDownLatch latch = mInitializationLatch; + if (latch == null) { + return; + } + while (true) { + try { + latch.await(); + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Loads account list and corresponding account types (potentially with data sets). Always called + * on a background thread. + */ + protected void loadAccountsInBackground() { + if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { + Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start"); + } + TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground"); + final long startTime = SystemClock.currentThreadTimeMillis(); + final long startTimeWall = SystemClock.elapsedRealtime(); + + // Account types, keyed off the account type and data set concatenation. + final Map accountTypesByTypeAndDataSet = new ArrayMap<>(); + + // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can + // be multiple account types (with different data sets) for the same type of account, each + // type string may have multiple AccountType entries. + final Map> accountTypesByType = new ArrayMap<>(); + + final List allAccounts = new ArrayList<>(); + final List contactWritableAccounts = new ArrayList<>(); + final List groupWritableAccounts = new ArrayList<>(); + final Set extensionPackages = new HashSet<>(); + + final AccountManager am = mAccountManager; + + final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes(); + final AuthenticatorDescription[] auths = am.getAuthenticatorTypes(); + + // First process sync adapters to find any that provide contact data. + for (SyncAdapterType sync : syncs) { + if (!ContactsContract.AUTHORITY.equals(sync.authority)) { + // Skip sync adapters that don't provide contact data. + continue; + } + + // Look for the formatting details provided by each sync + // adapter, using the authenticator to find general resources. + final String type = sync.accountType; + final AuthenticatorDescription auth = findAuthenticator(auths, type); + if (auth == null) { + Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it."); + continue; + } + + AccountType accountType; + if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) { + accountType = new GoogleAccountType(mContext, auth.packageName); + } else if (ExchangeAccountType.isExchangeType(type)) { + accountType = new ExchangeAccountType(mContext, auth.packageName, type); + } else if (SamsungAccountType.isSamsungAccountType(mContext, type, auth.packageName)) { + accountType = new SamsungAccountType(mContext, auth.packageName, type); + } else { + Log.d( + TAG, "Registering external account type=" + type + ", packageName=" + auth.packageName); + accountType = new ExternalAccountType(mContext, auth.packageName, false); + } + if (!accountType.isInitialized()) { + if (accountType.isEmbedded()) { + throw new IllegalStateException( + "Problem initializing embedded type " + accountType.getClass().getCanonicalName()); + } else { + // Skip external account types that couldn't be initialized. + continue; + } + } + + accountType.accountType = auth.type; + accountType.titleRes = auth.labelId; + accountType.iconRes = auth.iconId; + + addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); + + // Check to see if the account type knows of any other non-sync-adapter packages + // that may provide other data sets of contact data. + extensionPackages.addAll(accountType.getExtensionPackageNames()); + } + + // If any extension packages were specified, process them as well. + if (!extensionPackages.isEmpty()) { + Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages"); + for (String extensionPackage : extensionPackages) { + ExternalAccountType accountType = new ExternalAccountType(mContext, extensionPackage, true); + if (!accountType.isInitialized()) { + // Skip external account types that couldn't be initialized. + continue; + } + if (!accountType.hasContactsMetadata()) { + Log.w( + TAG, + "Skipping extension package " + + extensionPackage + + " because" + + " it doesn't have the CONTACTS_STRUCTURE metadata"); + continue; + } + if (TextUtils.isEmpty(accountType.accountType)) { + Log.w( + TAG, + "Skipping extension package " + + extensionPackage + + " because" + + " the CONTACTS_STRUCTURE metadata doesn't have the accountType" + + " attribute"); + continue; + } + Log.d( + TAG, + "Registering extension package account type=" + + accountType.accountType + + ", dataSet=" + + accountType.dataSet + + ", packageName=" + + extensionPackage); + + addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); + } + } + timings.addSplit("Loaded account types"); + + // Map in accounts to associate the account names with each account type entry. + Account[] accounts = mAccountManager.getAccounts(); + for (Account account : accounts) { + boolean syncable = ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; + + if (syncable) { + List accountTypes = accountTypesByType.get(account.type); + if (accountTypes != null) { + // Add an account-with-data-set entry for each account type that is + // authenticated by this account. + for (AccountType accountType : accountTypes) { + AccountWithDataSet accountWithDataSet = + new AccountWithDataSet(account.name, account.type, accountType.dataSet); + allAccounts.add(accountWithDataSet); + if (accountType.areContactsWritable()) { + contactWritableAccounts.add(accountWithDataSet); + } + if (accountType.isGroupMembershipEditable()) { + groupWritableAccounts.add(accountWithDataSet); + } + } + } + } + } + + Collections.sort(allAccounts, ACCOUNT_COMPARATOR); + Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR); + Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR); + + timings.addSplit("Loaded accounts"); + + synchronized (this) { + mAccountTypesWithDataSets = accountTypesByTypeAndDataSet; + mAccounts = allAccounts; + mContactWritableAccounts = contactWritableAccounts; + mGroupWritableAccounts = groupWritableAccounts; + mInvitableAccountTypes = + findAllInvitableAccountTypes(mContext, allAccounts, accountTypesByTypeAndDataSet); + } + + timings.dumpToLog(); + final long endTimeWall = SystemClock.elapsedRealtime(); + final long endTime = SystemClock.currentThreadTimeMillis(); + + Log.i( + TAG, + "Loaded meta-data for " + + mAccountTypesWithDataSets.size() + + " account types, " + + mAccounts.size() + + " accounts in " + + (endTimeWall - startTimeWall) + + "ms(wall) " + + (endTime - startTime) + + "ms(cpu)"); + + if (mInitializationLatch != null) { + mInitializationLatch.countDown(); + mInitializationLatch = null; + } + if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { + Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish"); + } + + // Check filter validity since filter may become obsolete after account update. It must be + // done from UI thread. + mMainThreadHandler.post(mCheckFilterValidityRunnable); + } + + // Bookkeeping method for tracking the known account types in the given maps. + private void addAccountType( + AccountType accountType, + Map accountTypesByTypeAndDataSet, + Map> accountTypesByType) { + accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType); + List accountsForType = accountTypesByType.get(accountType.accountType); + if (accountsForType == null) { + accountsForType = new ArrayList<>(); + } + accountsForType.add(accountType); + accountTypesByType.put(accountType.accountType, accountsForType); + } + + /** Return list of all known, contact writable {@link AccountWithDataSet}'s. */ + @Override + public List getAccounts(boolean contactWritableOnly) { + ensureAccountsLoaded(); + return contactWritableOnly ? mContactWritableAccounts : mAccounts; + } + + /** Return the list of all known, group writable {@link AccountWithDataSet}'s. */ + public List getGroupWritableAccounts() { + ensureAccountsLoaded(); + return mGroupWritableAccounts; + } + + /** + * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link + * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching + * {@link FallbackAccountType}. + */ + @Override + public DataKind getKindOrFallback(AccountType type, String mimeType) { + ensureAccountsLoaded(); + DataKind kind = null; + + // Try finding account type and kind matching request + if (type != null) { + kind = type.getKindForMimetype(mimeType); + } + + if (kind == null) { + // Nothing found, so try fallback as last resort + kind = mFallbackAccountType.getKindForMimetype(mimeType); + } + + if (kind == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType); + } + } + + return kind; + } + + /** Return {@link AccountType} for the given account type and data set. */ + @Override + public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) { + ensureAccountsLoaded(); + synchronized (this) { + AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet); + return type != null ? type : mFallbackAccountType; + } + } + + /** + * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which + * support the "invite" feature and have one or more account. This is an unfiltered list. See + * {@link #getUsableInvitableAccountTypes()}. + */ + private Map getAllInvitableAccountTypes() { + ensureAccountsLoaded(); + return mInvitableAccountTypes; + } + + @Override + public Map getUsableInvitableAccountTypes() { + ensureAccountsLoaded(); + // Since this method is not thread-safe, it's possible for multiple threads to encounter + // the situation where (1) the cache has not been initialized yet or + // (2) an async task to refresh the account type list in the cache has already been + // started. Hence we use {@link AtomicBoolean}s and return cached values immediately + // while we compute the actual result in the background. We use this approach instead of + // using "synchronized" because computing the account type list involves a DB read, and + // can potentially cause a deadlock situation if this method is called from code which + // holds the DB lock. The trade-off of potentially having an incorrect list of invitable + // account types for a short period of time seems more manageable than enforcing the + // context in which this method is called. + + // Computing the list of usable invitable account types is done on the fly as requested. + // If this method has never been called before, then block until the list has been computed. + if (!mInvitablesCacheIsInitialized.get()) { + mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext)); + mInvitablesCacheIsInitialized.set(true); + } else { + // Otherwise, there is a value in the cache. If the value has expired and + // an async task has not already been started by another thread, then kick off a new + // async task to compute the list. + if (mInvitableAccountTypeCache.isExpired() + && mInvitablesTaskIsRunning.compareAndSet(false, true)) { + new FindInvitablesTask().execute(); + } + } + + return mInvitableAccountTypeCache.getCachedValue(); + } + + /** + * Return all usable {@link AccountType}s that support the "invite" feature from the list of all + * potential invitable account types (retrieved from {@link #getAllInvitableAccountTypes}). A + * usable invitable account type means: (1) there is at least 1 raw contact in the database with + * that account type, and (2) the app contributing the account type is not disabled. + * + *

Warning: Don't use on the UI thread because this can scan the database. + */ + private Map findUsableInvitableAccountTypes( + Context context) { + Map allInvitables = getAllInvitableAccountTypes(); + if (allInvitables.isEmpty()) { + return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; + } + + final Map result = new ArrayMap<>(); + result.putAll(allInvitables); + + final PackageManager packageManager = context.getPackageManager(); + for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) { + AccountType accountType = allInvitables.get(accountTypeWithDataSet); + + // Make sure that account types don't come from apps that are disabled. + Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, SAMPLE_CONTACT_URI); + if (invitableIntent == null) { + result.remove(accountTypeWithDataSet); + continue; + } + ResolveInfo resolveInfo = + packageManager.resolveActivity(invitableIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (resolveInfo == null) { + // If we can't find an activity to start for this intent, then there's no point in + // showing this option to the user. + result.remove(accountTypeWithDataSet); + continue; + } + + // Make sure that there is at least 1 raw contact with this account type. This check + // is non-trivial and should not be done on the UI thread. + if (!accountTypeWithDataSet.hasData(context)) { + result.remove(accountTypeWithDataSet); + } + } + + return Collections.unmodifiableMap(result); + } + + @Override + public List getAccountTypes(boolean contactWritableOnly) { + ensureAccountsLoaded(); + final List accountTypes = new ArrayList<>(); + synchronized (this) { + for (AccountType type : mAccountTypesWithDataSets.values()) { + if (!contactWritableOnly || type.areContactsWritable()) { + accountTypes.add(type); + } + } + } + return accountTypes; + } + + /** + * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a {@link + * Map}. Note that the cached value is valid only for {@link + * #TIME_TO_LIVE} milliseconds. + */ + private static final class InvitableAccountTypeCache { + + /** + * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds has + * elapsed. + */ + private static final long TIME_TO_LIVE = 60000; + + private Map mInvitableAccountTypes; + + private long mTimeLastSet; + + /** + * Returns true if the data in this cache is stale and needs to be refreshed. Returns false + * otherwise. + */ + public boolean isExpired() { + return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE; + } + + /** + * Returns the cached value. Note that the caller is responsible for checking {@link + * #isExpired()} to ensure that the value is not stale. + */ + public Map getCachedValue() { + return mInvitableAccountTypes; + } + + public void setCachedValue(Map map) { + mInvitableAccountTypes = map; + mTimeLastSet = SystemClock.elapsedRealtime(); + } + } + + /** + * Background task to find all usable {@link AccountType}s that support the "invite" feature from + * the list of all potential invitable account types. Once the work is completed, the list of + * account types is stored in the {@link AccountTypeManager}'s {@link InvitableAccountTypeCache}. + */ + private class FindInvitablesTask + extends AsyncTask> { + + @Override + protected Map doInBackground(Void... params) { + return findUsableInvitableAccountTypes(mContext); + } + + @Override + protected void onPostExecute(Map accountTypes) { + mInvitableAccountTypeCache.setCachedValue(accountTypes); + mInvitablesTaskIsRunning.set(false); + } + } +} diff --git a/java/com/android/contacts/common/model/BuilderWrapper.java b/java/com/android/contacts/common/model/BuilderWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..9c666e59c8cfed3a33b5a710d9209752467408ce --- /dev/null +++ b/java/com/android/contacts/common/model/BuilderWrapper.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentProviderOperation.Builder; + +/** + * This class is created for the purpose of compatibility and make the type of + * ContentProviderOperation available on pre-M SDKs. Since ContentProviderOperation is usually + * created by Builder and we don’t have access to the type via Builder, so we need to create a + * wrapper class for Builder first and include type. Then we could use the builder and the type in + * this class to create a wrapper of ContentProviderOperation. + */ +public class BuilderWrapper { + + private Builder mBuilder; + private int mType; + + public BuilderWrapper(Builder builder, int type) { + mBuilder = builder; + mType = type; + } + + public int getType() { + return mType; + } + + public void setType(int mType) { + this.mType = mType; + } + + public Builder getBuilder() { + return mBuilder; + } + + public void setBuilder(Builder mBuilder) { + this.mBuilder = mBuilder; + } +} diff --git a/java/com/android/contacts/common/model/CPOWrapper.java b/java/com/android/contacts/common/model/CPOWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..4a67e670002276932055668c95517004a63d8580 --- /dev/null +++ b/java/com/android/contacts/common/model/CPOWrapper.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentProviderOperation; + +/** + * This class is created for the purpose of compatibility and make the type of + * ContentProviderOperation available on pre-M SDKs. + */ +public class CPOWrapper { + + private ContentProviderOperation mOperation; + private int mType; + + public CPOWrapper(ContentProviderOperation builder, int type) { + mOperation = builder; + mType = type; + } + + public int getType() { + return mType; + } + + public void setType(int type) { + this.mType = type; + } + + public ContentProviderOperation getOperation() { + return mOperation; + } + + public void setOperation(ContentProviderOperation operation) { + this.mOperation = operation; + } +} diff --git a/java/com/android/contacts/common/model/Contact.java b/java/com/android/contacts/common/model/Contact.java new file mode 100644 index 0000000000000000000000000000000000000000..ad0b66efee68d888a4a450ee6f274f586bc1c1a1 --- /dev/null +++ b/java/com/android/contacts/common/model/Contact.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentValues; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.DisplayNameSources; +import android.support.annotation.VisibleForTesting; +import com.android.contacts.common.GroupMetaData; +import com.android.contacts.common.model.account.AccountType; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; + +/** + * A Contact represents a single person or logical entity as perceived by the user. The information + * about a contact can come from multiple data sources, which are each represented by a RawContact + * object. Thus, a Contact is associated with a collection of RawContact objects. + * + *

The aggregation of raw contacts into a single contact is performed automatically, and it is + * also possible for users to manually split and join raw contacts into various contacts. + * + *

Only the {@link ContactLoader} class can create a Contact object with various flags to allow + * partial loading of contact data. Thus, an instance of this class should be treated as a read-only + * object. + */ +public class Contact { + + private final Uri mRequestedUri; + private final Uri mLookupUri; + private final Uri mUri; + private final long mDirectoryId; + private final String mLookupKey; + private final long mId; + private final long mNameRawContactId; + private final int mDisplayNameSource; + private final long mPhotoId; + private final String mPhotoUri; + private final String mDisplayName; + private final String mAltDisplayName; + private final String mPhoneticName; + private final boolean mStarred; + private final Integer mPresence; + private final boolean mSendToVoicemail; + private final String mCustomRingtone; + private final boolean mIsUserProfile; + private final Contact.Status mStatus; + private final Exception mException; + private ImmutableList mRawContacts; + private ImmutableList mInvitableAccountTypes; + private String mDirectoryDisplayName; + private String mDirectoryType; + private String mDirectoryAccountType; + private String mDirectoryAccountName; + private int mDirectoryExportSupport; + private ImmutableList mGroups; + private byte[] mPhotoBinaryData; + /** + * Small version of the contact photo loaded from a blob instead of from a file. If a large + * contact photo is not available yet, then this has the same value as mPhotoBinaryData. + */ + private byte[] mThumbnailPhotoBinaryData; + + /** Constructor for special results, namely "no contact found" and "error". */ + private Contact(Uri requestedUri, Contact.Status status, Exception exception) { + if (status == Status.ERROR && exception == null) { + throw new IllegalArgumentException("ERROR result must have exception"); + } + mStatus = status; + mException = exception; + mRequestedUri = requestedUri; + mLookupUri = null; + mUri = null; + mDirectoryId = -1; + mLookupKey = null; + mId = -1; + mRawContacts = null; + mNameRawContactId = -1; + mDisplayNameSource = DisplayNameSources.UNDEFINED; + mPhotoId = -1; + mPhotoUri = null; + mDisplayName = null; + mAltDisplayName = null; + mPhoneticName = null; + mStarred = false; + mPresence = null; + mInvitableAccountTypes = null; + mSendToVoicemail = false; + mCustomRingtone = null; + mIsUserProfile = false; + } + + /** Constructor to call when contact was found */ + public Contact( + Uri requestedUri, + Uri uri, + Uri lookupUri, + long directoryId, + String lookupKey, + long id, + long nameRawContactId, + int displayNameSource, + long photoId, + String photoUri, + String displayName, + String altDisplayName, + String phoneticName, + boolean starred, + Integer presence, + boolean sendToVoicemail, + String customRingtone, + boolean isUserProfile) { + mStatus = Status.LOADED; + mException = null; + mRequestedUri = requestedUri; + mLookupUri = lookupUri; + mUri = uri; + mDirectoryId = directoryId; + mLookupKey = lookupKey; + mId = id; + mRawContacts = null; + mNameRawContactId = nameRawContactId; + mDisplayNameSource = displayNameSource; + mPhotoId = photoId; + mPhotoUri = photoUri; + mDisplayName = displayName; + mAltDisplayName = altDisplayName; + mPhoneticName = phoneticName; + mStarred = starred; + mPresence = presence; + mInvitableAccountTypes = null; + mSendToVoicemail = sendToVoicemail; + mCustomRingtone = customRingtone; + mIsUserProfile = isUserProfile; + } + + public Contact(Uri requestedUri, Contact from) { + mRequestedUri = requestedUri; + + mStatus = from.mStatus; + mException = from.mException; + mLookupUri = from.mLookupUri; + mUri = from.mUri; + mDirectoryId = from.mDirectoryId; + mLookupKey = from.mLookupKey; + mId = from.mId; + mNameRawContactId = from.mNameRawContactId; + mDisplayNameSource = from.mDisplayNameSource; + mPhotoId = from.mPhotoId; + mPhotoUri = from.mPhotoUri; + mDisplayName = from.mDisplayName; + mAltDisplayName = from.mAltDisplayName; + mPhoneticName = from.mPhoneticName; + mStarred = from.mStarred; + mPresence = from.mPresence; + mRawContacts = from.mRawContacts; + mInvitableAccountTypes = from.mInvitableAccountTypes; + + mDirectoryDisplayName = from.mDirectoryDisplayName; + mDirectoryType = from.mDirectoryType; + mDirectoryAccountType = from.mDirectoryAccountType; + mDirectoryAccountName = from.mDirectoryAccountName; + mDirectoryExportSupport = from.mDirectoryExportSupport; + + mGroups = from.mGroups; + + mPhotoBinaryData = from.mPhotoBinaryData; + mSendToVoicemail = from.mSendToVoicemail; + mCustomRingtone = from.mCustomRingtone; + mIsUserProfile = from.mIsUserProfile; + } + + public static Contact forError(Uri requestedUri, Exception exception) { + return new Contact(requestedUri, Status.ERROR, exception); + } + + public static Contact forNotFound(Uri requestedUri) { + return new Contact(requestedUri, Status.NOT_FOUND, null); + } + + /** @param exportSupport See {@link Directory#EXPORT_SUPPORT}. */ + public void setDirectoryMetaData( + String displayName, + String directoryType, + String accountType, + String accountName, + int exportSupport) { + mDirectoryDisplayName = displayName; + mDirectoryType = directoryType; + mDirectoryAccountType = accountType; + mDirectoryAccountName = accountName; + mDirectoryExportSupport = exportSupport; + } + + /** + * Returns the URI for the contact that contains both the lookup key and the ID. This is the best + * URI to reference a contact. For directory contacts, this is the same a the URI as returned by + * {@link #getUri()} + */ + public Uri getLookupUri() { + return mLookupUri; + } + + public String getLookupKey() { + return mLookupKey; + } + + /** + * Returns the contact Uri that was passed to the provider to make the query. This is the same as + * the requested Uri, unless the requested Uri doesn't specify a Contact: If it either references + * a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will always reference the full + * aggregate contact. + */ + public Uri getUri() { + return mUri; + } + + /** Returns the contact ID. */ + @VisibleForTesting + public long getId() { + return mId; + } + + /** + * @return true when an exception happened during loading, in which case {@link #getException} + * returns the actual exception object. + */ + public boolean isError() { + return mStatus == Status.ERROR; + } + + public Exception getException() { + return mException; + } + + /** @return true if the specified contact is successfully loaded. */ + public boolean isLoaded() { + return mStatus == Status.LOADED; + } + + public long getNameRawContactId() { + return mNameRawContactId; + } + + public int getDisplayNameSource() { + return mDisplayNameSource; + } + + public long getPhotoId() { + return mPhotoId; + } + + public String getPhotoUri() { + return mPhotoUri; + } + + public String getDisplayName() { + return mDisplayName; + } + + public boolean getStarred() { + return mStarred; + } + + public Integer getPresence() { + return mPresence; + } + + /** + * This can return non-null invitable account types only if the {@link ContactLoader} was + * configured to load invitable account types in its constructor. + */ + public ImmutableList getInvitableAccountTypes() { + return mInvitableAccountTypes; + } + + /* package */ void setInvitableAccountTypes(ImmutableList accountTypes) { + mInvitableAccountTypes = accountTypes; + } + + public ImmutableList getRawContacts() { + return mRawContacts; + } + + /* package */ void setRawContacts(ImmutableList rawContacts) { + mRawContacts = rawContacts; + } + + public long getDirectoryId() { + return mDirectoryId; + } + + public boolean isDirectoryEntry() { + return mDirectoryId != -1 + && mDirectoryId != Directory.DEFAULT + && mDirectoryId != Directory.LOCAL_INVISIBLE; + } + + /* package */ void setPhotoBinaryData(byte[] photoBinaryData) { + mPhotoBinaryData = photoBinaryData; + } + + public byte[] getThumbnailPhotoBinaryData() { + return mThumbnailPhotoBinaryData; + } + + /* package */ void setThumbnailPhotoBinaryData(byte[] photoBinaryData) { + mThumbnailPhotoBinaryData = photoBinaryData; + } + + public ArrayList getContentValues() { + if (mRawContacts.size() != 1) { + throw new IllegalStateException("Cannot extract content values from an aggregated contact"); + } + + RawContact rawContact = mRawContacts.get(0); + ArrayList result = rawContact.getContentValues(); + + // If the photo was loaded using the URI, create an entry for the photo + // binary data. + if (mPhotoId == 0 && mPhotoBinaryData != null) { + ContentValues photo = new ContentValues(); + photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); + photo.put(Photo.PHOTO, mPhotoBinaryData); + result.add(photo); + } + + return result; + } + + /** + * This can return non-null group meta-data only if the {@link ContactLoader} was configured to + * load group metadata in its constructor. + */ + public ImmutableList getGroupMetaData() { + return mGroups; + } + + /* package */ void setGroupMetaData(ImmutableList groups) { + mGroups = groups; + } + + public boolean isUserProfile() { + return mIsUserProfile; + } + + @Override + public String toString() { + return "{requested=" + + mRequestedUri + + ",lookupkey=" + + mLookupKey + + ",uri=" + + mUri + + ",status=" + + mStatus + + "}"; + } + + private enum Status { + /** Contact is successfully loaded */ + LOADED, + /** There was an error loading the contact */ + ERROR, + /** Contact is not found */ + NOT_FOUND, + } +} diff --git a/java/com/android/contacts/common/model/ContactLoader.java b/java/com/android/contacts/common/model/ContactLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..eb16bffcd717e2c3a84941349221b84c350ebfca --- /dev/null +++ b/java/com/android/contacts/common/model/ContactLoader.java @@ -0,0 +1,998 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.model; + +import android.content.AsyncTaskLoader; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetFileDescriptor; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.Groups; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.GroupMetaData; +import com.android.contacts.common.model.account.AccountType; +import com.android.contacts.common.model.account.AccountTypeWithDataSet; +import com.android.contacts.common.model.dataitem.DataItem; +import com.android.contacts.common.model.dataitem.PhoneDataItem; +import com.android.contacts.common.model.dataitem.PhotoDataItem; +import com.android.contacts.common.util.Constants; +import com.android.contacts.common.util.ContactLoaderUtils; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.compat.CompatUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** Loads a single Contact and all it constituent RawContacts. */ +public class ContactLoader extends AsyncTaskLoader { + + private static final String TAG = ContactLoader.class.getSimpleName(); + + /** A short-lived cache that can be set by {@link #cacheResult()} */ + private static Contact sCachedResult = null; + + private final Uri mRequestedUri; + private final Set mNotifiedRawContactIds = Sets.newHashSet(); + private Uri mLookupUri; + private boolean mLoadGroupMetaData; + private boolean mLoadInvitableAccountTypes; + private boolean mPostViewNotification; + private boolean mComputeFormattedPhoneNumber; + private Contact mContact; + private ForceLoadContentObserver mObserver; + + public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) { + this(context, lookupUri, false, false, postViewNotification, false); + } + + public ContactLoader( + Context context, + Uri lookupUri, + boolean loadGroupMetaData, + boolean loadInvitableAccountTypes, + boolean postViewNotification, + boolean computeFormattedPhoneNumber) { + super(context); + mLookupUri = lookupUri; + mRequestedUri = lookupUri; + mLoadGroupMetaData = loadGroupMetaData; + mLoadInvitableAccountTypes = loadInvitableAccountTypes; + mPostViewNotification = postViewNotification; + mComputeFormattedPhoneNumber = computeFormattedPhoneNumber; + } + + /** + * Parses a {@link Contact} stored as a JSON string in a lookup URI. + * + * @param lookupUri The contact information to parse . + * @return The parsed {@code Contact} information. + */ + public static Contact parseEncodedContactEntity(Uri lookupUri) { + try { + return loadEncodedContactEntity(lookupUri, lookupUri); + } catch (JSONException je) { + return null; + } + } + + private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException { + final String jsonString = uri.getEncodedFragment(); + final JSONObject json = new JSONObject(jsonString); + + final long directoryId = + Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY)); + + final String displayName = json.optString(Contacts.DISPLAY_NAME); + final String altDisplayName = json.optString(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName); + final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE); + final String photoUri = json.optString(Contacts.PHOTO_URI, null); + final Contact contact = + new Contact( + uri, + uri, + lookupUri, + directoryId, + null /* lookupKey */, + -1 /* id */, + -1 /* nameRawContactId */, + displayNameSource, + 0 /* photoId */, + photoUri, + displayName, + altDisplayName, + null /* phoneticName */, + false /* starred */, + null /* presence */, + false /* sendToVoicemail */, + null /* customRingtone */, + false /* isUserProfile */); + + final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null); + final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME); + if (accountName != null) { + final String accountType = json.getString(RawContacts.ACCOUNT_TYPE); + contact.setDirectoryMetaData( + directoryName, + null, + accountName, + accountType, + json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY)); + } else { + contact.setDirectoryMetaData( + directoryName, + null, + null, + null, + json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT)); + } + + final ContentValues values = new ContentValues(); + values.put(Data._ID, -1); + values.put(Data.CONTACT_ID, -1); + final RawContact rawContact = new RawContact(values); + + final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE); + final Iterator keys = items.keys(); + while (keys.hasNext()) { + final String mimetype = (String) keys.next(); + + // Could be single object or array. + final JSONObject obj = items.optJSONObject(mimetype); + if (obj == null) { + final JSONArray array = items.getJSONArray(mimetype); + for (int i = 0; i < array.length(); i++) { + final JSONObject item = array.getJSONObject(i); + processOneRecord(rawContact, item, mimetype); + } + } else { + processOneRecord(rawContact, obj, mimetype); + } + } + + contact.setRawContacts(new ImmutableList.Builder().add(rawContact).build()); + return contact; + } + + private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype) + throws JSONException { + final ContentValues itemValues = new ContentValues(); + itemValues.put(Data.MIMETYPE, mimetype); + itemValues.put(Data._ID, -1); + + final Iterator iterator = item.keys(); + while (iterator.hasNext()) { + String name = (String) iterator.next(); + final Object o = item.get(name); + if (o instanceof String) { + itemValues.put(name, (String) o); + } else if (o instanceof Integer) { + itemValues.put(name, (Integer) o); + } + } + rawContact.addDataItemValues(itemValues); + } + + @Override + public Contact loadInBackground() { + Log.e(TAG, "loadInBackground=" + mLookupUri); + try { + final ContentResolver resolver = getContext().getContentResolver(); + final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mLookupUri); + final Contact cachedResult = sCachedResult; + sCachedResult = null; + // Is this the same Uri as what we had before already? In that case, reuse that result + final Contact result; + final boolean resultIsCached; + if (cachedResult != null && UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) { + // We are using a cached result from earlier. Below, we should make sure + // we are not doing any more network or disc accesses + result = new Contact(mRequestedUri, cachedResult); + resultIsCached = true; + } else { + if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) { + result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri); + } else { + result = loadContactEntity(resolver, uriCurrentFormat); + } + resultIsCached = false; + } + if (result.isLoaded()) { + if (result.isDirectoryEntry()) { + if (!resultIsCached) { + loadDirectoryMetaData(result); + } + } else if (mLoadGroupMetaData) { + if (result.getGroupMetaData() == null) { + loadGroupMetaData(result); + } + } + if (mComputeFormattedPhoneNumber) { + computeFormattedPhoneNumbers(result); + } + if (!resultIsCached) { + loadPhotoBinaryData(result); + } + + // Note ME profile should never have "Add connection" + if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) { + loadInvitableAccountTypes(result); + } + } + return result; + } catch (Exception e) { + Log.e(TAG, "Error loading the contact: " + mLookupUri, e); + return Contact.forError(mRequestedUri, e); + } + } + + private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) { + Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY); + Cursor cursor = + resolver.query(entityUri, ContactQuery.COLUMNS, null, null, Contacts.Entity.RAW_CONTACT_ID); + if (cursor == null) { + Log.e(TAG, "No cursor returned in loadContactEntity"); + return Contact.forNotFound(mRequestedUri); + } + + try { + if (!cursor.moveToFirst()) { + cursor.close(); + return Contact.forNotFound(mRequestedUri); + } + + // Create the loaded contact starting with the header data. + Contact contact = loadContactHeaderData(cursor, contactUri); + + // Fill in the raw contacts, which is wrapped in an Entity and any + // status data. Initially, result has empty entities and statuses. + long currentRawContactId = -1; + RawContact rawContact = null; + ImmutableList.Builder rawContactsBuilder = + new ImmutableList.Builder(); + do { + long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID); + if (rawContactId != currentRawContactId) { + // First time to see this raw contact id, so create a new entity, and + // add it to the result's entities. + currentRawContactId = rawContactId; + rawContact = new RawContact(loadRawContactValues(cursor)); + rawContactsBuilder.add(rawContact); + } + if (!cursor.isNull(ContactQuery.DATA_ID)) { + ContentValues data = loadDataValues(cursor); + rawContact.addDataItemValues(data); + } + } while (cursor.moveToNext()); + + contact.setRawContacts(rawContactsBuilder.build()); + + return contact; + } finally { + cursor.close(); + } + } + + /** + * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger photo + * will also be stored if available. + */ + private void loadPhotoBinaryData(Contact contactData) { + loadThumbnailBinaryData(contactData); + + // Try to load the large photo from a file using the photo URI. + String photoUri = contactData.getPhotoUri(); + if (photoUri != null) { + try { + final InputStream inputStream; + final AssetFileDescriptor fd; + final Uri uri = Uri.parse(photoUri); + final String scheme = uri.getScheme(); + if ("http".equals(scheme) || "https".equals(scheme)) { + // Support HTTP urls that might come from extended directories + inputStream = new URL(photoUri).openStream(); + fd = null; + } else { + fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r"); + inputStream = fd.createInputStream(); + } + byte[] buffer = new byte[16 * 1024]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + int size; + while ((size = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, size); + } + contactData.setPhotoBinaryData(baos.toByteArray()); + } finally { + inputStream.close(); + if (fd != null) { + fd.close(); + } + } + return; + } catch (IOException ioe) { + // Just fall back to the case below. + } + } + + // If we couldn't load from a file, fall back to the data blob. + contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData()); + } + + private void loadThumbnailBinaryData(Contact contactData) { + final long photoId = contactData.getPhotoId(); + if (photoId <= 0) { + // No photo ID + return; + } + + for (RawContact rawContact : contactData.getRawContacts()) { + for (DataItem dataItem : rawContact.getDataItems()) { + if (dataItem.getId() == photoId) { + if (!(dataItem instanceof PhotoDataItem)) { + break; + } + + final PhotoDataItem photo = (PhotoDataItem) dataItem; + contactData.setThumbnailPhotoBinaryData(photo.getPhoto()); + break; + } + } + } + } + + /** Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}. */ + private void loadInvitableAccountTypes(Contact contactData) { + final ImmutableList.Builder resultListBuilder = + new ImmutableList.Builder(); + if (!contactData.isUserProfile()) { + Map invitables = + AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes(); + if (!invitables.isEmpty()) { + final Map resultMap = Maps.newHashMap(invitables); + + // Remove the ones that already have a raw contact in the current contact + for (RawContact rawContact : contactData.getRawContacts()) { + final AccountTypeWithDataSet type = + AccountTypeWithDataSet.get( + rawContact.getAccountTypeString(), rawContact.getDataSet()); + resultMap.remove(type); + } + + resultListBuilder.addAll(resultMap.values()); + } + } + + // Set to mInvitableAccountTypes + contactData.setInvitableAccountTypes(resultListBuilder.build()); + } + + /** Extracts Contact level columns from the cursor. */ + private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) { + final String directoryParameter = + contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); + final long directoryId = + directoryParameter == null ? Directory.DEFAULT : Long.parseLong(directoryParameter); + final long contactId = cursor.getLong(ContactQuery.CONTACT_ID); + final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY); + final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID); + final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE); + final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME); + final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME); + final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME); + final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); + final String photoUri = cursor.getString(ContactQuery.PHOTO_URI); + final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0; + final Integer presence = + cursor.isNull(ContactQuery.CONTACT_PRESENCE) + ? null + : cursor.getInt(ContactQuery.CONTACT_PRESENCE); + final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1; + final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE); + final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1; + + Uri lookupUri; + if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { + lookupUri = + ContentUris.withAppendedId( + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId); + } else { + lookupUri = contactUri; + } + + return new Contact( + mRequestedUri, + contactUri, + lookupUri, + directoryId, + lookupKey, + contactId, + nameRawContactId, + displayNameSource, + photoId, + photoUri, + displayName, + altDisplayName, + phoneticName, + starred, + presence, + sendToVoicemail, + customRingtone, + isUserProfile); + } + + /** Extracts RawContact level columns from the cursor. */ + private ContentValues loadRawContactValues(Cursor cursor) { + ContentValues cv = new ContentValues(); + + cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID)); + + cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME); + cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET); + cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY); + cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION); + cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4); + cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED); + cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID); + cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED); + + return cv; + } + + /** Extracts Data level columns from the cursor. */ + private ContentValues loadDataValues(Cursor cursor) { + ContentValues cv = new ContentValues(); + + cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID)); + + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION); + cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY); + cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY); + cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE); + cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID); + cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY); + cursorColumnToContentValues(cursor, cv, ContactQuery.TIMES_USED); + cursorColumnToContentValues(cursor, cv, ContactQuery.LAST_TIME_USED); + if (CompatUtils.isMarshmallowCompatible()) { + cursorColumnToContentValues(cursor, cv, ContactQuery.CARRIER_PRESENCE); + } + + return cv; + } + + private void cursorColumnToContentValues(Cursor cursor, ContentValues values, int index) { + switch (cursor.getType(index)) { + case Cursor.FIELD_TYPE_NULL: + // don't put anything in the content values + break; + case Cursor.FIELD_TYPE_INTEGER: + values.put(ContactQuery.COLUMNS[index], cursor.getLong(index)); + break; + case Cursor.FIELD_TYPE_STRING: + values.put(ContactQuery.COLUMNS[index], cursor.getString(index)); + break; + case Cursor.FIELD_TYPE_BLOB: + values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index)); + break; + default: + throw new IllegalStateException("Invalid or unhandled data type"); + } + } + + private void loadDirectoryMetaData(Contact result) { + long directoryId = result.getDirectoryId(); + + Cursor cursor = + getContext() + .getContentResolver() + .query( + ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), + DirectoryQuery.COLUMNS, + null, + null, + null); + if (cursor == null) { + return; + } + try { + if (cursor.moveToFirst()) { + final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); + final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); + final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); + final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); + final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); + final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); + String directoryType = null; + if (!TextUtils.isEmpty(packageName)) { + PackageManager pm = getContext().getPackageManager(); + try { + Resources resources = pm.getResourcesForApplication(packageName); + directoryType = resources.getString(typeResourceId); + } catch (NameNotFoundException e) { + Log.w( + TAG, "Contact directory resource not found: " + packageName + "." + typeResourceId); + } + } + + result.setDirectoryMetaData( + displayName, directoryType, accountType, accountName, exportSupport); + } + } finally { + cursor.close(); + } + } + + /** + * Loads groups meta-data for all groups associated with all constituent raw contacts' accounts. + */ + private void loadGroupMetaData(Contact result) { + StringBuilder selection = new StringBuilder(); + ArrayList selectionArgs = new ArrayList(); + final HashSet accountsSeen = new HashSet<>(); + for (RawContact rawContact : result.getRawContacts()) { + final String accountName = rawContact.getAccountName(); + final String accountType = rawContact.getAccountTypeString(); + final String dataSet = rawContact.getDataSet(); + final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet); + if (accountName != null && accountType != null && !accountsSeen.contains(accountKey)) { + accountsSeen.add(accountKey); + if (selection.length() != 0) { + selection.append(" OR "); + } + selection.append("(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?"); + selectionArgs.add(accountName); + selectionArgs.add(accountType); + + if (dataSet != null) { + selection.append(" AND " + Groups.DATA_SET + "=?"); + selectionArgs.add(dataSet); + } else { + selection.append(" AND " + Groups.DATA_SET + " IS NULL"); + } + selection.append(")"); + } + } + final ImmutableList.Builder groupListBuilder = + new ImmutableList.Builder(); + final Cursor cursor = + getContext() + .getContentResolver() + .query( + Groups.CONTENT_URI, + GroupQuery.COLUMNS, + selection.toString(), + selectionArgs.toArray(new String[0]), + null); + if (cursor != null) { + try { + while (cursor.moveToNext()) { + final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); + final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); + final String dataSet = cursor.getString(GroupQuery.DATA_SET); + final long groupId = cursor.getLong(GroupQuery.ID); + final String title = cursor.getString(GroupQuery.TITLE); + final boolean defaultGroup = + !cursor.isNull(GroupQuery.AUTO_ADD) && cursor.getInt(GroupQuery.AUTO_ADD) != 0; + final boolean favorites = + !cursor.isNull(GroupQuery.FAVORITES) && cursor.getInt(GroupQuery.FAVORITES) != 0; + + groupListBuilder.add( + new GroupMetaData( + accountName, accountType, dataSet, groupId, title, defaultGroup, favorites)); + } + } finally { + cursor.close(); + } + } + result.setGroupMetaData(groupListBuilder.build()); + } + + /** + * Iterates over all data items that represent phone numbers are tries to calculate a formatted + * number. This function can safely be called several times as no unformatted data is overwritten + */ + private void computeFormattedPhoneNumbers(Contact contactData) { + final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); + final ImmutableList rawContacts = contactData.getRawContacts(); + final int rawContactCount = rawContacts.size(); + for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) { + final RawContact rawContact = rawContacts.get(rawContactIndex); + final List dataItems = rawContact.getDataItems(); + final int dataCount = dataItems.size(); + for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { + final DataItem dataItem = dataItems.get(dataIndex); + if (dataItem instanceof PhoneDataItem) { + final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem; + phoneDataItem.computeFormattedPhoneNumber(countryIso); + } + } + } + } + + @Override + public void deliverResult(Contact result) { + unregisterObserver(); + + // The creator isn't interested in any further updates + if (isReset() || result == null) { + return; + } + + mContact = result; + + if (result.isLoaded()) { + mLookupUri = result.getLookupUri(); + + if (!result.isDirectoryEntry()) { + Log.i(TAG, "Registering content observer for " + mLookupUri); + if (mObserver == null) { + mObserver = new ForceLoadContentObserver(); + } + getContext().getContentResolver().registerContentObserver(mLookupUri, true, mObserver); + } + + if (mPostViewNotification) { + // inform the source of the data that this contact is being looked at + postViewNotificationToSyncAdapter(); + } + } + + super.deliverResult(mContact); + } + + /** + * Posts a message to the contributing sync adapters that have opted-in, notifying them that the + * contact has just been loaded + */ + private void postViewNotificationToSyncAdapter() { + Context context = getContext(); + for (RawContact rawContact : mContact.getRawContacts()) { + final long rawContactId = rawContact.getId(); + if (mNotifiedRawContactIds.contains(rawContactId)) { + continue; // Already notified for this raw contact. + } + mNotifiedRawContactIds.add(rawContactId); + final AccountType accountType = rawContact.getAccountType(context); + final String serviceName = accountType.getViewContactNotifyServiceClassName(); + final String servicePackageName = accountType.getViewContactNotifyServicePackageName(); + if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) { + final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); + final Intent intent = new Intent(); + intent.setClassName(servicePackageName, serviceName); + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE); + try { + context.startService(intent); + } catch (Exception e) { + Log.e(TAG, "Error sending message to source-app", e); + } + } + } + } + + private void unregisterObserver() { + if (mObserver != null) { + getContext().getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + } + + public Uri getLookupUri() { + return mLookupUri; + } + + public void setLookupUri(Uri lookupUri) { + mLookupUri = lookupUri; + } + + @Override + protected void onStartLoading() { + if (mContact != null) { + deliverResult(mContact); + } + + if (takeContentChanged() || mContact == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + unregisterObserver(); + mContact = null; + } + + /** + * Projection used for the query that loads all data for the entire contact (except for social + * stream items). + */ + private static class ContactQuery { + + public static final int NAME_RAW_CONTACT_ID = 0; + public static final int DISPLAY_NAME_SOURCE = 1; + public static final int LOOKUP_KEY = 2; + public static final int DISPLAY_NAME = 3; + public static final int ALT_DISPLAY_NAME = 4; + public static final int PHONETIC_NAME = 5; + public static final int PHOTO_ID = 6; + public static final int STARRED = 7; + public static final int CONTACT_PRESENCE = 8; + public static final int CONTACT_STATUS = 9; + public static final int CONTACT_STATUS_TIMESTAMP = 10; + public static final int CONTACT_STATUS_RES_PACKAGE = 11; + public static final int CONTACT_STATUS_LABEL = 12; + public static final int CONTACT_ID = 13; + public static final int RAW_CONTACT_ID = 14; + public static final int ACCOUNT_NAME = 15; + public static final int ACCOUNT_TYPE = 16; + public static final int DATA_SET = 17; + public static final int DIRTY = 18; + public static final int VERSION = 19; + public static final int SOURCE_ID = 20; + public static final int SYNC1 = 21; + public static final int SYNC2 = 22; + public static final int SYNC3 = 23; + public static final int SYNC4 = 24; + public static final int DELETED = 25; + public static final int DATA_ID = 26; + public static final int DATA1 = 27; + public static final int DATA2 = 28; + public static final int DATA3 = 29; + public static final int DATA4 = 30; + public static final int DATA5 = 31; + public static final int DATA6 = 32; + public static final int DATA7 = 33; + public static final int DATA8 = 34; + public static final int DATA9 = 35; + public static final int DATA10 = 36; + public static final int DATA11 = 37; + public static final int DATA12 = 38; + public static final int DATA13 = 39; + public static final int DATA14 = 40; + public static final int DATA15 = 41; + public static final int DATA_SYNC1 = 42; + public static final int DATA_SYNC2 = 43; + public static final int DATA_SYNC3 = 44; + public static final int DATA_SYNC4 = 45; + public static final int DATA_VERSION = 46; + public static final int IS_PRIMARY = 47; + public static final int IS_SUPERPRIMARY = 48; + public static final int MIMETYPE = 49; + public static final int GROUP_SOURCE_ID = 50; + public static final int PRESENCE = 51; + public static final int CHAT_CAPABILITY = 52; + public static final int STATUS = 53; + public static final int STATUS_RES_PACKAGE = 54; + public static final int STATUS_ICON = 55; + public static final int STATUS_LABEL = 56; + public static final int STATUS_TIMESTAMP = 57; + public static final int PHOTO_URI = 58; + public static final int SEND_TO_VOICEMAIL = 59; + public static final int CUSTOM_RINGTONE = 60; + public static final int IS_USER_PROFILE = 61; + public static final int TIMES_USED = 62; + public static final int LAST_TIME_USED = 63; + public static final int CARRIER_PRESENCE = 64; + static final String[] COLUMNS_INTERNAL = + new String[] { + Contacts.NAME_RAW_CONTACT_ID, + Contacts.DISPLAY_NAME_SOURCE, + Contacts.LOOKUP_KEY, + Contacts.DISPLAY_NAME, + Contacts.DISPLAY_NAME_ALTERNATIVE, + Contacts.PHONETIC_NAME, + Contacts.PHOTO_ID, + Contacts.STARRED, + Contacts.CONTACT_PRESENCE, + Contacts.CONTACT_STATUS, + Contacts.CONTACT_STATUS_TIMESTAMP, + Contacts.CONTACT_STATUS_RES_PACKAGE, + Contacts.CONTACT_STATUS_LABEL, + Contacts.Entity.CONTACT_ID, + Contacts.Entity.RAW_CONTACT_ID, + RawContacts.ACCOUNT_NAME, + RawContacts.ACCOUNT_TYPE, + RawContacts.DATA_SET, + RawContacts.DIRTY, + RawContacts.VERSION, + RawContacts.SOURCE_ID, + RawContacts.SYNC1, + RawContacts.SYNC2, + RawContacts.SYNC3, + RawContacts.SYNC4, + RawContacts.DELETED, + Contacts.Entity.DATA_ID, + Data.DATA1, + Data.DATA2, + Data.DATA3, + Data.DATA4, + Data.DATA5, + Data.DATA6, + Data.DATA7, + Data.DATA8, + Data.DATA9, + Data.DATA10, + Data.DATA11, + Data.DATA12, + Data.DATA13, + Data.DATA14, + Data.DATA15, + Data.SYNC1, + Data.SYNC2, + Data.SYNC3, + Data.SYNC4, + Data.DATA_VERSION, + Data.IS_PRIMARY, + Data.IS_SUPER_PRIMARY, + Data.MIMETYPE, + GroupMembership.GROUP_SOURCE_ID, + Data.PRESENCE, + Data.CHAT_CAPABILITY, + Data.STATUS, + Data.STATUS_RES_PACKAGE, + Data.STATUS_ICON, + Data.STATUS_LABEL, + Data.STATUS_TIMESTAMP, + Contacts.PHOTO_URI, + Contacts.SEND_TO_VOICEMAIL, + Contacts.CUSTOM_RINGTONE, + Contacts.IS_USER_PROFILE, + Data.TIMES_USED, + Data.LAST_TIME_USED + }; + static final String[] COLUMNS; + + static { + List projectionList = Lists.newArrayList(COLUMNS_INTERNAL); + if (CompatUtils.isMarshmallowCompatible()) { + projectionList.add(Data.CARRIER_PRESENCE); + } + COLUMNS = projectionList.toArray(new String[projectionList.size()]); + } + } + + /** Projection used for the query that loads all data for the entire contact. */ + private static class DirectoryQuery { + + public static final int DISPLAY_NAME = 0; + public static final int PACKAGE_NAME = 1; + public static final int TYPE_RESOURCE_ID = 2; + public static final int ACCOUNT_TYPE = 3; + public static final int ACCOUNT_NAME = 4; + public static final int EXPORT_SUPPORT = 5; + static final String[] COLUMNS = + new String[] { + Directory.DISPLAY_NAME, + Directory.PACKAGE_NAME, + Directory.TYPE_RESOURCE_ID, + Directory.ACCOUNT_TYPE, + Directory.ACCOUNT_NAME, + Directory.EXPORT_SUPPORT, + }; + } + + private static class GroupQuery { + + public static final int ACCOUNT_NAME = 0; + public static final int ACCOUNT_TYPE = 1; + public static final int DATA_SET = 2; + public static final int ID = 3; + public static final int TITLE = 4; + public static final int AUTO_ADD = 5; + public static final int FAVORITES = 6; + static final String[] COLUMNS = + new String[] { + Groups.ACCOUNT_NAME, + Groups.ACCOUNT_TYPE, + Groups.DATA_SET, + Groups._ID, + Groups.TITLE, + Groups.AUTO_ADD, + Groups.FAVORITES, + }; + } + + private static class AccountKey { + + private final String mAccountName; + private final String mAccountType; + private final String mDataSet; + + public AccountKey(String accountName, String accountType, String dataSet) { + mAccountName = accountName; + mAccountType = accountType; + mDataSet = dataSet; + } + + @Override + public int hashCode() { + return Objects.hash(mAccountName, mAccountType, mDataSet); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AccountKey)) { + return false; + } + final AccountKey other = (AccountKey) obj; + return Objects.equals(mAccountName, other.mAccountName) + && Objects.equals(mAccountType, other.mAccountType) + && Objects.equals(mDataSet, other.mDataSet); + } + } +} diff --git a/java/com/android/contacts/common/model/RawContact.java b/java/com/android/contacts/common/model/RawContact.java new file mode 100644 index 0000000000000000000000000000000000000000..9efc8a87859048b0e3922f74e665a34260427031 --- /dev/null +++ b/java/com/android/contacts/common/model/RawContact.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import com.android.contacts.common.model.account.AccountType; +import com.android.contacts.common.model.account.AccountWithDataSet; +import com.android.contacts.common.model.dataitem.DataItem; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * RawContact represents a single raw contact in the raw contacts database. It has specialized + * getters/setters for raw contact items, and also contains a collection of DataItem objects. A + * RawContact contains the information from a single account. + * + *

This allows RawContact objects to be thought of as a class with raw contact fields (like + * account type, name, data set, sync state, etc.) and a list of DataItem objects that represent + * contact information elements (like phone numbers, email, address, etc.). + */ +public final class RawContact implements Parcelable { + + /** Create for building the parcelable. */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public RawContact createFromParcel(Parcel parcel) { + return new RawContact(parcel); + } + + @Override + public RawContact[] newArray(int i) { + return new RawContact[i]; + } + }; + + private final ContentValues mValues; + private final ArrayList mDataItems; + private AccountTypeManager mAccountTypeManager; + + /** A RawContact object can be created with or without a context. */ + public RawContact() { + this(new ContentValues()); + } + + public RawContact(ContentValues values) { + mValues = values; + mDataItems = new ArrayList(); + } + + /** + * Constructor for the parcelable. + * + * @param parcel The parcel to de-serialize from. + */ + private RawContact(Parcel parcel) { + mValues = parcel.readParcelable(ContentValues.class.getClassLoader()); + mDataItems = new ArrayList<>(); + parcel.readTypedList(mDataItems, NamedDataItem.CREATOR); + } + + public static RawContact createFrom(Entity entity) { + final ContentValues values = entity.getEntityValues(); + final ArrayList subValues = entity.getSubValues(); + + RawContact rawContact = new RawContact(values); + for (Entity.NamedContentValues subValue : subValues) { + rawContact.addNamedDataItemValues(subValue.uri, subValue.values); + } + return rawContact; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeParcelable(mValues, i); + parcel.writeTypedList(mDataItems); + } + + public AccountTypeManager getAccountTypeManager(Context context) { + if (mAccountTypeManager == null) { + mAccountTypeManager = AccountTypeManager.getInstance(context); + } + return mAccountTypeManager; + } + + public ContentValues getValues() { + return mValues; + } + + /** Returns the id of the raw contact. */ + public Long getId() { + return getValues().getAsLong(RawContacts._ID); + } + + /** Returns the account name of the raw contact. */ + public String getAccountName() { + return getValues().getAsString(RawContacts.ACCOUNT_NAME); + } + + /** Returns the account type of the raw contact. */ + public String getAccountTypeString() { + return getValues().getAsString(RawContacts.ACCOUNT_TYPE); + } + + /** Returns the data set of the raw contact. */ + public String getDataSet() { + return getValues().getAsString(RawContacts.DATA_SET); + } + + public boolean isDirty() { + return getValues().getAsBoolean(RawContacts.DIRTY); + } + + public String getSourceId() { + return getValues().getAsString(RawContacts.SOURCE_ID); + } + + public String getSync1() { + return getValues().getAsString(RawContacts.SYNC1); + } + + public String getSync2() { + return getValues().getAsString(RawContacts.SYNC2); + } + + public String getSync3() { + return getValues().getAsString(RawContacts.SYNC3); + } + + public String getSync4() { + return getValues().getAsString(RawContacts.SYNC4); + } + + public boolean isDeleted() { + return getValues().getAsBoolean(RawContacts.DELETED); + } + + public long getContactId() { + return getValues().getAsLong(Contacts.Entity.CONTACT_ID); + } + + public boolean isStarred() { + return getValues().getAsBoolean(Contacts.STARRED); + } + + public AccountType getAccountType(Context context) { + return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet()); + } + + /** + * Sets the account name, account type, and data set strings. Valid combinations for account-name, + * account-type, data-set 1) null, null, null (local account) 2) non-null, non-null, null (valid + * account without data-set) 3) non-null, non-null, non-null (valid account with data-set) + */ + private void setAccount(String accountName, String accountType, String dataSet) { + final ContentValues values = getValues(); + if (accountName == null) { + if (accountType == null && dataSet == null) { + // This is a local account + values.putNull(RawContacts.ACCOUNT_NAME); + values.putNull(RawContacts.ACCOUNT_TYPE); + values.putNull(RawContacts.DATA_SET); + return; + } + } else { + if (accountType != null) { + // This is a valid account, either with or without a dataSet. + values.put(RawContacts.ACCOUNT_NAME, accountName); + values.put(RawContacts.ACCOUNT_TYPE, accountType); + if (dataSet == null) { + values.putNull(RawContacts.DATA_SET); + } else { + values.put(RawContacts.DATA_SET, dataSet); + } + return; + } + } + throw new IllegalArgumentException( + "Not a valid combination of account name, type, and data set."); + } + + public void setAccount(AccountWithDataSet accountWithDataSet) { + if (accountWithDataSet != null) { + setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet); + } else { + setAccount(null, null, null); + } + } + + public void setAccountToLocal() { + setAccount(null, null, null); + } + + /** Creates and inserts a DataItem object that wraps the content values, and returns it. */ + public void addDataItemValues(ContentValues values) { + addNamedDataItemValues(Data.CONTENT_URI, values); + } + + public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) { + final NamedDataItem namedItem = new NamedDataItem(uri, values); + mDataItems.add(namedItem); + return namedItem; + } + + public ArrayList getContentValues() { + final ArrayList list = new ArrayList<>(mDataItems.size()); + for (NamedDataItem dataItem : mDataItems) { + if (Data.CONTENT_URI.equals(dataItem.mUri)) { + list.add(dataItem.mContentValues); + } + } + return list; + } + + public List getDataItems() { + final ArrayList list = new ArrayList<>(mDataItems.size()); + for (NamedDataItem dataItem : mDataItems) { + if (Data.CONTENT_URI.equals(dataItem.mUri)) { + list.add(DataItem.createFrom(dataItem.mContentValues)); + } + } + return list; + } + + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("RawContact: ").append(mValues); + for (RawContact.NamedDataItem namedDataItem : mDataItems) { + sb.append("\n ").append(namedDataItem.mUri); + sb.append("\n -> ").append(namedDataItem.mContentValues); + } + return sb.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(mValues, mDataItems); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + RawContact other = (RawContact) obj; + return Objects.equals(mValues, other.mValues) && Objects.equals(mDataItems, other.mDataItems); + } + + public static final class NamedDataItem implements Parcelable { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public NamedDataItem createFromParcel(Parcel parcel) { + return new NamedDataItem(parcel); + } + + @Override + public NamedDataItem[] newArray(int i) { + return new NamedDataItem[i]; + } + }; + public final Uri mUri; + // This use to be a DataItem. DataItem creation is now delayed until the point of request + // since there is no benefit to storing them here due to the multiple inheritance. + // Eventually instanceof still has to be used anyways to determine which sub-class of + // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or + // parcelable. + // + // Instead of having a common DataItem super class, we should refactor this to be a generic + // Object where the object is a concrete class that no longer relies on ContentValues. + // (this will also make the classes easier to use). + // Since instanceof is used later anyways, having a list of Objects won't hurt and is no + // worse than having a DataItem. + public final ContentValues mContentValues; + + public NamedDataItem(Uri uri, ContentValues values) { + this.mUri = uri; + this.mContentValues = values; + } + + public NamedDataItem(Parcel parcel) { + this.mUri = parcel.readParcelable(Uri.class.getClassLoader()); + this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeParcelable(mUri, i); + parcel.writeParcelable(mContentValues, i); + } + + @Override + public int hashCode() { + return Objects.hash(mUri, mContentValues); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final NamedDataItem other = (NamedDataItem) obj; + return Objects.equals(mUri, other.mUri) + && Objects.equals(mContentValues, other.mContentValues); + } + } +} diff --git a/java/com/android/contacts/common/model/account/AccountType.java b/java/com/android/contacts/common/model/account/AccountType.java new file mode 100644 index 0000000000000000000000000000000000000000..1ae485a5f52a6676c71a9dbe782a402b914ed678 --- /dev/null +++ b/java/com/android/contacts/common/model/account/AccountType.java @@ -0,0 +1,501 @@ +/* + * 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.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.RawContacts; +import android.support.annotation.VisibleForTesting; +import android.util.ArrayMap; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Internal structure that represents constraints and styles for a specific data source, such as the + * various data types they support, including details on how those types should be rendered and + * edited. + * + *

In the future this may be inflated from XML defined by a data source. + */ +public abstract class AccountType { + + private static final String TAG = "AccountType"; + /** {@link Comparator} to sort by {@link DataKind#weight}. */ + private static Comparator sWeightComparator = + new Comparator() { + @Override + public int compare(DataKind object1, DataKind object2) { + return object1.weight - object2.weight; + } + }; + /** The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. */ + public String accountType = null; + /** The {@link RawContacts#DATA_SET} these constraints apply to. */ + public String dataSet = null; + /** + * Package that resources should be loaded from. Will be null for embedded types, in which case + * resources are stored in this package itself. + * + *

TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and {@link + * #getViewContactNotifyServicePackageName()}. + * + *

There's the following invariants: - {@link #syncAdapterPackageName} is always set to the + * actual sync adapter package name. - {@link #resourcePackageName} too is set to the same value, + * unless {@link #isEmbedded()}, in which case it'll be null. There's an unfortunate exception of + * {@link FallbackAccountType}. Even though it {@link #isEmbedded()}, but we set non-null to + * {@link #resourcePackageName} for unit tests. + */ + public String resourcePackageName; + /** + * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) or + * the sync adapter (for external type, including extensions). + */ + public String syncAdapterPackageName; + + public int titleRes; + public int iconRes; + protected boolean mIsInitialized; + /** Set of {@link DataKind} supported by this source. */ + private ArrayList mKinds = new ArrayList<>(); + /** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */ + private Map mMimeKinds = new ArrayMap<>(); + + /** + * Return a string resource loaded from the given package (or the current package if {@code + * packageName} is null), unless {@code resId} is -1, in which case it returns {@code + * defaultValue}. + * + *

(The behavior is undefined if the resource or package doesn't exist.) + */ + @VisibleForTesting + static CharSequence getResourceText( + Context context, String packageName, int resId, String defaultValue) { + if (resId != -1 && packageName != null) { + final PackageManager pm = context.getPackageManager(); + return pm.getText(packageName, resId, null); + } else if (resId != -1) { + return context.getText(resId); + } else { + return defaultValue; + } + } + + public static Drawable getDisplayIcon( + Context context, int titleRes, int iconRes, String syncAdapterPackageName) { + if (titleRes != -1 && syncAdapterPackageName != null) { + final PackageManager pm = context.getPackageManager(); + return pm.getDrawable(syncAdapterPackageName, iconRes, null); + } else if (titleRes != -1) { + return context.getResources().getDrawable(iconRes); + } else { + return null; + } + } + + /** + * Whether this account type was able to be fully initialized. This may be false if (for example) + * the package name associated with the account type could not be found. + */ + public final boolean isInitialized() { + return mIsInitialized; + } + + /** + * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType}, + * {@link GoogleAccountType} or {@link ExternalAccountType}. + *

If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns + * {@code false}) it's considered critical, and the application will crash. On the other hand + * if it's not an embedded type, we just skip loading the type. + */ + public boolean isEmbedded() { + return true; + } + + public boolean isExtension() { + return false; + } + + /** + * @return True if contacts can be created and edited using this app. If false, there could still + * be an external editor as provided by {@link #getEditContactActivityClassName()} or {@link + * #getCreateContactActivityClassName()} + */ + public abstract boolean areContactsWritable(); + + /** + * Returns an optional custom edit activity. + * + *

Only makes sense for non-embedded account types. The activity class should reside in the + * sync adapter package as determined by {@link #syncAdapterPackageName}. + */ + public String getEditContactActivityClassName() { + return null; + } + + /** + * Returns an optional custom new contact activity. + * + *

Only makes sense for non-embedded account types. The activity class should reside in the + * sync adapter package as determined by {@link #syncAdapterPackageName}. + */ + public String getCreateContactActivityClassName() { + return null; + } + + /** + * Returns an optional custom invite contact activity. + * + *

Only makes sense for non-embedded account types. The activity class should reside in the + * sync adapter package as determined by {@link #syncAdapterPackageName}. + */ + public String getInviteContactActivityClassName() { + return null; + } + + /** + * Returns an optional service that can be launched whenever a contact is being looked at. This + * allows the sync adapter to provide more up-to-date information. + * + *

The service class should reside in the sync adapter package as determined by {@link + * #getViewContactNotifyServicePackageName()}. + */ + public String getViewContactNotifyServiceClassName() { + return null; + } + + /** + * TODO This is way too hacky should be removed. + * + *

This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} is + * the authenticator package name but the notification service is in the sync adapter package. See + * {@link #resourcePackageName} -- we should clean up those. + */ + public String getViewContactNotifyServicePackageName() { + return syncAdapterPackageName; + } + + /** Returns an optional Activity string that can be used to view the group. */ + public String getViewGroupActivity() { + return null; + } + + public CharSequence getDisplayLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + return getResourceText(context, syncAdapterPackageName, titleRes, accountType); + } + + /** @return resource ID for the "invite contact" action label, or -1 if not defined. */ + protected int getInviteContactActionResId() { + return -1; + } + + /** @return resource ID for the "view group" label, or -1 if not defined. */ + protected int getViewGroupLabelResId() { + return -1; + } + + /** Returns {@link AccountTypeWithDataSet} for this type. */ + public AccountTypeWithDataSet getAccountTypeAndDataSet() { + return AccountTypeWithDataSet.get(accountType, dataSet); + } + + /** + * Returns a list of additional package names that should be inspected as additional external + * account types. This allows for a primary account type to indicate other packages that may not + * be sync adapters but which still provide contact data, perhaps under a separate data set within + * the account. + */ + public List getExtensionPackageNames() { + return new ArrayList(); + } + + /** + * Returns an optional custom label for the "invite contact" action, which will be shown on the + * contact card. (If not defined, returns null.) + */ + public CharSequence getInviteContactActionLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), ""); + } + + /** + * Returns a label for the "view group" action. If not defined, this falls back to our own "View + * Updates" string + */ + public CharSequence getViewGroupLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + final CharSequence customTitle = + getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null); + + return customTitle == null ? context.getText(R.string.view_updates_from_group) : customTitle; + } + + public Drawable getDisplayIcon(Context context) { + return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName); + } + + /** Whether or not groups created under this account type have editable membership lists. */ + public abstract boolean isGroupMembershipEditable(); + + /** Return list of {@link DataKind} supported, sorted by {@link DataKind#weight}. */ + public ArrayList getSortedDataKinds() { + // TODO: optimize by marking if already sorted + Collections.sort(mKinds, sWeightComparator); + return mKinds; + } + + /** Find the {@link DataKind} for a specific MIME-type, if it's handled by this data source. */ + public DataKind getKindForMimetype(String mimeType) { + return this.mMimeKinds.get(mimeType); + } + + /** Add given {@link DataKind} to list of those provided by this source. */ + public DataKind addKind(DataKind kind) throws DefinitionException { + if (kind.mimeType == null) { + throw new DefinitionException("null is not a valid mime type"); + } + if (mMimeKinds.get(kind.mimeType) != null) { + throw new DefinitionException("mime type '" + kind.mimeType + "' is already registered"); + } + + kind.resourcePackageName = this.resourcePackageName; + this.mKinds.add(kind); + this.mMimeKinds.put(kind.mimeType, kind); + return kind; + } + + /** + * Generic method of inflating a given {@link ContentValues} into a user-readable {@link + * CharSequence}. For example, an inflater could combine the multiple columns of {@link + * StructuredPostal} together using a string resource before presenting to the user. + */ + public interface StringInflater { + + CharSequence inflateUsing(Context context, ContentValues values); + } + + protected static class DefinitionException extends Exception { + + public DefinitionException(String message) { + super(message); + } + + public DefinitionException(String message, Exception inner) { + super(message, inner); + } + } + + /** + * Description of a specific "type" or "label" of a {@link DataKind} row, such as {@link + * Phone#TYPE_WORK}. Includes constraints on total number of rows a {@link Contacts} may have of + * this type, and details on how user-defined labels are stored. + */ + public static class EditType { + + public int rawValue; + public int labelRes; + public boolean secondary; + /** + * The number of entries allowed for the type. -1 if not specified. + * + * @see DataKind#typeOverallMax + */ + public int specificMax; + + public String customColumn; + + public EditType(int rawValue, int labelRes) { + this.rawValue = rawValue; + this.labelRes = labelRes; + this.specificMax = -1; + } + + public EditType setSecondary(boolean secondary) { + this.secondary = secondary; + return this; + } + + public EditType setSpecificMax(int specificMax) { + this.specificMax = specificMax; + return this; + } + + public EditType setCustomColumn(String customColumn) { + this.customColumn = customColumn; + return this; + } + + @Override + public boolean equals(Object object) { + if (object instanceof EditType) { + final EditType other = (EditType) object; + return other.rawValue == rawValue; + } + return false; + } + + @Override + public int hashCode() { + return rawValue; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + " rawValue=" + + rawValue + + " labelRes=" + + labelRes + + " secondary=" + + secondary + + " specificMax=" + + specificMax + + " customColumn=" + + customColumn; + } + } + + public static class EventEditType extends EditType { + + private boolean mYearOptional; + + public EventEditType(int rawValue, int labelRes) { + super(rawValue, labelRes); + } + + public boolean isYearOptional() { + return mYearOptional; + } + + public EventEditType setYearOptional(boolean yearOptional) { + mYearOptional = yearOptional; + return this; + } + + @Override + public String toString() { + return super.toString() + " mYearOptional=" + mYearOptional; + } + } + + /** + * Description of a user-editable field on a {@link DataKind} row, such as {@link Phone#NUMBER}. + * Includes flags to apply to an {@link EditText}, and the column where this field is stored. + */ + public static final class EditField { + + public String column; + public int titleRes; + public int inputType; + public int minLines; + public boolean optional; + public boolean shortForm; + public boolean longForm; + + public EditField(String column, int titleRes) { + this.column = column; + this.titleRes = titleRes; + } + + public EditField(String column, int titleRes, int inputType) { + this(column, titleRes); + this.inputType = inputType; + } + + public EditField setOptional(boolean optional) { + this.optional = optional; + return this; + } + + public EditField setShortForm(boolean shortForm) { + this.shortForm = shortForm; + return this; + } + + public EditField setLongForm(boolean longForm) { + this.longForm = longForm; + return this; + } + + public EditField setMinLines(int minLines) { + this.minLines = minLines; + return this; + } + + public boolean isMultiLine() { + return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + ":" + + " column=" + + column + + " titleRes=" + + titleRes + + " inputType=" + + inputType + + " minLines=" + + minLines + + " optional=" + + optional + + " shortForm=" + + shortForm + + " longForm=" + + longForm; + } + } + + /** + * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the current + * locale. + */ + public static class DisplayLabelComparator implements Comparator { + + private final Context mContext; + /** {@link Comparator} for the current locale. */ + private final Collator mCollator = Collator.getInstance(); + + public DisplayLabelComparator(Context context) { + mContext = context; + } + + private String getDisplayLabel(AccountType type) { + CharSequence label = type.getDisplayLabel(mContext); + return (label == null) ? "" : label.toString(); + } + + @Override + public int compare(AccountType lhs, AccountType rhs) { + return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs)); + } + } +} diff --git a/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java new file mode 100644 index 0000000000000000000000000000000000000000..a32ebe139bc6aa59b77d2ceb47d5706c84e995b4 --- /dev/null +++ b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; +import java.util.Objects; + +/** Encapsulates an "account type" string and a "data set" string. */ +public class AccountTypeWithDataSet { + + private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID}; + private static final Uri RAW_CONTACTS_URI_LIMIT_1 = + RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1") + .build(); + + /** account type. Can be null for fallback type. */ + public final String accountType; + + /** dataSet may be null, but never be "". */ + public final String dataSet; + + private AccountTypeWithDataSet(String accountType, String dataSet) { + this.accountType = TextUtils.isEmpty(accountType) ? null : accountType; + this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet; + } + + public static AccountTypeWithDataSet get(String accountType, String dataSet) { + return new AccountTypeWithDataSet(accountType, dataSet); + } + + /** + * Return true if there are any contacts in the database with this account type and data set. + * Touches DB. Don't use in the UI thread. + */ + public boolean hasData(Context context) { + final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?"; + final String selection; + final String[] args; + if (TextUtils.isEmpty(dataSet)) { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL"; + args = new String[] {accountType}; + } else { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?"; + args = new String[] {accountType, dataSet}; + } + + final Cursor c = + context + .getContentResolver() + .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null); + if (c == null) { + return false; + } + try { + return c.moveToFirst(); + } finally { + c.close(); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AccountTypeWithDataSet)) { + return false; + } + + AccountTypeWithDataSet other = (AccountTypeWithDataSet) o; + return Objects.equals(accountType, other.accountType) && Objects.equals(dataSet, other.dataSet); + } + + @Override + public int hashCode() { + return (accountType == null ? 0 : accountType.hashCode()) + ^ (dataSet == null ? 0 : dataSet.hashCode()); + } + + @Override + public String toString() { + return "[" + accountType + "/" + dataSet + "]"; + } +} diff --git a/java/com/android/contacts/common/model/account/AccountWithDataSet.java b/java/com/android/contacts/common/model/account/AccountWithDataSet.java new file mode 100644 index 0000000000000000000000000000000000000000..71faf509cf98557afe4cef8054cae2b02730eab1 --- /dev/null +++ b/java/com/android/contacts/common/model/account/AccountWithDataSet.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.accounts.Account; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** Wrapper for an account that includes a data set (which may be null). */ +public class AccountWithDataSet implements Parcelable { + + // For Parcelable + public static final Creator CREATOR = + new Creator() { + public AccountWithDataSet createFromParcel(Parcel source) { + return new AccountWithDataSet(source); + } + + public AccountWithDataSet[] newArray(int size) { + return new AccountWithDataSet[size]; + } + }; + private static final String STRINGIFY_SEPARATOR = "\u0001"; + private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002"; + private static final Pattern STRINGIFY_SEPARATOR_PAT = + Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR)); + private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT = + Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR)); + private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID}; + private static final Uri RAW_CONTACTS_URI_LIMIT_1 = + RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1") + .build(); + public final String name; + public final String type; + public final String dataSet; + private final AccountTypeWithDataSet mAccountTypeWithDataSet; + + public AccountWithDataSet(String name, String type, String dataSet) { + this.name = emptyToNull(name); + this.type = emptyToNull(type); + this.dataSet = emptyToNull(dataSet); + mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet); + } + + public AccountWithDataSet(Parcel in) { + this.name = in.readString(); + this.type = in.readString(); + this.dataSet = in.readString(); + mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet); + } + + private static String emptyToNull(String text) { + return TextUtils.isEmpty(text) ? null : text; + } + + private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) { + if (!TextUtils.isEmpty(account.name)) { + sb.append(account.name); + } + sb.append(STRINGIFY_SEPARATOR); + if (!TextUtils.isEmpty(account.type)) { + sb.append(account.type); + } + sb.append(STRINGIFY_SEPARATOR); + if (!TextUtils.isEmpty(account.dataSet)) { + sb.append(account.dataSet); + } + + return sb; + } + + /** + * Unpack a string created by {@link #stringify}. + * + * @throws IllegalArgumentException if it's an invalid string. + */ + public static AccountWithDataSet unstringify(String s) { + final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3); + if (array.length < 3) { + throw new IllegalArgumentException("Invalid string " + s); + } + return new AccountWithDataSet( + array[0], array[1], TextUtils.isEmpty(array[2]) ? null : array[2]); + } + + /** Pack a list of {@link AccountWithDataSet} into a string. */ + public static String stringifyList(List accounts) { + final StringBuilder sb = new StringBuilder(); + + for (AccountWithDataSet account : accounts) { + if (sb.length() > 0) { + sb.append(ARRAY_STRINGIFY_SEPARATOR); + } + addStringified(sb, account); + } + + return sb.toString(); + } + + /** + * Unpack a list of {@link AccountWithDataSet} into a string. + * + * @throws IllegalArgumentException if it's an invalid string. + */ + public static List unstringifyList(String s) { + final ArrayList ret = new ArrayList<>(); + if (TextUtils.isEmpty(s)) { + return ret; + } + + final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s); + + for (int i = 0; i < array.length; i++) { + ret.add(unstringify(array[i])); + } + + return ret; + } + + public boolean isLocalAccount() { + return name == null && type == null; + } + + public Account getAccountOrNull() { + if (name != null && type != null) { + return new Account(name, type); + } + return null; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(type); + dest.writeString(dataSet); + } + + public AccountTypeWithDataSet getAccountTypeWithDataSet() { + return mAccountTypeWithDataSet; + } + + /** + * Return {@code true} if this account has any contacts in the database. Touches DB. Don't use in + * the UI thread. + */ + public boolean hasData(Context context) { + final String BASE_SELECTION = + RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?"; + final String selection; + final String[] args; + if (TextUtils.isEmpty(dataSet)) { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL"; + args = new String[] {type, name}; + } else { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?"; + args = new String[] {type, name, dataSet}; + } + + final Cursor c = + context + .getContentResolver() + .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null); + if (c == null) { + return false; + } + try { + return c.moveToFirst(); + } finally { + c.close(); + } + } + + public boolean equals(Object obj) { + if (obj instanceof AccountWithDataSet) { + AccountWithDataSet other = (AccountWithDataSet) obj; + return Objects.equals(name, other.name) + && Objects.equals(type, other.type) + && Objects.equals(dataSet, other.dataSet); + } + return false; + } + + public int hashCode() { + int result = 17; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (dataSet != null ? dataSet.hashCode() : 0); + return result; + } + + public String toString() { + return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}"; + } + + /** Pack the instance into a string. */ + public String stringify() { + return addStringified(new StringBuilder(), this).toString(); + } +} diff --git a/java/com/android/contacts/common/model/account/BaseAccountType.java b/java/com/android/contacts/common/model/account/BaseAccountType.java new file mode 100644 index 0000000000000000000000000000000000000000..21b555917b2253bc626424a7480273c56ae4fea2 --- /dev/null +++ b/java/com/android/contacts/common/model/account/BaseAccountType.java @@ -0,0 +1,1890 @@ +/* + * 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.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.content.res.Resources; +import android.provider.ContactsContract.CommonDataKinds.BaseTypes; +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.Relation; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.ArrayMap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.inputmethod.EditorInfo; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import com.android.contacts.common.util.ContactDisplayUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public abstract class BaseAccountType extends AccountType { + + public static final StringInflater ORGANIZATION_BODY_INFLATER = + new StringInflater() { + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final CharSequence companyValue = + values.containsKey(Organization.COMPANY) + ? values.getAsString(Organization.COMPANY) + : null; + final CharSequence titleValue = + values.containsKey(Organization.TITLE) + ? values.getAsString(Organization.TITLE) + : null; + + if (companyValue != null && titleValue != null) { + return companyValue + ": " + titleValue; + } else if (companyValue == null) { + return titleValue; + } else { + return companyValue; + } + } + }; + protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE; + protected static final int FLAGS_EMAIL = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + protected static final int FLAGS_PERSON_NAME = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS + | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME; + protected static final int FLAGS_PHONETIC = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC; + protected static final int FLAGS_GENERIC_NAME = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; + protected static final int FLAGS_NOTE = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT; + protected static final int FLAGS_WEBSITE = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI; + protected static final int FLAGS_POSTAL = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + protected static final int FLAGS_SIP_ADDRESS = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same + // basic format as email addresses + protected static final int FLAGS_RELATION = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS + | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME; + + // Specify the maximum number of lines that can be used to display various field types. If no + // value is specified for a particular type, we use the default value from {@link DataKind}. + protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10; + protected static final int MAX_LINES_FOR_GROUP = 10; + protected static final int MAX_LINES_FOR_NOTE = 100; + private static final String TAG = "BaseAccountType"; + + public BaseAccountType() { + this.accountType = null; + this.dataSet = null; + this.titleRes = R.string.account_phone; + this.iconRes = R.mipmap.ic_contacts_launcher; + } + + protected static EditType buildPhoneType(int type) { + return new EditType(type, Phone.getTypeLabelResource(type)); + } + + protected static EditType buildEmailType(int type) { + return new EditType(type, Email.getTypeLabelResource(type)); + } + + protected static EditType buildPostalType(int type) { + return new EditType(type, StructuredPostal.getTypeLabelResource(type)); + } + + protected static EditType buildImType(int type) { + return new EditType(type, Im.getProtocolLabelResource(type)); + } + + protected static EditType buildEventType(int type, boolean yearOptional) { + return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional); + } + + protected static EditType buildRelationType(int type) { + return new EditType(type, Relation.getTypeLabelResource(type)); + } + + // Utility methods to keep code shorter. + private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) { + return attrs.getAttributeBooleanValue(null, attribute, defaultValue); + } + + private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) { + return attrs.getAttributeIntValue(null, attribute, defaultValue); + } + + private static String getAttr(AttributeSet attrs, String attribute) { + return attrs.getAttributeValue(null, attribute); + } + + protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + R.string.nameLabelsGroup, + Weight.NONE, + true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME) + .setShortForm(true)); + + boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + + if (!displayOrderPrimary) { + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } else { + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } + + return kind; + } + + protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + R.string.name_phonetic, + Weight.NONE, + true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC) + .setShortForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC) + .setLongForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC) + .setLongForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC) + .setLongForm(true)); + + return kind; + } + + protected DataKind addDataKindNickname(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Nickname.CONTENT_ITEM_TYPE, R.string.nicknameLabelsGroup, Weight.NICKNAME, true)); + kind.typeOverallMax = 1; + kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME)); + + return kind; + } + + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, Weight.PHONE, true)); + kind.iconAltRes = R.drawable.ic_message_24dp; + kind.iconAltDescriptionRes = R.string.sms; + kind.actionHeader = new PhoneActionInflater(); + kind.actionAltHeader = new PhoneActionAltInflater(); + kind.actionBody = new SimpleInflater(Phone.NUMBER); + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add( + buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, Weight.EMAIL, true)); + kind.actionHeader = new EmailActionInflater(); + kind.actionBody = new SimpleInflater(Email.DATA); + kind.typeColumn = Email.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add(buildEmailType(Email.TYPE_MOBILE)); + kind.typeList.add( + buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + StructuredPostal.CONTENT_ITEM_TYPE, + R.string.postalLabelsGroup, + Weight.STRUCTURED_POSTAL, + true)); + kind.actionHeader = new PostalActionInflater(); + kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER)); + kind.typeList.add( + buildPostalType(StructuredPostal.TYPE_CUSTOM) + .setSecondary(true) + .setCustomColumn(StructuredPostal.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS; + + return kind; + } + + protected DataKind addDataKindIm(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, Weight.IM, true)); + kind.actionHeader = new ImActionInflater(); + kind.actionBody = new SimpleInflater(Im.DATA); + + // NOTE: even though a traditional "type" exists, for editing + // purposes we're using the protocol to pick labels + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + kind.typeColumn = Im.PROTOCOL; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildImType(Im.PROTOCOL_AIM)); + kind.typeList.add(buildImType(Im.PROTOCOL_MSN)); + kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO)); + kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE)); + kind.typeList.add(buildImType(Im.PROTOCOL_QQ)); + kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK)); + kind.typeList.add(buildImType(Im.PROTOCOL_ICQ)); + kind.typeList.add(buildImType(Im.PROTOCOL_JABBER)); + kind.typeList.add( + buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + protected DataKind addDataKindOrganization(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Organization.CONTENT_ITEM_TYPE, + R.string.organizationLabelsGroup, + Weight.ORGANIZATION, + true)); + kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup); + kind.actionBody = ORGANIZATION_BODY_INFLATER; + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME)); + kind.fieldList.add( + new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME)); + + return kind; + } + + protected DataKind addDataKindPhoto(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, Weight.NONE, true)); + kind.typeOverallMax = 1; + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + return kind; + } + + protected DataKind addDataKindNote(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, Weight.NOTE, true)); + kind.typeOverallMax = 1; + kind.actionHeader = new SimpleInflater(R.string.label_notes); + kind.actionBody = new SimpleInflater(Note.NOTE); + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE; + + return kind; + } + + protected DataKind addDataKindWebsite(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Website.CONTENT_ITEM_TYPE, R.string.websiteLabelsGroup, Weight.WEBSITE, true)); + kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup); + kind.actionBody = new SimpleInflater(Website.URL); + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + return kind; + } + + protected DataKind addDataKindSipAddress(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + SipAddress.CONTENT_ITEM_TYPE, + R.string.label_sip_address, + Weight.SIP_ADDRESS, + true)); + + kind.actionHeader = new SimpleInflater(R.string.label_sip_address); + kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS); + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS)); + kind.typeOverallMax = 1; + + return kind; + } + + protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + GroupMembership.CONTENT_ITEM_TYPE, + R.string.groupsLabel, + Weight.GROUP_MEMBERSHIP, + true)); + + kind.typeOverallMax = 1; + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP; + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return false; + } + + /** Parses the content of the EditSchema tag in contacts.xml. */ + protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException, DefinitionException { + + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + final int depth = parser.getDepth(); + if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) { + continue; // Not direct child tag + } + + final String tag = parser.getName(); + + if (Tag.DATA_KIND.equals(tag)) { + for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) { + addKind(kind); + } + } else { + Log.w(TAG, "Skipping unknown tag " + tag); + } + } + } + + private interface Tag { + + String DATA_KIND = "DataKind"; + String TYPE = "Type"; + } + + private interface Attr { + + String MAX_OCCURRENCE = "maxOccurs"; + String DATE_WITH_TIME = "dateWithTime"; + String YEAR_OPTIONAL = "yearOptional"; + String KIND = "kind"; + String TYPE = "type"; + } + + protected interface Weight { + + int NONE = -1; + int PHONE = 10; + int EMAIL = 15; + int STRUCTURED_POSTAL = 25; + int NICKNAME = 111; + int EVENT = 120; + int ORGANIZATION = 125; + int NOTE = 130; + int IM = 140; + int SIP_ADDRESS = 145; + int GROUP_MEMBERSHIP = 150; + int WEBSITE = 160; + int RELATIONSHIP = 999; + } + + /** + * Simple inflater that assumes a string resource has a "%s" that will be filled from the given + * column. + */ + public static class SimpleInflater implements StringInflater { + + private final int mStringRes; + private final String mColumnName; + + public SimpleInflater(int stringRes) { + this(stringRes, null); + } + + public SimpleInflater(String columnName) { + this(-1, columnName); + } + + public SimpleInflater(int stringRes, String columnName) { + mStringRes = stringRes; + mColumnName = columnName; + } + + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final boolean validColumn = values.containsKey(mColumnName); + final boolean validString = mStringRes > 0; + + final CharSequence stringValue = validString ? context.getText(mStringRes) : null; + final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null; + + if (validString && validColumn) { + return String.format(stringValue.toString(), columnValue); + } else if (validString) { + return stringValue; + } else if (validColumn) { + return columnValue; + } else { + return null; + } + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + " mStringRes=" + + mStringRes + + " mColumnName" + + mColumnName; + } + + public String getColumnNameForTest() { + return mColumnName; + } + } + + public abstract static class CommonInflater implements StringInflater { + + protected abstract int getTypeLabelResource(Integer type); + + protected boolean isCustom(Integer type) { + return type == BaseTypes.TYPE_CUSTOM; + } + + protected String getTypeColumn() { + return Phone.TYPE; + } + + protected String getLabelColumn() { + return Phone.LABEL; + } + + protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) { + final int labelRes = getTypeLabelResource(type); + if (type == null) { + return res.getText(labelRes); + } else if (isCustom(type)) { + return res.getString(labelRes, label == null ? "" : label); + } else { + return res.getText(labelRes); + } + } + + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final Integer type = values.getAsInteger(getTypeColumn()); + final String label = values.getAsString(getLabelColumn()); + return getTypeLabel(context.getResources(), type, label); + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + } + + public static class PhoneActionInflater extends CommonInflater { + + @Override + protected boolean isCustom(Integer type) { + return ContactDisplayUtils.isCustomPhoneType(type); + } + + @Override + protected int getTypeLabelResource(Integer type) { + return ContactDisplayUtils.getPhoneLabelResourceId(type); + } + } + + public static class PhoneActionAltInflater extends CommonInflater { + + @Override + protected boolean isCustom(Integer type) { + return ContactDisplayUtils.isCustomPhoneType(type); + } + + @Override + protected int getTypeLabelResource(Integer type) { + return ContactDisplayUtils.getSmsLabelResourceId(type); + } + } + + public static class EmailActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) { + return R.string.email; + } + switch (type) { + case Email.TYPE_HOME: + return R.string.email_home; + case Email.TYPE_WORK: + return R.string.email_work; + case Email.TYPE_OTHER: + return R.string.email_other; + case Email.TYPE_MOBILE: + return R.string.email_mobile; + default: + return R.string.email_custom; + } + } + } + + public static class EventActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + return Event.getTypeResource(type); + } + } + + public static class RelationActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type); + } + } + + public static class PostalActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) { + return R.string.map_other; + } + switch (type) { + case StructuredPostal.TYPE_HOME: + return R.string.map_home; + case StructuredPostal.TYPE_WORK: + return R.string.map_work; + case StructuredPostal.TYPE_OTHER: + return R.string.map_other; + default: + return R.string.map_custom; + } + } + } + + public static class ImActionInflater extends CommonInflater { + + @Override + protected String getTypeColumn() { + return Im.PROTOCOL; + } + + @Override + protected String getLabelColumn() { + return Im.CUSTOM_PROTOCOL; + } + + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) { + return R.string.chat; + } + switch (type) { + case Im.PROTOCOL_AIM: + return R.string.chat_aim; + case Im.PROTOCOL_MSN: + return R.string.chat_msn; + case Im.PROTOCOL_YAHOO: + return R.string.chat_yahoo; + case Im.PROTOCOL_SKYPE: + return R.string.chat_skype; + case Im.PROTOCOL_QQ: + return R.string.chat_qq; + case Im.PROTOCOL_GOOGLE_TALK: + return R.string.chat_gtalk; + case Im.PROTOCOL_ICQ: + return R.string.chat_icq; + case Im.PROTOCOL_JABBER: + return R.string.chat_jabber; + case Im.PROTOCOL_NETMEETING: + return R.string.chat; + default: + return R.string.chat; + } + } + } + + // TODO Extract it to its own class, and move all KindBuilders to it as well. + private static class KindParser { + + public static final KindParser INSTANCE = new KindParser(); + + private final Map mBuilders = new ArrayMap<>(); + + private KindParser() { + addBuilder(new NameKindBuilder()); + addBuilder(new NicknameKindBuilder()); + addBuilder(new PhoneKindBuilder()); + addBuilder(new EmailKindBuilder()); + addBuilder(new StructuredPostalKindBuilder()); + addBuilder(new ImKindBuilder()); + addBuilder(new OrganizationKindBuilder()); + addBuilder(new PhotoKindBuilder()); + addBuilder(new NoteKindBuilder()); + addBuilder(new WebsiteKindBuilder()); + addBuilder(new SipAddressKindBuilder()); + addBuilder(new GroupMembershipKindBuilder()); + addBuilder(new EventKindBuilder()); + addBuilder(new RelationshipKindBuilder()); + } + + private void addBuilder(KindBuilder builder) { + mBuilders.put(builder.getTagName(), builder); + } + + /** + * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns {@link + * DataKind}s. (Usually just one, but there are three for the "name" kind.) + * + *

This method returns a list, because we need to add 3 kinds for the name data kind. + * (structured, display and phonetic) + */ + public List parseDataKindTag( + Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final String kind = getAttr(attrs, Attr.KIND); + final KindBuilder builder = mBuilders.get(kind); + if (builder != null) { + return builder.parseDataKind(context, parser, attrs); + } else { + throw new DefinitionException("Undefined data kind '" + kind + "'"); + } + } + } + + private abstract static class KindBuilder { + + public abstract String getTagName(); + + /** DataKind tag parser specific to each kind. Subclasses must implement it. */ + public abstract List parseDataKind( + Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException; + + /** Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind tag. */ + protected final DataKind newDataKind( + Context context, + XmlPullParser parser, + AttributeSet attrs, + boolean isPseudo, + String mimeType, + String typeColumn, + int titleRes, + int weight, + StringInflater actionHeader, + StringInflater actionBody) + throws DefinitionException, XmlPullParserException, IOException { + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Adding DataKind: " + mimeType); + } + + final DataKind kind = new DataKind(mimeType, titleRes, weight, true); + kind.typeColumn = typeColumn; + kind.actionHeader = actionHeader; + kind.actionBody = actionBody; + kind.fieldList = new ArrayList<>(); + + // Get more information from the tag... + // A pseudo data kind doesn't have corresponding tag the XML, so we skip this. + if (!isPseudo) { + kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1); + + // Process "Type" tags. + // If a kind has the type column, contacts.xml must have at least one type + // definition. Otherwise, it mustn't have a type definition. + if (kind.typeColumn != null) { + // Parse and add types. + kind.typeList = new ArrayList<>(); + parseTypes(context, parser, attrs, kind, true); + if (kind.typeList.size() == 0) { + throw new DefinitionException("Kind " + kind.mimeType + " must have at least one type"); + } + } else { + // Make sure it has no types. + parseTypes(context, parser, attrs, kind, false /* can't have types */); + } + } + + return kind; + } + + /** + * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds them to + * the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type, so throws + * {@link DefinitionException}. + */ + private void parseTypes( + Context context, + XmlPullParser parser, + AttributeSet attrs, + DataKind kind, + boolean canHaveTypes) + throws DefinitionException, XmlPullParserException, IOException { + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + final int depth = parser.getDepth(); + if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) { + continue; // Not direct child tag + } + + final String tag = parser.getName(); + if (Tag.TYPE.equals(tag)) { + if (canHaveTypes) { + kind.typeList.add(parseTypeTag(parser, attrs, kind)); + } else { + throw new DefinitionException("Kind " + kind.mimeType + " can't have types"); + } + } else { + throw new DefinitionException("Unknown tag: " + tag); + } + } + } + + /** + * Parses a single Type element and returns an {@link EditType} built from it. Uses {@link + * #buildEditTypeForTypeTag} defined in subclasses to actually build an {@link EditType}. + */ + private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind) + throws DefinitionException { + + final String typeName = getAttr(attrs, Attr.TYPE); + + final EditType et = buildEditTypeForTypeTag(attrs, typeName); + if (et == null) { + throw new DefinitionException( + "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'"); + } + et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1); + + return et; + } + + /** + * Returns an {@link EditType} for the given "type". Subclasses may optionally use the + * attributes in the tag to set optional values. (e.g. "yearOptional" for the event kind) + */ + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + return null; + } + + protected final void throwIfList(DataKind kind) throws DefinitionException { + if (kind.typeOverallMax != 1) { + throw new DefinitionException("Kind " + kind.mimeType + " must have 'overallMax=\"1\"'"); + } + } + } + + /** DataKind parser for Name. (structured, display, phonetic) */ + private static class NameKindBuilder extends KindBuilder { + + private static void checkAttributeTrue(boolean value, String attrName) + throws DefinitionException { + if (!value) { + throw new DefinitionException(attrName + " must be true"); + } + } + + @Override + public String getTagName() { + return "name"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + + // Build 3 data kinds: + // - StructuredName.CONTENT_ITEM_TYPE + // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME + // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME + + final boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + + final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false); + final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false); + final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false); + final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false); + final boolean supportsPhoneticFamilyName = + getAttr(attrs, "supportsPhoneticFamilyName", false); + final boolean supportsPhoneticMiddleName = + getAttr(attrs, "supportsPhoneticMiddleName", false); + final boolean supportsPhoneticGivenName = getAttr(attrs, "supportsPhoneticGivenName", false); + + // For now, every things must be supported. + checkAttributeTrue(supportsDisplayName, "supportsDisplayName"); + checkAttributeTrue(supportsPrefix, "supportsPrefix"); + checkAttributeTrue(supportsMiddleName, "supportsMiddleName"); + checkAttributeTrue(supportsSuffix, "supportsSuffix"); + checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName"); + checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName"); + checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName"); + + final List kinds = new ArrayList<>(); + + // Structured name + final DataKind ks = + newDataKind( + context, + parser, + attrs, + false, + StructuredName.CONTENT_ITEM_TYPE, + null, + R.string.nameLabelsGroup, + Weight.NONE, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + + throwIfList(ks); + kinds.add(ks); + + // Note about setLongForm/setShortForm below. + // We need to set this only when the type supports display name. (=supportsDisplayName) + // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields + // "optional". + + ks.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)); + ks.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + ks.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)); + ks.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + // Display name + final DataKind kd = + newDataKind( + context, + parser, + attrs, + true, + DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + null, + R.string.nameLabelsGroup, + Weight.NONE, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + kd.typeOverallMax = 1; + kinds.add(kd); + + kd.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME) + .setShortForm(true)); + + if (!displayOrderPrimary) { + kd.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } else { + kd.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } + + // Phonetic name + final DataKind kp = + newDataKind( + context, + parser, + attrs, + true, + DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + null, + R.string.name_phonetic, + Weight.NONE, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + kp.typeOverallMax = 1; + kinds.add(kp); + + // We may want to change the order depending on displayOrderPrimary too. + kp.fieldList.add( + new EditField( + DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC) + .setShortForm(true)); + kp.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, + FLAGS_PHONETIC) + .setLongForm(true)); + kp.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, + R.string.name_phonetic_middle, + FLAGS_PHONETIC) + .setLongForm(true)); + kp.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC) + .setLongForm(true)); + return kinds; + } + } + + private static class NicknameKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "nickname"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Nickname.CONTENT_ITEM_TYPE, + null, + R.string.nicknameLabelsGroup, + Weight.NICKNAME, + new SimpleInflater(R.string.nicknameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + + kind.fieldList.add( + new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT); + + throwIfList(kind); + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class PhoneKindBuilder extends KindBuilder { + + /** Just to avoid line-wrapping... */ + protected static EditType build(int type, boolean secondary) { + return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary); + } + + @Override + public String getTagName() { + return "phone"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Phone.CONTENT_ITEM_TYPE, + Phone.TYPE, + R.string.phoneLabelsGroup, + Weight.PHONE, + new PhoneActionInflater(), + new SimpleInflater(Phone.NUMBER)); + + kind.iconAltRes = R.drawable.ic_message_24dp; + kind.iconAltDescriptionRes = R.string.sms; + kind.actionAltHeader = new PhoneActionAltInflater(); + + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + if ("home".equals(type)) { + return build(Phone.TYPE_HOME, false); + } + if ("mobile".equals(type)) { + return build(Phone.TYPE_MOBILE, false); + } + if ("work".equals(type)) { + return build(Phone.TYPE_WORK, false); + } + if ("fax_work".equals(type)) { + return build(Phone.TYPE_FAX_WORK, true); + } + if ("fax_home".equals(type)) { + return build(Phone.TYPE_FAX_HOME, true); + } + if ("pager".equals(type)) { + return build(Phone.TYPE_PAGER, true); + } + if ("other".equals(type)) { + return build(Phone.TYPE_OTHER, false); + } + if ("callback".equals(type)) { + return build(Phone.TYPE_CALLBACK, true); + } + if ("car".equals(type)) { + return build(Phone.TYPE_CAR, true); + } + if ("company_main".equals(type)) { + return build(Phone.TYPE_COMPANY_MAIN, true); + } + if ("isdn".equals(type)) { + return build(Phone.TYPE_ISDN, true); + } + if ("main".equals(type)) { + return build(Phone.TYPE_MAIN, true); + } + if ("other_fax".equals(type)) { + return build(Phone.TYPE_OTHER_FAX, true); + } + if ("radio".equals(type)) { + return build(Phone.TYPE_RADIO, true); + } + if ("telex".equals(type)) { + return build(Phone.TYPE_TELEX, true); + } + if ("tty_tdd".equals(type)) { + return build(Phone.TYPE_TTY_TDD, true); + } + if ("work_mobile".equals(type)) { + return build(Phone.TYPE_WORK_MOBILE, true); + } + if ("work_pager".equals(type)) { + return build(Phone.TYPE_WORK_PAGER, true); + } + + // Note "assistant" used to be a custom column for the fallback type, but not anymore. + if ("assistant".equals(type)) { + return build(Phone.TYPE_ASSISTANT, true); + } + if ("mms".equals(type)) { + return build(Phone.TYPE_MMS, true); + } + if ("custom".equals(type)) { + return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL); + } + return null; + } + } + + private static class EmailKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "email"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Email.CONTENT_ITEM_TYPE, + Email.TYPE, + R.string.emailLabelsGroup, + Weight.EMAIL, + new EmailActionInflater(), + new SimpleInflater(Email.DATA)); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("home".equals(type)) { + return buildEmailType(Email.TYPE_HOME); + } + if ("work".equals(type)) { + return buildEmailType(Email.TYPE_WORK); + } + if ("other".equals(type)) { + return buildEmailType(Email.TYPE_OTHER); + } + if ("mobile".equals(type)) { + return buildEmailType(Email.TYPE_MOBILE); + } + if ("custom".equals(type)) { + return buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL); + } + return null; + } + } + + private static class StructuredPostalKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "postal"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + StructuredPostal.CONTENT_ITEM_TYPE, + StructuredPostal.TYPE, + R.string.postalLabelsGroup, + Weight.STRUCTURED_POSTAL, + new PostalActionInflater(), + new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS)); + + if (getAttr(attrs, "needsStructured", false)) { + if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) { + // Japanese order + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + } else { + // Generic order + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + } + } else { + kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS; + kind.fieldList.add( + new EditField( + StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL)); + } + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("home".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_HOME); + } + if ("work".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_WORK); + } + if ("other".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_OTHER); + } + if ("custom".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_CUSTOM) + .setSecondary(true) + .setCustomColumn(Email.LABEL); + } + return null; + } + } + + private static class ImKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "im"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + + // IM is special: + // - It uses "protocol" as the custom label field + // - Its TYPE is fixed to TYPE_OTHER + + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Im.CONTENT_ITEM_TYPE, + Im.PROTOCOL, + R.string.imLabelsGroup, + Weight.IM, + new ImActionInflater(), + new SimpleInflater(Im.DATA) // header / action + ); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + if ("aim".equals(type)) { + return buildImType(Im.PROTOCOL_AIM); + } + if ("msn".equals(type)) { + return buildImType(Im.PROTOCOL_MSN); + } + if ("yahoo".equals(type)) { + return buildImType(Im.PROTOCOL_YAHOO); + } + if ("skype".equals(type)) { + return buildImType(Im.PROTOCOL_SKYPE); + } + if ("qq".equals(type)) { + return buildImType(Im.PROTOCOL_QQ); + } + if ("google_talk".equals(type)) { + return buildImType(Im.PROTOCOL_GOOGLE_TALK); + } + if ("icq".equals(type)) { + return buildImType(Im.PROTOCOL_ICQ); + } + if ("jabber".equals(type)) { + return buildImType(Im.PROTOCOL_JABBER); + } + if ("custom".equals(type)) { + return buildImType(Im.PROTOCOL_CUSTOM) + .setSecondary(true) + .setCustomColumn(Im.CUSTOM_PROTOCOL); + } + return null; + } + } + + private static class OrganizationKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "organization"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Organization.CONTENT_ITEM_TYPE, + null, + R.string.organizationLabelsGroup, + Weight.ORGANIZATION, + new SimpleInflater(R.string.organizationLabelsGroup), + ORGANIZATION_BODY_INFLATER); + + kind.fieldList.add( + new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME)); + kind.fieldList.add( + new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME)); + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class PhotoKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "photo"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Photo.CONTENT_ITEM_TYPE, + null /* no type */, + Weight.NONE, + -1, + null, + null // no header, no body + ); + + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class NoteKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "note"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Note.CONTENT_ITEM_TYPE, + null, + R.string.label_notes, + Weight.NOTE, + new SimpleInflater(R.string.label_notes), + new SimpleInflater(Note.NOTE)); + + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE; + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class WebsiteKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "website"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Website.CONTENT_ITEM_TYPE, + null, + R.string.websiteLabelsGroup, + Weight.WEBSITE, + new SimpleInflater(R.string.websiteLabelsGroup), + new SimpleInflater(Website.URL)); + + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class SipAddressKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "sip_address"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + SipAddress.CONTENT_ITEM_TYPE, + null, + R.string.label_sip_address, + Weight.SIP_ADDRESS, + new SimpleInflater(R.string.label_sip_address), + new SimpleInflater(SipAddress.SIP_ADDRESS)); + + kind.fieldList.add( + new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS)); + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class GroupMembershipKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "group_membership"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + GroupMembership.CONTENT_ITEM_TYPE, + null, + R.string.groupsLabel, + Weight.GROUP_MEMBERSHIP, + null, + null); + + kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1)); + kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP; + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + /** + * Event DataKind parser. + * + *

Event DataKind is used only for Google/Exchange types, so this parser is not used for now. + */ + private static class EventKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "event"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Event.CONTENT_ITEM_TYPE, + Event.TYPE, + R.string.eventLabelsGroup, + Weight.EVENT, + new EventActionInflater(), + new SimpleInflater(Event.START_DATE)); + + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) { + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT; + } else { + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + } + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false); + + if ("birthday".equals(type)) { + return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1); + } + if ("anniversary".equals(type)) { + return buildEventType(Event.TYPE_ANNIVERSARY, yo); + } + if ("other".equals(type)) { + return buildEventType(Event.TYPE_OTHER, yo); + } + if ("custom".equals(type)) { + return buildEventType(Event.TYPE_CUSTOM, yo) + .setSecondary(true) + .setCustomColumn(Event.LABEL); + } + return null; + } + } + + /** + * Relationship DataKind parser. + * + *

Relationship DataKind is used only for Google/Exchange types, so this parser is not used for + * now. + */ + private static class RelationshipKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "relationship"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Relation.CONTENT_ITEM_TYPE, + Relation.TYPE, + R.string.relationLabelsGroup, + Weight.RELATIONSHIP, + new RelationActionInflater(), + new SimpleInflater(Relation.NAME)); + + kind.fieldList.add( + new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("assistant".equals(type)) { + return buildRelationType(Relation.TYPE_ASSISTANT); + } + if ("brother".equals(type)) { + return buildRelationType(Relation.TYPE_BROTHER); + } + if ("child".equals(type)) { + return buildRelationType(Relation.TYPE_CHILD); + } + if ("domestic_partner".equals(type)) { + return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER); + } + if ("father".equals(type)) { + return buildRelationType(Relation.TYPE_FATHER); + } + if ("friend".equals(type)) { + return buildRelationType(Relation.TYPE_FRIEND); + } + if ("manager".equals(type)) { + return buildRelationType(Relation.TYPE_MANAGER); + } + if ("mother".equals(type)) { + return buildRelationType(Relation.TYPE_MOTHER); + } + if ("parent".equals(type)) { + return buildRelationType(Relation.TYPE_PARENT); + } + if ("partner".equals(type)) { + return buildRelationType(Relation.TYPE_PARTNER); + } + if ("referred_by".equals(type)) { + return buildRelationType(Relation.TYPE_REFERRED_BY); + } + if ("relative".equals(type)) { + return buildRelationType(Relation.TYPE_RELATIVE); + } + if ("sister".equals(type)) { + return buildRelationType(Relation.TYPE_SISTER); + } + if ("spouse".equals(type)) { + return buildRelationType(Relation.TYPE_SPOUSE); + } + if ("custom".equals(type)) { + return buildRelationType(Relation.TYPE_CUSTOM) + .setSecondary(true) + .setCustomColumn(Relation.LABEL); + } + return null; + } + } +} diff --git a/java/com/android/contacts/common/model/account/ExchangeAccountType.java b/java/com/android/contacts/common/model/account/ExchangeAccountType.java new file mode 100644 index 0000000000000000000000000000000000000000..a27028e808ba962336939c41ca2d40981e7f2291 --- /dev/null +++ b/java/com/android/contacts/common/model/account/ExchangeAccountType.java @@ -0,0 +1,365 @@ +/* + * 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.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import java.util.ArrayList; +import java.util.Locale; + +public class ExchangeAccountType extends BaseAccountType { + + private static final String TAG = "ExchangeAccountType"; + + private static final String ACCOUNT_TYPE_AOSP = "com.android.exchange"; + private static final String ACCOUNT_TYPE_GOOGLE_1 = "com.google.android.exchange"; + private static final String ACCOUNT_TYPE_GOOGLE_2 = "com.google.android.gm.exchange"; + + public ExchangeAccountType(Context context, String authenticatorPackageName, String type) { + this.accountType = type; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindEvent(context); + addDataKindWebsite(context); + addDataKindGroupMembership(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + public static boolean isExchangeType(String type) { + return ACCOUNT_TYPE_AOSP.equals(type) + || ACCOUNT_TYPE_GOOGLE_1.equals(type) + || ACCOUNT_TYPE_GOOGLE_2.equals(type); + } + + @Override + protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)); + + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + @Override + protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + R.string.nameLabelsGroup, + Weight.NONE, + true)); + + boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setOptional(true)); + if (!displayOrderPrimary) { + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)); + } else { + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)); + } + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setOptional(true)); + + return kind; + } + + @Override + protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + R.string.name_phonetic, + Weight.NONE, + true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + @Override + protected DataKind addDataKindNickname(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindNickname(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME)); + + return kind; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true).setSpecificMax(1)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeOverallMax = 3; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + @Override + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindStructuredPostal(context); + + final boolean useJapaneseOrder = + Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage()); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1)); + + kind.fieldList = new ArrayList<>(); + if (useJapaneseOrder) { + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + } else { + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + } + + return kind; + } + + @Override + protected DataKind addDataKindIm(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindIm(context); + + // Types are not supported for IM. There can be 3 IMs, but OWA only shows only the first + kind.typeOverallMax = 3; + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + @Override + protected DataKind addDataKindOrganization(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindOrganization(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME)); + kind.fieldList.add( + new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME)); + + return kind; + } + + @Override + protected DataKind addDataKindPhoto(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhoto(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + + return kind; + } + + @Override + protected DataKind addDataKindNote(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindNote(context); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + + return kind; + } + + protected DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeOverallMax = 1; + + kind.typeColumn = Event.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, false).setSpecificMax(1)); + + kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + protected DataKind addDataKindWebsite(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindWebsite(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/java/com/android/contacts/common/model/account/ExternalAccountType.java b/java/com/android/contacts/common/model/account/ExternalAccountType.java new file mode 100644 index 0000000000000000000000000000000000000000..aca1f70d245671298a0c65f124f1a18c4483c927 --- /dev/null +++ b/java/com/android/contacts/common/model/account/ExternalAccountType.java @@ -0,0 +1,443 @@ +/* + * 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.contacts.common.model.account; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** A general contacts account type descriptor. */ +public class ExternalAccountType extends BaseAccountType { + + private static final String TAG = "ExternalAccountType"; + + private static final String SYNC_META_DATA = "android.content.SyncAdapter"; + + /** + * The metadata name for so-called "contacts.xml". + * + *

On LMP and later, we also accept the "alternate" name. This is to allow sync adapters to + * have a contacts.xml without making it visible on older platforms. If you modify this also + * update the corresponding list in ContactsProvider/PhotoPriorityResolver + */ + private static final String[] METADATA_CONTACTS_NAMES = + new String[] { + "android.provider.ALTERNATE_CONTACTS_STRUCTURE", "android.provider.CONTACTS_STRUCTURE" + }; + + private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource"; + private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType"; + private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind"; + private static final String TAG_EDIT_SCHEMA = "EditSchema"; + + private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity"; + private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel"; + private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService"; + private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity"; + private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel"; + private static final String ATTR_DATA_SET = "dataSet"; + private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames"; + + // The following attributes should only be set in non-sync-adapter account types. They allow + // for the account type and resource IDs to be specified without an associated authenticator. + private static final String ATTR_ACCOUNT_TYPE = "accountType"; + private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel"; + private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon"; + + private final boolean mIsExtension; + + private String mEditContactActivityClassName; + private String mCreateContactActivityClassName; + private String mInviteContactActivity; + private String mInviteActionLabelAttribute; + private int mInviteActionLabelResId; + private String mViewContactNotifyService; + private String mViewGroupActivity; + private String mViewGroupLabelAttribute; + private int mViewGroupLabelResId; + private List mExtensionPackageNames; + private String mAccountTypeLabelAttribute; + private String mAccountTypeIconAttribute; + private boolean mHasContactsMetadata; + private boolean mHasEditSchema; + + public ExternalAccountType(Context context, String resPackageName, boolean isExtension) { + this(context, resPackageName, isExtension, null); + } + + /** + * Constructor used for testing to initialize with any arbitrary XML. + * + * @param injectedMetadata If non-null, it'll be used to initialize the type. Only set by tests. + * If null, the metadata is loaded from the specified package. + */ + ExternalAccountType( + Context context, + String packageName, + boolean isExtension, + XmlResourceParser injectedMetadata) { + this.mIsExtension = isExtension; + this.resourcePackageName = packageName; + this.syncAdapterPackageName = packageName; + + final XmlResourceParser parser; + if (injectedMetadata == null) { + parser = loadContactsXml(context, packageName); + } else { + parser = injectedMetadata; + } + boolean needLineNumberInErrorLog = true; + try { + if (parser != null) { + inflate(context, parser); + } + + // Done parsing; line number no longer needed in error log. + needLineNumberInErrorLog = false; + if (mHasEditSchema) { + checkKindExists(StructuredName.CONTENT_ITEM_TYPE); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME); + checkKindExists(Photo.CONTENT_ITEM_TYPE); + } else { + // Bring in name and photo from fallback source, which are non-optional + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindPhoto(context); + } + } catch (DefinitionException e) { + final StringBuilder error = new StringBuilder(); + error.append("Problem reading XML"); + if (needLineNumberInErrorLog && (parser != null)) { + error.append(" in line "); + error.append(parser.getLineNumber()); + } + error.append(" for external package "); + error.append(packageName); + + Log.e(TAG, error.toString(), e); + return; + } finally { + if (parser != null) { + parser.close(); + } + } + + mExtensionPackageNames = new ArrayList(); + mInviteActionLabelResId = + resolveExternalResId( + context, + mInviteActionLabelAttribute, + syncAdapterPackageName, + ATTR_INVITE_CONTACT_ACTION_LABEL); + mViewGroupLabelResId = + resolveExternalResId( + context, + mViewGroupLabelAttribute, + syncAdapterPackageName, + ATTR_VIEW_GROUP_ACTION_LABEL); + titleRes = + resolveExternalResId( + context, mAccountTypeLabelAttribute, syncAdapterPackageName, ATTR_ACCOUNT_LABEL); + iconRes = + resolveExternalResId( + context, mAccountTypeIconAttribute, syncAdapterPackageName, ATTR_ACCOUNT_ICON); + + // If we reach this point, the account type has been successfully initialized. + mIsInitialized = true; + } + + /** + * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package. + * + *

This method looks through all services in the package that handle sync adapter intents for + * the first one that contains CONTACTS_STRUCTURE metadata. We have to look through all sync + * adapters in the package in case there are contacts and other sync adapters (eg, calendar) in + * the same package. + * + *

Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case the + * account type *will* be initialized with minimal configuration. + */ + public static XmlResourceParser loadContactsXml(Context context, String resPackageName) { + final PackageManager pm = context.getPackageManager(); + final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName); + final List intentServices = + pm.queryIntentServices(intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); + + if (intentServices != null) { + for (final ResolveInfo resolveInfo : intentServices) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo == null) { + continue; + } + for (String metadataName : METADATA_CONTACTS_NAMES) { + final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, metadataName); + if (parser != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d( + TAG, + String.format( + "Metadata loaded from: %s, %s, %s", + serviceInfo.packageName, serviceInfo.name, metadataName)); + } + return parser; + } + } + } + } + + // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata. + return null; + } + + /** Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata. */ + public static boolean hasContactsXml(Context context, String resPackageName) { + return loadContactsXml(context, resPackageName) != null; + } + + /** + * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in the + * resource package. + * + *

If the argument is in the invalid format or isn't a resource name, it returns -1. + * + * @param context context + * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel" + * @param packageName name of the package containing the resource. + * @param xmlAttributeName attribute name which the resource came from. Used for logging. + */ + @VisibleForTesting + static int resolveExternalResId( + Context context, String resourceName, String packageName, String xmlAttributeName) { + if (TextUtils.isEmpty(resourceName)) { + return -1; // Empty text is okay. + } + if (resourceName.charAt(0) != '@') { + Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'"); + return -1; + } + final String name = resourceName.substring(1); + final Resources res; + try { + res = context.getPackageManager().getResourcesForApplication(packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to load package " + packageName); + return -1; + } + final int resId = res.getIdentifier(name, null, packageName); + if (resId == 0) { + Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName); + return -1; + } + return resId; + } + + private void checkKindExists(String mimeType) throws DefinitionException { + if (getKindForMimetype(mimeType) == null) { + throw new DefinitionException(mimeType + " must be supported"); + } + } + + @Override + public boolean isEmbedded() { + return false; + } + + @Override + public boolean isExtension() { + return mIsExtension; + } + + @Override + public boolean areContactsWritable() { + return mHasEditSchema; + } + + /** Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml. */ + public boolean hasContactsMetadata() { + return mHasContactsMetadata; + } + + @Override + public String getEditContactActivityClassName() { + return mEditContactActivityClassName; + } + + @Override + public String getCreateContactActivityClassName() { + return mCreateContactActivityClassName; + } + + @Override + public String getInviteContactActivityClassName() { + return mInviteContactActivity; + } + + @Override + protected int getInviteContactActionResId() { + return mInviteActionLabelResId; + } + + @Override + public String getViewContactNotifyServiceClassName() { + return mViewContactNotifyService; + } + + @Override + public String getViewGroupActivity() { + return mViewGroupActivity; + } + + @Override + protected int getViewGroupLabelResId() { + return mViewGroupLabelResId; + } + + @Override + public List getExtensionPackageNames() { + return mExtensionPackageNames; + } + + /** + * Inflate this {@link AccountType} from the given parser. This may only load details matching the + * publicly-defined schema. + */ + protected void inflate(Context context, XmlPullParser parser) throws DefinitionException { + final AttributeSet attrs = Xml.asAttributeSet(parser); + + try { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Drain comments and whitespace + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("No start tag found"); + } + + String rootTag = parser.getName(); + if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) + && !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) { + throw new IllegalStateException( + "Top level element must be " + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag); + } + + mHasContactsMetadata = true; + + int attributeCount = parser.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + String attr = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, attr + "=" + value); + } + if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) { + mEditContactActivityClassName = value; + } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) { + mCreateContactActivityClassName = value; + } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) { + mInviteContactActivity = value; + } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) { + mInviteActionLabelAttribute = value; + } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) { + mViewContactNotifyService = value; + } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) { + mViewGroupActivity = value; + } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) { + mViewGroupLabelAttribute = value; + } else if (ATTR_DATA_SET.equals(attr)) { + dataSet = value; + } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) { + mExtensionPackageNames.add(value); + } else if (ATTR_ACCOUNT_TYPE.equals(attr)) { + accountType = value; + } else if (ATTR_ACCOUNT_LABEL.equals(attr)) { + mAccountTypeLabelAttribute = value; + } else if (ATTR_ACCOUNT_ICON.equals(attr)) { + mAccountTypeIconAttribute = value; + } else { + Log.e(TAG, "Unsupported attribute " + attr); + } + } + + // Parse all children kinds + final int startDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > startDepth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) { + continue; // Not a direct child tag + } + + String tag = parser.getName(); + if (TAG_EDIT_SCHEMA.equals(tag)) { + mHasEditSchema = true; + parseEditSchema(context, parser, attrs); + } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) { + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactsDataKind); + final DataKind kind = new DataKind(); + + kind.mimeType = a.getString(R.styleable.ContactsDataKind_android_mimeType); + final String summaryColumn = + a.getString(R.styleable.ContactsDataKind_android_summaryColumn); + if (summaryColumn != null) { + // Inflate a specific column as summary when requested + kind.actionHeader = new SimpleInflater(summaryColumn); + } + final String detailColumn = + a.getString(R.styleable.ContactsDataKind_android_detailColumn); + if (detailColumn != null) { + // Inflate specific column as summary + kind.actionBody = new SimpleInflater(detailColumn); + } + + a.recycle(); + + addKind(kind); + } + } + } catch (XmlPullParserException e) { + throw new DefinitionException("Problem reading XML", e); + } catch (IOException e) { + throw new DefinitionException("Problem reading XML", e); + } + } +} diff --git a/java/com/android/contacts/common/model/account/FallbackAccountType.java b/java/com/android/contacts/common/model/account/FallbackAccountType.java new file mode 100644 index 0000000000000000000000000000000000000000..976a7b89243751de91287f169afd74ecd7071504 --- /dev/null +++ b/java/com/android/contacts/common/model/account/FallbackAccountType.java @@ -0,0 +1,77 @@ +/* + * 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.contacts.common.model.account; + +import android.content.Context; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; + +public class FallbackAccountType extends BaseAccountType { + + private static final String TAG = "FallbackAccountType"; + + private FallbackAccountType(Context context, String resPackageName) { + this.accountType = null; + this.dataSet = null; + this.titleRes = R.string.account_phone; + this.iconRes = R.mipmap.ic_contacts_launcher; + + // Note those are only set for unit tests. + this.resourcePackageName = resPackageName; + this.syncAdapterPackageName = resPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindSipAddress(context); + addDataKindGroupMembership(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + public FallbackAccountType(Context context) { + this(context, null); + } + + /** + * Used to compare with an {@link ExternalAccountType} built from a test contacts.xml. In order to + * build {@link DataKind}s with the same resource package name, {@code resPackageName} is + * injectable. + */ + static AccountType createWithPackageNameForTest(Context context, String resPackageName) { + return new FallbackAccountType(context, resPackageName); + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/java/com/android/contacts/common/model/account/GoogleAccountType.java b/java/com/android/contacts/common/model/account/GoogleAccountType.java new file mode 100644 index 0000000000000000000000000000000000000000..2f1fe0ed6dee74681b6a6031cf85241c608db8d3 --- /dev/null +++ b/java/com/android/contacts/common/model/account/GoogleAccountType.java @@ -0,0 +1,206 @@ +/* + * 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.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GoogleAccountType extends BaseAccountType { + + /** + * The package name that we should load contacts.xml from and rely on to handle G+ account + * actions. Even though this points to gms, in some cases gms will still hand off responsibility + * to the G+ app. + */ + public static final String PLUS_EXTENSION_PACKAGE_NAME = "com.google.android.gms"; + + public static final String ACCOUNT_TYPE = "com.google"; + private static final String TAG = "GoogleAccountType"; + private static final List mExtensionPackages = + new ArrayList<>(Collections.singletonList(PLUS_EXTENSION_PACKAGE_NAME)); + + public GoogleAccountType(Context context, String authenticatorPackageName) { + this.accountType = ACCOUNT_TYPE; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindSipAddress(context); + addDataKindGroupMembership(context); + addDataKindRelation(context); + addDataKindEvent(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + @Override + public List getExtensionPackageNames() { + return mExtensionPackages; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add( + buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeColumn = Email.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add( + buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + private DataKind addDataKindRelation(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Relation.CONTENT_ITEM_TYPE, + R.string.relationLabelsGroup, + Weight.RELATIONSHIP, + true)); + kind.actionHeader = new RelationActionInflater(); + kind.actionBody = new SimpleInflater(Relation.NAME); + + kind.typeColumn = Relation.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT)); + kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_CHILD)); + kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FATHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND)); + kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER)); + kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARENT)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY)); + kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE)); + kind.typeList.add(buildRelationType(Relation.TYPE_SISTER)); + kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE)); + kind.typeList.add( + buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION)); + + return kind; + } + + private DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeColumn = Event.TYPE; + kind.typeList = new ArrayList<>(); + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1)); + kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false)); + kind.typeList.add(buildEventType(Event.TYPE_OTHER, false)); + kind.typeList.add( + buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } + + @Override + public String getViewContactNotifyServiceClassName() { + return "com.google.android.syncadapters.contacts." + "SyncHighResPhotoIntentService"; + } + + @Override + public String getViewContactNotifyServicePackageName() { + return "com.google.android.syncadapters.contacts"; + } +} diff --git a/java/com/android/contacts/common/model/account/SamsungAccountType.java b/java/com/android/contacts/common/model/account/SamsungAccountType.java new file mode 100644 index 0000000000000000000000000000000000000000..45406bc2be69606e28b217c453908f245dad689e --- /dev/null +++ b/java/com/android/contacts/common/model/account/SamsungAccountType.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import java.util.ArrayList; +import java.util.Locale; + +/** + * A writable account type that can be used to support samsung contacts. This may not perfectly + * match Samsung's latest intended account schema. + * + *

This is only used to partially support Samsung accounts. The DataKind labels & fields are + * setup to support the values used by Samsung. But, not everything in the Samsung account type is + * supported. The Samsung account type includes a "Message Type" mimetype that we have no intention + * of showing inside the Contact editor. Similarly, we don't handle the "Ringtone" mimetype here + * since managing ringtones is handled in a different flow. + */ +public class SamsungAccountType extends BaseAccountType { + + private static final String TAG = "KnownExternalAccountType"; + private static final String ACCOUNT_TYPE_SAMSUNG = "com.osp.app.signin"; + + public SamsungAccountType(Context context, String authenticatorPackageName, String type) { + this.accountType = type; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindGroupMembership(context); + addDataKindRelation(context); + addDataKindEvent(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + /** + * Returns {@code TRUE} if this is samsung's account type and Samsung hasn't bothered to define a + * contacts.xml to provide a more accurate definition than ours. + */ + public static boolean isSamsungAccountType(Context context, String type, String packageName) { + return ACCOUNT_TYPE_SAMSUNG.equals(type) + && !ExternalAccountType.hasContactsXml(context, packageName); + } + + @Override + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindStructuredPostal(context); + + final boolean useJapaneseOrder = + Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage()); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1)); + + kind.fieldList = new ArrayList<>(); + if (useJapaneseOrder) { + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + } else { + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + } + + return kind; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add( + buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeColumn = Email.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add( + buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + private DataKind addDataKindRelation(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Relation.CONTENT_ITEM_TYPE, R.string.relationLabelsGroup, 160, true)); + kind.actionHeader = new RelationActionInflater(); + kind.actionBody = new SimpleInflater(Relation.NAME); + + kind.typeColumn = Relation.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT)); + kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_CHILD)); + kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FATHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND)); + kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER)); + kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARENT)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY)); + kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE)); + kind.typeList.add(buildRelationType(Relation.TYPE_SISTER)); + kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE)); + kind.typeList.add( + buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION)); + + return kind; + } + + private DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, 150, true)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeColumn = Event.TYPE; + kind.typeList = new ArrayList<>(); + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1)); + kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false)); + kind.typeList.add(buildEventType(Event.TYPE_OTHER, false)); + kind.typeList.add( + buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/DataItem.java b/java/com/android/contacts/common/model/dataitem/DataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..dc746055b58b7e050127b9cfe260563c02d7e4d0 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/DataItem.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Identity; +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.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.provider.ContactsContract.Contacts.Data; +import android.provider.ContactsContract.Contacts.Entity; +import com.android.contacts.common.Collapser; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.model.account.AccountType.EditType; + +/** This is the base class for data items, which represents a row from the Data table. */ +public class DataItem implements Collapser.Collapsible { + + private final ContentValues mContentValues; + protected DataKind mKind; + + protected DataItem(ContentValues values) { + mContentValues = values; + } + + /** + * Factory for creating subclasses of DataItem objects based on the mimetype in the content + * values. Raw contact is the raw contact that this data item is associated with. + */ + public static DataItem createFrom(ContentValues values) { + final String mimeType = values.getAsString(Data.MIMETYPE); + if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new GroupMembershipDataItem(values); + } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new StructuredNameDataItem(values); + } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new PhoneDataItem(values); + } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new EmailDataItem(values); + } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new StructuredPostalDataItem(values); + } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new ImDataItem(values); + } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new OrganizationDataItem(values); + } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new NicknameDataItem(values); + } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new NoteDataItem(values); + } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new WebsiteDataItem(values); + } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new SipAddressDataItem(values); + } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new EventDataItem(values); + } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new RelationDataItem(values); + } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new IdentityDataItem(values); + } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new PhotoDataItem(values); + } + + // generic + return new DataItem(values); + } + + public ContentValues getContentValues() { + return mContentValues; + } + + public Long getRawContactId() { + return mContentValues.getAsLong(Data.RAW_CONTACT_ID); + } + + public void setRawContactId(long rawContactId) { + mContentValues.put(Data.RAW_CONTACT_ID, rawContactId); + } + + /** Returns the data id. */ + public long getId() { + return mContentValues.getAsLong(Data._ID); + } + + /** Returns the mimetype of the data. */ + public String getMimeType() { + return mContentValues.getAsString(Data.MIMETYPE); + } + + public void setMimeType(String mimeType) { + mContentValues.put(Data.MIMETYPE, mimeType); + } + + public boolean isPrimary() { + Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY); + return primary != null && primary != 0; + } + + public boolean isSuperPrimary() { + Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY); + return superPrimary != null && superPrimary != 0; + } + + public boolean hasKindTypeColumn(DataKind kind) { + final String key = kind.typeColumn; + return key != null + && mContentValues.containsKey(key) + && mContentValues.getAsInteger(key) != null; + } + + public int getKindTypeColumn(DataKind kind) { + final String key = kind.typeColumn; + return mContentValues.getAsInteger(key); + } + + /** + * Indicates the carrier presence value for the current {@link DataItem}. + * + * @return {@link Data#CARRIER_PRESENCE_VT_CAPABLE} if the {@link DataItem} supports carrier video + * calling, {@code 0} otherwise. + */ + public int getCarrierPresence() { + return mContentValues.getAsInteger(Data.CARRIER_PRESENCE); + } + + /** + * This builds the data string depending on the type of data item by using the generic DataKind + * object underneath. + */ + public String buildDataString(Context context, DataKind kind) { + if (kind.actionBody == null) { + return null; + } + CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues); + return actionBody == null ? null : actionBody.toString(); + } + + /** + * This builds the data string(intended for display) depending on the type of data item. It + * returns the same value as {@link #buildDataString} by default, but certain data items can + * override it to provide their version of formatted data strings. + * + * @return Data string representing the data item, possibly formatted for display + */ + public String buildDataStringForDisplay(Context context, DataKind kind) { + return buildDataString(context, kind); + } + + public DataKind getDataKind() { + return mKind; + } + + public void setDataKind(DataKind kind) { + mKind = kind; + } + + public Integer getTimesUsed() { + return mContentValues.getAsInteger(Entity.TIMES_USED); + } + + public Long getLastTimeUsed() { + return mContentValues.getAsLong(Entity.LAST_TIME_USED); + } + + @Override + public void collapseWith(DataItem that) { + DataKind thisKind = getDataKind(); + DataKind thatKind = that.getDataKind(); + // If this does not have a type and that does, or if that's type is higher precedence, + // use that's type + if ((!hasKindTypeColumn(thisKind) && that.hasKindTypeColumn(thatKind)) + || (that.hasKindTypeColumn(thatKind) + && getTypePrecedence(thisKind, getKindTypeColumn(thisKind)) + > getTypePrecedence(thatKind, that.getKindTypeColumn(thatKind)))) { + mContentValues.put(thatKind.typeColumn, that.getKindTypeColumn(thatKind)); + mKind = thatKind; + } + + // Choose the max of the maxLines and maxLabelLines values. + mKind.maxLinesForDisplay = Math.max(thisKind.maxLinesForDisplay, thatKind.maxLinesForDisplay); + + // If any of the collapsed entries are super primary make the whole thing super primary. + if (isSuperPrimary() || that.isSuperPrimary()) { + mContentValues.put(Data.IS_SUPER_PRIMARY, 1); + mContentValues.put(Data.IS_PRIMARY, 1); + } + + // If any of the collapsed entries are primary make the whole thing primary. + if (isPrimary() || that.isPrimary()) { + mContentValues.put(Data.IS_PRIMARY, 1); + } + + // Add up the times used + mContentValues.put( + Entity.TIMES_USED, + (getTimesUsed() == null ? 0 : getTimesUsed()) + + (that.getTimesUsed() == null ? 0 : that.getTimesUsed())); + + // Use the most recent time + mContentValues.put( + Entity.LAST_TIME_USED, + Math.max( + getLastTimeUsed() == null ? 0 : getLastTimeUsed(), + that.getLastTimeUsed() == null ? 0 : that.getLastTimeUsed())); + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (mKind == null || t.getDataKind() == null) { + return false; + } + return MoreContactUtils.shouldCollapse( + getMimeType(), + buildDataString(context, mKind), + t.getMimeType(), + t.buildDataString(context, t.getDataKind())); + } + + /** + * Return the precedence for the the given {@link EditType#rawValue}, where lower numbers are + * higher precedence. + */ + private static int getTypePrecedence(DataKind kind, int rawValue) { + for (int i = 0; i < kind.typeList.size(); i++) { + final EditType type = kind.typeList.get(i); + if (type.rawValue == rawValue) { + return i; + } + } + return Integer.MAX_VALUE; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/DataKind.java b/java/com/android/contacts/common/model/dataitem/DataKind.java new file mode 100644 index 0000000000000000000000000000000000000000..3b470a2ae647f687632cfb26978908b4833f7ebb --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/DataKind.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.Data; +import com.android.contacts.common.model.account.AccountType.EditField; +import com.android.contacts.common.model.account.AccountType.EditType; +import com.android.contacts.common.model.account.AccountType.StringInflater; +import com.google.common.collect.Iterators; +import java.text.SimpleDateFormat; +import java.util.List; + +/** + * Description of a specific data type, usually marked by a unique {@link Data#MIMETYPE}. Includes + * details about how to view and edit {@link Data} rows of this kind, including the possible {@link + * EditType} labels and editable {@link EditField}. + */ +public final class DataKind { + + public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName"; + public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName"; + public static final String PSEUDO_COLUMN_PHONETIC_NAME = "#phoneticName"; + + public String resourcePackageName; + public String mimeType; + public int titleRes; + public int iconAltRes; + public int iconAltDescriptionRes; + public int weight; + public boolean editable; + + public StringInflater actionHeader; + public StringInflater actionAltHeader; + public StringInflater actionBody; + + public String typeColumn; + + /** Maximum number of values allowed in the list. -1 represents infinity. */ + public int typeOverallMax; + + public List typeList; + public List fieldList; + + public ContentValues defaultValues; + + /** + * If this is a date field, this specifies the format of the date when saving. The date includes + * year, month and day. If this is not a date field or the date field is not editable, this value + * should be ignored. + */ + public SimpleDateFormat dateFormatWithoutYear; + + /** + * If this is a date field, this specifies the format of the date when saving. The date includes + * month and day. If this is not a date field, the field is not editable or dates without year are + * not supported, this value should be ignored. + */ + public SimpleDateFormat dateFormatWithYear; + + /** The number of lines available for displaying this kind of data. Defaults to 1. */ + public int maxLinesForDisplay; + + public DataKind() { + maxLinesForDisplay = 1; + } + + public DataKind(String mimeType, int titleRes, int weight, boolean editable) { + this.mimeType = mimeType; + this.titleRes = titleRes; + this.weight = weight; + this.editable = editable; + this.typeOverallMax = -1; + maxLinesForDisplay = 1; + } + + public static String toString(SimpleDateFormat format) { + return format == null ? "(null)" : format.toPattern(); + } + + public static String toString(Iterable list) { + if (list == null) { + return "(null)"; + } else { + return Iterators.toString(list.iterator()); + } + } + + public String getKindString(Context context) { + return (titleRes == -1 || titleRes == 0) ? "" : context.getString(titleRes); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("DataKind:"); + sb.append(" resPackageName=").append(resourcePackageName); + sb.append(" mimeType=").append(mimeType); + sb.append(" titleRes=").append(titleRes); + sb.append(" iconAltRes=").append(iconAltRes); + sb.append(" iconAltDescriptionRes=").append(iconAltDescriptionRes); + sb.append(" weight=").append(weight); + sb.append(" editable=").append(editable); + sb.append(" actionHeader=").append(actionHeader); + sb.append(" actionAltHeader=").append(actionAltHeader); + sb.append(" actionBody=").append(actionBody); + sb.append(" typeColumn=").append(typeColumn); + sb.append(" typeOverallMax=").append(typeOverallMax); + sb.append(" typeList=").append(toString(typeList)); + sb.append(" fieldList=").append(toString(fieldList)); + sb.append(" defaultValues=").append(defaultValues); + sb.append(" dateFormatWithoutYear=").append(toString(dateFormatWithoutYear)); + sb.append(" dateFormatWithYear=").append(toString(dateFormatWithYear)); + + return sb.toString(); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/EmailDataItem.java b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..2fe297816f7a2b846a55e368a0873c198296cd79 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Email; + +/** + * Represents an email data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Email}. + */ +public class EmailDataItem extends DataItem { + + /* package */ EmailDataItem(ContentValues values) { + super(values); + } + + public String getAddress() { + return getContentValues().getAsString(Email.ADDRESS); + } + + public String getDisplayName() { + return getContentValues().getAsString(Email.DISPLAY_NAME); + } + + public String getData() { + return getContentValues().getAsString(Email.DATA); + } + + public String getLabel() { + return getContentValues().getAsString(Email.LABEL); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/EventDataItem.java b/java/com/android/contacts/common/model/dataitem/EventDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..15d9880b1439eea6882eb92e59b7cfe1bb7ef8cd --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/EventDataItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.text.TextUtils; + +/** + * Represents an event data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Event}. + */ +public class EventDataItem extends DataItem { + + /* package */ EventDataItem(ContentValues values) { + super(values); + } + + public String getStartDate() { + return getContentValues().getAsString(Event.START_DATE); + } + + public String getLabel() { + return getContentValues().getAsString(Event.LABEL); + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (!(t instanceof EventDataItem) || mKind == null || t.getDataKind() == null) { + return false; + } + final EventDataItem that = (EventDataItem) t; + // Events can be different (anniversary, birthday) but have the same start date + if (!TextUtils.equals(getStartDate(), that.getStartDate())) { + return false; + } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) { + return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind()); + } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) { + return false; + } else if (getKindTypeColumn(mKind) == Event.TYPE_CUSTOM + && !TextUtils.equals(getLabel(), that.getLabel())) { + // Check if custom types are not the same + return false; + } + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..f921b3c9da7999e29d853fd032dd1a319d07606e --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; + +/** + * Represents a group memebership data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.GroupMembership}. + */ +public class GroupMembershipDataItem extends DataItem { + + /* package */ GroupMembershipDataItem(ContentValues values) { + super(values); + } + + public Long getGroupRowId() { + return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID); + } + + public String getGroupSourceId() { + return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..2badf92f77e8a8b04d390db1870464e64647a67c --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Identity; + +/** + * Represents an identity data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Identity}. + */ +public class IdentityDataItem extends DataItem { + + /* package */ IdentityDataItem(ContentValues values) { + super(values); + } + + public String getIdentity() { + return getContentValues().getAsString(Identity.IDENTITY); + } + + public String getNamespace() { + return getContentValues().getAsString(Identity.NAMESPACE); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/ImDataItem.java b/java/com/android/contacts/common/model/dataitem/ImDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..16b9fd0949a257d2eaa2061fcad13946b80bad1e --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/ImDataItem.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.text.TextUtils; + +/** + * Represents an IM data item, wrapping the columns in {@link ContactsContract.CommonDataKinds.Im}. + */ +public class ImDataItem extends DataItem { + + private final boolean mCreatedFromEmail; + + /* package */ ImDataItem(ContentValues values) { + super(values); + mCreatedFromEmail = false; + } + + private ImDataItem(ContentValues values, boolean createdFromEmail) { + super(values); + mCreatedFromEmail = createdFromEmail; + } + + public static ImDataItem createFromEmail(EmailDataItem item) { + final ImDataItem im = new ImDataItem(new ContentValues(item.getContentValues()), true); + im.setMimeType(Im.CONTENT_ITEM_TYPE); + return im; + } + + public String getData() { + if (mCreatedFromEmail) { + return getContentValues().getAsString(Email.DATA); + } else { + return getContentValues().getAsString(Im.DATA); + } + } + + public String getLabel() { + return getContentValues().getAsString(Im.LABEL); + } + + /** Values are one of Im.PROTOCOL_ */ + public Integer getProtocol() { + return getContentValues().getAsInteger(Im.PROTOCOL); + } + + public boolean isProtocolValid() { + return getProtocol() != null; + } + + public String getCustomProtocol() { + return getContentValues().getAsString(Im.CUSTOM_PROTOCOL); + } + + public int getChatCapability() { + Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY); + return result == null ? 0 : result; + } + + public boolean isCreatedFromEmail() { + return mCreatedFromEmail; + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (!(t instanceof ImDataItem) || mKind == null || t.getDataKind() == null) { + return false; + } + final ImDataItem that = (ImDataItem) t; + // IM can have the same data put different protocol. These should not collapse. + if (!getData().equals(that.getData())) { + return false; + } else if (!isProtocolValid() || !that.isProtocolValid()) { + // Deal with invalid protocol as if it was custom. If either has a non valid + // protocol, check to see if the other has a valid that is not custom + if (isProtocolValid()) { + return getProtocol() == Im.PROTOCOL_CUSTOM; + } else if (that.isProtocolValid()) { + return that.getProtocol() == Im.PROTOCOL_CUSTOM; + } + return true; + } else if (getProtocol() != that.getProtocol()) { + return false; + } else if (getProtocol() == Im.PROTOCOL_CUSTOM + && !TextUtils.equals(getCustomProtocol(), that.getCustomProtocol())) { + // Check if custom protocols are not the same + return false; + } + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..a448be78624477e1ed7c83829db2a097dc5dd52a --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Nickname; + +/** + * Represents a nickname data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Nickname}. + */ +public class NicknameDataItem extends DataItem { + + public NicknameDataItem(ContentValues values) { + super(values); + } + + public String getName() { + return getContentValues().getAsString(Nickname.NAME); + } + + public String getLabel() { + return getContentValues().getAsString(Nickname.LABEL); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/NoteDataItem.java b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..b55ecc3e5daf29ada6b322e15797b66e197e9bed --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Note; + +/** + * Represents a note data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Note}. + */ +public class NoteDataItem extends DataItem { + + /* package */ NoteDataItem(ContentValues values) { + super(values); + } + + public String getNote() { + return getContentValues().getAsString(Note.NOTE); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..b3312483837e338da512560e1dcb9c83dce04dee --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Organization; + +/** + * Represents an organization data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Organization}. + */ +public class OrganizationDataItem extends DataItem { + + /* package */ OrganizationDataItem(ContentValues values) { + super(values); + } + + public String getCompany() { + return getContentValues().getAsString(Organization.COMPANY); + } + + public String getLabel() { + return getContentValues().getAsString(Organization.LABEL); + } + + public String getTitle() { + return getContentValues().getAsString(Organization.TITLE); + } + + public String getDepartment() { + return getContentValues().getAsString(Organization.DEPARTMENT); + } + + public String getJobDescription() { + return getContentValues().getAsString(Organization.JOB_DESCRIPTION); + } + + public String getSymbol() { + return getContentValues().getAsString(Organization.SYMBOL); + } + + public String getPhoneticName() { + return getContentValues().getAsString(Organization.PHONETIC_NAME); + } + + public String getOfficeLocation() { + return getContentValues().getAsString(Organization.OFFICE_LOCATION); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..e1f56456afdd1de2322e1d9d14f2e44a99f1a072 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; + +/** + * Represents a phone data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Phone}. + */ +public class PhoneDataItem extends DataItem { + + public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber"; + + /* package */ PhoneDataItem(ContentValues values) { + super(values); + } + + public String getNumber() { + return getContentValues().getAsString(Phone.NUMBER); + } + + /** Returns the normalized phone number in E164 format. */ + public String getNormalizedNumber() { + return getContentValues().getAsString(Phone.NORMALIZED_NUMBER); + } + + public String getFormattedPhoneNumber() { + return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER); + } + + public String getLabel() { + return getContentValues().getAsString(Phone.LABEL); + } + + public void computeFormattedPhoneNumber(String defaultCountryIso) { + final String phoneNumber = getNumber(); + if (phoneNumber != null) { + final String formattedPhoneNumber = + PhoneNumberUtilsCompat.formatNumber( + phoneNumber, getNormalizedNumber(), defaultCountryIso); + getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber); + } + } + + /** + * Returns the formatted phone number (if already computed using {@link + * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number. + */ + @Override + public String buildDataStringForDisplay(Context context, DataKind kind) { + final String formatted = getFormattedPhoneNumber(); + if (formatted != null) { + return formatted; + } else { + return getNumber(); + } + } +} diff --git a/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..0bf7a318b023325abddcb09b79700c9cd7f6ac27 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts.Photo; + +/** + * Represents a photo data item, wrapping the columns in {@link ContactsContract.Contacts.Photo}. + */ +public class PhotoDataItem extends DataItem { + + /* package */ PhotoDataItem(ContentValues values) { + super(values); + } + + public Long getPhotoFileId() { + return getContentValues().getAsLong(Photo.PHOTO_FILE_ID); + } + + public byte[] getPhoto() { + return getContentValues().getAsByteArray(Photo.PHOTO); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/RelationDataItem.java b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..fdbcbb31335d8102749c3b47ae898d68eef32a5a --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.text.TextUtils; + +/** + * Represents a relation data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Relation}. + */ +public class RelationDataItem extends DataItem { + + /* package */ RelationDataItem(ContentValues values) { + super(values); + } + + public String getName() { + return getContentValues().getAsString(Relation.NAME); + } + + public String getLabel() { + return getContentValues().getAsString(Relation.LABEL); + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (!(t instanceof RelationDataItem) || mKind == null || t.getDataKind() == null) { + return false; + } + final RelationDataItem that = (RelationDataItem) t; + // Relations can have different types (assistant, father) but have the same name + if (!TextUtils.equals(getName(), that.getName())) { + return false; + } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) { + return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind()); + } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) { + return false; + } else if (getKindTypeColumn(mKind) == Relation.TYPE_CUSTOM + && !TextUtils.equals(getLabel(), that.getLabel())) { + // Check if custom types are not the same + return false; + } + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..0ca9eae6d71623fc16ffa567f89086a92a7a361b --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; + +/** + * Represents a sip address data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.SipAddress}. + */ +public class SipAddressDataItem extends DataItem { + + /* package */ SipAddressDataItem(ContentValues values) { + super(values); + } + + public String getSipAddress() { + return getContentValues().getAsString(SipAddress.SIP_ADDRESS); + } + + public String getLabel() { + return getContentValues().getAsString(SipAddress.LABEL); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..22bf037f1b298509ff2504473c770c26d84b2410 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.Contacts.Data; + +/** + * Represents a structured name data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.StructuredName}. + */ +public class StructuredNameDataItem extends DataItem { + + public StructuredNameDataItem() { + super(new ContentValues()); + getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); + } + + /* package */ StructuredNameDataItem(ContentValues values) { + super(values); + } + + public String getDisplayName() { + return getContentValues().getAsString(StructuredName.DISPLAY_NAME); + } + + public void setDisplayName(String name) { + getContentValues().put(StructuredName.DISPLAY_NAME, name); + } + + public String getGivenName() { + return getContentValues().getAsString(StructuredName.GIVEN_NAME); + } + + public String getFamilyName() { + return getContentValues().getAsString(StructuredName.FAMILY_NAME); + } + + public String getPrefix() { + return getContentValues().getAsString(StructuredName.PREFIX); + } + + public String getMiddleName() { + return getContentValues().getAsString(StructuredName.MIDDLE_NAME); + } + + public String getSuffix() { + return getContentValues().getAsString(StructuredName.SUFFIX); + } + + public String getPhoneticGivenName() { + return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME); + } + + public void setPhoneticGivenName(String name) { + getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name); + } + + public String getPhoneticMiddleName() { + return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + } + + public void setPhoneticMiddleName(String name) { + getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name); + } + + public String getPhoneticFamilyName() { + return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME); + } + + public void setPhoneticFamilyName(String name) { + getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name); + } + + public String getFullNameStyle() { + return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE); + } + + public boolean isSuperPrimary() { + final ContentValues contentValues = getContentValues(); + return contentValues == null || !contentValues.containsKey(StructuredName.IS_SUPER_PRIMARY) + ? false + : contentValues.getAsBoolean(StructuredName.IS_SUPER_PRIMARY); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..18aae282c2d2053e5082c63854f4b5dc2a8ba7ce --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; + +/** + * Represents a structured postal data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.StructuredPostal}. + */ +public class StructuredPostalDataItem extends DataItem { + + /* package */ StructuredPostalDataItem(ContentValues values) { + super(values); + } + + public String getFormattedAddress() { + return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS); + } + + public String getLabel() { + return getContentValues().getAsString(StructuredPostal.LABEL); + } + + public String getStreet() { + return getContentValues().getAsString(StructuredPostal.STREET); + } + + public String getPOBox() { + return getContentValues().getAsString(StructuredPostal.POBOX); + } + + public String getNeighborhood() { + return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD); + } + + public String getCity() { + return getContentValues().getAsString(StructuredPostal.CITY); + } + + public String getRegion() { + return getContentValues().getAsString(StructuredPostal.REGION); + } + + public String getPostcode() { + return getContentValues().getAsString(StructuredPostal.POSTCODE); + } + + public String getCountry() { + return getContentValues().getAsString(StructuredPostal.COUNTRY); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java new file mode 100644 index 0000000000000000000000000000000000000000..b8400ecd1b3d8d398e02138092fbba80c028fb33 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Website; + +/** + * Represents a website data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Website}. + */ +public class WebsiteDataItem extends DataItem { + + /* package */ WebsiteDataItem(ContentValues values) { + super(values); + } + + public String getUrl() { + return getContentValues().getAsString(Website.URL); + } + + public String getLabel() { + return getContentValues().getAsString(Website.LABEL); + } +} diff --git a/java/com/android/contacts/common/preference/ContactsPreferences.java b/java/com/android/contacts/common/preference/ContactsPreferences.java new file mode 100644 index 0000000000000000000000000000000000000000..7f0d99acde0a345974a740bb5f2712ddb5b805c9 --- /dev/null +++ b/java/com/android/contacts/common/preference/ContactsPreferences.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 com.android.contacts.common.preference; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; +import com.android.contacts.common.R; +import com.android.contacts.common.model.account.AccountWithDataSet; + +/** Manages user preferences for contacts. */ +public class ContactsPreferences implements OnSharedPreferenceChangeListener { + + /** The value for the DISPLAY_ORDER key to show the given name first. */ + public static final int DISPLAY_ORDER_PRIMARY = 1; + + /** The value for the DISPLAY_ORDER key to show the family name first. */ + public static final int DISPLAY_ORDER_ALTERNATIVE = 2; + + public static final String DISPLAY_ORDER_KEY = "android.contacts.DISPLAY_ORDER"; + + /** The value for the SORT_ORDER key corresponding to sort by given name first. */ + public static final int SORT_ORDER_PRIMARY = 1; + + public static final String SORT_ORDER_KEY = "android.contacts.SORT_ORDER"; + + /** The value for the SORT_ORDER key corresponding to sort by family name first. */ + public static final int SORT_ORDER_ALTERNATIVE = 2; + + public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones"; + + public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false; + + /** + * Value to use when a preference is unassigned and needs to be read from the shared preferences + */ + private static final int PREFERENCE_UNASSIGNED = -1; + + private final Context mContext; + private final SharedPreferences mPreferences; + private int mSortOrder = PREFERENCE_UNASSIGNED; + private int mDisplayOrder = PREFERENCE_UNASSIGNED; + private String mDefaultAccount = null; + private ChangeListener mListener = null; + private Handler mHandler; + private String mDefaultAccountKey; + private String mDefaultAccountSavedKey; + + public ContactsPreferences(Context context) { + mContext = context; + mHandler = new Handler(); + mPreferences = mContext.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); + mDefaultAccountKey = + mContext.getResources().getString(R.string.contact_editor_default_account_key); + mDefaultAccountSavedKey = + mContext.getResources().getString(R.string.contact_editor_anything_saved_key); + maybeMigrateSystemSettings(); + } + + public boolean isSortOrderUserChangeable() { + return mContext.getResources().getBoolean(R.bool.config_sort_order_user_changeable); + } + + public int getDefaultSortOrder() { + if (mContext.getResources().getBoolean(R.bool.config_default_sort_order_primary)) { + return SORT_ORDER_PRIMARY; + } else { + return SORT_ORDER_ALTERNATIVE; + } + } + + public int getSortOrder() { + if (!isSortOrderUserChangeable()) { + return getDefaultSortOrder(); + } + if (mSortOrder == PREFERENCE_UNASSIGNED) { + mSortOrder = mPreferences.getInt(SORT_ORDER_KEY, getDefaultSortOrder()); + } + return mSortOrder; + } + + public void setSortOrder(int sortOrder) { + mSortOrder = sortOrder; + final Editor editor = mPreferences.edit(); + editor.putInt(SORT_ORDER_KEY, sortOrder); + editor.commit(); + } + + public boolean isDisplayOrderUserChangeable() { + return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable); + } + + public int getDefaultDisplayOrder() { + if (mContext.getResources().getBoolean(R.bool.config_default_display_order_primary)) { + return DISPLAY_ORDER_PRIMARY; + } else { + return DISPLAY_ORDER_ALTERNATIVE; + } + } + + public int getDisplayOrder() { + if (!isDisplayOrderUserChangeable()) { + return getDefaultDisplayOrder(); + } + if (mDisplayOrder == PREFERENCE_UNASSIGNED) { + mDisplayOrder = mPreferences.getInt(DISPLAY_ORDER_KEY, getDefaultDisplayOrder()); + } + return mDisplayOrder; + } + + public void setDisplayOrder(int displayOrder) { + mDisplayOrder = displayOrder; + final Editor editor = mPreferences.edit(); + editor.putInt(DISPLAY_ORDER_KEY, displayOrder); + editor.commit(); + } + + public boolean isDefaultAccountUserChangeable() { + return mContext.getResources().getBoolean(R.bool.config_default_account_user_changeable); + } + + public String getDefaultAccount() { + if (!isDefaultAccountUserChangeable()) { + return mDefaultAccount; + } + if (TextUtils.isEmpty(mDefaultAccount)) { + final String accountString = mPreferences.getString(mDefaultAccountKey, mDefaultAccount); + if (!TextUtils.isEmpty(accountString)) { + final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(accountString); + mDefaultAccount = accountWithDataSet.name; + } + } + return mDefaultAccount; + } + + public void setDefaultAccount(AccountWithDataSet accountWithDataSet) { + mDefaultAccount = accountWithDataSet == null ? null : accountWithDataSet.name; + final Editor editor = mPreferences.edit(); + if (TextUtils.isEmpty(mDefaultAccount)) { + editor.remove(mDefaultAccountKey); + } else { + editor.putString(mDefaultAccountKey, accountWithDataSet.stringify()); + } + editor.putBoolean(mDefaultAccountSavedKey, true); + editor.commit(); + } + + public void registerChangeListener(ChangeListener listener) { + if (mListener != null) { + unregisterChangeListener(); + } + + mListener = listener; + + // Reset preferences to "unknown" because they may have changed while the + // listener was unregistered. + mDisplayOrder = PREFERENCE_UNASSIGNED; + mSortOrder = PREFERENCE_UNASSIGNED; + mDefaultAccount = null; + + mPreferences.registerOnSharedPreferenceChangeListener(this); + } + + public void unregisterChangeListener() { + if (mListener != null) { + mListener = null; + } + + mPreferences.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) { + // This notification is not sent on the Ui thread. Use the previously created Handler + // to switch to the Ui thread + mHandler.post( + new Runnable() { + @Override + public void run() { + refreshValue(key); + } + }); + } + + /** + * Forces the value for the given key to be looked up from shared preferences and notifies the + * registered {@link ChangeListener} + * + * @param key the {@link SharedPreferences} key to look up + */ + public void refreshValue(String key) { + if (DISPLAY_ORDER_KEY.equals(key)) { + mDisplayOrder = PREFERENCE_UNASSIGNED; + mDisplayOrder = getDisplayOrder(); + } else if (SORT_ORDER_KEY.equals(key)) { + mSortOrder = PREFERENCE_UNASSIGNED; + mSortOrder = getSortOrder(); + } else if (mDefaultAccountKey.equals(key)) { + mDefaultAccount = null; + mDefaultAccount = getDefaultAccount(); + } + if (mListener != null) { + mListener.onChange(); + } + } + + /** + * If there are currently no preferences (which means this is the first time we are run), For sort + * order and display order, check to see if there are any preferences stored in system settings + * (pre-L) which can be copied into our own SharedPreferences. For default account setting, check + * to see if there are any preferences stored in the previous SharedPreferences which can be + * copied into current SharedPreferences. + */ + private void maybeMigrateSystemSettings() { + if (!mPreferences.contains(SORT_ORDER_KEY)) { + int sortOrder = getDefaultSortOrder(); + try { + sortOrder = Settings.System.getInt(mContext.getContentResolver(), SORT_ORDER_KEY); + } catch (SettingNotFoundException e) { + } + setSortOrder(sortOrder); + } + + if (!mPreferences.contains(DISPLAY_ORDER_KEY)) { + int displayOrder = getDefaultDisplayOrder(); + try { + displayOrder = Settings.System.getInt(mContext.getContentResolver(), DISPLAY_ORDER_KEY); + } catch (SettingNotFoundException e) { + } + setDisplayOrder(displayOrder); + } + + if (!mPreferences.contains(mDefaultAccountKey)) { + final SharedPreferences previousPrefs = + PreferenceManager.getDefaultSharedPreferences(mContext); + final String defaultAccount = previousPrefs.getString(mDefaultAccountKey, null); + if (!TextUtils.isEmpty(defaultAccount)) { + final AccountWithDataSet accountWithDataSet = + AccountWithDataSet.unstringify(defaultAccount); + setDefaultAccount(accountWithDataSet); + } + } + } + + public interface ChangeListener { + + void onChange(); + } +} diff --git a/java/com/android/contacts/common/preference/DisplayOrderPreference.java b/java/com/android/contacts/common/preference/DisplayOrderPreference.java new file mode 100644 index 0000000000000000000000000000000000000000..8dda57f9f26ae05bef68285fb649444abdd6e18a --- /dev/null +++ b/java/com/android/contacts/common/preference/DisplayOrderPreference.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.preference; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import com.android.contacts.common.R; + +/** Custom preference: view-name-as (first name first or last name first). */ +public final class DisplayOrderPreference extends ListPreference { + + private ContactsPreferences mPreferences; + private Context mContext; + + public DisplayOrderPreference(Context context) { + super(context); + prepare(); + } + + public DisplayOrderPreference(Context context, AttributeSet attrs) { + super(context, attrs); + prepare(); + } + + private void prepare() { + mContext = getContext(); + mPreferences = new ContactsPreferences(mContext); + setEntries( + new String[] { + mContext.getString(R.string.display_options_view_given_name_first), + mContext.getString(R.string.display_options_view_family_name_first), + }); + setEntryValues( + new String[] { + String.valueOf(ContactsPreferences.DISPLAY_ORDER_PRIMARY), + String.valueOf(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE), + }); + setValue(String.valueOf(mPreferences.getDisplayOrder())); + } + + @Override + protected boolean shouldPersist() { + return false; // This preference takes care of its own storage + } + + @Override + public CharSequence getSummary() { + switch (mPreferences.getDisplayOrder()) { + case ContactsPreferences.DISPLAY_ORDER_PRIMARY: + return mContext.getString(R.string.display_options_view_given_name_first); + case ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE: + return mContext.getString(R.string.display_options_view_family_name_first); + } + return null; + } + + @Override + protected boolean persistString(String value) { + int newValue = Integer.parseInt(value); + if (newValue != mPreferences.getDisplayOrder()) { + mPreferences.setDisplayOrder(newValue); + notifyChanged(); + } + return true; + } + + @Override + // UX recommendation is not to show cancel button on such lists. + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + builder.setNegativeButton(null, null); + } +} diff --git a/java/com/android/contacts/common/preference/SortOrderPreference.java b/java/com/android/contacts/common/preference/SortOrderPreference.java new file mode 100644 index 0000000000000000000000000000000000000000..9b6f578601b1997c8ae608af059b0948cfcbdc38 --- /dev/null +++ b/java/com/android/contacts/common/preference/SortOrderPreference.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.preference; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import com.android.contacts.common.R; + +/** Custom preference: sort-by. */ +public final class SortOrderPreference extends ListPreference { + + private ContactsPreferences mPreferences; + private Context mContext; + + public SortOrderPreference(Context context) { + super(context); + prepare(); + } + + public SortOrderPreference(Context context, AttributeSet attrs) { + super(context, attrs); + prepare(); + } + + private void prepare() { + mContext = getContext(); + mPreferences = new ContactsPreferences(mContext); + setEntries( + new String[] { + mContext.getString(R.string.display_options_sort_by_given_name), + mContext.getString(R.string.display_options_sort_by_family_name), + }); + setEntryValues( + new String[] { + String.valueOf(ContactsPreferences.SORT_ORDER_PRIMARY), + String.valueOf(ContactsPreferences.SORT_ORDER_ALTERNATIVE), + }); + setValue(String.valueOf(mPreferences.getSortOrder())); + } + + @Override + protected boolean shouldPersist() { + return false; // This preference takes care of its own storage + } + + @Override + public CharSequence getSummary() { + switch (mPreferences.getSortOrder()) { + case ContactsPreferences.SORT_ORDER_PRIMARY: + return mContext.getString(R.string.display_options_sort_by_given_name); + case ContactsPreferences.SORT_ORDER_ALTERNATIVE: + return mContext.getString(R.string.display_options_sort_by_family_name); + } + return null; + } + + @Override + protected boolean persistString(String value) { + int newValue = Integer.parseInt(value); + if (newValue != mPreferences.getSortOrder()) { + mPreferences.setSortOrder(newValue); + notifyChanged(); + } + return true; + } + + @Override + // UX recommendation is not to show cancel button on such lists. + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + builder.setNegativeButton(null, null); + } +} diff --git a/java/com/android/contacts/common/res/color/popup_menu_color.xml b/java/com/android/contacts/common/res/color/popup_menu_color.xml new file mode 100644 index 0000000000000000000000000000000000000000..c52bd5b5046babffb6531e17438699a08c34771d --- /dev/null +++ b/java/com/android/contacts/common/res/color/popup_menu_color.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/res/color/tab_text_color.xml b/java/com/android/contacts/common/res/color/tab_text_color.xml similarity index 83% rename from res/color/tab_text_color.xml rename to java/com/android/contacts/common/res/color/tab_text_color.xml index 5ef1fe33b12319b1c1eac3fecae354dee77a98cf..71ef3e903f3e56658ce554c7b64a083c2f940c4d 100644 --- a/res/color/tab_text_color.xml +++ b/java/com/android/contacts/common/res/color/tab_text_color.xml @@ -16,6 +16,6 @@ --> - - + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..d86b2195afb90f0a4fbc29808538530ad76b764d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ddbb2c459647e3b93d068e37de24f02c4b6dc3e3 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d5942dcad2de787175d0dd426ed91096e2b77e28 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc5065155baeba719d76845d4398431c289cde0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..503e58e22f7d39fcba9a7793d8e6d285233e8f10 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..9695529351a0e736fc7b469572eaffff0bff0f89 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..540ab4deeaf68d799ce6b01a2b7679a7c78b1e8b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..017e4bbf71ec8e30561efa3b7f84a4bc1523b075 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..703d30b92583a3fc339d8a4bf341f0853410eb86 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b1113cfef22bcec86ead7ae67be12326276cab Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..deb3a6dc1db49106b72e5fe195d2dd41f6c243a6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..06bd18fbb32cb70607f68fd2ec8318e54e9da1f7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..d829d11e20aa80a6ef27e56b5956502d89b0240d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..1ba12950c81dd66dc20f2b1877211c8c4019e222 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..5ff3ac57484a0e11d87f932faf68900feaad7315 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..b4ebfc7b2ebb9363ccdd308ada2ba4045a632e47 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..03fd2fb108bc599a117eb739751d9157ceb432c0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..e8cb0f5fec5b5599df3d1c9a4fb1f513066bedac Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..45137967c532567a1e553b89f90b951c8b63390d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..1c9bb81fa728fb670f55c5b7c34d647d3ec8a948 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..57177b7c6fb1adb122b1171231a4214bdaa3b3e4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..56708b0bad6c193edb0bb0c7f39897566aff4b20 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..10ae5a70c4fce44cfebe24f4d7d05861ec6c4cbc Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png new file mode 100644 index 0000000000000000000000000000000000000000..84b1227bde69e11b4a01ddc9f603ff2b68bf4fb3 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..ccdda670172b4521f169c01e47b47535d61613bc Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..3aa29b8520b8a6a6d3a1cdff0037327e2ce0d02e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..603ddc8950b3d6ce62d05b586fe1672fdc182f6b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..97905c9f59f2e9e1a39596965bd1840d52c4a424 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..c74bfab13d0fa3f5ac132714891d938a2bf8aae1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..4ea7afa00e2bfe057472ed5a196080fc80ad7383 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..cddf9be75cb961b8eb07c840f03b94cf9d75c4f0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..86578be45ae3fbbd3a3c0be96fd487cdca7f95b1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..e9afcc9248a49f4cfd95a0cb07504add2789043a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..2054530ed2870bfa7dbc60a2d142ba3776e63d33 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 0000000000000000000000000000000000000000..a0f17568e27a95f5854a49244dbbbb5f3b616490 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..ae937176e07806c0253f2ad1b2a98af020c7048f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..0d80482a9621c6bc017fbac202e3aede36aeec73 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..4139942d6882b4de3cccf96de261072d85bbb4f2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 0000000000000000000000000000000000000000..569d28f543c606a2d7c59a30a02b050b4a06b666 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec4c96a7ec79659fb416d9f8c3242ff006290d8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d86d611648c50533b116b87c2135890095a733f1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..4139942d6882b4de3cccf96de261072d85bbb4f2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 0000000000000000000000000000000000000000..065ff62ceee695bd6e83b065562790f8ee46a930 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..013d5e711b34c8919dcdcf8811bf6d4612e22653 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..947f03cecca31f3a3fedd9781b9702455363d023 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..6d09d72788e0fcca869f201a1111399c91f19d0b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..63c7456f04b82ea7179f900829c30a8182538ea4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..f709f2ce43c6d6606a11ebc2a7ab1e72d6b1b7be Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..4139942d6882b4de3cccf96de261072d85bbb4f2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 0000000000000000000000000000000000000000..af5855420ec10eafba6256c00a976bf9c77c741e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..cb801ac1b711324673e9380ae3856df337e73727 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..2b23b1ec5449473922022425cb7849973d1a9806 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1a21fb4008ec02c97cc5a9b9976952277ff3c539 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3dddca51635a99190254ac188cead7290fac43d0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..77f9de5e3ccb30fb6e580454412c98e2f6c553c1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..9d359db9f058e0afab7ef829500791b845dfdac5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..590a728ad9c29acaa2bef70bb7dcde00254d9965 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8a2df3992c0a64cab9b4c1e03e264e4a97d244f3 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ad268bf2fee1d43e48ff0bd96dda701447f4adb8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b3000d31ea00d25f214b947e4826fe5f61a8e8df Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..353e064951788a29a64eef439284fd97e904f374 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..201ad40cbf7abe441b6ca5fd87c4281ca60a38be Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..9aac6d79b89a7d70ac792cba5efbf149bd01f0aa Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..39c16ed2d7e5a856667492fe94ffd8c32bd65bb3 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..8415096825987fcbdbcecaca425149a0d58ad4a8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..b8fc39aee48a941a5200619fe22b87d90178784f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..736dfd6fa758b32f8c70ef564b2889a786cdea56 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..8c44e70159048f51e40d1b770972b7e12061894c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..c16c6c5de8db3c9475af51b6cd409f4d30d4a91d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..c67170e3136a57c4ddf632e57fd5825f998fdb1c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3072b75699814e04c8328548e87540aac102eba4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b1c725da4a3cec7b655a06cb3a3c1eba39bdeb Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..38e0a2882afccf81da8107628cdde589dd23fa0d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png new file mode 100644 index 0000000000000000000000000000000000000000..fc4ddd32ccca3e4b0759f201db3edd3cb0d63b5f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..1b43a07d03f1508976440cd4875ac09ed4521be2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..af75db4b406042adae2e57abe4e18e1dffca673f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..64995d147efb3690172f9516e9decabce8e9ad69 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9655b6d9047732337bda5aaefd2b719b98f017 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..a5a30213dcbae4468fe47ab1bee5b7342ae805c8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf8e03623c94b68d31963ffe7e59c72c3dcc059 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..7d5d66de34f8c50b65bf2699380aa89fc602cd9c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..86578be45ae3fbbd3a3c0be96fd487cdca7f95b1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..3226ab760aaa0c15b1fcab6d993bce639f0676dd Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..061904c42c15f2f314d8a27a7665453463586a93 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 0000000000000000000000000000000000000000..1d9371de077a7d6e059f17d13af1e6addbd3298c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..64bd6912ce2948590e7ed6bdd304a24493ee482a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..046b24a9638c19b54393ae38c14124d7b1ecb375 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..1ff3373701ccf18b5423d5dcdb0a9132523e870b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..2eb7c7ebc37bc4f7abed2c22a5ddff26788478fa Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..71f7827015af99a8696c1dce3ad785d2dfd313ba Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bb7327251cce6893bb534c243167e8b80bb3cf24 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6256300b420e27596c39ee1b073723d76d0d8e1f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ef45e933a99b720cc5f6127e6da22bc2fa679244 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..40eed1d124d759ad2c36c7456136dc4028ca49d5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..5769f11787bc882205b96d500efd6245ef0fabc0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..48e75beeef2ceeb733eebdf4096d34fd23620b6d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..09c0e3efd8361b7f44089a299e24ba0b6c1e47e3 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e188d4a37f937699916a3487ea75dc075688afbc Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c571b2e3e776762bb90733f664f9d66e2c7f321c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..d2f709942920102a170dbdd27d757d4cd518a166 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..ce5f704ec09421f999a02232c434a492ed193194 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..3d0580f93d135028d8c4965f14a9dec2d6675052 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..f91b718478ac22a6818740c82e2145d4907acd9b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..2fbd458e91dfb9d9ca17229f8617ebd453572c22 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..2cdb2d7a12043ec3d0ddc21af7bde51d56b4c0e7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..65a6b7bbbde4b76dfe997a5720796a46474cf5e0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..48483a0b6d76b0c5b537973de19480358927dd0e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..90679117746a6b0f9408e5ba6e663570f74cfc37 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..e053c757aec4579d7a6dfa0478f16dd2c6718ad9 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..763767b4f6f4e3c64cdaabcd7bfd2197363812d5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..aea15f0be51cfef4c218f7362a2ab739ac04245b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7c289d4971337ec3693780d13b26c146c58a5f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png new file mode 100644 index 0000000000000000000000000000000000000000..fdfafed9ae4e950b6a33db7d596f5eeeecbf30e4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..43319dc926a7bab2ce773a898a9bf22b6e860b87 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..2d43c4d5b0676d1f6ebb8b40a2ccd9e161c37725 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..d2671edf7acd822a2d936e73c0b439a2c8a25292 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..c1783de67545797e41a7ecaad7e8f76879a95410 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..ca9d7d66be1bc28f13a2f2812170cc7ec81f223f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..eda10e6123e1e1383c4617228ec0c96680d60dc7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..b652725426387d8c03830afcf05bec5400f45257 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..86578be45ae3fbbd3a3c0be96fd487cdca7f95b1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..5532e88c2c65f7fc9f37bf5a90c5868864b47c9d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..f4af9265719d65e1c500c2a9de627aff162a18cc Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 0000000000000000000000000000000000000000..8fb0636cf583b0c47a0c5476e8b95e2c9481b448 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..f4f00ca0f2a166d2d467cdb6a82ebbdb2d5c7c2b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..142c5457d57993a847f700842068922b710bd282 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..72c51b0d5e296acab82146374139aeab1750b929 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8d67e448fc015039913e7e5bc48abf49be2b4744 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..90ead2e4551b165530bd2430b3d69c34263c5c4e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2656cad18db542a5653af00756fdcf276c51f2f8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..670bf796cb6d63868519c9a930fc53fd215e60b1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..24142c7291d2aae6fe5353e2757dd53fe84520d5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..03cad4c902bdd64923a2c4fe3da0fa2591cb41e4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f44df1afd6e175ebd614b59ef971e68e96134d42 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c41a5fcffa8c4dc3684252fbfd61623837e19d50 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..436a82da6d24900fb8f466878463b6ba4b969aa2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..a70c60c03c37a8da24cdf4b508da12e056f2c855 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..c64b9defeff158216be5d1b16eace8ea07acfedf Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..ff1759b8f94a037909c2a207c9438ad386abbe35 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..878e694ad7860ced52f147401d8a89958f4e9c4c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4138f15af3cc9c96a459c563bd0468f5892164 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..0fec2f2b5ecf6186fb9755dd4cec97a5bf26794d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..fa682b11b568f950b4da96415fa6a39824e35ac5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..6c45bc8e690bd5cdc7ece5b6aa8ba4464b3f40e2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png new file mode 100644 index 0000000000000000000000000000000000000000..955f7383b5a21238ee90f490c6b621214f6a307a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0a79824b8ffdb6ef7da089b69866c9ecdd395c19 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..184f7418d50ec4554539137f1abcaa3170b4643c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8f744f0391a2312f76d07c0668754fe83e346710 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6cdeeaaa00de23226af4891cb1c229352d1140 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..89d29b7f54542626191223403b66b467e30faaa0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..55f1d1369fa7a41ee8d798cd79035fb704f6ca4a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..8d897ba5a1577fec735cbed05ddae410211e04d6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..4ab5ad0ee1dd6e4f21c43fa6f4c3c2022f175146 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..d0979e9ebc66f8c299fabe5f63021f91e91e0b98 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..52c00ddcd2b715610e2c6bc9f2ac220fe0bda0cd Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..3e4ca684e950e83576cce90e907d8d6c42a1ede4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..230d649bf730871f6f538cd7cf957d0aa894f899 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..1352a1702a7bd044d5e9eb2424f2501b3360abe8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..7ddf14a0d14cb5a49c8e24fa99a757fc33d297fb Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png new file mode 100644 index 0000000000000000000000000000000000000000..2ffb2ecae6cfe3a892012a71d20f6d33a0b4e3ca Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ae01a04ae85bc3d34325cc3397630c21585a3286 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1741675def6be036e0d491e2d385bb9a950c65e8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b0e020573d37e8b4acac23fcd3e01cc39531b5e4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..903c1623d48af804a1297a08512f4e278d04cb67 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5540ff60d286d71b39e48f870ea30f15e0b30e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d3ff0ecb6b40829e854bb729d71d8b6bd0e683bd Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5b96af5b7adf9fba6e4856deac39852627834936 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3a82cab3b4f2cfe77333cee6121d867353753d5e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fa7c17ac45923f50462b3b1f5e016348852fe206 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..33d40d8b6246e4f62c3791c4ea59525ec5f2191c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa2cca80cef9e50a0ebf4ec94b8f3f87c732520 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png new file mode 100644 index 0000000000000000000000000000000000000000..b072ad11f24d2ac826149ed610adc0c81debf8f7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..d90782a32274398cd681601dab5b1baef34d7502 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png new file mode 100644 index 0000000000000000000000000000000000000000..0643ea55fe3f77b88a1c1cc76acac188d2c4cda0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6e1aa0f7fb7776734ea9b77377131418916834 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable/dialog_background_material.xml b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml new file mode 100644 index 0000000000000000000000000000000000000000..1b71cd63a172a2254f09a588a3068d8680fbd967 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml new file mode 100644 index 0000000000000000000000000000000000000000..67645ff9109a425e0fa9c99739105225cc0e139c --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml new file mode 100644 index 0000000000000000000000000000000000000000..56fab8f6fb90e5a133eb429acc0497fb9ea50d94 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/ic_call.xml b/java/com/android/contacts/common/res/drawable/ic_call.xml new file mode 100644 index 0000000000000000000000000000000000000000..0fedd452faea449bd886d5c96df304422c29a20e --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_call.xml @@ -0,0 +1,19 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..3c6c8b5349815fd4e8bebd10fdd218fbad226edd --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml @@ -0,0 +1,19 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_more_vert.xml b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000000000000000000000000000000000000..fcc3d9e4fe766e15718c41f8f9cb160d4dc0af29 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..0af90edb3b0e727c89633671f399b22c195b87ac --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml @@ -0,0 +1,20 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac932f87c57c21e39092ef098c77d7088b0a72d1 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml new file mode 100644 index 0000000000000000000000000000000000000000..4838de58a529a032f7f109cc8df8fa8a56d0c64b --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml new file mode 100644 index 0000000000000000000000000000000000000000..80180604432bc0e94805a3c8cb6aa56e864de9d8 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1b5cba43603c9ca2335a1e650b2234d686190d8 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml @@ -0,0 +1,21 @@ + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_all.xml b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml new file mode 100644 index 0000000000000000000000000000000000000000..9cc6fbc967354f71d0b3cae231f89888f5601c19 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b3e7a415e016a2b5595a16446771524465d2270 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml new file mode 100644 index 0000000000000000000000000000000000000000..a12e0993e28c3a7a2f82048154d5a7525691e0d3 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_work_profile.xml b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml new file mode 100644 index 0000000000000000000000000000000000000000..fc21100c0e18fdff7cb9104195e4f37b16602f81 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml new file mode 100644 index 0000000000000000000000000000000000000000..94e30950789741240c23a96af27543e6fa501cd4 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml new file mode 100644 index 0000000000000000000000000000000000000000..91ab763a57af135c2be2d6cc59ac0df4f3147816 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_light.xml b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml new file mode 100644 index 0000000000000000000000000000000000000000..d41accb02f615d3830ee5e8eb5cc5aedbfb78b5b --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..5b774fd20302637c6eca01794f0a4ae5b9861315 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml new file mode 100644 index 0000000000000000000000000000000000000000..35fff99c271ac042de294898040141151843f5e5 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml new file mode 100644 index 0000000000000000000000000000000000000000..27614a1ac70103683c4bcdb32e21c09f2f42f136 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/unread_count_background.xml b/java/com/android/contacts/common/res/drawable/unread_count_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..4fc6b9b60f15d77ea8824b6870b43b61c5cf9c92 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/unread_count_background.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..bef30a434a29c8dae4239a0d3616f76e3f2eeb5e --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml new file mode 100644 index 0000000000000000000000000000000000000000..2aa97722db324ae6a0d992d6b34f25de277264b7 --- /dev/null +++ b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/account_filter_header.xml b/java/com/android/contacts/common/res/layout/account_filter_header.xml new file mode 100644 index 0000000000000000000000000000000000000000..a12ab08fdb4a8a06d883a6e21f7a896bffd7a55e --- /dev/null +++ b/java/com/android/contacts/common/res/layout/account_filter_header.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..587626e8d7ca201275e7afc2c5eb8de4e214c44b --- /dev/null +++ b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml new file mode 100644 index 0000000000000000000000000000000000000000..33821166e5038472c34f2e8b382696f8c9f1a262 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/call_subject_history.xml b/java/com/android/contacts/common/res/layout/call_subject_history.xml new file mode 100644 index 0000000000000000000000000000000000000000..733f1d8b6f2958f66ae3f388cf0b5332ada11d70 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/call_subject_history.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..c378f24b2bff6efdeb2a9816d8eac417a4157521 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml @@ -0,0 +1,29 @@ + + + + diff --git a/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml new file mode 100644 index 0000000000000000000000000000000000000000..02a5c809c3856df054ba75ea9fcef909560b195d --- /dev/null +++ b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/contact_list_card.xml b/java/com/android/contacts/common/res/layout/contact_list_card.xml new file mode 100644 index 0000000000000000000000000000000000000000..a04f4cad9ce9b0fb5dc244bfe1b65e2dd2e38b48 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/contact_list_card.xml @@ -0,0 +1,39 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/contact_list_content.xml b/java/com/android/contacts/common/res/layout/contact_list_content.xml new file mode 100644 index 0000000000000000000000000000000000000000..3ee27a0adc3a2186db018d37b729edab97b054b7 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/contact_list_content.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/default_account_checkbox.xml b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml new file mode 100644 index 0000000000000000000000000000000000000000..b7c0cf6442eb651b2ed63c8fa42280e0fc4574b4 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/java/com/android/contacts/common/res/layout/dialog_call_subject.xml b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml new file mode 100644 index 0000000000000000000000000000000000000000..709bb50cb4db33fc83c0dfdb2a98771366233873 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/directory_header.xml b/java/com/android/contacts/common/res/layout/directory_header.xml new file mode 100644 index 0000000000000000000000000000000000000000..b8f5163c0d41f70a1c9db2b80e28ace416b6f89a --- /dev/null +++ b/java/com/android/contacts/common/res/layout/directory_header.xml @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/list_separator.xml b/java/com/android/contacts/common/res/layout/list_separator.xml new file mode 100644 index 0000000000000000000000000000000000000000..ab60605c56b77d4b66a58be11302ea7dc85f6e36 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/list_separator.xml @@ -0,0 +1,27 @@ + + + diff --git a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml new file mode 100644 index 0000000000000000000000000000000000000000..8a3bd6088011c685cc8330d85028d451ed6dbcf5 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/select_account_list_item.xml b/java/com/android/contacts/common/res/layout/select_account_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..fbd31e5738d09ffb343e796307f009defb29e4e1 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/select_account_list_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/unread_count_tab.xml b/java/com/android/contacts/common/res/layout/unread_count_tab.xml new file mode 100644 index 0000000000000000000000000000000000000000..83481ee2d0f9fb431f2699b64c14eb6aad0c0f57 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/unread_count_tab.xml @@ -0,0 +1,43 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..64eff002fb71043b01f5b14aa9563d34fb4cc693 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b4ee8215ae6cf0e096cf6335adfbe7f889914301 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6feeadfbe1d9a10fa898f1980ae85e9853ce81d0 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..01a3fde9d7bb0855b37e90a50f028fa5f8414f84 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..328e067eec56058a823257fc616d370612e98cfc Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml new file mode 100644 index 0000000000000000000000000000000000000000..e05c6d658499e38f5e355297b5c2616b7728fe45 --- /dev/null +++ b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml @@ -0,0 +1,20 @@ + + + + false + + + true + + + false + + + true + + + false + + + true + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml new file mode 100644 index 0000000000000000000000000000000000000000..8def554986e54c571a492aaed96fdf61ecaefab8 --- /dev/null +++ b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml @@ -0,0 +1,17 @@ + + + + false + + + false + + + false + + + false + + + false + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values-land/integers.xml b/java/com/android/contacts/common/res/values-land/integers.xml new file mode 100644 index 0000000000000000000000000000000000000000..26bac62220a17dfce075a436cfa49a45f05bc7e4 --- /dev/null +++ b/java/com/android/contacts/common/res/values-land/integers.xml @@ -0,0 +1,22 @@ + + + + 3 + + + 60 + diff --git a/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml new file mode 100644 index 0000000000000000000000000000000000000000..be4eb0bb08c87f6e490c490ac1a5a60d250b7577 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml @@ -0,0 +1,22 @@ + + + + 3 + + + 20 + diff --git a/java/com/android/contacts/common/res/values-sw600dp/dimens.xml b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf67a1e723561492ec8ab61d27dbeab4239b18e7 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml @@ -0,0 +1,29 @@ + + + + 0dip + + @dimen/list_visible_scrollbar_padding + + 24dip + 16dip + + + 32dp + + 32dp + diff --git a/java/com/android/contacts/common/res/values-sw600dp/integers.xml b/java/com/android/contacts/common/res/values-sw600dp/integers.xml new file mode 100644 index 0000000000000000000000000000000000000000..31aeee9955b0c4fee7c2aeff407280670609e29d --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw600dp/integers.xml @@ -0,0 +1,24 @@ + + + + 3 + + + + 15 + diff --git a/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml new file mode 100644 index 0000000000000000000000000000000000000000..577716d24412691a464ffd66ce7e160da751dabb --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml @@ -0,0 +1,22 @@ + + + + 4 + + + 30 + diff --git a/java/com/android/contacts/common/res/values-sw720dp/integers.xml b/java/com/android/contacts/common/res/values-sw720dp/integers.xml new file mode 100644 index 0000000000000000000000000000000000000000..05e3093519cd6f253c7ff4d9d4ca56def4ee2783 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw720dp/integers.xml @@ -0,0 +1,22 @@ + + + + 2 + + + 20 + diff --git a/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml new file mode 100644 index 0000000000000000000000000000000000000000..08023992b4f4bf9033285c82e7ca702f5880f317 --- /dev/null +++ b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml @@ -0,0 +1,17 @@ + + + + false + + + true + + + false + + + true + + + false + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml new file mode 100644 index 0000000000000000000000000000000000000000..08023992b4f4bf9033285c82e7ca702f5880f317 --- /dev/null +++ b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml @@ -0,0 +1,17 @@ + + + + false + + + true + + + false + + + true + + + false + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values/animation_constants.xml b/java/com/android/contacts/common/res/values/animation_constants.xml new file mode 100644 index 0000000000000000000000000000000000000000..9eec7d6c84a5c4825ab81742201f12341272e6bf --- /dev/null +++ b/java/com/android/contacts/common/res/values/animation_constants.xml @@ -0,0 +1,19 @@ + + + + 250 + diff --git a/java/com/android/contacts/common/res/values/attrs.xml b/java/com/android/contacts/common/res/values/attrs.xml new file mode 100644 index 0000000000000000000000000000000000000000..44d04f02502c92978fe590268cbff9659c13b596 --- /dev/null +++ b/java/com/android/contacts/common/res/values/attrs.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/values/colors.xml b/java/com/android/contacts/common/res/values/colors.xml new file mode 100644 index 0000000000000000000000000000000000000000..7524eff584e50a10bc61f696566e11f8af8385e3 --- /dev/null +++ b/java/com/android/contacts/common/res/values/colors.xml @@ -0,0 +1,158 @@ + + + + + #eeeeee + + #44ff0000 + + + #a0ffffff + + + #30000000 + + + #363636 + + @color/dialer_secondary_text_color + + + #2A56C6 + + + #AAAAAA + + + #D0D0D0 + + + #363636 + + + #DDDDDD + + + #7F000000 + + + #CCCCCC + + #7f000000 + + #fff + #000 + + + + #DB4437 + #E91E63 + #9C27B0 + #673AB7 + #3F51B5 + #4285F4 + #039BE5 + #0097A7 + #009688 + #0F9D58 + #689F38 + #EF6C00 + #FF5722 + #757575 + + + + + #C53929 + #C2185B + #7B1FA2 + #512DA8 + #303F9F + #3367D6 + #0277BD + #006064 + #00796B + #0B8043 + #33691E + #E65100 + #E64A19 + #424242 + + + + #607D8B + + #455A64 + + + #cccccc + + #ffffff + + @color/dialer_theme_color + + #ffffff + + #008aa1 + + #ffffff + @color/tab_ripple_color + #f50057 + #1C3AA9 + + + @color/contactscommon_actionbar_background_color + + + + #ffffff + #a6ffffff + + + #000000 + + #ffffff + + #737373 + @color/searchbox_hint_text_color + + @color/dialtacts_theme_color + + + #f9f9f9 + #FFFFFF + + + #d1041c + + + #000000 + + + #d8d8d8 + + + #00c853 + + + #ffffff + @color/searchbox_hint_text_color + diff --git a/java/com/android/contacts/common/res/values/dimens.xml b/java/com/android/contacts/common/res/values/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..642eb31a4ddb63843fc85807bb2763f05787ee02 --- /dev/null +++ b/java/com/android/contacts/common/res/values/dimens.xml @@ -0,0 +1,161 @@ + + + + + + 0dip + + + 32dip + + 18dp + 8dp + + + 23dip + + 16dip + + + 16dip + + + + 48sp + + + 0dip + + + 32dip + + 16dip + @dimen/list_visible_scrollbar_padding + + 8dip + + 56dp + + + 0dip + + + 12dp + + + 1dp + + + 32dip + + + 48dip + + + 16sp + 40dp + 15dp + 12dp + + + 20sp + 10dip + + + 40dp + 20dp + 1dp + 67% + + + 56dp + + 56dp + + 8dp + + 88dp + + 16dp + + 16dp + + + 2dp + + 14sp + 2dp + 16dp + 2dp + 0dp + 2dp + 12sp + 2dp + + + 4dp + + 48dp + + 56dp + + 16dp + + 14dp + + 15dp + + 20sp + + + 16dp + + 57dp + + 24sp + + + 40dp + + 2dp + + + 20dp + + 8dp + + 50dp + + 60dp + + 16sp + + 14sp + + 15dp + diff --git a/java/com/android/contacts/common/res/values/donottranslate_config.xml b/java/com/android/contacts/common/res/values/donottranslate_config.xml new file mode 100644 index 0000000000000000000000000000000000000000..324437928a336ea881a1030b67b168f1b9ed0f69 --- /dev/null +++ b/java/com/android/contacts/common/res/values/donottranslate_config.xml @@ -0,0 +1,95 @@ + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + ContactEditorUtils_default_account + + + ContactEditorUtils_anything_saved + + + default + + + default + + + + + + + + + vcf + + + contacts.vcf + + + 1 + + + 99999 + + + + + + true + + + true + + + true + + pref_build_version + pref_open_source_licenses + pref_privacy_policy + pref_terms_of_service + + + diff --git a/java/com/android/contacts/common/res/values/ids.xml b/java/com/android/contacts/common/res/values/ids.xml new file mode 100644 index 0000000000000000000000000000000000000000..871f5a63624a2ffcf752310f72269655800d4a0c --- /dev/null +++ b/java/com/android/contacts/common/res/values/ids.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/values/integers.xml b/java/com/android/contacts/common/res/values/integers.xml new file mode 100644 index 0000000000000000000000000000000000000000..d38ad1da05363e535b72c67f50ba7c5053ad5d1c --- /dev/null +++ b/java/com/android/contacts/common/res/values/integers.xml @@ -0,0 +1,39 @@ + + + + + + + 2 + 3 + + + 30 + + + 0 + + 0 + + + 250 + + + 100 + diff --git a/java/com/android/contacts/common/res/values/strings.xml b/java/com/android/contacts/common/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..15e1f15d973fee31305c2d78af99cc7fe0e7b6d6 --- /dev/null +++ b/java/com/android/contacts/common/res/values/strings.xml @@ -0,0 +1,798 @@ + + + + + + Text copied + + Copy to clipboard + + + Call + %s + + + Call home + + Call mobile + + Call work + + Call work fax + + Call home fax + + Call pager + + Call + + Call callback + + Call car + + Call company main + + Call ISDN + + Call main + + Call fax + + Call radio + + Call telex + + Call TTY/TDD + + Call work mobile + + Call work pager + + Call + %s + + + Call MMS + + %s (Call) + + + Text + %s + + + Text home + + Text mobile + + Text work + + Text work fax + + Text home fax + + Text pager + + Text + + Text callback + + Text car + + Text company main + + Text ISDN + + Text main + + Text fax + + Text radio + + Text telex + + Text TTY/TDD + + Text work mobile + + Text work pager + + Text + %s + + + Text MMS + + %s (Message) + + + Clear frequently contacted? + + + You\'ll clear the frequently contacted list in the + Contacts and Phone apps, and force email apps to learn your addressing preferences from + scratch. + + + + Clearing frequently contacted\u2026 + + + Available + + + Away + + + Busy + + + Contacts + + + Other + + + Directory + + + Work directory + + + All contacts + + + Me + + + Searching\u2026 + + + More than %d found. + + + No contacts + + + + 1 found + %d found + + + + Quick contact for %1$s + + + (No name) + + + Frequently called + + + Frequently contacted + + + View contact + + + All contacts with phone numbers + + + Work profile contacts + + + View updates + + + Device-only, unsynced + + + Name + + + Nickname + + + Name + + First name + + Last name + + Name prefix + + Middle name + + Name suffix + + + Phonetic name + + + Phonetic first name + + Phonetic middle name + + Phonetic last name + + + Phone + + + Email + + + Address + + + IM + + + Organization + + + Relationship + + + Special date + + + Text message + + + Address + + + Company + + + Title + + + Notes + + + SIP + + + Website + + + Groups + + + Email home + + Email mobile + + Email work + + Email + + Email %s + + + Email + + + Street + + PO box + + Neighborhood + + City + + State + + ZIP code + + Country + + + View home address + + View work address + + View address + + View %s address + + + Chat using AIM + + Chat using Windows Live + + Chat using Yahoo + + Chat using Skype + + Chat using QQ + + Chat using Google Talk + + Chat using ICQ + + Chat using Jabber + + + Chat + + + delete + + + Expand or collapse name fields + + + Expand or collapse phonetic + name fields + + + All contacts + + + Done + + + Cancel + + + Contacts in %s + + + Contacts in custom view + + + Single contact + + + Save imported contacts to: + + + Import from SIM card + + + Import from SIM ^1 - ^2 + + + Import from SIM %1$s + + + Import from .vcf file + + + Cancel import of %s? + + + Cancel export of %s? + + + Couldn\'t cancel vCard import/export + + + Unknown error. + + + Couldn\'t open \"%s\": %s. + + + Couldn\'t start the exporter: \"%s\". + + + There is no exportable contact. + + + You have disabled a required permission. + + + An error occurred during export: \"%s\". + + + Required filename is too long (\"%s\"). + + + I/O error + + + Not enough memory. The file may be too large. + + + Couldn\'t parse vCard for an unexpected reason. + + + The format isn\'t supported. + + + Couldn\'t collect meta information of given vCard file(s). + + + One or more files couldn\'t be imported (%s). + + + Finished exporting %s. + + + Finished exporting contacts. + + + Finished exporting contacts, click the notification to share contacts. + + + Tap to share contacts. + + + Exporting %s canceled. + + + Exporting contact data + + + Contact data is being exported. + + + Couldn\'t get database information. + + + There are no exportable contacts. If you do have contacts on your device, some data providers may not allow the contacts to be exported from the device. + + + The vCard composer didn\'t start properly. + + + Couldn\'t export + + + The contact data wasn\'t exported.\nReason: \"%s\" + + + Importing %s + + + Couldn\'t read vCard data + + + Reading vCard data canceled + + + Finished importing vCard %s + + + Importing %s canceled + + + %s will be imported shortly. + + The file will be imported shortly. + + vCard import request was rejected. Try again later. + + %s will be exported shortly. + + + The file will be exported shortly. + + + Contacts will be exported shortly. + + + vCard export request was rejected. Try again later. + + contact + + + Caching vCard(s) to local temporary storage. The actual import will start soon. + + + Couldn\'t import vCard. + + + Contact received over NFC + + + Export contacts? + + + Caching + + + Importing %s/%s: %s + + + Export to .vcf file + + + + + Sort by + + + First name + + + Last name + + + Name format + + + First name first + + + Last name first + + + Default account for new contacts + + + Sync contact metadata [DOGFOOD] + + + Sync contact metadata + + + About Contacts + + + Settings + + + Share visible contacts + + + Failed to share visible contacts. + + + Share favorite contacts + + + Share all contacts + + + Failed to share contacts. + + + Import/export contacts + + + Import contacts + + + This contact can\'t be shared. + + + There are no contacts to share. + + + Search + + + Find contacts + + + Favorites + + + No contacts. + + + No visible contacts. + + + No favorites + + + No contacts in %s + + + Clear frequents + + + Select SIM card + + + Manage accounts + + + Import/export + + + sans-serif + + + via %1$s + + + %1$s via %2$s + + + sans-serif-medium + + + stop searching + + + Clear search + + + sans-serif + + + Contact display options + + + Account + + + Always use this for calls + + + Call with + + + Call with a note + + + Type a note to send with call ... + + + SEND & CALL + + + %1$s / %2$s + + + %1$s%2$s + + + %1$s tab. + + + + + %1$s tab. %2$d unread item. + + + %1$s tab. %2$d unread items. + + + + + Build version + + + Open source licenses + + + License details for open source software + + + Privacy policy + + + Terms of service + + + Open source licenses + + + Failed to open the url. + + + Place video call + diff --git a/java/com/android/contacts/common/res/values/styles.xml b/java/com/android/contacts/common/res/values/styles.xml new file mode 100644 index 0000000000000000000000000000000000000000..07d4a022577e40f6b66b41d4610b5e741340f665 --- /dev/null +++ b/java/com/android/contacts/common/res/values/styles.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/testing/InjectedServices.java b/java/com/android/contacts/common/testing/InjectedServices.java new file mode 100644 index 0000000000000000000000000000000000000000..5ab5e5feb2f02f9147af08500f8d12663834eba0 --- /dev/null +++ b/java/com/android/contacts/common/testing/InjectedServices.java @@ -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. + */ + +package com.android.contacts.common.testing; + +import android.content.ContentResolver; +import android.content.SharedPreferences; +import android.util.ArrayMap; +import java.util.Map; + +/** + * A mechanism for providing alternative (mock) services to the application while running tests. + * Activities, Services and the Application should check with this class to see if a particular + * service has been overridden. + */ +public class InjectedServices { + + private ContentResolver mContentResolver; + private SharedPreferences mSharedPreferences; + private Map mSystemServices; + + public ContentResolver getContentResolver() { + return mContentResolver; + } + + public void setContentResolver(ContentResolver contentResolver) { + this.mContentResolver = contentResolver; + } + + public SharedPreferences getSharedPreferences() { + return mSharedPreferences; + } + + public void setSharedPreferences(SharedPreferences sharedPreferences) { + this.mSharedPreferences = sharedPreferences; + } + + public void setSystemService(String name, Object service) { + if (mSystemServices == null) { + mSystemServices = new ArrayMap<>(); + } + + mSystemServices.put(name, service); + } + + public Object getSystemService(String name) { + if (mSystemServices != null) { + return mSystemServices.get(name); + } + return null; + } +} diff --git a/java/com/android/contacts/common/util/AccountFilterUtil.java b/java/com/android/contacts/common/util/AccountFilterUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..18743c65ea97f5ec3ace9fa7dc65295fbf53b99e --- /dev/null +++ b/java/com/android/contacts/common/util/AccountFilterUtil.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.View; +import android.widget.TextView; +import com.android.contacts.common.R; +import com.android.contacts.common.list.ContactListFilter; +import com.android.contacts.common.list.ContactListFilterController; + +/** Utility class for account filter manipulation. */ +public class AccountFilterUtil { + + public static final String EXTRA_CONTACT_LIST_FILTER = "contactListFilter"; + private static final String TAG = AccountFilterUtil.class.getSimpleName(); + + /** + * Find TextView with the id "account_filter_header" and set correct text for the account filter + * header. + * + * @param filterContainer View containing TextView with id "account_filter_header" + * @return true when header text is set in the call. You may use this for conditionally showing or + * hiding this entire view. + */ + public static boolean updateAccountFilterTitleForPeople( + View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) { + return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, false); + } + + /** + * Similar to {@link #updateAccountFilterTitleForPeople(View, ContactListFilter, boolean, + * boolean)}, but for Phone UI. + */ + public static boolean updateAccountFilterTitleForPhone( + View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) { + return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, true); + } + + private static boolean updateAccountFilterTitle( + View filterContainer, + ContactListFilter filter, + boolean showTitleForAllAccounts, + boolean forPhone) { + final Context context = filterContainer.getContext(); + final TextView headerTextView = + (TextView) filterContainer.findViewById(R.id.account_filter_header); + + boolean textWasSet = false; + if (filter != null) { + if (forPhone) { + if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { + if (showTitleForAllAccounts) { + headerTextView.setText(R.string.list_filter_phones); + textWasSet = true; + } + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + headerTextView.setText( + context.getString(R.string.listAllContactsInAccount, filter.accountName)); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + headerTextView.setText(R.string.listCustomView); + textWasSet = true; + } else { + Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected."); + } + } else { + if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { + if (showTitleForAllAccounts) { + headerTextView.setText(R.string.list_filter_all_accounts); + textWasSet = true; + } + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + headerTextView.setText( + context.getString(R.string.listAllContactsInAccount, filter.accountName)); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + headerTextView.setText(R.string.listCustomView); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { + headerTextView.setText(R.string.listSingleContact); + textWasSet = true; + } else { + Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected."); + } + } + } else { + Log.w(TAG, "Filter is null."); + } + return textWasSet; + } + + /** This will update filter via a given ContactListFilterController. */ + public static void handleAccountFilterResult( + ContactListFilterController filterController, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + final ContactListFilter filter = data.getParcelableExtra(EXTRA_CONTACT_LIST_FILTER); + if (filter == null) { + return; + } + if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + filterController.selectCustomFilter(); + } else { + filterController.setContactListFilter(filter, true); + } + } + } +} diff --git a/java/com/android/contacts/common/util/BitmapUtil.java b/java/com/android/contacts/common/util/BitmapUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..20f916a3fb1d6c2e3d7083afefe79469328fc441 --- /dev/null +++ b/java/com/android/contacts/common/util/BitmapUtil.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +/** Provides static functions to decode bitmaps at the optimal size */ +public class BitmapUtil { + + private BitmapUtil() {} + + /** + * Returns Width or Height of the picture, depending on which size is smaller. Doesn't actually + * decode the picture, so it is pretty efficient to run. + */ + public static int getSmallerExtentFromBytes(byte[] bytes) { + final BitmapFactory.Options options = new BitmapFactory.Options(); + + // don't actually decode the picture, just return its bounds + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + + // test what the best sample size is + return Math.min(options.outWidth, options.outHeight); + } + + /** + * Finds the optimal sampleSize for loading the picture + * + * @param originalSmallerExtent Width or height of the picture, whichever is smaller + * @param targetExtent Width or height of the target view, whichever is bigger. + *

If either one of the parameters is 0 or smaller, no sampling is applied + */ + public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) { + // If we don't know sizes, we can't do sampling. + if (targetExtent < 1) { + return 1; + } + if (originalSmallerExtent < 1) { + return 1; + } + + // Test what the best sample size is. To do that, we find the sample size that gives us + // the best trade-off between resulting image size and memory requirement. We allow + // the down-sampled image to be 20% smaller than the target size. That way we can get around + // unfortunate cases where e.g. a 720 picture is requested for 362 and not down-sampled at + // all. Why 20%? Why not. Prove me wrong. + int extent = originalSmallerExtent; + int sampleSize = 1; + while ((extent >> 1) >= targetExtent * 0.8f) { + sampleSize <<= 1; + extent >>= 1; + } + + return sampleSize; + } + + /** Decodes the bitmap with the given sample size */ + public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) { + final BitmapFactory.Options options; + if (sampleSize <= 1) { + options = null; + } else { + options = new BitmapFactory.Options(); + options.inSampleSize = sampleSize; + } + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + } + + /** + * Retrieves a copy of the specified drawable resource, rotated by a specified angle. + * + * @param resources The current resources. + * @param resourceId The resource ID of the drawable to rotate. + * @param angle The angle of rotation. + * @return Rotated drawable. + */ + public static Drawable getRotatedDrawable( + android.content.res.Resources resources, int resourceId, float angle) { + + // Get the original drawable and make a copy which will be rotated. + Bitmap original = BitmapFactory.decodeResource(resources, resourceId); + Bitmap rotated = + Bitmap.createBitmap(original.getWidth(), original.getHeight(), Bitmap.Config.ARGB_8888); + + // Perform the rotation. + Canvas tempCanvas = new Canvas(rotated); + tempCanvas.rotate(angle, original.getWidth() / 2, original.getHeight() / 2); + tempCanvas.drawBitmap(original, 0, 0, null); + + return new BitmapDrawable(resources, rotated); + } + + /** + * Given an input bitmap, scales it to the given width/height and makes it round. + * + * @param input {@link Bitmap} to scale and crop + * @param targetWidth desired output width + * @param targetHeight desired output height + * @return output bitmap scaled to the target width/height and cropped to an oval. The cropping + * algorithm will try to fit as much of the input into the output as possible, while + * preserving the target width/height ratio. + */ + public static Bitmap getRoundedBitmap(Bitmap input, int targetWidth, int targetHeight) { + if (input == null) { + return null; + } + final Bitmap.Config inputConfig = input.getConfig(); + final Bitmap result = + Bitmap.createBitmap( + targetWidth, targetHeight, inputConfig != null ? inputConfig : Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(result); + final Paint paint = new Paint(); + canvas.drawARGB(0, 0, 0, 0); + paint.setAntiAlias(true); + final RectF dst = new RectF(0, 0, targetWidth, targetHeight); + canvas.drawOval(dst, paint); + + // Specifies that only pixels present in the destination (i.e. the drawn oval) should + // be overwritten with pixels from the input bitmap. + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + + final int inputWidth = input.getWidth(); + final int inputHeight = input.getHeight(); + + // Choose the largest scale factor that will fit inside the dimensions of the + // input bitmap. + final float scaleBy = + Math.min((float) inputWidth / targetWidth, (float) inputHeight / targetHeight); + + final int xCropAmountHalved = (int) (scaleBy * targetWidth / 2); + final int yCropAmountHalved = (int) (scaleBy * targetHeight / 2); + + final Rect src = + new Rect( + inputWidth / 2 - xCropAmountHalved, + inputHeight / 2 - yCropAmountHalved, + inputWidth / 2 + xCropAmountHalved, + inputHeight / 2 + yCropAmountHalved); + + canvas.drawBitmap(input, src, dst, paint); + return result; + } +} diff --git a/java/com/android/contacts/common/util/CommonDateUtils.java b/java/com/android/contacts/common/util/CommonDateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..312e691f82eb2f6abd7d6f172bd3062f483be99d --- /dev/null +++ b/java/com/android/contacts/common/util/CommonDateUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.util; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** Common date utilities. */ +public class CommonDateUtils { + + // All the SimpleDateFormats in this class use the UTC timezone + public static final SimpleDateFormat NO_YEAR_DATE_FORMAT = + new SimpleDateFormat("--MM-dd", Locale.US); + public static final SimpleDateFormat FULL_DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd", Locale.US); + public static final SimpleDateFormat DATE_AND_TIME_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT = + new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + + /** Exchange requires 8:00 for birthdays */ + public static final int DEFAULT_HOUR = 8; +} diff --git a/java/com/android/contacts/common/util/Constants.java b/java/com/android/contacts/common/util/Constants.java new file mode 100644 index 0000000000000000000000000000000000000000..172e8c348fe4e680f97fbf1054ac069a338499fa --- /dev/null +++ b/java/com/android/contacts/common/util/Constants.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +public class Constants { + + /** + * Log tag for performance measurement. To enable: adb shell setprop log.tag.ContactsPerf VERBOSE + */ + public static final String PERFORMANCE_TAG = "ContactsPerf"; + + // Used for lookup URI that contains an encoded JSON string. + public static final String LOOKUP_URI_ENCODED = "encoded"; +} diff --git a/java/com/android/contacts/common/util/ContactDisplayUtils.java b/java/com/android/contacts/common/util/ContactDisplayUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1586784db9eb6659b66c3a734e7a392f8a41e6ed --- /dev/null +++ b/java/com/android/contacts/common/util/ContactDisplayUtils.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.util; + +import static android.provider.ContactsContract.CommonDataKinds.Phone; + +import android.content.Context; +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TtsSpan; +import android.util.Log; +import android.util.Patterns; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import java.util.Objects; + +/** Methods for handling various contact data labels. */ +public class ContactDisplayUtils { + + public static final int INTERACTION_CALL = 1; + public static final int INTERACTION_SMS = 2; + private static final String TAG = ContactDisplayUtils.class.getSimpleName(); + + /** + * Checks if the given data type is a custom type. + * + * @param type Phone data type. + * @return {@literal true} if the type is custom. {@literal false} if not. + */ + public static boolean isCustomPhoneType(Integer type) { + return type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT; + } + + /** + * Gets a display label for a given phone type. + * + * @param type The type of number. + * @param customLabel A custom label to use if the phone is determined to be of custom type + * determined by {@link #isCustomPhoneType(Integer))} + * @param interactionType whether this is a call or sms. Either {@link #INTERACTION_CALL} or + * {@link #INTERACTION_SMS}. + * @param context The application context. + * @return An appropriate string label + */ + public static CharSequence getLabelForCallOrSms( + Integer type, CharSequence customLabel, int interactionType, @NonNull Context context) { + Objects.requireNonNull(context); + + if (isCustomPhoneType(type)) { + return (customLabel == null) ? "" : customLabel; + } else { + int resId; + if (interactionType == INTERACTION_SMS) { + resId = getSmsLabelResourceId(type); + } else { + resId = getPhoneLabelResourceId(type); + if (interactionType != INTERACTION_CALL) { + Log.e( + TAG, + "Un-recognized interaction type: " + + interactionType + + ". Defaulting to ContactDisplayUtils.INTERACTION_CALL."); + } + } + + return context.getResources().getText(resId); + } + } + + /** + * Find a label for calling. + * + * @param type The type of number. + * @return An appropriate string label. + */ + public static int getPhoneLabelResourceId(Integer type) { + if (type == null) { + return R.string.call_other; + } + switch (type) { + case Phone.TYPE_HOME: + return R.string.call_home; + case Phone.TYPE_MOBILE: + return R.string.call_mobile; + case Phone.TYPE_WORK: + return R.string.call_work; + case Phone.TYPE_FAX_WORK: + return R.string.call_fax_work; + case Phone.TYPE_FAX_HOME: + return R.string.call_fax_home; + case Phone.TYPE_PAGER: + return R.string.call_pager; + case Phone.TYPE_OTHER: + return R.string.call_other; + case Phone.TYPE_CALLBACK: + return R.string.call_callback; + case Phone.TYPE_CAR: + return R.string.call_car; + case Phone.TYPE_COMPANY_MAIN: + return R.string.call_company_main; + case Phone.TYPE_ISDN: + return R.string.call_isdn; + case Phone.TYPE_MAIN: + return R.string.call_main; + case Phone.TYPE_OTHER_FAX: + return R.string.call_other_fax; + case Phone.TYPE_RADIO: + return R.string.call_radio; + case Phone.TYPE_TELEX: + return R.string.call_telex; + case Phone.TYPE_TTY_TDD: + return R.string.call_tty_tdd; + case Phone.TYPE_WORK_MOBILE: + return R.string.call_work_mobile; + case Phone.TYPE_WORK_PAGER: + return R.string.call_work_pager; + case Phone.TYPE_ASSISTANT: + return R.string.call_assistant; + case Phone.TYPE_MMS: + return R.string.call_mms; + default: + return R.string.call_custom; + } + } + + /** + * Find a label for sending an sms. + * + * @param type The type of number. + * @return An appropriate string label. + */ + public static int getSmsLabelResourceId(Integer type) { + if (type == null) { + return R.string.sms_other; + } + switch (type) { + case Phone.TYPE_HOME: + return R.string.sms_home; + case Phone.TYPE_MOBILE: + return R.string.sms_mobile; + case Phone.TYPE_WORK: + return R.string.sms_work; + case Phone.TYPE_FAX_WORK: + return R.string.sms_fax_work; + case Phone.TYPE_FAX_HOME: + return R.string.sms_fax_home; + case Phone.TYPE_PAGER: + return R.string.sms_pager; + case Phone.TYPE_OTHER: + return R.string.sms_other; + case Phone.TYPE_CALLBACK: + return R.string.sms_callback; + case Phone.TYPE_CAR: + return R.string.sms_car; + case Phone.TYPE_COMPANY_MAIN: + return R.string.sms_company_main; + case Phone.TYPE_ISDN: + return R.string.sms_isdn; + case Phone.TYPE_MAIN: + return R.string.sms_main; + case Phone.TYPE_OTHER_FAX: + return R.string.sms_other_fax; + case Phone.TYPE_RADIO: + return R.string.sms_radio; + case Phone.TYPE_TELEX: + return R.string.sms_telex; + case Phone.TYPE_TTY_TDD: + return R.string.sms_tty_tdd; + case Phone.TYPE_WORK_MOBILE: + return R.string.sms_work_mobile; + case Phone.TYPE_WORK_PAGER: + return R.string.sms_work_pager; + case Phone.TYPE_ASSISTANT: + return R.string.sms_assistant; + case Phone.TYPE_MMS: + return R.string.sms_mms; + default: + return R.string.sms_custom; + } + } + + /** + * Whether the given text could be a phone number. + * + *

Note this will miss many things that are legitimate phone numbers, for example, phone + * numbers with letters. + */ + public static boolean isPossiblePhoneNumber(CharSequence text) { + return text != null && Patterns.PHONE.matcher(text.toString()).matches(); + } + + /** + * Returns a Spannable for the given message with a telephone {@link TtsSpan} set for the given + * phone number text wherever it is found within the message. + */ + public static Spannable getTelephoneTtsSpannable( + @Nullable String message, @Nullable String phoneNumber) { + if (message == null) { + return null; + } + final Spannable spannable = new SpannableString(message); + int start = phoneNumber == null ? -1 : message.indexOf(phoneNumber); + while (start >= 0) { + final int end = start + phoneNumber.length(); + final TtsSpan ttsSpan = PhoneNumberUtilsCompat.createTtsSpan(phoneNumber); + spannable.setSpan( + ttsSpan, + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // this is consistenly done in a misleading way.. + start = message.indexOf(phoneNumber, end); + } + return spannable; + } + + /** + * Retrieves a string from a string template that takes 1 phone number as argument, span the + * number with a telephone {@link TtsSpan}, and return the spanned string. + * + * @param resources to retrieve the string from + * @param stringId ID of the string + * @param number to pass in the template + * @return CharSequence with the phone number wrapped in a TtsSpan + */ + public static CharSequence getTtsSpannedPhoneNumber( + Resources resources, int stringId, String number) { + String msg = resources.getString(stringId, number); + return ContactDisplayUtils.getTelephoneTtsSpannable(msg, number); + } + + /** + * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}. + * Defaults to the name that is non-null. + * + * @param namePrimary the primary name. + * @param nameAlternative the alternative name. + * @param contactsPreferences the ContactsPreferences used to determine the preferred display + * name. + * @return namePrimary or nameAlternative depending on the value of displayOrderPreference. + */ + public static String getPreferredDisplayName( + String namePrimary, + String nameAlternative, + @Nullable ContactsPreferences contactsPreferences) { + if (contactsPreferences == null) { + return namePrimary != null ? namePrimary : nameAlternative; + } + if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + return namePrimary; + } + + if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE + && !TextUtils.isEmpty(nameAlternative)) { + return nameAlternative; + } + + return namePrimary; + } + + /** + * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}. + * Defaults to the name that is non-null. + * + * @param namePrimary the primary name. + * @param nameAlternative the alternative name. + * @param contactsPreferences the ContactsPreferences used to determine the preferred sort order. + * @return namePrimary or nameAlternative depending on the value of displayOrderPreference. + */ + public static String getPreferredSortName( + String namePrimary, + String nameAlternative, + @Nullable ContactsPreferences contactsPreferences) { + if (contactsPreferences == null) { + return namePrimary != null ? namePrimary : nameAlternative; + } + + if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { + return namePrimary; + } + + if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_ALTERNATIVE + && !TextUtils.isEmpty(nameAlternative)) { + return nameAlternative; + } + + return namePrimary; + } +} diff --git a/java/com/android/contacts/common/util/ContactListViewUtils.java b/java/com/android/contacts/common/util/ContactListViewUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..278c27d5c5a19218ee493a23ba6e060b9b92d75c --- /dev/null +++ b/java/com/android/contacts/common/util/ContactListViewUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.view.View; +import android.widget.ListView; +import com.android.contacts.common.R; +import com.android.dialer.util.ViewUtil; + +/** Utilities for configuring ListViews with a card background. */ +public class ContactListViewUtils { + + // These two constants will help add more padding for the text inside the card. + private static final double TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO = 1.1; + + private static void addPaddingToView( + ListView listView, int parentWidth, int listSpaceWeight, int listViewWeight) { + if (listSpaceWeight > 0 && listViewWeight > 0) { + double paddingPercent = + (double) listSpaceWeight / (double) (listSpaceWeight * 2 + listViewWeight); + listView.setPadding( + (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO), + listView.getPaddingTop(), + (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO), + listView.getPaddingBottom()); + // The EdgeEffect and ScrollBar need to span to the edge of the ListView's padding. + listView.setClipToPadding(false); + listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + } + } + + /** + * Add padding to {@param listView} if this configuration has set both space weight and view + * weight on the layout. Use this util method instead of defining the padding in the layout file + * so that the {@param listView}'s padding can be set proportional to the card padding. + * + * @param listView ListView that we add padding to + * @param rootLayout layout that contains ListView and R.id.list_card + */ + public static void applyCardPaddingToView( + Resources resources, final ListView listView, final View rootLayout) { + // Set a padding on the list view so it appears in the center of the card + // in the layout if required. + final int listSpaceWeight = resources.getInteger(R.integer.contact_list_space_layout_weight); + final int listViewWeight = resources.getInteger(R.integer.contact_list_card_layout_weight); + if (listSpaceWeight > 0 && listViewWeight > 0) { + rootLayout.setBackgroundResource(0); + // Set the card view visible + View mCardView = rootLayout.findViewById(R.id.list_card); + if (mCardView == null) { + throw new RuntimeException( + "Your content must have a list card view who can be turned visible " + + "whenever it is necessary."); + } + mCardView.setVisibility(View.VISIBLE); + + // Add extra padding to the list view to make them appear in the center of the card. + // In order to avoid jumping, we skip drawing the next frame of the ListView. + ViewUtil.doOnPreDraw( + listView, + false, + new Runnable() { + @Override + public void run() { + // Use the rootLayout.getWidth() instead of listView.getWidth() since + // we sometimes hide the listView until we finish loading data. This would + // result in incorrect padding. + ContactListViewUtils.addPaddingToView( + listView, rootLayout.getWidth(), listSpaceWeight, listViewWeight); + } + }); + } + } +} diff --git a/java/com/android/contacts/common/util/ContactLoaderUtils.java b/java/com/android/contacts/common/util/ContactLoaderUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..e30971721d828a09770c913dd3e088545afa1042 --- /dev/null +++ b/java/com/android/contacts/common/util/ContactLoaderUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.net.Uri; +import android.provider.Contacts; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; + +/** Utility methods for the {@link ContactLoader}. */ +public final class ContactLoaderUtils { + + /** Static helper, not instantiable. */ + private ContactLoaderUtils() {} + + /** + * Transforms the given Uri and returns a Lookup-Uri that represents the contact. For legacy + * contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException} can be thrown + * if the URI is null or the authority is not recognized. + * + *

Do not call from the UI thread. + */ + @SuppressWarnings("deprecation") + public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) + throws IllegalArgumentException { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } + + final String authority = uri.getAuthority(); + + // Current Style Uri? + if (ContactsContract.AUTHORITY.equals(authority)) { + final String type = resolver.getType(uri); + // Contact-Uri? Good, return it + if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) { + return uri; + } + + // RawContact-Uri? Transform it to ContactUri + if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) { + final long rawContactId = ContentUris.parseId(uri); + return RawContacts.getContactLookupUri( + resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); + } + + // Anything else? We don't know what this is + throw new IllegalArgumentException("uri format is unknown"); + } + + // Legacy Style? Convert to RawContact + final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY; + if (OBSOLETE_AUTHORITY.equals(authority)) { + // Legacy Format. Convert to RawContact-Uri and then lookup the contact + final long rawContactId = ContentUris.parseId(uri); + return RawContacts.getContactLookupUri( + resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); + } + + throw new IllegalArgumentException("uri authority is unknown"); + } +} diff --git a/java/com/android/contacts/common/util/DateUtils.java b/java/com/android/contacts/common/util/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1935d727a2d451bbe37bb6b123a235cb3f660cc0 --- /dev/null +++ b/java/com/android/contacts/common/util/DateUtils.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.Time; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +/** Utility methods for processing dates. */ +public class DateUtils { + + public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); + + /** + * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year. Let's + * add a one-off hack for that day of the year + */ + public static final String NO_YEAR_DATE_FEB29TH = "--02-29"; + + // Variations of ISO 8601 date format. Do not change the order - it does affect the + // result in ambiguous cases. + private static final SimpleDateFormat[] DATE_FORMATS = { + CommonDateUtils.FULL_DATE_FORMAT, + CommonDateUtils.DATE_AND_TIME_FORMAT, + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US), + }; + + static { + for (SimpleDateFormat format : DATE_FORMATS) { + format.setLenient(true); + format.setTimeZone(UTC_TIMEZONE); + } + CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE); + } + + /** + * Parses the supplied string to see if it looks like a date. + * + * @param string The string representation of the provided date + * @param mustContainYear If true, the string is parsed as a date containing a year. If false, the + * string is parsed into a valid date even if the year field is missing. + * @return A Calendar object corresponding to the date if the string is successfully parsed. If + * not, null is returned. + */ + public static Calendar parseDate(String string, boolean mustContainYear) { + ParsePosition parsePosition = new ParsePosition(0); + Date date; + if (!mustContainYear) { + final boolean noYearParsed; + // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately + if (NO_YEAR_DATE_FEB29TH.equals(string)) { + return getUtcDate(0, Calendar.FEBRUARY, 29); + } else { + synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) { + date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition); + } + noYearParsed = parsePosition.getIndex() == string.length(); + } + + if (noYearParsed) { + return getUtcDate(date, true); + } + } + for (int i = 0; i < DATE_FORMATS.length; i++) { + SimpleDateFormat f = DATE_FORMATS[i]; + synchronized (f) { + parsePosition.setIndex(0); + date = f.parse(string, parsePosition); + if (parsePosition.getIndex() == string.length()) { + return getUtcDate(date, false); + } + } + } + return null; + } + + private static final Calendar getUtcDate(Date date, boolean noYear) { + final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US); + calendar.setTime(date); + if (noYear) { + calendar.set(Calendar.YEAR, 0); + } + return calendar; + } + + private static final Calendar getUtcDate(int year, int month, int dayOfMonth) { + final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US); + calendar.clear(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); + return calendar; + } + + public static boolean isYearSet(Calendar cal) { + // use the Calendar.YEAR field to track whether or not the year is set instead of + // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become + // true irregardless of what the previous value was + return cal.get(Calendar.YEAR) > 1; + } + + /** + * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with longForm + * set to {@code true} by default. + * + * @param context Valid context + * @param string String representation of a date to parse + * @return Returns the same date in a cleaned up format. If the supplied string does not look like + * a date, return it unchanged. + */ + public static String formatDate(Context context, String string) { + return formatDate(context, string, true); + } + + /** + * Parses the supplied string to see if it looks like a date. + * + * @param context Valid context + * @param string String representation of a date to parse + * @param longForm If true, return the date formatted into its long string representation. If + * false, return the date formatted using its short form representation (i.e. 12/11/2012) + * @return Returns the same date in a cleaned up format. If the supplied string does not look like + * a date, return it unchanged. + */ + public static String formatDate(Context context, String string, boolean longForm) { + if (string == null) { + return null; + } + + string = string.trim(); + if (string.length() == 0) { + return string; + } + final Calendar cal = parseDate(string, false); + + // we weren't able to parse the string successfully so just return it unchanged + if (cal == null) { + return string; + } + + final boolean isYearSet = isYearSet(cal); + final java.text.DateFormat outFormat; + if (!isYearSet) { + outFormat = getLocalizedDateFormatWithoutYear(context); + } else { + outFormat = + longForm ? DateFormat.getLongDateFormat(context) : DateFormat.getDateFormat(context); + } + synchronized (outFormat) { + outFormat.setTimeZone(UTC_TIMEZONE); + return outFormat.format(cal.getTime()); + } + } + + public static boolean isMonthBeforeDay(Context context) { + char[] dateFormatOrder = DateFormat.getDateFormatOrder(context); + for (int i = 0; i < dateFormatOrder.length; i++) { + if (dateFormatOrder[i] == 'd') { + return false; + } + if (dateFormatOrder[i] == 'M') { + return true; + } + } + return false; + } + + /** + * Returns a SimpleDateFormat object without the year fields by using a regular expression to + * eliminate the year in the string pattern. In the rare occurence that the resulting pattern + * cannot be reconverted into a SimpleDateFormat, it uses the provided context to determine + * whether the month field should be displayed before the day field, and returns either "MMMM dd" + * or "dd MMMM" converted into a SimpleDateFormat. + */ + public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) { + final String pattern = + ((SimpleDateFormat) SimpleDateFormat.getDateInstance(java.text.DateFormat.LONG)) + .toPattern(); + // Determine the correct regex pattern for year. + // Special case handling for Spanish locale by checking for "de" + final String yearPattern = + pattern.contains("de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*"; + try { + // Eliminate the substring in pattern that matches the format for that of year + return new SimpleDateFormat(pattern.replaceAll(yearPattern, "")); + } catch (IllegalArgumentException e) { + return new SimpleDateFormat(DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM"); + } + } + + /** + * Given a calendar (possibly containing only a day of the year), returns the earliest possible + * anniversary of the date that is equal to or after the current point in time if the date does + * not contain a year, or the date converted to the local time zone (if the date contains a year. + * + * @param target The date we wish to convert(in the UTC time zone). + * @return If date does not contain a year (year < 1900), returns the next earliest anniversary + * that is after the current point in time (in the local time zone). Otherwise, returns the + * adjusted Date in the local time zone. + */ + public static Date getNextAnnualDate(Calendar target) { + final Calendar today = Calendar.getInstance(); + today.setTime(new Date()); + + // Round the current time to the exact start of today so that when we compare + // today against the target date, both dates are set to exactly 0000H. + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + + final boolean isYearSet = isYearSet(target); + final int targetYear = target.get(Calendar.YEAR); + final int targetMonth = target.get(Calendar.MONTH); + final int targetDay = target.get(Calendar.DAY_OF_MONTH); + final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29); + final GregorianCalendar anniversary = new GregorianCalendar(); + // Convert from the UTC date to the local date. Set the year to today's year if the + // there is no provided year (targetYear < 1900) + anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear, targetMonth, targetDay); + // If the anniversary's date is before the start of today and there is no year set, + // increment the year by 1 so that the returned date is always equal to or greater than + // today. If the day is a leap year, keep going until we get the next leap year anniversary + // Otherwise if there is already a year set, simply return the exact date. + if (!isYearSet) { + int anniversaryYear = today.get(Calendar.YEAR); + if (anniversary.before(today) || (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) { + // If the target date is not Feb 29, then set the anniversary to the next year. + // Otherwise, keep going until we find the next leap year (this is not guaranteed + // to be in 4 years time). + do { + anniversaryYear += 1; + } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear)); + anniversary.set(anniversaryYear, targetMonth, targetDay); + } + } + return anniversary.getTime(); + } + + /** + * Determine the difference, in days between two dates. Uses similar logic as the {@link + * android.text.format.DateUtils.getRelativeTimeSpanString} method. + * + * @param time Instance of time object to use for calculations. + * @param date1 First date to check. + * @param date2 Second date to check. + * @return The absolute difference in days between the two dates. + */ + public static int getDayDifference(Time time, long date1, long date2) { + time.set(date1); + int startDay = Time.getJulianDay(date1, time.gmtoff); + + time.set(date2); + int currentDay = Time.getJulianDay(date2, time.gmtoff); + + return Math.abs(currentDay - startDay); + } +} diff --git a/java/com/android/contacts/common/util/FabUtil.java b/java/com/android/contacts/common/util/FabUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..b1bb2e653a91aa707a7119bd91a4b8e0bac4799a --- /dev/null +++ b/java/com/android/contacts/common/util/FabUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.graphics.Outline; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.ListView; +import com.android.contacts.common.R; +import com.android.dialer.compat.CompatUtils; + +/** Provides static functions to work with views */ +public class FabUtil { + + private static final ViewOutlineProvider OVAL_OUTLINE_PROVIDER = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()); + } + }; + + private FabUtil() {} + + /** + * Configures the floating action button, clipping it to a circle and setting its translation z + * + * @param fabView the float action button's view + * @param res the resources file + */ + public static void setupFloatingActionButton(View fabView, Resources res) { + if (CompatUtils.isLollipopCompatible()) { + fabView.setOutlineProvider(OVAL_OUTLINE_PROVIDER); + fabView.setTranslationZ( + res.getDimensionPixelSize(R.dimen.floating_action_button_translation_z)); + } + } + + /** + * Adds padding to the bottom of the given {@link ListView} so that the floating action button + * does not obscure any content. + * + * @param listView to add the padding to + * @param res valid resources object + */ + public static void addBottomPaddingToListViewForFab(ListView listView, Resources res) { + final int fabPadding = + res.getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding); + listView.setPaddingRelative( + listView.getPaddingStart(), + listView.getPaddingTop(), + listView.getPaddingEnd(), + listView.getPaddingBottom() + fabPadding); + listView.setClipToPadding(false); + } +} diff --git a/java/com/android/contacts/common/util/MaterialColorMapUtils.java b/java/com/android/contacts/common/util/MaterialColorMapUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..a2d9847eccaf74a158fd656d83acac13c07b0031 --- /dev/null +++ b/java/com/android/contacts/common/util/MaterialColorMapUtils.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Trace; +import com.android.contacts.common.R; + +public class MaterialColorMapUtils { + + private final TypedArray sPrimaryColors; + private final TypedArray sSecondaryColors; + + public MaterialColorMapUtils(Resources resources) { + sPrimaryColors = + resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors); + sSecondaryColors = + resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors_dark); + } + + public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) { + final int primaryColor = resources.getColor(R.color.quickcontact_default_photo_tint_color); + final int secondaryColor = + resources.getColor(R.color.quickcontact_default_photo_tint_color_dark); + return new MaterialPalette(primaryColor, secondaryColor); + } + + /** + * Returns the hue component of a color int. + * + * @return A value between 0.0f and 1.0f + */ + public static float hue(int color) { + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + int V = Math.max(b, Math.max(r, g)); + int temp = Math.min(b, Math.min(r, g)); + + float H; + + if (V == temp) { + H = 0; + } else { + final float vtemp = V - temp; + final float cr = (V - r) / vtemp; + final float cg = (V - g) / vtemp; + final float cb = (V - b) / vtemp; + + if (r == V) { + H = cb - cg; + } else if (g == V) { + H = 2 + cr - cb; + } else { + H = 4 + cg - cr; + } + + H /= 6.f; + if (H < 0) { + H++; + } + } + + return H; + } + + /** + * Return primary and secondary colors from the Material color palette that are similar to {@param + * color}. + */ + public MaterialPalette calculatePrimaryAndSecondaryColor(int color) { + Trace.beginSection("calculatePrimaryAndSecondaryColor"); + + final float colorHue = hue(color); + float minimumDistance = Float.MAX_VALUE; + int indexBestMatch = 0; + for (int i = 0; i < sPrimaryColors.length(); i++) { + final int primaryColor = sPrimaryColors.getColor(i, 0); + final float comparedHue = hue(primaryColor); + // No need to be perceptually accurate when calculating color distances since + // we are only mapping to 15 colors. Being slightly inaccurate isn't going to change + // the mapping very often. + final float distance = Math.abs(comparedHue - colorHue); + if (distance < minimumDistance) { + minimumDistance = distance; + indexBestMatch = i; + } + } + + Trace.endSection(); + return new MaterialPalette( + sPrimaryColors.getColor(indexBestMatch, 0), sSecondaryColors.getColor(indexBestMatch, 0)); + } + + public static class MaterialPalette implements Parcelable { + + public static final Creator CREATOR = + new Creator() { + @Override + public MaterialPalette createFromParcel(Parcel in) { + return new MaterialPalette(in); + } + + @Override + public MaterialPalette[] newArray(int size) { + return new MaterialPalette[size]; + } + }; + public final int mPrimaryColor; + public final int mSecondaryColor; + + public MaterialPalette(int primaryColor, int secondaryColor) { + mPrimaryColor = primaryColor; + mSecondaryColor = secondaryColor; + } + + private MaterialPalette(Parcel in) { + mPrimaryColor = in.readInt(); + mSecondaryColor = in.readInt(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MaterialPalette other = (MaterialPalette) obj; + if (mPrimaryColor != other.mPrimaryColor) { + return false; + } + if (mSecondaryColor != other.mSecondaryColor) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mPrimaryColor; + result = prime * result + mSecondaryColor; + return result; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPrimaryColor); + dest.writeInt(mSecondaryColor); + } + } +} diff --git a/java/com/android/contacts/common/util/NameConverter.java b/java/com/android/contacts/common/util/NameConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..ae3275d14f2c4fe2dc3f9cb40b8097bc21e88dd9 --- /dev/null +++ b/java/com/android/contacts/common/util/NameConverter.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.text.TextUtils; +import com.android.contacts.common.model.dataitem.StructuredNameDataItem; +import java.util.Map; +import java.util.TreeMap; + +/** + * Utility class for converting between a display name and structured name (and vice-versa), via + * calls to the contact provider. + */ +public class NameConverter { + + /** The array of fields that comprise a structured name. */ + public static final String[] STRUCTURED_NAME_FIELDS = + new String[] { + StructuredName.PREFIX, + StructuredName.GIVEN_NAME, + StructuredName.MIDDLE_NAME, + StructuredName.FAMILY_NAME, + StructuredName.SUFFIX + }; + + /** + * Converts the given structured name (provided as a map from {@link StructuredName} fields to + * corresponding values) into a display name string. + * + *

Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. See ContactsProvider2.completeName() + * for the underlying method call. + * + * @param context Activity context. + * @param structuredName The structured name map to convert. + * @return The display name computed from the structured name map. + */ + public static String structuredNameToDisplayName( + Context context, Map structuredName) { + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + for (String key : STRUCTURED_NAME_FIELDS) { + if (structuredName.containsKey(key)) { + appendQueryParameter(builder, key, structuredName.get(key)); + } + } + return fetchDisplayName(context, builder.build()); + } + + /** + * Converts the given structured name (provided as ContentValues) into a display name string. + * + * @param context Activity context. + * @param values The content values containing values comprising the structured name. + */ + public static String structuredNameToDisplayName(Context context, ContentValues values) { + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + for (String key : STRUCTURED_NAME_FIELDS) { + if (values.containsKey(key)) { + appendQueryParameter(builder, key, values.getAsString(key)); + } + } + return fetchDisplayName(context, builder.build()); + } + + /** Helper method for fetching the display name via the given URI. */ + private static String fetchDisplayName(Context context, Uri uri) { + String displayName = null; + Cursor cursor = + context + .getContentResolver() + .query( + uri, + new String[] { + StructuredName.DISPLAY_NAME, + }, + null, + null, + null); + + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + displayName = cursor.getString(0); + } + } finally { + cursor.close(); + } + } + return displayName; + } + + /** + * Converts the given display name string into a structured name (as a map from {@link + * StructuredName} fields to corresponding values). + * + *

Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. + * + * @param context Activity context. + * @param displayName The display name to convert. + * @return The structured name map computed from the display name. + */ + public static Map displayNameToStructuredName( + Context context, String displayName) { + Map structuredName = new TreeMap(); + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + + appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName); + Cursor cursor = + context + .getContentResolver() + .query(builder.build(), STRUCTURED_NAME_FIELDS, null, null, null); + + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) { + structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i)); + } + } + } finally { + cursor.close(); + } + } + return structuredName; + } + + /** + * Converts the given display name string into a structured name (inserting the structured values + * into a new or existing ContentValues object). + * + *

Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. + * + * @param context Activity context. + * @param displayName The display name to convert. + * @param contentValues The content values object to place the structured name values into. If + * null, a new one will be created and returned. + * @return The ContentValues object containing the structured name fields derived from the display + * name. + */ + public static ContentValues displayNameToStructuredName( + Context context, String displayName, ContentValues contentValues) { + if (contentValues == null) { + contentValues = new ContentValues(); + } + Map mapValues = displayNameToStructuredName(context, displayName); + for (String key : mapValues.keySet()) { + contentValues.put(key, mapValues.get(key)); + } + return contentValues; + } + + private static void appendQueryParameter(Builder builder, String field, String value) { + if (!TextUtils.isEmpty(value)) { + builder.appendQueryParameter(field, value); + } + } + + /** + * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues. Parsed + * data should be {@link StructuredName#PHONETIC_FAMILY_NAME}, {@link + * StructuredName#PHONETIC_MIDDLE_NAME}, and {@link StructuredName#PHONETIC_GIVEN_NAME}. If this + * method cannot parse given phoneticName, null values will be stored. + * + * @param phoneticName Phonetic name to be parsed + * @param values ContentValues to be used for storing data. If null, new instance will be created. + * @return ContentValues with parsed data. Those data can be null. + */ + public static StructuredNameDataItem parsePhoneticName( + String phoneticName, StructuredNameDataItem item) { + String family = null; + String middle = null; + String given = null; + + if (!TextUtils.isEmpty(phoneticName)) { + String[] strings = phoneticName.split(" ", 3); + switch (strings.length) { + case 1: + family = strings[0]; + break; + case 2: + family = strings[0]; + given = strings[1]; + break; + case 3: + family = strings[0]; + middle = strings[1]; + given = strings[2]; + break; + } + } + + if (item == null) { + item = new StructuredNameDataItem(); + } + item.setPhoneticFamilyName(family); + item.setPhoneticMiddleName(middle); + item.setPhoneticGivenName(given); + return item; + } + + /** Constructs and returns a phonetic full name from given parts. */ + public static String buildPhoneticName(String family, String middle, String given) { + if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle) || !TextUtils.isEmpty(given)) { + StringBuilder sb = new StringBuilder(); + if (!TextUtils.isEmpty(family)) { + sb.append(family.trim()).append(' '); + } + if (!TextUtils.isEmpty(middle)) { + sb.append(middle.trim()).append(' '); + } + if (!TextUtils.isEmpty(given)) { + sb.append(given.trim()).append(' '); + } + sb.setLength(sb.length() - 1); // Yank the last space + return sb.toString(); + } else { + return null; + } + } +} diff --git a/java/com/android/contacts/common/util/SearchUtil.java b/java/com/android/contacts/common/util/SearchUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..314d565b28abceb226543ff1d80a491ef855b08c --- /dev/null +++ b/java/com/android/contacts/common/util/SearchUtil.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.support.annotation.VisibleForTesting; + +/** Methods related to search. */ +public class SearchUtil { + + /** + * Given a string with lines delimited with '\n', finds the matching line to the given substring. + * + * @param contents The string to search. + * @param substring The substring to search for. + * @return A MatchedLine object containing the matching line and the startIndex of the substring + * match within that line. + */ + public static MatchedLine findMatchingLine(String contents, String substring) { + final MatchedLine matched = new MatchedLine(); + + // Snippet may contain multiple lines separated by "\n". + // Locate the lines of the content that contain the substring. + final int index = SearchUtil.contains(contents, substring); + if (index != -1) { + // Match found. Find the corresponding line. + int start = index - 1; + while (start > -1) { + if (contents.charAt(start) == '\n') { + break; + } + start--; + } + int end = index + 1; + while (end < contents.length()) { + if (contents.charAt(end) == '\n') { + break; + } + end++; + } + matched.line = contents.substring(start + 1, end); + matched.startIndex = index - (start + 1); + } + return matched; + } + + /** + * Similar to String.contains() with two main differences: + * + *

1) Only searches token prefixes. A token is defined as any combination of letters or + * numbers. + * + *

2) Returns the starting index where the substring is found. + * + * @param value The string to search. + * @param substring The substring to look for. + * @return The starting index where the substring is found. {@literal -1} if substring is not + * found in value. + */ + @VisibleForTesting + static int contains(String value, String substring) { + if (value.length() < substring.length()) { + return -1; + } + + // i18n support + // Generate the code points for the substring once. + // There will be a maximum of substring.length code points. But may be fewer. + // Since the array length is not an accurate size, we need to keep a separate variable. + final int[] substringCodePoints = new int[substring.length()]; + int substringLength = 0; // may not equal substring.length()!! + for (int i = 0; i < substring.length(); ) { + final int codePoint = Character.codePointAt(substring, i); + substringCodePoints[substringLength] = codePoint; + substringLength++; + i += Character.charCount(codePoint); + } + + for (int i = 0; i < value.length(); i = findNextTokenStart(value, i)) { + int numMatch = 0; + for (int j = i; j < value.length() && numMatch < substringLength; ++numMatch) { + int valueCp = Character.toLowerCase(value.codePointAt(j)); + int substringCp = substringCodePoints[numMatch]; + if (valueCp != substringCp) { + break; + } + j += Character.charCount(valueCp); + } + if (numMatch == substringLength) { + return i; + } + } + return -1; + } + + /** + * Find the start of the next token. A token is composed of letters and numbers. Any other + * character are considered delimiters. + * + * @param line The string to search for the next token. + * @param startIndex The index to start searching. 0 based indexing. + * @return The index for the start of the next token. line.length() if next token not found. + */ + @VisibleForTesting + static int findNextTokenStart(String line, int startIndex) { + int index = startIndex; + + // If already in token, eat remainder of token. + while (index <= line.length()) { + if (index == line.length()) { + // No more tokens. + return index; + } + final int codePoint = line.codePointAt(index); + if (!Character.isLetterOrDigit(codePoint)) { + break; + } + index += Character.charCount(codePoint); + } + + // Out of token, eat all consecutive delimiters. + while (index <= line.length()) { + if (index == line.length()) { + return index; + } + final int codePoint = line.codePointAt(index); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + index += Character.charCount(codePoint); + } + + return index; + } + + /** + * Anything other than letter and numbers are considered delimiters. Remove start and end + * delimiters since they are not relevant to search. + * + * @param query The query string to clean. + * @return The cleaned query. Empty string if all characters are cleaned out. + */ + public static String cleanStartAndEndOfSearchQuery(String query) { + int start = 0; + while (start < query.length()) { + int codePoint = query.codePointAt(start); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + start += Character.charCount(codePoint); + } + + if (start == query.length()) { + // All characters are delimiters. + return ""; + } + + int end = query.length() - 1; + while (end > -1) { + if (Character.isLowSurrogate(query.charAt(end))) { + // Assume valid i18n string. There should be a matching high surrogate before it. + end--; + } + int codePoint = query.codePointAt(end); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + end--; + } + + // end is a letter or digit. + return query.substring(start, end + 1); + } + + public static class MatchedLine { + + public int startIndex = -1; + public String line; + + @Override + public String toString() { + return "MatchedLine{" + "line='" + line + '\'' + ", startIndex=" + startIndex + '}'; + } + } +} diff --git a/java/com/android/contacts/common/util/StopWatch.java b/java/com/android/contacts/common/util/StopWatch.java new file mode 100644 index 0000000000000000000000000000000000000000..b944b98671668ba85979d2f85700ba3311f8dedf --- /dev/null +++ b/java/com/android/contacts/common/util/StopWatch.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.util.Log; +import java.util.ArrayList; + +/** A {@link StopWatch} records start, laps and stop, and print them to logcat. */ +public class StopWatch { + + private final String mLabel; + + private final ArrayList mTimes = new ArrayList<>(); + private final ArrayList mLapLabels = new ArrayList<>(); + + private StopWatch(String label) { + mLabel = label; + lap(""); + } + + /** Create a new instance and start it. */ + public static StopWatch start(String label) { + return new StopWatch(label); + } + + /** Return a dummy instance that does no operations. */ + public static StopWatch getNullStopWatch() { + return NullStopWatch.INSTANCE; + } + + /** Record a lap. */ + public void lap(String lapLabel) { + mTimes.add(System.currentTimeMillis()); + mLapLabels.add(lapLabel); + } + + /** Stop it and log the result, if the total time >= {@code timeThresholdToLog}. */ + public void stopAndLog(String TAG, int timeThresholdToLog) { + + lap(""); + + final long start = mTimes.get(0); + final long stop = mTimes.get(mTimes.size() - 1); + + final long total = stop - start; + if (total < timeThresholdToLog) { + return; + } + + final StringBuilder sb = new StringBuilder(); + sb.append(mLabel); + sb.append(","); + sb.append(total); + sb.append(": "); + + long last = start; + for (int i = 1; i < mTimes.size(); i++) { + final long current = mTimes.get(i); + sb.append(mLapLabels.get(i)); + sb.append(","); + sb.append((current - last)); + sb.append(" "); + last = current; + } + Log.v(TAG, sb.toString()); + } + + private static class NullStopWatch extends StopWatch { + + public static final NullStopWatch INSTANCE = new NullStopWatch(); + + public NullStopWatch() { + super(null); + } + + @Override + public void lap(String lapLabel) { + // noop + } + + @Override + public void stopAndLog(String TAG, int timeThresholdToLog) { + // noop + } + } +} diff --git a/java/com/android/contacts/common/util/TelephonyManagerUtils.java b/java/com/android/contacts/common/util/TelephonyManagerUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..b664268ca67977b9c6f7eca40ca14091b957e5e9 --- /dev/null +++ b/java/com/android/contacts/common/util/TelephonyManagerUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +import android.content.Context; +import android.telephony.TelephonyManager; + +/** This class provides several TelephonyManager util functions. */ +public class TelephonyManagerUtils { + + /** + * Gets the voicemail tag from Telephony Manager. + * + * @param context Current application context + * @return Voicemail tag, the alphabetic identifier associated with the voice mail number. + */ + public static String getVoiceMailAlphaTag(Context context) { + final TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final String voiceMailLabel = telephonyManager.getVoiceMailAlphaTag(); + return voiceMailLabel; + } + + /** + * @param context Current application context. + * @return True if there is a subscription which supports video calls. False otherwise. + */ + public static boolean hasVideoCallSubscription(Context context) { + // TODO: Check the telephony manager's subscriptions to see if any support video calls. + return true; + } +} diff --git a/java/com/android/contacts/common/util/TrafficStatsTags.java b/java/com/android/contacts/common/util/TrafficStatsTags.java new file mode 100644 index 0000000000000000000000000000000000000000..b0e7fb5832ef1a2a4de50ae6685846d1aac33a5f --- /dev/null +++ b/java/com/android/contacts/common/util/TrafficStatsTags.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +public class TrafficStatsTags { + + public static final int CONTACT_PHOTO_DOWNLOAD_TAG = 0x0001; + public static final int TAG_MAX = 0x9999; +} diff --git a/java/com/android/contacts/common/util/UriUtils.java b/java/com/android/contacts/common/util/UriUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..4690942ba237b71143c18227012a41fb92fb912b --- /dev/null +++ b/java/com/android/contacts/common/util/UriUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.net.Uri; +import android.provider.ContactsContract; +import java.util.List; + +/** Utility methods for dealing with URIs. */ +public class UriUtils { + + /** Static helper, not instantiable. */ + private UriUtils() {} + + /** Checks whether two URI are equal, taking care of the case where either is null. */ + public static boolean areEqual(Uri uri1, Uri uri2) { + if (uri1 == null && uri2 == null) { + return true; + } + if (uri1 == null || uri2 == null) { + return false; + } + return uri1.equals(uri2); + } + + /** Parses a string into a URI and returns null if the given string is null. */ + public static Uri parseUriOrNull(String uriString) { + if (uriString == null) { + return null; + } + return Uri.parse(uriString); + } + + /** Converts a URI into a string, returns null if the given URI is null. */ + public static String uriToString(Uri uri) { + return uri == null ? null : uri.toString(); + } + + public static boolean isEncodedContactUri(Uri uri) { + if (uri == null) { + return false; + } + final String lastPathSegment = uri.getLastPathSegment(); + if (lastPathSegment == null) { + return false; + } + return lastPathSegment.equals(Constants.LOOKUP_URI_ENCODED); + } + + /** + * @return {@code uri} as-is if the authority is of contacts provider. Otherwise or {@code uri} is + * null, return null otherwise + */ + public static Uri nullForNonContactsUri(Uri uri) { + if (uri == null) { + return null; + } + return ContactsContract.AUTHORITY.equals(uri.getAuthority()) ? uri : null; + } + + /** Parses the given URI to determine the original lookup key of the contact. */ + public static String getLookupKeyFromUri(Uri lookupUri) { + // Would be nice to be able to persist the lookup key somehow to avoid having to parse + // the uri entirely just to retrieve the lookup key, but every uri is already parsed + // once anyway to check if it is an encoded JSON uri, so this has negligible effect + // on performance. + if (lookupUri != null && !UriUtils.isEncodedContactUri(lookupUri)) { + final List segments = lookupUri.getPathSegments(); + // This returns the third path segment of the uri, where the lookup key is located. + // See {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + return (segments.size() < 3) ? null : Uri.encode(segments.get(2)); + } else { + return null; + } + } +} diff --git a/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java new file mode 100644 index 0000000000000000000000000000000000000000..2988a5a58d2ff725b6df5530a4354a3635a13869 --- /dev/null +++ b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; +import com.android.dialer.util.TouchPointManager; + +/** + * Linear layout for an activity that listens to all touch events on the screen and saves the touch + * point. Typically touch events are handled by child views--this class intercepts those touch + * events before passing them on to the child. + */ +public class ActivityTouchLinearLayout extends LinearLayout { + + public ActivityTouchLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return false; + } +} diff --git a/java/com/android/contacts/common/widget/FloatingActionButtonController.java b/java/com/android/contacts/common/widget/FloatingActionButtonController.java new file mode 100644 index 0000000000000000000000000000000000000000..f03129779265dda6dcb9bae32389a505c6892ca1 --- /dev/null +++ b/java/com/android/contacts/common/widget/FloatingActionButtonController.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.app.Activity; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.ImageButton; +import com.android.contacts.common.R; +import com.android.contacts.common.util.FabUtil; +import com.android.dialer.animation.AnimUtils; + +/** Controls the movement and appearance of the FAB (Floating Action Button). */ +public class FloatingActionButtonController { + + public static final int ALIGN_MIDDLE = 0; + public static final int ALIGN_QUARTER_END = 1; + public static final int ALIGN_END = 2; + + private static final int FAB_SCALE_IN_DURATION = 266; + private static final int FAB_SCALE_IN_FADE_IN_DELAY = 100; + private static final int FAB_ICON_FADE_OUT_DURATION = 66; + + private final int mAnimationDuration; + private final int mFloatingActionButtonWidth; + private final int mFloatingActionButtonMarginRight; + private final View mFloatingActionButtonContainer; + private final ImageButton mFloatingActionButton; + private final Interpolator mFabInterpolator; + private int mScreenWidth; + + public FloatingActionButtonController(Activity activity, View container, ImageButton button) { + Resources resources = activity.getResources(); + mFabInterpolator = + AnimationUtils.loadInterpolator(activity, android.R.interpolator.fast_out_slow_in); + mFloatingActionButtonWidth = + resources.getDimensionPixelSize(R.dimen.floating_action_button_width); + mFloatingActionButtonMarginRight = + resources.getDimensionPixelOffset(R.dimen.floating_action_button_margin_right); + mAnimationDuration = resources.getInteger(R.integer.floating_action_button_animation_duration); + mFloatingActionButtonContainer = container; + mFloatingActionButton = button; + FabUtil.setupFloatingActionButton(mFloatingActionButtonContainer, resources); + } + + /** + * Passes the screen width into the class. Necessary for translation calculations. Should be + * called as soon as parent View width is available. + * + * @param screenWidth The width of the screen in pixels. + */ + public void setScreenWidth(int screenWidth) { + mScreenWidth = screenWidth; + } + + public boolean isVisible() { + return mFloatingActionButtonContainer.getVisibility() == View.VISIBLE; + } + + /** + * Sets FAB as View.VISIBLE or View.GONE. + * + * @param visible Whether or not to make the container visible. + */ + public void setVisible(boolean visible) { + mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public void changeIcon(Drawable icon, String description) { + if (mFloatingActionButton.getDrawable() != icon + || !mFloatingActionButton.getContentDescription().equals(description)) { + mFloatingActionButton.setImageDrawable(icon); + mFloatingActionButton.setContentDescription(description); + } + } + + /** + * Updates the FAB location (middle to right position) as the PageView scrolls. + * + * @param positionOffset A fraction used to calculate position of the FAB during page scroll. + */ + public void onPageScrolled(float positionOffset) { + // As the page is scrolling, if we're on the first tab, update the FAB position so it + // moves along with it. + mFloatingActionButtonContainer.setTranslationX( + (int) (positionOffset * getTranslationXForAlignment(ALIGN_END))); + } + + /** + * Aligns the FAB to the described location + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @param animate Whether or not to animate the transition. + */ + public void align(int align, boolean animate) { + align(align, 0 /*offsetX */, 0 /* offsetY */, animate); + } + + /** + * Aligns the FAB to the described location plus specified additional offsets. + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @param offsetX Additional offsetX to translate by. + * @param offsetY Additional offsetY to translate by. + * @param animate Whether or not to animate the transition. + */ + public void align(int align, int offsetX, int offsetY, boolean animate) { + if (mScreenWidth == 0) { + return; + } + + int translationX = getTranslationXForAlignment(align); + + // Skip animation if container is not shown; animation causes container to show again. + if (animate && mFloatingActionButtonContainer.isShown()) { + mFloatingActionButtonContainer + .animate() + .translationX(translationX + offsetX) + .translationY(offsetY) + .setInterpolator(mFabInterpolator) + .setDuration(mAnimationDuration) + .start(); + } else { + mFloatingActionButtonContainer.setTranslationX(translationX + offsetX); + mFloatingActionButtonContainer.setTranslationY(offsetY); + } + } + + /** + * Resizes width and height of the floating action bar container. + * + * @param dimension The new dimensions for the width and height. + * @param animate Whether to animate this change. + */ + public void resize(int dimension, boolean animate) { + if (animate) { + AnimUtils.changeDimensions(mFloatingActionButtonContainer, dimension, dimension); + } else { + mFloatingActionButtonContainer.getLayoutParams().width = dimension; + mFloatingActionButtonContainer.getLayoutParams().height = dimension; + mFloatingActionButtonContainer.requestLayout(); + } + } + + /** + * Scales the floating action button from no height and width to its actual dimensions. This is an + * animation for showing the floating action button. + * + * @param delayMs The delay for the effect, in milliseconds. + */ + public void scaleIn(int delayMs) { + setVisible(true); + AnimUtils.scaleIn(mFloatingActionButtonContainer, FAB_SCALE_IN_DURATION, delayMs); + AnimUtils.fadeIn( + mFloatingActionButton, FAB_SCALE_IN_DURATION, delayMs + FAB_SCALE_IN_FADE_IN_DELAY, null); + } + + /** Immediately remove the affects of the last call to {@link #scaleOut}. */ + public void resetIn() { + mFloatingActionButton.setAlpha(1f); + mFloatingActionButton.setVisibility(View.VISIBLE); + mFloatingActionButtonContainer.setScaleX(1); + mFloatingActionButtonContainer.setScaleY(1); + } + + /** + * Scales the floating action button from its actual dimensions to no height and width. This is an + * animation for hiding the floating action button. + */ + public void scaleOut() { + AnimUtils.scaleOut(mFloatingActionButtonContainer, mAnimationDuration); + // Fade out the icon faster than the scale out animation, so that the icon scaling is less + // obvious. We don't want it to scale, but the resizing the container is not as performant. + AnimUtils.fadeOut(mFloatingActionButton, FAB_ICON_FADE_OUT_DURATION, null); + } + + /** + * Calculates the X offset of the FAB to the given alignment, adjusted for whether or not the view + * is in RTL mode. + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @return The translationX for the given alignment. + */ + public int getTranslationXForAlignment(int align) { + int result = 0; + switch (align) { + case ALIGN_MIDDLE: + // Moves the FAB to exactly center screen. + return 0; + case ALIGN_QUARTER_END: + // Moves the FAB a quarter of the screen width. + result = mScreenWidth / 4; + break; + case ALIGN_END: + // Moves the FAB half the screen width. Same as aligning right with a marginRight. + result = + mScreenWidth / 2 - mFloatingActionButtonWidth / 2 - mFloatingActionButtonMarginRight; + break; + } + if (isLayoutRtl()) { + result *= -1; + } + return result; + } + + private boolean isLayoutRtl() { + return mFloatingActionButtonContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } +} diff --git a/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java new file mode 100644 index 0000000000000000000000000000000000000000..d84d8f75736b239d657fe614ed43f51767d6e831 --- /dev/null +++ b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * Custom {@link ImageView} that improves layouting performance. + * + *

This improves the performance by not passing requestLayout() to its parent, taking advantage + * of knowing that image size won't change once set. + */ +public class LayoutSuppressingImageView extends ImageView { + + public LayoutSuppressingImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void requestLayout() { + forceLayout(); + } +} diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..63f8ca5803c5b05e862fa04be845214e73cc04fa --- /dev/null +++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.widget; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.TextView; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneAccountCompat; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import java.util.ArrayList; +import java.util.List; + +/** + * Dialog that allows the user to select a phone accounts for a given action. Optionally provides + * the choice to set the phone account as default. + */ +public class SelectPhoneAccountDialogFragment extends DialogFragment { + + private static final String ARG_TITLE_RES_ID = "title_res_id"; + private static final String ARG_CAN_SET_DEFAULT = "can_set_default"; + private static final String ARG_ACCOUNT_HANDLES = "account_handles"; + private static final String ARG_IS_DEFAULT_CHECKED = "is_default_checked"; + private static final String ARG_LISTENER = "listener"; + private static final String ARG_CALL_ID = "call_id"; + + private int mTitleResId; + private boolean mCanSetDefault; + private List mAccountHandles; + private boolean mIsSelected; + private boolean mIsDefaultChecked; + private SelectPhoneAccountListener mListener; + + public SelectPhoneAccountDialogFragment() {} + + /** + * Create new fragment instance with default title and no option to set as default. + * + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static SelectPhoneAccountDialogFragment newInstance( + List accountHandles, + SelectPhoneAccountListener listener, + @Nullable String callId) { + return newInstance( + R.string.select_account_dialog_title, false, accountHandles, listener, callId); + } + + /** + * Create new fragment instance. This method also allows specifying a custom title and "set + * default" checkbox. + * + * @param titleResId The resource ID for the string to use in the title of the dialog. + * @param canSetDefault {@code true} if the dialog should include an option to set the selection + * as the default. False otherwise. + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static SelectPhoneAccountDialogFragment newInstance( + int titleResId, + boolean canSetDefault, + List accountHandles, + SelectPhoneAccountListener listener, + @Nullable String callId) { + ArrayList accountHandlesCopy = new ArrayList<>(); + if (accountHandles != null) { + accountHandlesCopy.addAll(accountHandles); + } + SelectPhoneAccountDialogFragment fragment = new SelectPhoneAccountDialogFragment(); + final Bundle args = new Bundle(); + args.putInt(ARG_TITLE_RES_ID, titleResId); + args.putBoolean(ARG_CAN_SET_DEFAULT, canSetDefault); + args.putParcelableArrayList(ARG_ACCOUNT_HANDLES, accountHandlesCopy); + args.putParcelable(ARG_LISTENER, listener); + args.putString(ARG_CALL_ID, callId); + fragment.setArguments(args); + fragment.setListener(listener); + return fragment; + } + + public void setListener(SelectPhoneAccountListener listener) { + mListener = listener; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(ARG_IS_DEFAULT_CHECKED, mIsDefaultChecked); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID); + mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT); + mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES); + mListener = getArguments().getParcelable(ARG_LISTENER); + if (savedInstanceState != null) { + mIsDefaultChecked = savedInstanceState.getBoolean(ARG_IS_DEFAULT_CHECKED); + } + mIsSelected = false; + + final DialogInterface.OnClickListener selectionListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mIsSelected = true; + PhoneAccountHandle selectedAccountHandle = mAccountHandles.get(which); + Bundle result = new Bundle(); + result.putParcelable( + SelectPhoneAccountListener.EXTRA_SELECTED_ACCOUNT_HANDLE, selectedAccountHandle); + result.putBoolean(SelectPhoneAccountListener.EXTRA_SET_DEFAULT, mIsDefaultChecked); + result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId()); + if (mListener != null) { + mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_SELECTED, result); + } + } + }; + + final CompoundButton.OnCheckedChangeListener checkListener = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton check, boolean isChecked) { + mIsDefaultChecked = isChecked; + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + ListAdapter selectAccountListAdapter = + new SelectAccountListAdapter( + builder.getContext(), R.layout.select_account_list_item, mAccountHandles); + + AlertDialog dialog = + builder + .setTitle(mTitleResId) + .setAdapter(selectAccountListAdapter, selectionListener) + .create(); + + if (mCanSetDefault) { + // Generate custom checkbox view, lint suppressed since no appropriate parent (is dialog) + @SuppressLint("InflateParams") + LinearLayout checkboxLayout = + (LinearLayout) + LayoutInflater.from(builder.getContext()) + .inflate(R.layout.default_account_checkbox, null); + + CheckBox cb = (CheckBox) checkboxLayout.findViewById(R.id.default_account_checkbox_view); + cb.setOnCheckedChangeListener(checkListener); + cb.setChecked(mIsDefaultChecked); + + dialog.getListView().addFooterView(checkboxLayout); + } + + return dialog; + } + + @Override + public void onStop() { + if (!mIsSelected && mListener != null) { + Bundle result = new Bundle(); + result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId()); + mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, result); + } + super.onStop(); + } + + @Nullable + private String getCallId() { + return getArguments().getString(ARG_CALL_ID); + } + + public static class SelectPhoneAccountListener extends ResultReceiver { + + static final int RESULT_SELECTED = 1; + static final int RESULT_DISMISSED = 2; + + static final String EXTRA_SELECTED_ACCOUNT_HANDLE = "extra_selected_account_handle"; + static final String EXTRA_SET_DEFAULT = "extra_set_default"; + static final String EXTRA_CALL_ID = "extra_call_id"; + + public SelectPhoneAccountListener() { + super(new Handler()); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == RESULT_SELECTED) { + onPhoneAccountSelected( + resultData.getParcelable(EXTRA_SELECTED_ACCOUNT_HANDLE), + resultData.getBoolean(EXTRA_SET_DEFAULT), + resultData.getString(EXTRA_CALL_ID)); + } else if (resultCode == RESULT_DISMISSED) { + onDialogDismissed(resultData.getString(EXTRA_CALL_ID)); + } + } + + public void onPhoneAccountSelected( + PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {} + + public void onDialogDismissed(@Nullable String callId) {} + } + + private static class SelectAccountListAdapter extends ArrayAdapter { + + private int mResId; + + public SelectAccountListAdapter( + Context context, int resource, List accountHandles) { + super(context, resource, accountHandles); + mResId = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + View rowView; + final ViewHolder holder; + + if (convertView == null) { + // Cache views for faster scrolling + rowView = inflater.inflate(mResId, null); + holder = new ViewHolder(); + holder.labelTextView = (TextView) rowView.findViewById(R.id.label); + holder.numberTextView = (TextView) rowView.findViewById(R.id.number); + holder.imageView = (ImageView) rowView.findViewById(R.id.icon); + rowView.setTag(holder); + } else { + rowView = convertView; + holder = (ViewHolder) rowView.getTag(); + } + + PhoneAccountHandle accountHandle = getItem(position); + PhoneAccount account = + getContext().getSystemService(TelecomManager.class).getPhoneAccount(accountHandle); + if (account == null) { + return rowView; + } + holder.labelTextView.setText(account.getLabel()); + if (account.getAddress() == null + || TextUtils.isEmpty(account.getAddress().getSchemeSpecificPart())) { + holder.numberTextView.setVisibility(View.GONE); + } else { + holder.numberTextView.setVisibility(View.VISIBLE); + holder.numberTextView.setText( + PhoneNumberUtilsCompat.createTtsSpannable( + account.getAddress().getSchemeSpecificPart())); + } + holder.imageView.setImageDrawable( + PhoneAccountCompat.createIconDrawable(account, getContext())); + return rowView; + } + + private static final class ViewHolder { + + TextView labelTextView; + TextView numberTextView; + ImageView imageView; + } + } +} diff --git a/java/com/android/dialer/animation/AnimUtils.java b/java/com/android/dialer/animation/AnimUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..9c9396e566ecad49835bc9392d602eda1b5ec12e --- /dev/null +++ b/java/com/android/dialer/animation/AnimUtils.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; +import com.android.dialer.compat.PathInterpolatorCompat; + +public class AnimUtils { + + public static final int DEFAULT_DURATION = -1; + public static final int NO_DELAY = 0; + + public static final Interpolator EASE_IN = PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f); + public static final Interpolator EASE_OUT = PathInterpolatorCompat.create(0.4f, 0.0f, 1.0f, 1.0f); + public static final Interpolator EASE_OUT_EASE_IN = + PathInterpolatorCompat.create(0.4f, 0, 0.2f, 1); + + public static void crossFadeViews(View fadeIn, View fadeOut, int duration) { + fadeIn(fadeIn, duration); + fadeOut(fadeOut, duration); + } + + public static void fadeOut(View fadeOut, int duration) { + fadeOut(fadeOut, duration, null); + } + + public static void fadeOut(final View fadeOut, int durationMs, final AnimationCallback callback) { + fadeOut.setAlpha(1); + final ViewPropertyAnimator animator = fadeOut.animate(); + animator.cancel(); + animator + .alpha(0) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fadeOut.setVisibility(View.GONE); + if (callback != null) { + callback.onAnimationEnd(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeOut.setVisibility(View.GONE); + fadeOut.setAlpha(0); + if (callback != null) { + callback.onAnimationCancel(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + public static void fadeIn(View fadeIn, int durationMs) { + fadeIn(fadeIn, durationMs, NO_DELAY, null); + } + + public static void fadeIn( + final View fadeIn, int durationMs, int delay, final AnimationCallback callback) { + fadeIn.setAlpha(0); + final ViewPropertyAnimator animator = fadeIn.animate(); + animator.cancel(); + + animator.setStartDelay(delay); + animator + .alpha(1) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + fadeIn.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeIn.setAlpha(1); + if (callback != null) { + callback.onAnimationCancel(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (callback != null) { + callback.onAnimationEnd(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + /** + * Scales in the view from scale of 0 to actual dimensions. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + * @param startDelayMs The delay to applying the scaling in milliseconds. + */ + public static void scaleIn(final View view, int durationMs, int startDelayMs) { + AnimatorListenerAdapter listener = + (new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + view.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setScaleX(1); + view.setScaleY(1); + } + }); + scaleInternal( + view, + 0 /* startScaleValue */, + 1 /* endScaleValue */, + durationMs, + startDelayMs, + listener, + EASE_IN); + } + + /** + * Scales out the view from actual dimensions to 0. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + */ + public static void scaleOut(final View view, int durationMs) { + AnimatorListenerAdapter listener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setVisibility(View.GONE); + view.setScaleX(0); + view.setScaleY(0); + } + }; + + scaleInternal( + view, + 1 /* startScaleValue */, + 0 /* endScaleValue */, + durationMs, + NO_DELAY, + listener, + EASE_OUT); + } + + private static void scaleInternal( + final View view, + int startScaleValue, + int endScaleValue, + int durationMs, + int startDelay, + AnimatorListenerAdapter listener, + Interpolator interpolator) { + view.setScaleX(startScaleValue); + view.setScaleY(startScaleValue); + + final ViewPropertyAnimator animator = view.animate(); + animator.cancel(); + + animator + .setInterpolator(interpolator) + .scaleX(endScaleValue) + .scaleY(endScaleValue) + .setListener(listener) + .withLayer(); + + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.setStartDelay(startDelay); + + animator.start(); + } + + /** + * Animates a view to the new specified dimensions. + * + * @param view The view to change the dimensions of. + * @param newWidth The new width of the view. + * @param newHeight The new height of the view. + */ + public static void changeDimensions(final View view, final int newWidth, final int newHeight) { + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + + final int oldWidth = view.getWidth(); + final int oldHeight = view.getHeight(); + final int deltaWidth = newWidth - oldWidth; + final int deltaHeight = newHeight - oldHeight; + + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + Float value = (Float) animator.getAnimatedValue(); + + view.getLayoutParams().width = (int) (value * deltaWidth + oldWidth); + view.getLayoutParams().height = (int) (value * deltaHeight + oldHeight); + view.requestLayout(); + } + }); + animator.start(); + } + + public static class AnimationCallback { + + public void onAnimationEnd() {} + + public void onAnimationCancel() {} + } +} diff --git a/java/com/android/dialer/animation/AnimationListenerAdapter.java b/java/com/android/dialer/animation/AnimationListenerAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..3f847f2b6aaf2c90117136d55d46eb0b07aaec30 --- /dev/null +++ b/java/com/android/dialer/animation/AnimationListenerAdapter.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.animation; + +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; + +/** + * Provides empty implementations of the methods in {@link AnimationListener} for convenience + * reasons. + */ +public class AnimationListenerAdapter implements AnimationListener { + + /** {@inheritDoc} */ + @Override + public void onAnimationStart(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationEnd(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationRepeat(Animation animation) {} +} diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..80f294acc0c44186b3d41501e54a131eac309d5c --- /dev/null +++ b/java/com/android/dialer/app/AndroidManifest.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/Bindings.java b/java/com/android/dialer/app/Bindings.java new file mode 100644 index 0000000000000000000000000000000000000000..2beb4018418df777666fa8e9cdc0a4f2adccb61c --- /dev/null +++ b/java/com/android/dialer/app/Bindings.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app; + +import android.content.Context; +import com.android.dialer.app.bindings.DialerBindings; +import com.android.dialer.app.bindings.DialerBindingsFactory; +import com.android.dialer.app.bindings.DialerBindingsStub; +import com.android.dialer.app.legacybindings.DialerLegacyBindings; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsFactory; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsStub; +import java.util.Objects; + +/** Accessor for the in call UI bindings. */ +public class Bindings { + + private static DialerBindings instance; + private static DialerLegacyBindings legacyInstance; + + private Bindings() {} + + public static DialerBindings get(Context context) { + Objects.requireNonNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerBindingsFactory) { + instance = ((DialerBindingsFactory) application).newDialerBindings(); + } + + if (instance == null) { + instance = new DialerBindingsStub(); + } + return instance; + } + + public static DialerLegacyBindings getLegacy(Context context) { + Objects.requireNonNull(context); + if (legacyInstance != null) { + return legacyInstance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerLegacyBindingsFactory) { + legacyInstance = ((DialerLegacyBindingsFactory) application).newDialerLegacyBindings(); + } + + if (legacyInstance == null) { + legacyInstance = new DialerLegacyBindingsStub(); + } + return legacyInstance; + } + + public static void setForTesting(DialerBindings testInstance) { + instance = testInstance; + } + + public static void setLegacyBindingForTesting(DialerLegacyBindings testLegacyInstance) { + legacyInstance = testLegacyInstance; + } +} diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..cda2b2e2c15e808d1bb071ac64678146ae0eaf44 --- /dev/null +++ b/java/com/android/dialer/app/CallDetailActivity.java @@ -0,0 +1,480 @@ +/* + * 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.dialer.app; + +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.app.AppCompatActivity; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ListView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.ClipboardUtils; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.calllog.CallDetailHistoryAdapter; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener; +import com.android.dialer.app.calllog.CallTypeHelper; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.Assert; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.spam.Spam; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.TouchPointManager; + +/** + * Displays the details of a specific call log entry. + * + *

This activity can be either started with the URI of a single call log entry, or with the + * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries. + */ +@UsedByReflection(value = "AndroidManifest-app.xml") +public class CallDetailActivity extends AppCompatActivity + implements MenuItem.OnMenuItemClickListener, View.OnClickListener { + + /** A long array extra containing ids of call log entries to display. */ + public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS"; + /** If we are started with a voicemail, we'll find the uri to play with this extra. */ + public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI"; + /** If the activity was triggered from a notification. */ + public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION"; + + public static final String BLOCKED_OR_SPAM_QUERY_IDENTIFIER = "blockedOrSpamIdentifier"; + + private final AsyncTaskExecutor executor = AsyncTaskExecutors.createAsyncTaskExecutor(); + protected String mNumber; + private Context mContext; + private ContactInfoHelper mContactInfoHelper; + private ContactsPreferences mContactsPreferences; + private CallTypeHelper mCallTypeHelper; + private ContactPhotoManager mContactPhotoManager; + private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); + private LayoutInflater mInflater; + private Resources mResources; + private PhoneCallDetails mDetails; + private Uri mVoicemailUri; + private String mPostDialDigits = ""; + private ListView mHistoryList; + private QuickContactBadge mQuickContactBadge; + private TextView mCallerName; + private TextView mCallerNumber; + private TextView mAccountLabel; + private View mCallButton; + private View mEditBeforeCallActionItem; + private View mReportActionItem; + private View mCopyNumberActionItem; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + private CallLogAsyncTaskListener mCallLogAsyncTaskListener = + new CallLogAsyncTaskListener() { + @Override + public void onDeleteCall() { + finish(); + } + + @Override + public void onDeleteVoicemail() { + finish(); + } + + @Override + public void onGetCallDetails(final PhoneCallDetails[] details) { + if (details == null) { + // Somewhere went wrong: we're going to bail out and show error to users. + Toast.makeText(mContext, R.string.toast_call_detail_error, Toast.LENGTH_SHORT).show(); + finish(); + return; + } + + // All calls are from the same number and same contact, so pick the first detail. + mDetails = details[0]; + mNumber = TextUtils.isEmpty(mDetails.number) ? null : mDetails.number.toString(); + + if (mNumber == null) { + updateDataAndRender(details); + return; + } + + executor.submit( + BLOCKED_OR_SPAM_QUERY_IDENTIFIER, + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + mDetails.isBlocked = + mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly( + mNumber, mDetails.countryIso) + != null; + if (Spam.get(mContext).isSpamEnabled()) { + mDetails.isSpam = + hasIncomingCalls(details) + && Spam.get(mContext) + .checkSpamStatusSynchronous(mNumber, mDetails.countryIso); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + updateDataAndRender(details); + } + }); + } + + private void updateDataAndRender(PhoneCallDetails[] details) { + mPostDialDigits = + TextUtils.isEmpty(mDetails.postDialDigits) ? "" : mDetails.postDialDigits; + + final CharSequence callLocationOrType = getNumberTypeOrLocation(mDetails); + + final CharSequence displayNumber; + if (!TextUtils.isEmpty(mDetails.postDialDigits)) { + displayNumber = mDetails.number + mDetails.postDialDigits; + } else { + displayNumber = mDetails.displayNumber; + } + + final String displayNumberStr = + mBidiFormatter.unicodeWrap(displayNumber.toString(), TextDirectionHeuristics.LTR); + + mDetails.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); + + if (!TextUtils.isEmpty(mDetails.getPreferredName())) { + mCallerName.setText(mDetails.getPreferredName()); + mCallerNumber.setText(callLocationOrType + " " + displayNumberStr); + } else { + mCallerName.setText(displayNumberStr); + if (!TextUtils.isEmpty(callLocationOrType)) { + mCallerNumber.setText(callLocationOrType); + mCallerNumber.setVisibility(View.VISIBLE); + } else { + mCallerNumber.setVisibility(View.GONE); + } + } + + CharSequence accountLabel = + PhoneAccountUtils.getAccountLabel(mContext, mDetails.accountHandle); + CharSequence accountContentDescription = + PhoneCallDetails.createAccountLabelDescription( + mResources, mDetails.viaNumber, accountLabel); + if (!TextUtils.isEmpty(mDetails.viaNumber)) { + if (!TextUtils.isEmpty(accountLabel)) { + accountLabel = + mResources.getString( + R.string.call_log_via_number_phone_account, accountLabel, mDetails.viaNumber); + } else { + accountLabel = mResources.getString(R.string.call_log_via_number, mDetails.viaNumber); + } + } + if (!TextUtils.isEmpty(accountLabel)) { + mAccountLabel.setText(accountLabel); + mAccountLabel.setContentDescription(accountContentDescription); + mAccountLabel.setVisibility(View.VISIBLE); + } else { + mAccountLabel.setVisibility(View.GONE); + } + + final boolean canPlaceCallsTo = + PhoneNumberHelper.canPlaceCallsTo(mNumber, mDetails.numberPresentation); + mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE); + mCopyNumberActionItem.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE); + + final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber); + final boolean isVoicemailNumber = + PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber); + final boolean showEditNumberBeforeCallAction = + canPlaceCallsTo && !isSipNumber && !isVoicemailNumber; + mEditBeforeCallActionItem.setVisibility( + showEditNumberBeforeCallAction ? View.VISIBLE : View.GONE); + + final boolean showReportAction = + mContactInfoHelper.canReportAsInvalid(mDetails.sourceType, mDetails.objectId); + mReportActionItem.setVisibility(showReportAction ? View.VISIBLE : View.GONE); + + invalidateOptionsMenu(); + + mHistoryList.setAdapter( + new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details)); + + updateContactPhoto(mDetails.isSpam); + + findViewById(R.id.call_detail).setVisibility(View.VISIBLE); + } + + /** + * Determines the location geocode text for a call, or the phone number type (if available). + * + * @param details The call details. + * @return The phone number type or location. + */ + private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) { + if (details.isSpam) { + return mResources.getString(R.string.spam_number_call_log_label); + } else if (details.isBlocked) { + return mResources.getString(R.string.blocked_number_call_log_label); + } else if (!TextUtils.isEmpty(details.namePrimary)) { + return Phone.getTypeLabel(mResources, details.numberType, details.numberLabel); + } else { + return details.geocode; + } + } + }; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mContext = this; + mResources = getResources(); + mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)); + mContactsPreferences = new ContactsPreferences(mContext); + mCallTypeHelper = new CallTypeHelper(getResources()); + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mContext); + + mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.call_detail); + mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + + mHistoryList = (ListView) findViewById(R.id.history); + mHistoryList.addHeaderView(mInflater.inflate(R.layout.call_detail_header, null)); + mHistoryList.addFooterView(mInflater.inflate(R.layout.call_detail_footer, null), null, false); + + mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo); + mQuickContactBadge.setOverlay(null); + if (CompatUtils.hasPrioritizedMimeType()) { + mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + mCallerName = (TextView) findViewById(R.id.caller_name); + mCallerNumber = (TextView) findViewById(R.id.caller_number); + mAccountLabel = (TextView) findViewById(R.id.phone_account_label); + mContactPhotoManager = ContactPhotoManager.getInstance(this); + + mCallButton = findViewById(R.id.call_back_button); + mCallButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (TextUtils.isEmpty(mNumber)) { + return; + } + DialerUtils.startActivityWithErrorToast( + CallDetailActivity.this, + new CallIntentBuilder(getDialableNumber(), CallInitiationType.Type.CALL_DETAILS) + .build()); + } + }); + + mEditBeforeCallActionItem = findViewById(R.id.call_detail_action_edit_before_call); + mEditBeforeCallActionItem.setOnClickListener(this); + mReportActionItem = findViewById(R.id.call_detail_action_report); + mReportActionItem.setOnClickListener(this); + + mCopyNumberActionItem = findViewById(R.id.call_detail_action_copy); + mCopyNumberActionItem.setOnClickListener(this); + + if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) { + closeSystemDialogs(); + } + + Logger.get(this).logScreenView(ScreenEvent.Type.CALL_DETAILS, this); + } + + @Override + public void onResume() { + super.onResume(); + mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); + getCallDetails(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return super.dispatchTouchEvent(ev); + } + + public void getCallDetails() { + CallLogAsyncTaskUtil.getCallDetails(this, mCallLogAsyncTaskListener, getCallLogEntryUris()); + } + + /** + * Returns the list of URIs to show. + * + *

There are two ways the URIs can be provided to the activity: as the data on the intent, or + * as a list of ids in the call log added as an extra on the URI. + * + *

If both are available, the data on the intent takes precedence. + */ + private Uri[] getCallLogEntryUris() { + final Uri uri = getIntent().getData(); + if (uri != null) { + // If there is a data on the intent, it takes precedence over the extra. + return new Uri[] {uri}; + } + final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS); + final int numIds = ids == null ? 0 : ids.length; + final Uri[] uris = new Uri[numIds]; + for (int index = 0; index < numIds; ++index) { + uris[index] = + ContentUris.withAppendedId( + TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]); + } + return uris; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + final MenuItem deleteMenuItem = + menu.add( + Menu.NONE, R.id.call_detail_delete_menu_item, Menu.NONE, R.string.call_details_delete); + deleteMenuItem.setIcon(R.drawable.ic_delete_24dp); + deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + deleteMenuItem.setOnMenuItemClickListener(this); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.call_detail_delete_menu_item) { + Logger.get(mContext).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM); + if (hasVoicemail()) { + CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener); + } else { + final StringBuilder callIds = new StringBuilder(); + for (Uri callUri : getCallLogEntryUris()) { + if (callIds.length() != 0) { + callIds.append(","); + } + callIds.append(ContentUris.parseId(callUri)); + } + CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener); + } + } + return true; + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.call_detail_action_copy) { + ClipboardUtils.copyText(mContext, null, mNumber, true); + } else if (resId == R.id.call_detail_action_edit_before_call) { + Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(getDialableNumber())); + DialerUtils.startActivityWithErrorToast(mContext, dialIntent); + } else { + Assert.fail("Unexpected onClick event from " + view); + } + } + + // Loads and displays the contact photo. + private void updateContactPhoto(boolean isSpam) { + if (mDetails == null) { + return; + } + + mQuickContactBadge.assignContactUri(mDetails.contactUri); + final String displayName = + TextUtils.isEmpty(mDetails.namePrimary) + ? mDetails.displayNumber + : mDetails.namePrimary.toString(); + mQuickContactBadge.setContentDescription( + mResources.getString(R.string.description_contact_details, displayName)); + + final boolean isVoicemailNumber = + PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber); + if (isSpam) { + mQuickContactBadge.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact)); + return; + } + + final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType); + int contactType = ContactPhotoManager.TYPE_DEFAULT; + if (isVoicemailNumber) { + contactType = ContactPhotoManager.TYPE_VOICEMAIL; + } else if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } + + final String lookupKey = + mDetails.contactUri == null ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri); + + final DefaultImageRequest request = + new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); + + mContactPhotoManager.loadDirectoryPhoto( + mQuickContactBadge, + mDetails.photoUri, + false /* darkTheme */, + true /* isCircular */, + request); + } + + private void closeSystemDialogs() { + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + + private String getDialableNumber() { + return mNumber + mPostDialDigits; + } + + public boolean hasVoicemail() { + return mVoicemailUri != null; + } + + private static boolean hasIncomingCalls(PhoneCallDetails[] details) { + for (int i = 0; i < details.length; i++) { + if (details[i].hasIncomingCalls()) { + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/app/DialerApplication.java b/java/com/android/dialer/app/DialerApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..3b979212b75c6ab5099927d3df03f7fc7eaffa5e --- /dev/null +++ b/java/com/android/dialer/app/DialerApplication.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app; + +import android.app.Application; +import android.os.Trace; +import android.preference.PreferenceManager; +import com.android.dialer.blocking.BlockedNumbersAutoMigrator; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.inject.ApplicationModule; +import com.android.dialer.inject.DaggerDialerAppComponent; +import com.android.dialer.inject.DialerAppComponent; + +public class DialerApplication extends Application implements EnrichedCallManager.Factory { + + private static final String TAG = "DialerApplication"; + + private volatile DialerAppComponent component; + + @Override + public void onCreate() { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(); + new BlockedNumbersAutoMigrator( + this, + PreferenceManager.getDefaultSharedPreferences(this), + new FilteredNumberAsyncQueryHandler(this)) + .autoMigrate(); + Trace.endSection(); + } + + @Override + public EnrichedCallManager getEnrichedCallManager() { + return component().enrichedCallManager(); + } + + protected DialerAppComponent buildApplicationComponent() { + return DaggerDialerAppComponent.builder() + .applicationModule(new ApplicationModule(this)) + .build(); + } + + /** + * Returns the application component. + * + *

A single Component is created per application instance. Note that it won't be instantiated + * until it's first requested, but guarantees that only one will ever be created. + */ + private final DialerAppComponent component() { + // Double-check idiom for lazy initialization + DialerAppComponent result = component; + if (result == null) { + synchronized (this) { + result = component; + if (result == null) { + component = result = buildApplicationComponent(); + } + } + } + return result; + } +} diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..4c57cda70636cf4df6886a79daf861f3250f60ac --- /dev/null +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -0,0 +1,1484 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app; + +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Trace; +import android.provider.CallLog.Calls; +import android.speech.RecognizerIntent; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.telecom.PhoneAccount; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.DragEvent; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnDragListener; +import android.view.ViewTreeObserver; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.AbsListView.OnScrollListener; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.dialog.ClearFrequentsDialog; +import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; +import com.android.contacts.common.list.PhoneNumberListAdapter; +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker; +import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener; +import com.android.contacts.common.widget.FloatingActionButtonController; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.animation.AnimationListenerAdapter; +import com.android.dialer.app.calllog.CallLogFragment; +import com.android.dialer.app.calllog.CallLogNotificationsService; +import com.android.dialer.app.calllog.ClearCallLogDialog; +import com.android.dialer.app.dialpad.DialpadFragment; +import com.android.dialer.app.list.DragDropController; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.app.list.OnDragDropListener; +import com.android.dialer.app.list.OnListFragmentScrolledListener; +import com.android.dialer.app.list.PhoneFavoriteSquareTileView; +import com.android.dialer.app.list.RegularSearchFragment; +import com.android.dialer.app.list.SearchFragment; +import com.android.dialer.app.list.SmartDialSearchFragment; +import com.android.dialer.app.list.SpeedDialFragment; +import com.android.dialer.app.settings.DialerSettingsActivity; +import com.android.dialer.app.widget.ActionBarController; +import com.android.dialer.app.widget.SearchEditTextLayout; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.interactions.PhoneNumberInteraction; +import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.p13n.inference.P13nRanking; +import com.android.dialer.p13n.inference.protocol.P13nRanker; +import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener; +import com.android.dialer.p13n.logging.P13nLogger; +import com.android.dialer.p13n.logging.P13nLogging; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.util.TouchPointManager; +import com.android.dialer.util.TransactionSafeActivity; +import com.android.dialer.util.ViewUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** The dialer tab's title is 'phone', a more common name (see strings.xml). */ +@UsedByReflection(value = "AndroidManifest-app.xml") +public class DialtactsActivity extends TransactionSafeActivity + implements View.OnClickListener, + DialpadFragment.OnDialpadQueryChangedListener, + OnListFragmentScrolledListener, + CallLogFragment.HostInterface, + DialpadFragment.HostInterface, + ListsFragment.HostInterface, + SpeedDialFragment.HostInterface, + SearchFragment.HostInterface, + OnDragDropListener, + OnPhoneNumberPickerActionListener, + PopupMenu.OnMenuItemClickListener, + ViewPager.OnPageChangeListener, + ActionBarController.ActivityUi, + PhoneNumberInteraction.InteractionErrorListener, + PhoneNumberInteraction.DisambigDialogDismissedListener, + ActivityCompat.OnRequestPermissionsResultCallback { + + public static final boolean DEBUG = false; + @VisibleForTesting public static final String TAG_DIALPAD_FRAGMENT = "dialpad"; + private static final String ACTION_SHOW_TAB = "ACTION_SHOW_TAB"; + @VisibleForTesting public static final String EXTRA_SHOW_TAB = "EXTRA_SHOW_TAB"; + public static final String EXTRA_CLEAR_NEW_VOICEMAILS = "EXTRA_CLEAR_NEW_VOICEMAILS"; + private static final String TAG = "DialtactsActivity"; + private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui"; + private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui"; + private static final String KEY_SEARCH_QUERY = "search_query"; + private static final String KEY_FIRST_LAUNCH = "first_launch"; + private static final String KEY_WAS_CONFIGURATION_CHANGE = "was_configuration_change"; + private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown"; + private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search"; + private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial"; + private static final String TAG_FAVORITES_FRAGMENT = "favorites"; + /** Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. */ + private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER"; + + private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1; + public static final int ACTIVITY_REQUEST_CODE_CALL_COMPOSE = 2; + + private static final int FAB_SCALE_IN_DELAY_MS = 300; + /** Fragment containing the dialpad that slides into view */ + protected DialpadFragment mDialpadFragment; + + private CoordinatorLayout mParentLayout; + /** Fragment for searching phone numbers using the alphanumeric keyboard. */ + private RegularSearchFragment mRegularSearchFragment; + + /** Fragment for searching phone numbers using the dialpad. */ + private SmartDialSearchFragment mSmartDialSearchFragment; + + /** Animation that slides in. */ + private Animation mSlideIn; + + /** Animation that slides out. */ + private Animation mSlideOut; + /** Fragment containing the speed dial list, call history list, and all contacts list. */ + private ListsFragment mListsFragment; + /** + * Tracks whether onSaveInstanceState has been called. If true, no fragment transactions can be + * commited. + */ + private boolean mStateSaved; + + private boolean mIsRestarting; + private boolean mInDialpadSearch; + private boolean mInRegularSearch; + private boolean mClearSearchOnPause; + private boolean mIsDialpadShown; + private boolean mShowDialpadOnResume; + /** Whether or not the device is in landscape orientation. */ + private boolean mIsLandscape; + /** True if the dialpad is only temporarily showing due to being in call */ + private boolean mInCallDialpadUp; + /** True when this activity has been launched for the first time. */ + private boolean mFirstLaunch; + /** + * Search query to be applied to the SearchView in the ActionBar once onCreateOptionsMenu has been + * called. + */ + private String mPendingSearchViewQuery; + + private PopupMenu mOverflowMenu; + private EditText mSearchView; + private View mVoiceSearchButton; + private String mSearchQuery; + private String mDialpadQuery; + private DialerDatabaseHelper mDialerDatabaseHelper; + private DragDropController mDragDropController; + private ActionBarController mActionBarController; + private FloatingActionButtonController mFloatingActionButtonController; + private boolean mWasConfigurationChange; + + private P13nLogger mP13nLogger; + private P13nRanker mP13nRanker; + + AnimationListenerAdapter mSlideInListener = + new AnimationListenerAdapter() { + @Override + public void onAnimationEnd(Animation animation) { + maybeEnterSearchUi(); + } + }; + /** Listener for after slide out animation completes on dialer fragment. */ + AnimationListenerAdapter mSlideOutListener = + new AnimationListenerAdapter() { + @Override + public void onAnimationEnd(Animation animation) { + commitDialpadFragmentHide(); + } + }; + /** Listener used to send search queries to the phone search fragment. */ + private final TextWatcher mPhoneSearchQueryTextListener = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final String newText = s.toString(); + if (newText.equals(mSearchQuery)) { + // If the query hasn't changed (perhaps due to activity being destroyed + // and restored, or user launching the same DIAL intent twice), then there is + // no need to do anything here. + return; + } + if (DEBUG) { + LogUtil.v("DialtactsActivity.onTextChanged", "called with new query: " + newText); + LogUtil.v("DialtactsActivity.onTextChanged", "previous query: " + mSearchQuery); + } + mSearchQuery = newText; + + // Show search fragment only when the query string is changed to non-empty text. + if (!TextUtils.isEmpty(newText)) { + // Call enterSearchUi only if we are switching search modes, or showing a search + // fragment for the first time. + final boolean sameSearchMode = + (mIsDialpadShown && mInDialpadSearch) || (!mIsDialpadShown && mInRegularSearch); + if (!sameSearchMode) { + enterSearchUi(mIsDialpadShown, mSearchQuery, true /* animate */); + } + } + + if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { + mSmartDialSearchFragment.setQueryString(mSearchQuery); + } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { + mRegularSearchFragment.setQueryString(mSearchQuery); + } + } + + @Override + public void afterTextChanged(Editable s) {} + }; + /** Open the search UI when the user clicks on the search box. */ + private final View.OnClickListener mSearchViewOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!isInSearchUi()) { + mActionBarController.onSearchBoxTapped(); + enterSearchUi( + false /* smartDialSearch */, mSearchView.getText().toString(), true /* animate */); + } + } + }; + + private int mActionBarHeight; + private int mPreviouslySelectedTabIndex; + /** Handles the user closing the soft keyboard. */ + private final View.OnKeyListener mSearchEditTextLayoutListener = + new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { + if (TextUtils.isEmpty(mSearchView.getText().toString())) { + // If the search term is empty, close the search UI. + maybeExitSearchUi(); + } else { + // If the search term is not empty, show the dialpad fab. + showFabInSearchUi(); + } + } + return false; + } + }; + /** + * The text returned from a voice search query. Set in {@link #onActivityResult} and used in + * {@link #onResume()} to populate the search box. + */ + private String mVoiceSearchQuery; + + /** + * @param tab the TAB_INDEX_* constant in {@link ListsFragment} + * @return A intent that will open the DialtactsActivity into the specified tab. The intent for + * each tab will be unique. + */ + public static Intent getShowTabIntent(Context context, int tab) { + Intent intent = new Intent(context, DialtactsActivity.class); + intent.setAction(ACTION_SHOW_TAB); + intent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, tab); + intent.setData( + new Uri.Builder() + .scheme("intent") + .authority(context.getPackageName()) + .appendPath(TAG) + .appendQueryParameter(DialtactsActivity.EXTRA_SHOW_TAB, String.valueOf(tab)) + .build()); + + return intent; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return super.dispatchTouchEvent(ev); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(savedInstanceState); + + mFirstLaunch = true; + + final Resources resources = getResources(); + mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large); + + Trace.beginSection(TAG + " setContentView"); + setContentView(R.layout.dialtacts_activity); + Trace.endSection(); + getWindow().setBackgroundDrawable(null); + + Trace.beginSection(TAG + " setup Views"); + final ActionBar actionBar = getActionBarSafely(); + actionBar.setCustomView(R.layout.search_edittext); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setBackgroundDrawable(null); + + SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container); + searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); + + mActionBarController = new ActionBarController(this, searchEditTextLayout); + + mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); + mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); + mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); + searchEditTextLayout + .findViewById(R.id.search_magnifying_glass) + .setOnClickListener(mSearchViewOnClickListener); + searchEditTextLayout + .findViewById(R.id.search_box_start_search) + .setOnClickListener(mSearchViewOnClickListener); + searchEditTextLayout.setOnClickListener(mSearchViewOnClickListener); + searchEditTextLayout.setCallback( + new SearchEditTextLayout.Callback() { + @Override + public void onBackButtonClicked() { + onBackPressed(); + } + + @Override + public void onSearchViewClicked() { + // Hide FAB, as the keyboard is shown. + mFloatingActionButtonController.scaleOut(); + } + }); + + mIsLandscape = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + mPreviouslySelectedTabIndex = ListsFragment.TAB_INDEX_SPEED_DIAL; + final View floatingActionButtonContainer = findViewById(R.id.floating_action_button_container); + ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button); + floatingActionButton.setOnClickListener(this); + mFloatingActionButtonController = + new FloatingActionButtonController( + this, floatingActionButtonContainer, floatingActionButton); + + ImageButton optionsMenuButton = + (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button); + optionsMenuButton.setOnClickListener(this); + mOverflowMenu = buildOptionsMenu(optionsMenuButton); + optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); + + // Add the favorites fragment but only if savedInstanceState is null. Otherwise the + // fragment manager is responsible for recreating it. + if (savedInstanceState == null) { + getFragmentManager() + .beginTransaction() + .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) + .commit(); + } else { + mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); + mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI); + mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI); + mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH); + mWasConfigurationChange = savedInstanceState.getBoolean(KEY_WAS_CONFIGURATION_CHANGE); + mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN); + mActionBarController.restoreInstanceState(savedInstanceState); + } + + final boolean isLayoutRtl = ViewUtil.isRtl(); + if (mIsLandscape) { + mSlideIn = + AnimationUtils.loadAnimation( + this, isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); + mSlideOut = + AnimationUtils.loadAnimation( + this, isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right); + } else { + mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom); + mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom); + } + + mSlideIn.setInterpolator(AnimUtils.EASE_IN); + mSlideOut.setInterpolator(AnimUtils.EASE_OUT); + + mSlideIn.setAnimationListener(mSlideInListener); + mSlideOut.setAnimationListener(mSlideOutListener); + + mParentLayout = (CoordinatorLayout) findViewById(R.id.dialtacts_mainlayout); + mParentLayout.setOnDragListener(new LayoutOnDragListener()); + floatingActionButtonContainer + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + final ViewTreeObserver observer = + floatingActionButtonContainer.getViewTreeObserver(); + if (!observer.isAlive()) { + return; + } + observer.removeOnGlobalLayoutListener(this); + int screenWidth = mParentLayout.getWidth(); + mFloatingActionButtonController.setScreenWidth(screenWidth); + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + } + }); + + Trace.endSection(); + + Trace.beginSection(TAG + " initialize smart dialing"); + mDialerDatabaseHelper = Database.get(this).getDatabaseHelper(this); + SmartDialPrefix.initializeNanpSettings(this); + Trace.endSection(); + + mP13nLogger = P13nLogging.get(getApplicationContext()); + mP13nRanker = P13nRanking.get(getApplicationContext()); + Trace.endSection(); + } + + @NonNull + private ActionBar getActionBarSafely() { + return Assert.isNotNull(getSupportActionBar()); + } + + @Override + protected void onResume() { + Trace.beginSection(TAG + " onResume"); + super.onResume(); + + mStateSaved = false; + if (mFirstLaunch) { + displayFragment(getIntent()); + } else if (!phoneIsInUse() && mInCallDialpadUp) { + hideDialpadFragment(false, true); + mInCallDialpadUp = false; + } else if (mShowDialpadOnResume) { + showDialpadFragment(false); + mShowDialpadOnResume = false; + } + + // If there was a voice query result returned in the {@link #onActivityResult} callback, it + // will have been stashed in mVoiceSearchQuery since the search results fragment cannot be + // shown until onResume has completed. Active the search UI and set the search term now. + if (!TextUtils.isEmpty(mVoiceSearchQuery)) { + mActionBarController.onSearchBoxTapped(); + mSearchView.setText(mVoiceSearchQuery); + mVoiceSearchQuery = null; + } + + mFirstLaunch = false; + + if (mIsRestarting) { + // This is only called when the activity goes from resumed -> paused -> resumed, so it + // will not cause an extra view to be sent out on rotation + if (mIsDialpadShown) { + Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); + } + mIsRestarting = false; + } + + prepareVoiceSearchButton(); + if (!mWasConfigurationChange) { + mDialerDatabaseHelper.startSmartDialUpdateThread(); + } + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + + if (Calls.CONTENT_TYPE.equals(getIntent().getType())) { + // Externally specified extras take precedence to EXTRA_SHOW_TAB, which is only + // used internally. + final Bundle extras = getIntent().getExtras(); + if (extras != null && extras.getInt(Calls.EXTRA_CALL_TYPE_FILTER) == Calls.VOICEMAIL_TYPE) { + mListsFragment.showTab(ListsFragment.TAB_INDEX_VOICEMAIL); + } else { + mListsFragment.showTab(ListsFragment.TAB_INDEX_HISTORY); + } + } else if (getIntent().hasExtra(EXTRA_SHOW_TAB)) { + int index = getIntent().getIntExtra(EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_SPEED_DIAL); + if (index < mListsFragment.getTabCount()) { + // Hide dialpad since this is an explicit intent to show a specific tab, which is coming + // from missed call or voicemail notification. + hideDialpadFragment(false, false); + exitSearchUi(); + mListsFragment.showTab(index); + } + } + + if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) { + CallLogNotificationsService.markNewVoicemailsAsOld(this); + } + + setSearchBoxHint(); + + mP13nLogger.reset(); + mP13nRanker.refresh( + new P13nRefreshCompleteListener() { + @Override + public void onP13nRefreshComplete() { + // TODO: make zero-query search results visible + } + }); + Trace.endSection(); + } + + @Override + protected void onRestart() { + super.onRestart(); + mIsRestarting = true; + } + + @Override + protected void onPause() { + if (mClearSearchOnPause) { + hideDialpadAndSearchUi(); + mClearSearchOnPause = false; + } + if (mSlideOut.hasStarted() && !mSlideOut.hasEnded()) { + commitDialpadFragmentHide(); + } + super.onPause(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_SEARCH_QUERY, mSearchQuery); + outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); + outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch); + outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch); + outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown); + outState.putBoolean(KEY_WAS_CONFIGURATION_CHANGE, isChangingConfigurations()); + mActionBarController.saveInstanceState(outState); + mStateSaved = true; + } + + @Override + public void onAttachFragment(final Fragment fragment) { + if (fragment instanceof DialpadFragment) { + mDialpadFragment = (DialpadFragment) fragment; + if (!mIsDialpadShown && !mShowDialpadOnResume) { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.hide(mDialpadFragment); + transaction.commit(); + } + } else if (fragment instanceof SmartDialSearchFragment) { + mSmartDialSearchFragment = (SmartDialSearchFragment) fragment; + mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this); + if (!TextUtils.isEmpty(mDialpadQuery)) { + mSmartDialSearchFragment.setAddToContactNumber(mDialpadQuery); + } + } else if (fragment instanceof SearchFragment) { + mRegularSearchFragment = (RegularSearchFragment) fragment; + mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this); + } else if (fragment instanceof ListsFragment) { + mListsFragment = (ListsFragment) fragment; + mListsFragment.addOnPageChangeListener(this); + } + if (fragment instanceof SearchFragment) { + final SearchFragment searchFragment = (SearchFragment) fragment; + searchFragment.setReranker( + new CursorReranker() { + @Override + @MainThread + public Cursor rerankCursor(Cursor data) { + Assert.isMainThread(); + return mP13nRanker.rankCursor(data, PhoneQuery.PHONE_NUMBER); + } + }); + searchFragment.addOnLoadFinishedListener( + new OnLoadFinishedListener() { + @Override + public void onLoadFinished() { + mP13nLogger.onSearchQuery( + searchFragment.getQueryString(), + (PhoneNumberListAdapter) searchFragment.getAdapter()); + } + }); + } + } + + protected void handleMenuSettings() { + final Intent intent = new Intent(this, DialerSettingsActivity.class); + startActivity(intent); + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.floating_action_button) { + if (mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_ALL_CONTACTS + && !mInRegularSearch + && !mInDialpadSearch) { + DialerUtils.startActivityWithErrorToast( + this, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available); + Logger.get(this).logImpression(DialerImpression.Type.NEW_CONTACT_FAB); + } else if (!mIsDialpadShown) { + mInCallDialpadUp = false; + showDialpadFragment(true); + } + } else if (resId == R.id.voice_search_button) { + try { + startActivityForResult( + new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), + ACTIVITY_REQUEST_CODE_VOICE_SEARCH); + } catch (ActivityNotFoundException e) { + Toast.makeText( + DialtactsActivity.this, R.string.voice_search_not_available, Toast.LENGTH_SHORT) + .show(); + } + } else if (resId == R.id.dialtacts_options_menu_button) { + mOverflowMenu.show(); + } else { + Assert.fail("Unexpected onClick event from " + view); + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (!isSafeToCommitTransactions()) { + return true; + } + + int resId = item.getItemId(); + if (item.getItemId() == R.id.menu_delete_all) { + ClearCallLogDialog.show(getFragmentManager()); + return true; + } else if (resId == R.id.menu_clear_frequents) { + ClearFrequentsDialog.show(getFragmentManager()); + Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this); + return true; + } else if (resId == R.id.menu_call_settings) { + handleMenuSettings(); + Logger.get(this).logScreenView(ScreenEvent.Type.SETTINGS, this); + return true; + } + return false; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { + if (resultCode == RESULT_OK) { + final ArrayList matches = + data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); + if (matches.size() > 0) { + mVoiceSearchQuery = matches.get(0); + } else { + LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard"); + } + } else { + LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed: " + resultCode); + } + } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) { + if (resultCode != RESULT_OK) { + LogUtil.i( + "DialtactsActivity.onActivityResult", + "returned from call composer, error occurred (resultCode=" + resultCode + ")"); + String message = + getString(R.string.call_composer_connection_failed, getString(R.string.share_and_call)); + Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show(); + } else { + LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error"); + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Update the number of unread voicemails (potentially other tabs) displayed next to the tab icon. + */ + public void updateTabUnreadCounts() { + mListsFragment.updateTabUnreadCounts(); + } + + /** + * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual + * updates are handled by a callback which is invoked after the dialpad fragment is shown. + * + * @see #onDialpadShown + */ + private void showDialpadFragment(boolean animate) { + if (mIsDialpadShown || mStateSaved) { + return; + } + mIsDialpadShown = true; + + mListsFragment.setUserVisibleHint(false); + + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + if (mDialpadFragment == null) { + mDialpadFragment = new DialpadFragment(); + ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT); + } else { + ft.show(mDialpadFragment); + } + + mDialpadFragment.setAnimate(animate); + Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); + ft.commit(); + + if (animate) { + mFloatingActionButtonController.scaleOut(); + } else { + mFloatingActionButtonController.setVisible(false); + maybeEnterSearchUi(); + } + mActionBarController.onDialpadUp(); + + Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); + + //adjust the title, so the user will know where we're at when the activity start/resumes. + setTitle(R.string.launcherDialpadActivityLabel); + } + + /** Callback from child DialpadFragment when the dialpad is shown. */ + public void onDialpadShown() { + Assert.isNotNull(mDialpadFragment); + if (mDialpadFragment.getAnimate()) { + Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn); + } else { + mDialpadFragment.setYFraction(0); + } + + updateSearchFragmentPosition(); + } + + /** + * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in a + * callback after the hide animation ends. + * + * @see #commitDialpadFragmentHide + */ + public void hideDialpadFragment(boolean animate, boolean clearDialpad) { + if (mDialpadFragment == null || mDialpadFragment.getView() == null) { + return; + } + if (clearDialpad) { + // Temporarily disable accessibility when we clear the dialpad, since it should be + // invisible and should not announce anything. + mDialpadFragment + .getDigitsWidget() + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mDialpadFragment.clearDialpad(); + mDialpadFragment + .getDigitsWidget() + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } + if (!mIsDialpadShown) { + return; + } + mIsDialpadShown = false; + mDialpadFragment.setAnimate(animate); + mListsFragment.setUserVisibleHint(true); + mListsFragment.sendScreenViewForCurrentPosition(); + + updateSearchFragmentPosition(); + + mFloatingActionButtonController.align(getFabAlignment(), animate); + if (animate) { + mDialpadFragment.getView().startAnimation(mSlideOut); + } else { + commitDialpadFragmentHide(); + } + + mActionBarController.onDialpadDown(); + + if (isInSearchUi()) { + if (TextUtils.isEmpty(mSearchQuery)) { + exitSearchUi(); + } + } + //reset the title to normal. + setTitle(R.string.launcherActivityLabel); + } + + /** Finishes hiding the dialpad fragment after any animations are completed. */ + private void commitDialpadFragmentHide() { + if (!mStateSaved && mDialpadFragment != null && !mDialpadFragment.isHidden()) { + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.hide(mDialpadFragment); + ft.commit(); + } + mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); + } + + private void updateSearchFragmentPosition() { + SearchFragment fragment = null; + if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { + fragment = mSmartDialSearchFragment; + } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { + fragment = mRegularSearchFragment; + } + if (fragment != null && fragment.isVisible()) { + fragment.updatePosition(true /* animate */); + } + } + + @Override + public boolean isInSearchUi() { + return mInDialpadSearch || mInRegularSearch; + } + + @Override + public boolean hasSearchQuery() { + return !TextUtils.isEmpty(mSearchQuery); + } + + @Override + public boolean shouldShowActionBar() { + return mListsFragment.shouldShowActionBar(); + } + + private void setNotInSearchUi() { + mInDialpadSearch = false; + mInRegularSearch = false; + } + + private void hideDialpadAndSearchUi() { + if (mIsDialpadShown) { + hideDialpadFragment(false, true); + } else { + exitSearchUi(); + } + } + + private void prepareVoiceSearchButton() { + final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + if (canIntentBeHandled(voiceIntent)) { + mVoiceSearchButton.setVisibility(View.VISIBLE); + mVoiceSearchButton.setOnClickListener(this); + } else { + mVoiceSearchButton.setVisibility(View.GONE); + } + } + + public boolean isNearbyPlacesSearchEnabled() { + return false; + } + + protected int getSearchBoxHint() { + return R.string.dialer_hint_find_contact; + } + + /** Sets the hint text for the contacts search box */ + private void setSearchBoxHint() { + SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) + getActionBarSafely().getCustomView().findViewById(R.id.search_view_container); + ((TextView) searchEditTextLayout.findViewById(R.id.search_box_start_search)) + .setHint(getSearchBoxHint()); + } + + protected OptionsPopupMenu buildOptionsMenu(View invoker) { + final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); + popupMenu.inflate(R.menu.dialtacts_options); + popupMenu.setOnMenuItemClickListener(this); + return popupMenu; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mPendingSearchViewQuery != null) { + mSearchView.setText(mPendingSearchViewQuery); + mPendingSearchViewQuery = null; + } + if (mActionBarController != null) { + mActionBarController.restoreActionBarOffset(); + } + return false; + } + + /** + * Returns true if the intent is due to hitting the green send key (hardware call button: + * KEYCODE_CALL) while in a call. + * + * @param intent the intent that launched this activity + * @return true if the intent is due to hitting the green send key while in a call + */ + private boolean isSendKeyWhileInCall(Intent intent) { + // If there is a call in progress and the user launched the dialer by hitting the call + // button, go straight to the in-call screen. + final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction()); + + // When KEYCODE_CALL event is handled it dispatches an intent with the ACTION_CALL_BUTTON. + // Besides of checking the intent action, we must check if the phone is really during a + // call in order to decide whether to ignore the event or continue to display the activity. + if (callKey && phoneIsInUse()) { + TelecomUtil.showInCallScreen(this, false); + return true; + } + + return false; + } + + /** + * Sets the current tab based on the intent's request type + * + * @param intent Intent that contains information about which tab should be selected + */ + private void displayFragment(Intent intent) { + // If we got here by hitting send and we're in call forward along to the in-call activity + if (isSendKeyWhileInCall(intent)) { + finish(); + return; + } + + final boolean showDialpadChooser = + !ACTION_SHOW_TAB.equals(intent.getAction()) + && phoneIsInUse() + && !DialpadFragment.isAddCallMode(intent); + if (showDialpadChooser || (intent.getData() != null && isDialIntent(intent))) { + showDialpadFragment(false); + mDialpadFragment.setStartedFromNewIntent(true); + if (showDialpadChooser && !mDialpadFragment.isVisible()) { + mInCallDialpadUp = true; + } + } + } + + @Override + public void onNewIntent(Intent newIntent) { + setIntent(newIntent); + + mStateSaved = false; + displayFragment(newIntent); + + invalidateOptionsMenu(); + } + + /** Returns true if the given intent contains a phone number to populate the dialer with */ + private boolean isDialIntent(Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { + return true; + } + if (Intent.ACTION_VIEW.equals(action)) { + final Uri data = intent.getData(); + if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) { + return true; + } + } + return false; + } + + /** Shows the search fragment */ + private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) { + if (mStateSaved || getFragmentManager().isDestroyed()) { + // Weird race condition where fragment is doing work after the activity is destroyed + // due to talkback being on (b/10209937). Just return since we can't do any + // constructive here. + return; + } + + if (DEBUG) { + LogUtil.v("DialtactsActivity.enterSearchUi", "smart dial " + smartDialSearch); + } + + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + if (mInDialpadSearch && mSmartDialSearchFragment != null) { + transaction.remove(mSmartDialSearchFragment); + } else if (mInRegularSearch && mRegularSearchFragment != null) { + transaction.remove(mRegularSearchFragment); + } + + final String tag; + if (smartDialSearch) { + tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; + } else { + tag = TAG_REGULAR_SEARCH_FRAGMENT; + } + mInDialpadSearch = smartDialSearch; + mInRegularSearch = !smartDialSearch; + + mFloatingActionButtonController.scaleOut(); + + SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag); + if (animate) { + transaction.setCustomAnimations(android.R.animator.fade_in, 0); + } else { + transaction.setTransition(FragmentTransaction.TRANSIT_NONE); + } + if (fragment == null) { + if (smartDialSearch) { + fragment = new SmartDialSearchFragment(); + } else { + fragment = Bindings.getLegacy(this).newRegularSearchFragment(); + fragment.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + // Show the FAB when the user touches the lists fragment and the soft + // keyboard is hidden. + hideDialpadFragment(true, false); + showFabInSearchUi(); + v.performClick(); + return false; + } + }); + } + transaction.add(R.id.dialtacts_frame, fragment, tag); + } else { + transaction.show(fragment); + } + // DialtactsActivity will provide the options menu + fragment.setHasOptionsMenu(false); + fragment.setShowEmptyListForNullQuery(true); + if (!smartDialSearch) { + fragment.setQueryString(query); + } + transaction.commit(); + + if (animate) { + Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); + } + mListsFragment.setUserVisibleHint(false); + + if (smartDialSearch) { + Logger.get(this).logScreenView(ScreenEvent.Type.SMART_DIAL_SEARCH, this); + } else { + Logger.get(this).logScreenView(ScreenEvent.Type.REGULAR_SEARCH, this); + } + } + + /** Hides the search fragment */ + private void exitSearchUi() { + // See related bug in enterSearchUI(); + if (getFragmentManager().isDestroyed() || mStateSaved) { + return; + } + + mSearchView.setText(null); + + if (mDialpadFragment != null) { + mDialpadFragment.clearDialpad(); + } + + setNotInSearchUi(); + + // Restore the FAB for the lists fragment. + if (getFabAlignment() != FloatingActionButtonController.ALIGN_END) { + mFloatingActionButtonController.setVisible(false); + } + mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); + onPageScrolled(mListsFragment.getCurrentTabIndex(), 0 /* offset */, 0 /* pixelOffset */); + onPageSelected(mListsFragment.getCurrentTabIndex()); + + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + if (mSmartDialSearchFragment != null) { + transaction.remove(mSmartDialSearchFragment); + } + if (mRegularSearchFragment != null) { + transaction.remove(mRegularSearchFragment); + } + transaction.commit(); + + Assert.isNotNull(mListsFragment.getView()).animate().alpha(1).withLayer(); + + if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { + // If the dialpad fragment wasn't previously visible, then send a screen view because + // we are exiting regular search. Otherwise, the screen view will be sent by + // {@link #hideDialpadFragment}. + mListsFragment.sendScreenViewForCurrentPosition(); + mListsFragment.setUserVisibleHint(true); + } + + mActionBarController.onSearchUiExited(); + } + + @Override + public void onBackPressed() { + if (mStateSaved) { + return; + } + if (mIsDialpadShown) { + if (TextUtils.isEmpty(mSearchQuery) + || (mSmartDialSearchFragment != null + && mSmartDialSearchFragment.isVisible() + && mSmartDialSearchFragment.getAdapter().getCount() == 0)) { + exitSearchUi(); + } + hideDialpadFragment(true, false); + } else if (isInSearchUi()) { + exitSearchUi(); + DialerUtils.hideInputMethod(mParentLayout); + } else { + super.onBackPressed(); + } + } + + private void maybeEnterSearchUi() { + if (!isInSearchUi()) { + enterSearchUi(true /* isSmartDial */, mSearchQuery, false); + } + } + + /** @return True if the search UI was exited, false otherwise */ + private boolean maybeExitSearchUi() { + if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) { + exitSearchUi(); + DialerUtils.hideInputMethod(mParentLayout); + return true; + } + return false; + } + + private void showFabInSearchUi() { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.fab_ic_dial, null), + getResources().getString(R.string.action_menu_dialpad_button)); + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); + } + + @Override + public void onDialpadQueryChanged(String query) { + mDialpadQuery = query; + if (mSmartDialSearchFragment != null) { + mSmartDialSearchFragment.setAddToContactNumber(query); + } + final String normalizedQuery = + SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); + + if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { + if (DEBUG) { + LogUtil.v("DialtactsActivity.onDialpadQueryChanged", "new query: " + query); + } + if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { + // This callback can happen if the dialpad fragment is recreated because of + // activity destruction. In that case, don't update the search view because + // that would bring the user back to the search fragment regardless of the + // previous state of the application. Instead, just return here and let the + // fragment manager correctly figure out whatever fragment was last displayed. + if (!TextUtils.isEmpty(normalizedQuery)) { + mPendingSearchViewQuery = normalizedQuery; + } + return; + } + mSearchView.setText(normalizedQuery); + } + + try { + if (mDialpadFragment != null && mDialpadFragment.isVisible()) { + mDialpadFragment.process_quote_emergency_unquote(normalizedQuery); + } + } catch (Exception ignored) { + // Skip any exceptions for this piece of code + } + } + + @Override + public boolean onDialpadSpacerTouchWithEmptyQuery() { + if (mInDialpadSearch + && mSmartDialSearchFragment != null + && !mSmartDialSearchFragment.isShowingPermissionRequest()) { + hideDialpadFragment(true /* animate */, true /* clearDialpad */); + return true; + } + return false; + } + + @Override + public void onListFragmentScrollStateChange(int scrollState) { + if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + hideDialpadFragment(true, false); + DialerUtils.hideInputMethod(mParentLayout); + } + } + + @Override + public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) { + // TODO: No-op for now. This should eventually show/hide the actionBar based on + // interactions with the ListsFragments. + } + + private boolean phoneIsInUse() { + return TelecomUtil.isInCall(this); + } + + private boolean canIntentBeHandled(Intent intent) { + final PackageManager packageManager = getPackageManager(); + final List resolveInfo = + packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + return resolveInfo != null && resolveInfo.size() > 0; + } + + /** Called when the user has long-pressed a contact tile to start a drag operation. */ + @Override + public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { + mListsFragment.showRemoveView(true); + } + + @Override + public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {} + + /** Called when the user has released a contact tile after long-pressing it. */ + @Override + public void onDragFinished(int x, int y) { + mListsFragment.showRemoveView(false); + } + + @Override + public void onDroppedOnRemove() {} + + /** + * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer once it has + * been attached to the activity. + */ + @Override + public void setDragDropController(DragDropController dragController) { + mDragDropController = dragController; + mListsFragment.getRemoveView().setDragDropController(dragController); + } + + /** Implemented to satisfy {@link SpeedDialFragment.HostInterface} */ + @Override + public void showAllContactsTab() { + if (mListsFragment != null) { + mListsFragment.showTab(ListsFragment.TAB_INDEX_ALL_CONTACTS); + } + } + + /** Implemented to satisfy {@link CallLogFragment.HostInterface} */ + @Override + public void showDialpad() { + showDialpadFragment(true); + } + + @Override + public void enableFloatingButton(boolean enabled) { + LogUtil.d("DialtactsActivity.enableFloatingButton", "enable: %b", enabled); + // Floating button shouldn't be enabled when dialpad is shown. + if (!isDialpadShown() || !enabled) { + mFloatingActionButtonController.setVisible(enabled); + } + } + + @Override + public void onPickDataUri( + Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { + mClearSearchOnPause = true; + PhoneNumberInteraction.startInteractionForPhoneCall( + DialtactsActivity.this, dataUri, isVideoCall, callSpecificAppData); + } + + @Override + public void onPickPhoneNumber( + String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { + if (phoneNumber == null) { + // Invalid phone number, but let the call go through so that InCallUI can show + // an error message. + phoneNumber = ""; + } + + Intent intent = + new CallIntentBuilder(phoneNumber, callSpecificAppData).setIsVideoCall(isVideoCall).build(); + + DialerUtils.startActivityWithErrorToast(this, intent); + mClearSearchOnPause = true; + } + + @Override + public void onHomeInActionBarSelected() { + exitSearchUi(); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabIndex = mListsFragment.getCurrentTabIndex(); + + // Scroll the button from center to end when moving from the Speed Dial to Call History tab. + // In RTL, scroll when the current tab is Call History instead, since the order of the tabs + // is reversed and the ViewPager returns the left tab position during scroll. + boolean isRtl = ViewUtil.isRtl(); + if (!isRtl && tabIndex == ListsFragment.TAB_INDEX_SPEED_DIAL && !mIsLandscape) { + mFloatingActionButtonController.onPageScrolled(positionOffset); + } else if (isRtl && tabIndex == ListsFragment.TAB_INDEX_HISTORY && !mIsLandscape) { + mFloatingActionButtonController.onPageScrolled(1 - positionOffset); + } else if (tabIndex != ListsFragment.TAB_INDEX_SPEED_DIAL) { + mFloatingActionButtonController.onPageScrolled(1); + } + } + + @Override + public void onPageSelected(int position) { + updateMissedCalls(); + int tabIndex = mListsFragment.getCurrentTabIndex(); + mPreviouslySelectedTabIndex = tabIndex; + mFloatingActionButtonController.setVisible(true); + if (tabIndex == ListsFragment.TAB_INDEX_ALL_CONTACTS + && !mInRegularSearch + && !mInDialpadSearch) { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.ic_person_add_24dp, null), + getResources().getString(R.string.search_shortcut_create_new_contact)); + } else { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.fab_ic_dial, null), + getResources().getString(R.string.action_menu_dialpad_button)); + } + } + + @Override + public void onPageScrollStateChanged(int state) {} + + @Override + public boolean isActionBarShowing() { + return mActionBarController.isActionBarShowing(); + } + + @Override + public ActionBarController getActionBarController() { + return mActionBarController; + } + + @Override + public boolean isDialpadShown() { + return mIsDialpadShown; + } + + @Override + public int getDialpadHeight() { + if (mDialpadFragment != null) { + return mDialpadFragment.getDialpadHeight(); + } + return 0; + } + + @Override + public int getActionBarHideOffset() { + return getActionBarSafely().getHideOffset(); + } + + @Override + public void setActionBarHideOffset(int offset) { + getActionBarSafely().setHideOffset(offset); + } + + @Override + public int getActionBarHeight() { + return mActionBarHeight; + } + + private int getFabAlignment() { + if (!mIsLandscape + && !isInSearchUi() + && mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_SPEED_DIAL) { + return FloatingActionButtonController.ALIGN_MIDDLE; + } + return FloatingActionButtonController.ALIGN_END; + } + + private void updateMissedCalls() { + if (mPreviouslySelectedTabIndex == ListsFragment.TAB_INDEX_HISTORY) { + mListsFragment.markMissedCallsAsReadAndRemoveNotifications(); + } + } + + @Override + public void onDisambigDialogDismissed() { + // Don't do anything; the app will remain open with favorites tiles displayed. + } + + @Override + public void interactionError(@InteractionErrorCode int interactionErrorCode) { + switch (interactionErrorCode) { + case InteractionErrorCode.USER_LEAVING_ACTIVITY: + // This is expected to happen if the user exits the activity before the interaction occurs. + return; + case InteractionErrorCode.CONTACT_NOT_FOUND: + case InteractionErrorCode.CONTACT_HAS_NO_NUMBER: + case InteractionErrorCode.OTHER_ERROR: + default: + // All other error codes are unexpected. For example, it should be impossible to start an + // interaction with an invalid contact from the Dialtacts activity. + Assert.fail("PhoneNumberInteraction error: " + interactionErrorCode); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + // This should never happen; it should be impossible to start an interaction without the + // contacts permission from the Dialtacts activity. + Assert.fail( + String.format( + Locale.US, + "Permissions requested unexpectedly: %d/%s/%s", + requestCode, + Arrays.toString(permissions), + Arrays.toString(grantResults))); + } + + protected class OptionsPopupMenu extends PopupMenu { + + public OptionsPopupMenu(Context context, View anchor) { + super(context, anchor, Gravity.END); + } + + @Override + public void show() { + final boolean hasContactsPermission = + PermissionsUtil.hasContactsPermissions(DialtactsActivity.this); + final Menu menu = getMenu(); + final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); + clearFrequents.setVisible( + mListsFragment != null + && mListsFragment.getSpeedDialFragment() != null + && mListsFragment.getSpeedDialFragment().hasFrequents() + && hasContactsPermission); + + menu.findItem(R.id.menu_delete_all) + .setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this)); + super.show(); + } + } + + /** + * Listener that listens to drag events and sends their x and y coordinates to a {@link + * DragDropController}. + */ + private class LayoutOnDragListener implements OnDragListener { + + @Override + public boolean onDrag(View v, DragEvent event) { + if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) { + mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY()); + } + return true; + } + } +} diff --git a/java/com/android/dialer/app/FloatingActionButtonBehavior.java b/java/com/android/dialer/app/FloatingActionButtonBehavior.java new file mode 100644 index 0000000000000000000000000000000000000000..d4a79ca199a2f98e0cfc3e7f0a14e9c44024c799 --- /dev/null +++ b/java/com/android/dialer/app/FloatingActionButtonBehavior.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app; + +import android.content.Context; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.Snackbar.SnackbarLayout; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import com.android.dialer.proguard.UsedByReflection; + +/** + * Implements custom behavior for the movement of the FAB in response to the Snackbar. Because we + * are not using the design framework FloatingActionButton widget, we need to manually implement the + * Material Design behavior of having the FAB translate upward and downward with the appearance and + * disappearance of a Snackbar. + */ +@UsedByReflection(value = "dialtacts_activity.xml") +public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior { + + @UsedByReflection(value = "dialtacts_activity.xml") + public FloatingActionButtonBehavior(Context context, AttributeSet attrs) {} + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, FrameLayout child, View dependency) { + return dependency instanceof SnackbarLayout; + } + + @Override + public boolean onDependentViewChanged( + CoordinatorLayout parent, FrameLayout child, View dependency) { + float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight()); + child.setTranslationY(translationY); + return true; + } +} diff --git a/java/com/android/dialer/app/PhoneCallDetails.java b/java/com/android/dialer/app/PhoneCallDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..436f68eecdfcd321ed0cae7b87bd27ecc7ebcaa7 --- /dev/null +++ b/java/com/android/dialer/app/PhoneCallDetails.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app; + +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.contacts.common.ContactsUtils.UserType; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.calllog.PhoneNumberDisplayUtil; +import com.android.dialer.phonenumbercache.ContactInfo; + +/** The details of a phone call to be shown in the UI. */ +public class PhoneCallDetails { + + // The number of the other party involved in the call. + public CharSequence number; + // Post-dial digits associated with the outgoing call. + public String postDialDigits; + // The secondary line number the call was received via. + public String viaNumber; + // The number presenting rules set by the network, e.g., {@link Calls#PRESENTATION_ALLOWED} + public int numberPresentation; + // The country corresponding with the phone number. + public String countryIso; + // The geocoded location for the phone number. + public String geocode; + + /** + * The type of calls, as defined in the call log table, e.g., {@link Calls#INCOMING_TYPE}. + * + *

There might be multiple types if this represents a set of entries grouped together. + */ + public int[] callTypes; + + // The date of the call, in milliseconds since the epoch. + public long date; + // The duration of the call in milliseconds, or 0 for missed calls. + public long duration; + // The name of the contact, or the empty string. + public CharSequence namePrimary; + // The alternative name of the contact, e.g. last name first, or the empty string + public CharSequence nameAlternative; + /** + * The user's preference on name display order, last name first or first time first. {@see + * ContactsPreferences} + */ + public int nameDisplayOrder; + // The type of phone, e.g., {@link Phone#TYPE_HOME}, 0 if not available. + public int numberType; + // The custom label associated with the phone number in the contact, or the empty string. + public CharSequence numberLabel; + // The URI of the contact associated with this phone call. + public Uri contactUri; + + /** + * The photo URI of the picture of the contact that is associated with this phone call or null if + * there is none. + * + *

This is meant to store the high-res photo only. + */ + public Uri photoUri; + + // The source type of the contact associated with this call. + public int sourceType; + + // The object id type of the contact associated with this call. + public String objectId; + + // The unique identifier for the account associated with the call. + public PhoneAccountHandle accountHandle; + + // Features applicable to this call. + public int features; + + // Total data usage for this call. + public Long dataUsage; + + // Voicemail transcription + public String transcription; + + // The display string for the number. + public String displayNumber; + + // Whether the contact number is a voicemail number. + public boolean isVoicemail; + + /** The {@link UserType} of the contact */ + public @UserType long contactUserType; + + /** + * If this is a voicemail, whether the message is read. For other types of calls, this defaults to + * {@code true}. + */ + public boolean isRead = true; + + // If this call is a spam number. + public boolean isSpam = false; + + // If this call is a blocked number. + public boolean isBlocked = false; + + // Call location and date text. + public CharSequence callLocationAndDate; + + // Call description. + public CharSequence callDescription; + public String accountComponentName; + public String accountId; + public ContactInfo cachedContactInfo; + public int voicemailId; + public int previousGroup; + + /** + * Constructor with required fields for the details of a call with a number associated with a + * contact. + */ + public PhoneCallDetails( + CharSequence number, int numberPresentation, CharSequence postDialDigits) { + this.number = number; + this.numberPresentation = numberPresentation; + this.postDialDigits = postDialDigits.toString(); + } + /** + * Construct the "on {accountLabel} via {viaNumber}" accessibility description for the account + * list item, depending on the existence of the accountLabel and viaNumber. + * + * @param viaNumber The number that this call is being placed via. + * @param accountLabel The {@link PhoneAccount} label that this call is being placed with. + * @return The description of the account that this call has been placed on. + */ + public static CharSequence createAccountLabelDescription( + Resources resources, @Nullable String viaNumber, @Nullable CharSequence accountLabel) { + + if ((!TextUtils.isEmpty(viaNumber)) && !TextUtils.isEmpty(accountLabel)) { + String msg = + resources.getString( + R.string.description_via_number_phone_account, accountLabel, viaNumber); + CharSequence accountNumberLabel = + ContactDisplayUtils.getTelephoneTtsSpannable(msg, viaNumber); + return (accountNumberLabel == null) ? msg : accountNumberLabel; + } else if (!TextUtils.isEmpty(viaNumber)) { + CharSequence viaNumberLabel = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + resources, R.string.description_via_number, viaNumber); + return (viaNumberLabel == null) ? viaNumber : viaNumberLabel; + } else if (!TextUtils.isEmpty(accountLabel)) { + return TextUtils.expandTemplate( + resources.getString(R.string.description_phone_account), accountLabel); + } + return ""; + } + + /** + * Returns the preferred name for the call details as specified by the {@link #nameDisplayOrder} + * + * @return the preferred name + */ + public CharSequence getPreferredName() { + if (nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY + || TextUtils.isEmpty(nameAlternative)) { + return namePrimary; + } + return nameAlternative; + } + + public void updateDisplayNumber( + Context context, CharSequence formattedNumber, boolean isVoicemail) { + displayNumber = + PhoneNumberDisplayUtil.getDisplayNumber( + context, number, numberPresentation, formattedNumber, postDialDigits, isVoicemail) + .toString(); + } + + public boolean hasIncomingCalls() { + for (int i = 0; i < callTypes.length; i++) { + if (callTypes[i] == CallLog.Calls.INCOMING_TYPE + || callTypes[i] == CallLog.Calls.MISSED_TYPE + || callTypes[i] == CallLog.Calls.VOICEMAIL_TYPE + || callTypes[i] == CallLog.Calls.REJECTED_TYPE + || callTypes[i] == CallLog.Calls.BLOCKED_TYPE) { + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/app/SpecialCharSequenceMgr.java b/java/com/android/dialer/app/SpecialCharSequenceMgr.java new file mode 100644 index 0000000000000000000000000000000000000000..2ae19704a53fd3124a6fba8e8ad69c5fa7975f4b --- /dev/null +++ b/java/com/android/dialer/app/SpecialCharSequenceMgr.java @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.DialogFragment; +import android.app.KeyguardManager; +import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Looper; +import android.provider.Settings; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.Toast; +import com.android.common.io.MoreCloseables; +import com.android.contacts.common.compat.TelephonyManagerCompat; +import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; +import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.telecom.TelecomUtil; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to listen for some magic character sequences that are handled specially by the + * dialer. + * + *

Note the Phone app also handles these sequences too (in a couple of relatively obscure places + * in the UI), so there's a separate version of this class under apps/Phone. + * + *

TODO: there's lots of duplicated code between this class and the corresponding class under + * apps/Phone. Let's figure out a way to unify these two classes (in the framework? in a common + * shared library?) + */ +public class SpecialCharSequenceMgr { + + private static final String TAG = "SpecialCharSequenceMgr"; + + private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment"; + + private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE"; + private static final String MMI_IMEI_DISPLAY = "*#06#"; + private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#"; + /** ***** This code is used to handle SIM Contact queries ***** */ + private static final String ADN_PHONE_NUMBER_COLUMN_NAME = "number"; + + private static final String ADN_NAME_COLUMN_NAME = "name"; + private static final int ADN_QUERY_TOKEN = -1; + /** + * Remembers the previous {@link QueryHandler} and cancel the operation when needed, to prevent + * possible crash. + * + *

QueryHandler may call {@link ProgressDialog#dismiss()} when the screen is already gone, + * which will cause the app crash. This variable enables the class to prevent the crash on {@link + * #cleanup()}. + * + *

TODO: Remove this and replace it (and {@link #cleanup()}) with better implementation. One + * complication is that we have SpecialCharSequenceMgr in Phone package too, which has *slightly* + * different implementation. Note that Phone package doesn't have this problem, so the class on + * Phone side doesn't have this functionality. Fundamental fix would be to have one shared + * implementation and resolve this corner case more gracefully. + */ + private static QueryHandler sPreviousAdnQueryHandler; + + /** This class is never instantiated. */ + private SpecialCharSequenceMgr() {} + + public static boolean handleChars(Context context, String input, EditText textField) { + //get rid of the separators so that the string gets parsed correctly + String dialString = PhoneNumberUtils.stripSeparators(input); + + return handleDeviceIdDisplay(context, dialString) + || handleRegulatoryInfoDisplay(context, dialString) + || handlePinEntry(context, dialString) + || handleAdnEntry(context, dialString, textField) + || handleSecretCode(context, dialString); + + } + + /** + * Cleanup everything around this class. Must be run inside the main thread. + * + *

This should be called when the screen becomes background. + */ + public static void cleanup() { + if (Looper.myLooper() != Looper.getMainLooper()) { + Log.wtf(TAG, "cleanup() is called outside the main thread"); + return; + } + + if (sPreviousAdnQueryHandler != null) { + sPreviousAdnQueryHandler.cancel(); + sPreviousAdnQueryHandler = null; + } + } + + /** + * Handles secret codes to launch arbitrary activities in the form of *#*##*#*. If a secret + * code is encountered an Intent is started with the android_secret_code:// URI. + * + * @param context the context to use + * @param input the text to check for a secret code in + * @return true if a secret code was encountered + */ + static boolean handleSecretCode(Context context, String input) { + // Secret codes are in the form *#*##*#* + int len = input.length(); + if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) { + final Intent intent = + new Intent( + SECRET_CODE_ACTION, + Uri.parse("android_secret_code://" + input.substring(4, len - 4))); + context.sendBroadcast(intent); + return true; + } + + return false; + } + + /** + * Handle ADN requests by filling in the SIM contact number into the requested EditText. + * + *

This code works alongside the Asynchronous query handler {@link QueryHandler} and query + * cancel handler implemented in {@link SimContactQueryCookie}. + */ + static boolean handleAdnEntry(Context context, String input, EditText textField) { + /* ADN entries are of the form "N(N)(N)#" */ + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager == null + || telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) { + return false; + } + + // if the phone is keyguard-restricted, then just ignore this + // input. We want to make sure that sim card contacts are NOT + // exposed unless the phone is unlocked, and this code can be + // accessed from the emergency dialer. + KeyguardManager keyguardManager = + (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + if (keyguardManager.inKeyguardRestrictedInputMode()) { + return false; + } + + int len = input.length(); + if ((len > 1) && (len < 5) && (input.endsWith("#"))) { + try { + // get the ordinal number of the sim contact + final int index = Integer.parseInt(input.substring(0, len - 1)); + + // The original code that navigated to a SIM Contacts list view did not + // highlight the requested contact correctly, a requirement for PTCRB + // certification. This behaviour is consistent with the UI paradigm + // for touch-enabled lists, so it does not make sense to try to work + // around it. Instead we fill in the the requested phone number into + // the dialer text field. + + // create the async query handler + final QueryHandler handler = new QueryHandler(context.getContentResolver()); + + // create the cookie object + final SimContactQueryCookie sc = + new SimContactQueryCookie(index - 1, handler, ADN_QUERY_TOKEN); + + // setup the cookie fields + sc.contactNum = index - 1; + sc.setTextField(textField); + + // create the progress dialog + sc.progressDialog = new ProgressDialog(context); + sc.progressDialog.setTitle(R.string.simContacts_title); + sc.progressDialog.setMessage(context.getText(R.string.simContacts_emptyLoading)); + sc.progressDialog.setIndeterminate(true); + sc.progressDialog.setCancelable(true); + sc.progressDialog.setOnCancelListener(sc); + sc.progressDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + + List subscriptionAccountHandles = + PhoneAccountUtils.getSubscriptionPhoneAccounts(context); + Context applicationContext = context.getApplicationContext(); + boolean hasUserSelectedDefault = + subscriptionAccountHandles.contains( + TelecomUtil.getDefaultOutgoingPhoneAccount( + applicationContext, PhoneAccount.SCHEME_TEL)); + + if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) { + Uri uri = TelecomUtil.getAdnUriForPhoneAccount(applicationContext, null); + handleAdnQuery(handler, sc, uri); + } else { + SelectPhoneAccountListener callback = + new HandleAdnEntryAccountSelectedCallback(applicationContext, handler, sc); + + DialogFragment dialogFragment = + SelectPhoneAccountDialogFragment.newInstance( + subscriptionAccountHandles, callback, null); + dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT); + } + + return true; + } catch (NumberFormatException ex) { + // Ignore + } + } + return false; + } + + private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri) { + if (handler == null || cookie == null || uri == null) { + Log.w(TAG, "queryAdn parameters incorrect"); + return; + } + + // display the progress dialog + cookie.progressDialog.show(); + + // run the query. + handler.startQuery( + ADN_QUERY_TOKEN, + cookie, + uri, + new String[] {ADN_PHONE_NUMBER_COLUMN_NAME}, + null, + null, + null); + + if (sPreviousAdnQueryHandler != null) { + // It is harmless to call cancel() even after the handler's gone. + sPreviousAdnQueryHandler.cancel(); + } + sPreviousAdnQueryHandler = handler; + } + + static boolean handlePinEntry(final Context context, final String input) { + if ((input.startsWith("**04") || input.startsWith("**05")) && input.endsWith("#")) { + List subscriptionAccountHandles = + PhoneAccountUtils.getSubscriptionPhoneAccounts(context); + boolean hasUserSelectedDefault = + subscriptionAccountHandles.contains( + TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL)); + + if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) { + // Don't bring up the dialog for single-SIM or if the default outgoing account is + // a subscription account. + return TelecomUtil.handleMmi(context, input, null); + } else { + SelectPhoneAccountListener listener = new HandleMmiAccountSelectedCallback(context, input); + + DialogFragment dialogFragment = + SelectPhoneAccountDialogFragment.newInstance( + subscriptionAccountHandles, listener, null); + dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT); + } + return true; + } + return false; + } + + // TODO: Use TelephonyCapabilities.getDeviceIdLabel() to get the device id label instead of a + // hard-coded string. + static boolean handleDeviceIdDisplay(Context context, String input) { + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + + if (telephonyManager != null && input.equals(MMI_IMEI_DISPLAY)) { + int labelResId = + (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) + ? R.string.imei + : R.string.meid; + + List deviceIds = new ArrayList(); + if (TelephonyManagerCompat.getPhoneCount(telephonyManager) > 1 + && CompatUtils.isMethodAvailable( + TelephonyManagerCompat.TELEPHONY_MANAGER_CLASS, "getDeviceId", Integer.TYPE)) { + for (int slot = 0; slot < telephonyManager.getPhoneCount(); slot++) { + String deviceId = telephonyManager.getDeviceId(slot); + if (!TextUtils.isEmpty(deviceId)) { + deviceIds.add(deviceId); + } + } + } else { + deviceIds.add(telephonyManager.getDeviceId()); + } + + new AlertDialog.Builder(context) + .setTitle(labelResId) + .setItems(deviceIds.toArray(new String[deviceIds.size()]), null) + .setPositiveButton(android.R.string.ok, null) + .setCancelable(false) + .show(); + return true; + } + return false; + } + + private static boolean handleRegulatoryInfoDisplay(Context context, String input) { + if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) { + Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app"); + Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO); + try { + context.startActivity(showRegInfoIntent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "startActivity() failed: " + e); + } + return true; + } + return false; + } + + public static class HandleAdnEntryAccountSelectedCallback extends SelectPhoneAccountListener { + + private final Context mContext; + private final QueryHandler mQueryHandler; + private final SimContactQueryCookie mCookie; + + public HandleAdnEntryAccountSelectedCallback( + Context context, QueryHandler queryHandler, SimContactQueryCookie cookie) { + mContext = context; + mQueryHandler = queryHandler; + mCookie = cookie; + } + + @Override + public void onPhoneAccountSelected( + PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) { + Uri uri = TelecomUtil.getAdnUriForPhoneAccount(mContext, selectedAccountHandle); + handleAdnQuery(mQueryHandler, mCookie, uri); + // TODO: Show error dialog if result isn't valid. + } + } + + public static class HandleMmiAccountSelectedCallback extends SelectPhoneAccountListener { + + private final Context mContext; + private final String mInput; + + public HandleMmiAccountSelectedCallback(Context context, String input) { + mContext = context.getApplicationContext(); + mInput = input; + } + + @Override + public void onPhoneAccountSelected( + PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) { + TelecomUtil.handleMmi(mContext, mInput, selectedAccountHandle); + } + } + + /** + * Cookie object that contains everything we need to communicate to the handler's onQuery + * Complete, as well as what we need in order to cancel the query (if requested). + * + *

Note, access to the textField field is going to be synchronized, because the user can + * request a cancel at any time through the UI. + */ + private static class SimContactQueryCookie implements DialogInterface.OnCancelListener { + + public ProgressDialog progressDialog; + public int contactNum; + + // Used to identify the query request. + private int mToken; + private QueryHandler mHandler; + + // The text field we're going to update + private EditText textField; + + public SimContactQueryCookie(int number, QueryHandler handler, int token) { + contactNum = number; + mHandler = handler; + mToken = token; + } + + /** Synchronized getter for the EditText. */ + public synchronized EditText getTextField() { + return textField; + } + + /** Synchronized setter for the EditText. */ + public synchronized void setTextField(EditText text) { + textField = text; + } + + /** + * Cancel the ADN query by stopping the operation and signaling the cookie that a cancel request + * is made. + */ + @Override + public synchronized void onCancel(DialogInterface dialog) { + // close the progress dialog + if (progressDialog != null) { + progressDialog.dismiss(); + } + + // setting the textfield to null ensures that the UI does NOT get + // updated. + textField = null; + + // Cancel the operation if possible. + mHandler.cancelOperation(mToken); + } + } + + /** + * Asynchronous query handler that services requests to look up ADNs + * + *

Queries originate from {@link #handleAdnEntry}. + */ + private static class QueryHandler extends NoNullCursorAsyncQueryHandler { + + private boolean mCanceled; + + public QueryHandler(ContentResolver cr) { + super(cr); + } + + /** Override basic onQueryComplete to fill in the textfield when we're handed the ADN cursor. */ + @Override + protected void onNotNullableQueryComplete(int token, Object cookie, Cursor c) { + try { + sPreviousAdnQueryHandler = null; + if (mCanceled) { + return; + } + + SimContactQueryCookie sc = (SimContactQueryCookie) cookie; + + // close the progress dialog. + sc.progressDialog.dismiss(); + + // get the EditText to update or see if the request was cancelled. + EditText text = sc.getTextField(); + + // if the TextView is valid, and the cursor is valid and positionable on the + // Nth number, then we update the text field and display a toast indicating the + // caller name. + if ((c != null) && (text != null) && (c.moveToPosition(sc.contactNum))) { + String name = c.getString(c.getColumnIndexOrThrow(ADN_NAME_COLUMN_NAME)); + String number = c.getString(c.getColumnIndexOrThrow(ADN_PHONE_NUMBER_COLUMN_NAME)); + + // fill the text in. + text.getText().replace(0, 0, number); + + // display the name as a toast + Context context = sc.progressDialog.getContext(); + CharSequence msg = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + context.getResources(), R.string.menu_callNumber, name); + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } + } finally { + MoreCloseables.closeQuietly(c); + } + } + + public void cancel() { + mCanceled = true; + // Ask AsyncQueryHandler to cancel the whole request. This will fail when the query is + // already started. + cancelOperation(ADN_QUERY_TOKEN); + } + } +} diff --git a/java/com/android/dialer/app/alert/AlertManager.java b/java/com/android/dialer/app/alert/AlertManager.java new file mode 100644 index 0000000000000000000000000000000000000000..ec61802627a971d76198ff42ea7820f7edf486ac --- /dev/null +++ b/java/com/android/dialer/app/alert/AlertManager.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.alert; + +import android.view.View; + +/** Manages "alerts" to gain the user's attention. */ +public interface AlertManager { + + /** Inflates layoutId into a view that is ready to be inserted as an alert. */ + View inflate(int layoutId); + + void add(View view); + + void clear(); +} diff --git a/java/com/android/dialer/app/bindings/DialerBindings.java b/java/com/android/dialer/app/bindings/DialerBindings.java new file mode 100644 index 0000000000000000000000000000000000000000..e1f517860a59cadc1ee87d26fbd049d58cb7a400 --- /dev/null +++ b/java/com/android/dialer/app/bindings/DialerBindings.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.bindings; + +import com.android.dialer.common.ConfigProvider; + +/** This interface allows the container application to customize the dialer. */ +public interface DialerBindings { + + ConfigProvider getConfigProvider(); +} diff --git a/java/com/android/dialer/app/bindings/DialerBindingsFactory.java b/java/com/android/dialer/app/bindings/DialerBindingsFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..9f209f99ec72a54c542e1092d362ea5bf95c01ba --- /dev/null +++ b/java/com/android/dialer/app/bindings/DialerBindingsFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.bindings; + +/** + * This interface should be implementated by the Application subclass. It allows the dialer module + * to get references to the DialerBindings. + */ +public interface DialerBindingsFactory { + + DialerBindings newDialerBindings(); +} diff --git a/java/com/android/dialer/app/bindings/DialerBindingsStub.java b/java/com/android/dialer/app/bindings/DialerBindingsStub.java new file mode 100644 index 0000000000000000000000000000000000000000..f56743fa533f5d604c828b26aa772b493075c795 --- /dev/null +++ b/java/com/android/dialer/app/bindings/DialerBindingsStub.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.bindings; + +import com.android.dialer.common.ConfigProvider; + +/** Default implementation for dialer bindings. */ +public class DialerBindingsStub implements DialerBindings { + private ConfigProvider configProvider; + + @Override + public ConfigProvider getConfigProvider() { + if (configProvider == null) { + configProvider = + new ConfigProvider() { + @Override + public String getString(String key, String defaultValue) { + return defaultValue; + } + + @Override + public long getLong(String key, long defaultValue) { + return defaultValue; + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + return defaultValue; + } + }; + } + return configProvider; + } +} diff --git a/java/com/android/dialer/app/calllog/BlockReportSpamListener.java b/java/com/android/dialer/app/calllog/BlockReportSpamListener.java new file mode 100644 index 0000000000000000000000000000000000000000..66f40bcd72bab3a9e199328cafe0072b8f8a332c --- /dev/null +++ b/java/com/android/dialer/app/calllog/BlockReportSpamListener.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.app.FragmentManager; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.support.v7.widget.RecyclerView; +import com.android.dialer.blocking.BlockReportSpamDialogs; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ReportingLocation; +import com.android.dialer.spam.Spam; + +/** Listener to show dialogs for block and report spam actions. */ +public class BlockReportSpamListener implements CallLogListItemViewHolder.OnClickListener { + + private final Context mContext; + private final FragmentManager mFragmentManager; + private final RecyclerView.Adapter mAdapter; + private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + + public BlockReportSpamListener( + Context context, + FragmentManager fragmentManager, + RecyclerView.Adapter adapter, + FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { + mContext = context; + mFragmentManager = fragmentManager; + mAdapter = adapter; + mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler; + } + + @Override + public void onBlockReportSpam( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType) { + BlockReportSpamDialogs.BlockReportSpamDialogFragment.newInstance( + displayNumber, + Spam.get(mContext).isDialogReportSpamCheckedByDefault(), + new BlockReportSpamDialogs.OnSpamDialogClickListener() { + @Override + public void onClick(boolean isSpamChecked) { + LogUtil.i("BlockReportSpamListener.onBlockReportSpam", "onClick"); + if (isSpamChecked && Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression( + DialerImpression.Type + .REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG); + Spam.get(mContext) + .reportSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mFilteredNumberAsyncQueryHandler.blockNumber( + new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() { + @Override + public void onBlockComplete(Uri uri) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER); + mAdapter.notifyDataSetChanged(); + } + }, + number, + countryIso); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.BLOCK_REPORT_SPAM_DIALOG_TAG); + } + + @Override + public void onBlock( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType) { + BlockReportSpamDialogs.BlockDialogFragment.newInstance( + displayNumber, + Spam.get(mContext).isSpamEnabled(), + new BlockReportSpamDialogs.OnConfirmListener() { + @Override + public void onClick() { + LogUtil.i("BlockReportSpamListener.onBlock", "onClick"); + if (Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression( + DialerImpression.Type + .DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER); + Spam.get(mContext) + .reportSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mFilteredNumberAsyncQueryHandler.blockNumber( + new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() { + @Override + public void onBlockComplete(Uri uri) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER); + mAdapter.notifyDataSetChanged(); + } + }, + number, + countryIso); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.BLOCK_DIALOG_TAG); + } + + @Override + public void onUnblock( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType, + final boolean isSpam, + final Integer blockId) { + BlockReportSpamDialogs.UnblockDialogFragment.newInstance( + displayNumber, + isSpam, + new BlockReportSpamDialogs.OnConfirmListener() { + @Override + public void onClick() { + LogUtil.i("BlockReportSpamListener.onUnblock", "onClick"); + if (isSpam && Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER); + Spam.get(mContext) + .reportNotSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mFilteredNumberAsyncQueryHandler.unblock( + new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() { + @Override + public void onUnblockComplete(int rows, ContentValues values) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.USER_ACTION_UNBLOCKED_NUMBER); + mAdapter.notifyDataSetChanged(); + } + }, + blockId); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.UNBLOCK_DIALOG_TAG); + } + + @Override + public void onReportNotSpam( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType) { + BlockReportSpamDialogs.ReportNotSpamDialogFragment.newInstance( + displayNumber, + new BlockReportSpamDialogs.OnConfirmListener() { + @Override + public void onClick() { + LogUtil.i("BlockReportSpamListener.onReportNotSpam", "onClick"); + if (Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM); + Spam.get(mContext) + .reportNotSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mAdapter.notifyDataSetChanged(); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.NOT_SPAM_DIALOG_TAG); + } +} diff --git a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..ab6ef736279782032d78ab4a83323dd0ba35fb0c --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.icu.lang.UCharacter; +import android.icu.text.BreakIterator; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.CallLog.Calls; +import android.text.format.DateUtils; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import java.util.ArrayList; +import java.util.Locale; + +/** Adapter for a ListView containing history items from the details of a call. */ +public class CallDetailHistoryAdapter extends BaseAdapter { + + /** Each history item shows the detail of a call. */ + private static final int VIEW_TYPE_HISTORY_ITEM = 1; + + private final Context mContext; + private final LayoutInflater mLayoutInflater; + private final CallTypeHelper mCallTypeHelper; + private final PhoneCallDetails[] mPhoneCallDetails; + + /** List of items to be concatenated together for duration strings. */ + private ArrayList mDurationItems = new ArrayList<>(); + + public CallDetailHistoryAdapter( + Context context, + LayoutInflater layoutInflater, + CallTypeHelper callTypeHelper, + PhoneCallDetails[] phoneCallDetails) { + mContext = context; + mLayoutInflater = layoutInflater; + mCallTypeHelper = callTypeHelper; + mPhoneCallDetails = phoneCallDetails; + } + + @Override + public boolean isEnabled(int position) { + // None of history will be clickable. + return false; + } + + @Override + public int getCount() { + return mPhoneCallDetails.length; + } + + @Override + public Object getItem(int position) { + return mPhoneCallDetails[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public int getItemViewType(int position) { + return VIEW_TYPE_HISTORY_ITEM; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // Make sure we have a valid convertView to start with + final View result = + convertView == null + ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false) + : convertView; + + PhoneCallDetails details = mPhoneCallDetails[position]; + CallTypeIconsView callTypeIconView = + (CallTypeIconsView) result.findViewById(R.id.call_type_icon); + TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text); + TextView dateView = (TextView) result.findViewById(R.id.date); + TextView durationView = (TextView) result.findViewById(R.id.duration); + + int callType = details.callTypes[0]; + boolean isVideoCall = + (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO + && CallUtil.isVideoEnabled(mContext); + boolean isPulledCall = + (details.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY; + + callTypeIconView.clear(); + callTypeIconView.add(callType); + callTypeIconView.setShowVideo(isVideoCall); + callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall)); + // Set the date. + dateView.setText(formatDate(details.date)); + // Set the duration + if (Calls.VOICEMAIL_TYPE == callType || CallTypeHelper.isMissedCallType(callType)) { + durationView.setVisibility(View.GONE); + } else { + durationView.setVisibility(View.VISIBLE); + durationView.setText(formatDurationAndDataUsage(details.duration, details.dataUsage)); + } + + return result; + } + + /** + * Formats the provided date into a value suitable for display in the current locale. + * + *

For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016 + * may 25,20:02". + * + *

For pre-N devices, the returned value may not start with a capital if the local convention + * is to not capitalize day names. On N+ devices, the returned value is always capitalized. + */ + private CharSequence formatDate(long callDateMillis) { + CharSequence dateValue = + DateUtils.formatDateRange( + mContext, + callDateMillis /* startDate */, + callDateMillis /* endDate */, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_WEEKDAY + | DateUtils.FORMAT_SHOW_YEAR); + + // We want the beginning of the date string to be capitalized, even if the word at the beginning + // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba” + // (not capitalized). To handle this issue we apply title casing to the start of the sentence so + // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02". + // + // The ICU library was not available in Android until N, so we can only do this in N+ devices. + // Pre-N devices will still see incorrect capitalization in some languages. + if (VERSION.SDK_INT < VERSION_CODES.N) { + return dateValue; + } + + // Using the ICU library is safer than just applying toUpperCase() on the first letter of the + // word because in some languages, there can be multiple starting characters which should be + // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be + // capitalized together. + + // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the + // month ("May") are not lower-cased as part of the conversion. + return UCharacter.toTitleCase( + Locale.getDefault(), + dateValue.toString(), + BreakIterator.getSentenceInstance(), + UCharacter.TITLECASE_NO_LOWERCASE); + } + + private CharSequence formatDuration(long elapsedSeconds) { + long minutes = 0; + long seconds = 0; + + if (elapsedSeconds >= 60) { + minutes = elapsedSeconds / 60; + elapsedSeconds -= minutes * 60; + seconds = elapsedSeconds; + return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds); + } else { + seconds = elapsedSeconds; + return mContext.getString(R.string.callDetailsShortDurationFormat, seconds); + } + } + + /** + * Formats a string containing the call duration and the data usage (if specified). + * + * @param elapsedSeconds Total elapsed seconds. + * @param dataUsage Data usage in bytes, or null if not specified. + * @return String containing call duration and data usage. + */ + private CharSequence formatDurationAndDataUsage(long elapsedSeconds, Long dataUsage) { + CharSequence duration = formatDuration(elapsedSeconds); + + if (dataUsage != null) { + mDurationItems.clear(); + mDurationItems.add(duration); + mDurationItems.add(Formatter.formatShortFileSize(mContext, dataUsage)); + + return DialerUtils.join(mDurationItems); + } else { + return duration; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..ea09a8c0af5d94385b201e82aa1796cf4c87fe70 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -0,0 +1,915 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.app.Activity; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Trace; +import android.provider.CallLog; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.dialer.app.Bindings; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.common.Assert; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.common.LogUtil; +import com.android.dialer.enrichedcall.EnrichedCallCapabilities; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.phonenumbercache.CallLogQuery; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.spam.Spam; +import com.android.dialer.util.PermissionsUtil; +import java.util.Map; +import java.util.Set; + +/** Adapter class to fill in data for the Call Log. */ +public class CallLogAdapter extends GroupingListAdapter + implements GroupCreator, OnVoicemailDeletedListener, CapabilitiesListener { + + // Types of activities the call log adapter is used for + public static final int ACTIVITY_TYPE_CALL_LOG = 1; + public static final int ACTIVITY_TYPE_DIALTACTS = 2; + private static final int NO_EXPANDED_LIST_ITEM = -1; + public static final int ALERT_POSITION = 0; + private static final int VIEW_TYPE_ALERT = 1; + private static final int VIEW_TYPE_CALLLOG = 2; + + private static final String KEY_EXPANDED_POSITION = "expanded_position"; + private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; + + public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data"; + + protected final Activity mActivity; + protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + /** Cache for repeated requests to Telecom/Telephony. */ + protected final CallLogCache mCallLogCache; + + private final CallFetcher mCallFetcher; + private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + private final int mActivityType; + + /** Instance of helper class for managing views. */ + private final CallLogListItemHelper mCallLogListItemHelper; + /** Helper to group call log entries. */ + private final CallLogGroupBuilder mCallLogGroupBuilder; + + private final AsyncTaskExecutor mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); + private ContactInfoCache mContactInfoCache; + // Tracks the position of the currently expanded list item. + private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + // Tracks the rowId of the currently expanded list item, so the position can be updated if there + // are any changes to the call log entries, such as additions or removals. + private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; + + private final CallLogAlertManager mCallLogAlertManager; + /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */ + private final View.OnClickListener mExpandCollapseListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); + if (viewHolder == null) { + return; + } + + if (mVoicemailPlaybackPresenter != null) { + // Always reset the voicemail playback state on expand or collapse. + mVoicemailPlaybackPresenter.resetAll(); + } + + if (viewHolder.rowId == mCurrentlyExpandedRowId) { + // Hide actions, if the clicked item is the expanded item. + viewHolder.showActions(false); + + mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; + } else { + if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) { + CallLogAsyncTaskUtil.markCallAsRead(mActivity, viewHolder.callIds); + if (mActivityType == ACTIVITY_TYPE_DIALTACTS) { + ((DialtactsActivity) v.getContext()).updateTabUnreadCounts(); + } + } + expandViewHolderActions(viewHolder); + } + } + }; + + /** + * A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead + * if removing an item, it will be shown as an invisible view. This simplifies the calculation of + * item position. + */ + @NonNull private Set mHiddenRowIds = new ArraySet<>(); + /** + * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo + * timeout, all of the pending URIs will be deleted. + * + *

TODO: move this and OnVoicemailDeletedListener to somewhere like {@link + * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with + * hidden item or what to hide. + */ + @NonNull private final Set mHiddenItemUris = new ArraySet<>(); + + private CallLogListItemViewHolder.OnClickListener mBlockReportSpamListener; + /** + * Map, keyed by call Id, used to track the day group for a call. As call log entries are put into + * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are + * also assigned a secondary "day group". This map tracks the day group assigned to all calls in + * the call log. This information is used to trigger the display of a day group header above the + * call log entry at the start of a day group. Note: Multiple calls are grouped into a single + * primary "call group" in the call log, and the cursor used to bind rows includes all of these + * calls. When determining if a day group change has occurred it is necessary to look at the last + * entry in the call log to determine its day group. This map provides a means of determining the + * previous day group without having to reverse the cursor to the start of the previous day call + * log entry. + */ + private Map mDayGroups = new ArrayMap<>(); + + private boolean mLoading = true; + private ContactsPreferences mContactsPreferences; + + private boolean mIsSpamEnabled; + + @NonNull private final EnrichedCallManager mEnrichedCallManager; + + public CallLogAdapter( + Activity activity, + ViewGroup alertContainer, + CallFetcher callFetcher, + CallLogCache callLogCache, + ContactInfoCache contactInfoCache, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + int activityType) { + super(); + + mActivity = activity; + mCallFetcher = callFetcher; + mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; + if (mVoicemailPlaybackPresenter != null) { + mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); + } + + mActivityType = activityType; + + mContactInfoCache = contactInfoCache; + + if (!PermissionsUtil.hasContactsPermissions(activity)) { + mContactInfoCache.disableRequestProcessing(); + } + + Resources resources = mActivity.getResources(); + + mCallLogCache = callLogCache; + + PhoneCallDetailsHelper phoneCallDetailsHelper = + new PhoneCallDetailsHelper(mActivity, resources, mCallLogCache); + mCallLogListItemHelper = + new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache); + mCallLogGroupBuilder = new CallLogGroupBuilder(this); + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mActivity); + + mContactsPreferences = new ContactsPreferences(mActivity); + + mBlockReportSpamListener = + new BlockReportSpamListener( + mActivity, + ((Activity) mActivity).getFragmentManager(), + this, + mFilteredNumberAsyncQueryHandler); + setHasStableIds(true); + + mCallLogAlertManager = + new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer); + mEnrichedCallManager = EnrichedCallManager.Accessor.getInstance(activity.getApplication()); + } + + private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { + if (!TextUtils.isEmpty(viewHolder.voicemailUri)) { + Logger.get(mActivity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY); + } + + int lastExpandedPosition = mCurrentlyExpandedPosition; + // Show the actions for the clicked list item. + viewHolder.showActions(true); + mCurrentlyExpandedPosition = viewHolder.getAdapterPosition(); + mCurrentlyExpandedRowId = viewHolder.rowId; + + // If another item is expanded, notify it that it has changed. Its actions will be + // hidden when it is re-binded because we change mCurrentlyExpandedRowId above. + if (lastExpandedPosition != RecyclerView.NO_POSITION) { + notifyItemChanged(lastExpandedPosition); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition); + outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId); + } + + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mCurrentlyExpandedPosition = + savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); + mCurrentlyExpandedRowId = + savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); + } + } + + /** Requery on background thread when {@link Cursor} changes. */ + @Override + protected void onContentChanged() { + mCallFetcher.fetchCalls(); + } + + public void setLoading(boolean loading) { + mLoading = loading; + } + + public boolean isEmpty() { + if (mLoading) { + // We don't want the empty state to show when loading. + return false; + } else { + return getItemCount() == 0; + } + } + + public void clearFilteredNumbersCache() { + mFilteredNumberAsyncQueryHandler.clearCache(); + } + + public void onResume() { + if (PermissionsUtil.hasPermission(mActivity, android.Manifest.permission.READ_CONTACTS)) { + mContactInfoCache.start(); + } + mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); + mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled(); + mEnrichedCallManager.registerCapabilitiesListener(this); + notifyDataSetChanged(); + } + + public void onPause() { + pauseCache(); + for (Uri uri : mHiddenItemUris) { + CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null); + } + mEnrichedCallManager.unregisterCapabilitiesListener(this); + } + + public void onStop() { + mEnrichedCallManager.clearCachedData(); + } + + public CallLogAlertManager getAlertManager() { + return mCallLogAlertManager; + } + + @VisibleForTesting + /* package */ void pauseCache() { + mContactInfoCache.stop(); + mCallLogCache.reset(); + } + + @Override + protected void addGroups(Cursor cursor) { + mCallLogGroupBuilder.addGroups(cursor); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_ALERT) { + return mCallLogAlertManager.createViewHolder(parent); + } + return createCallLogEntryViewHolder(parent); + } + + /** + * Creates a new call log entry {@link ViewHolder}. + * + * @param parent the parent view. + * @return The {@link ViewHolder}. + */ + private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(mActivity); + View view = inflater.inflate(R.layout.call_log_list_item, parent, false); + CallLogListItemViewHolder viewHolder = + CallLogListItemViewHolder.create( + view, + mActivity, + mBlockReportSpamListener, + mExpandCollapseListener, + mCallLogCache, + mCallLogListItemHelper, + mVoicemailPlaybackPresenter); + + viewHolder.callLogEntryView.setTag(viewHolder); + + viewHolder.primaryActionView.setTag(viewHolder); + + return viewHolder; + } + + /** + * Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times + * when Dialer starts up for a single call log entry and should not. It invokes cross-process + * methods and the repeat execution can get costly. + * + * @param viewHolder The view corresponding to this entry. + * @param position The position of the entry. + */ + @Override + public void onBindViewHolder(ViewHolder viewHolder, int position) { + Trace.beginSection("onBindViewHolder: " + position); + switch (getItemViewType(position)) { + case VIEW_TYPE_ALERT: + //Do nothing + break; + default: + bindCallLogListViewHolder(viewHolder, position); + break; + } + Trace.endSection(); + } + + @Override + public void onViewRecycled(ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { + CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; + if (views.asyncTask != null) { + views.asyncTask.cancel(true); + } + } + } + + @Override + public void onViewAttachedToWindow(ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { + ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true; + } + } + + @Override + public void onViewDetachedFromWindow(ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { + ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false; + } + } + + /** + * Binds the view holder for the call log list item view. + * + * @param viewHolder The call log list item view holder. + * @param position The position of the list item. + */ + private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) { + Cursor c = (Cursor) getItem(position); + if (c == null) { + return; + } + CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; + views.isLoaded = false; + PhoneCallDetails details = createPhoneCallDetails(c, getGroupSize(position), views); + if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) { + views.callLogEntryView.setVisibility(View.GONE); + views.dayGroupHeader.setVisibility(View.GONE); + return; + } else { + views.callLogEntryView.setVisibility(View.VISIBLE); + // dayGroupHeader will be restored after loadAndRender() if it is needed. + } + if (mCurrentlyExpandedRowId == views.rowId) { + views.inflateActionViewStub(); + } + loadAndRender(views, views.rowId, details); + } + + private void loadAndRender( + final CallLogListItemViewHolder views, final long rowId, final PhoneCallDetails details) { + // Reset block and spam information since this view could be reused which may contain + // outdated data. + views.isSpam = false; + views.blockId = null; + views.isSpamFeatureEnabled = false; + views.isCallComposerCapable = + isCallComposerCapable(PhoneNumberUtils.formatNumberToE164(views.number, views.countryIso)); + final AsyncTask loadDataTask = + new AsyncTask() { + @Override + protected Boolean doInBackground(Void... params) { + views.blockId = + mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly( + views.number, views.countryIso); + details.isBlocked = views.blockId != null; + if (isCancelled()) { + return false; + } + if (mIsSpamEnabled) { + views.isSpamFeatureEnabled = true; + // Only display the call as a spam call if there are incoming calls in the list. + // Call log cards with only outgoing calls should never be displayed as spam. + views.isSpam = + details.hasIncomingCalls() + && Spam.get(mActivity) + .checkSpamStatusSynchronous(views.number, views.countryIso); + details.isSpam = views.isSpam; + if (isCancelled()) { + return false; + } + return loadData(views, rowId, details); + } else { + return loadData(views, rowId, details); + } + } + + @Override + protected void onPostExecute(Boolean success) { + views.isLoaded = true; + if (success) { + int currentGroup = getDayGroupForCall(views.rowId); + if (currentGroup != details.previousGroup) { + views.dayGroupHeaderVisibility = View.VISIBLE; + views.dayGroupHeaderText = getGroupDescription(currentGroup); + } else { + views.dayGroupHeaderVisibility = View.GONE; + } + render(views, details, rowId); + } + } + }; + + views.asyncTask = loadDataTask; + mAsyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask); + } + + @MainThread + private boolean isCallComposerCapable(@Nullable String e164Number) { + if (e164Number == null) { + return false; + } + + EnrichedCallCapabilities capabilities = mEnrichedCallManager.getCapabilities(e164Number); + if (capabilities == null) { + mEnrichedCallManager.requestCapabilities(e164Number); + return false; + } + return capabilities.supportsCallComposer(); + } + + /** + * Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main + * thread since cursor is not thread safe. + */ + @MainThread + private PhoneCallDetails createPhoneCallDetails( + Cursor cursor, int count, final CallLogListItemViewHolder views) { + Assert.isMainThread(); + final String number = cursor.getString(CallLogQuery.NUMBER); + final String postDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : ""; + final String viaNumber = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; + final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); + final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor); + final PhoneCallDetails details = + new PhoneCallDetails(number, numberPresentation, postDialDigits); + details.viaNumber = viaNumber; + details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); + details.date = cursor.getLong(CallLogQuery.DATE); + details.duration = cursor.getLong(CallLogQuery.DURATION); + details.features = getCallFeatures(cursor, count); + details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION); + details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION); + details.callTypes = getCallTypes(cursor, count); + + details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); + details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID); + details.cachedContactInfo = cachedContactInfo; + + if (!cursor.isNull(CallLogQuery.DATA_USAGE)) { + details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE); + } + + views.rowId = cursor.getLong(CallLogQuery.ID); + // Stash away the Ids of the calls so that we can support deleting a row in the call log. + views.callIds = getCallIds(cursor, count); + details.previousGroup = getPreviousDayGroup(cursor); + + // Store values used when the actions ViewStub is inflated on expansion. + views.number = number; + views.countryIso = details.countryIso; + views.postDialDigits = details.postDialDigits; + views.numberPresentation = numberPresentation; + + if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE + || details.callTypes[0] == CallLog.Calls.MISSED_TYPE) { + details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1; + } + views.callType = cursor.getInt(CallLogQuery.CALL_TYPE); + views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI); + + return details; + } + + /** + * Load data for call log. Any expensive operation should be put here to avoid blocking main + * thread. Do NOT put any cursor operation here since it's not thread safe. + */ + @WorkerThread + private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) { + Assert.isWorkerThread(); + if (rowId != views.rowId) { + LogUtil.i( + "CallLogAdapter.loadData", + "rowId of viewHolder changed after load task is issued, aborting load"); + return false; + } + + final PhoneAccountHandle accountHandle = + PhoneAccountUtils.getAccount(details.accountComponentName, details.accountId); + + final boolean isVoicemailNumber = + mCallLogCache.isVoicemailNumber(accountHandle, details.number); + + // Note: Binding of the action buttons is done as required in configureActionViews when the + // user expands the actions ViewStub. + + ContactInfo info = ContactInfo.EMPTY; + if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation) + && !isVoicemailNumber) { + // Lookup contacts with this number + // Only do remote lookup in first 5 rows. + info = + mContactInfoCache.getValue( + details.number + details.postDialDigits, + details.countryIso, + details.cachedContactInfo, + rowId + < Bindings.get(mActivity) + .getConfigProvider() + .getLong("number_of_call_to_do_remote_lookup", 5L)); + } + CharSequence formattedNumber = + info.formattedNumber == null + ? null + : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber); + details.updateDisplayNumber(mActivity, formattedNumber, isVoicemailNumber); + + views.displayNumber = details.displayNumber; + views.accountHandle = accountHandle; + details.accountHandle = accountHandle; + + if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) { + details.contactUri = info.lookupUri; + details.namePrimary = info.name; + details.nameAlternative = info.nameAlternative; + details.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); + details.numberType = info.type; + details.numberLabel = info.label; + details.photoUri = info.photoUri; + details.sourceType = info.sourceType; + details.objectId = info.objectId; + details.contactUserType = info.userType; + } + + views.info = info; + views.numberType = + (String) + Phone.getTypeLabel(mActivity.getResources(), details.numberType, details.numberLabel); + + mCallLogListItemHelper.updatePhoneCallDetails(details); + return true; + } + + /** + * Render item view given position. This is running on UI thread so DO NOT put any expensive + * operation into it. + */ + @MainThread + private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) { + Assert.isMainThread(); + if (rowId != views.rowId) { + LogUtil.i( + "CallLogAdapter.render", + "rowId of viewHolder changed after load task is issued, aborting render"); + return; + } + + // Default case: an item in the call log. + views.primaryActionView.setVisibility(View.VISIBLE); + views.workIconView.setVisibility( + details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE); + + mCallLogListItemHelper.setPhoneCallDetails(views, details); + if (mCurrentlyExpandedRowId == views.rowId) { + // In case ViewHolders were added/removed, update the expanded position if the rowIds + // match so that we can restore the correct expanded state on rebind. + mCurrentlyExpandedPosition = views.getAdapterPosition(); + views.showActions(true); + } else { + views.showActions(false); + } + views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility); + views.dayGroupHeader.setText(views.dayGroupHeaderText); + } + + @Override + public int getItemCount() { + return super.getItemCount() + (mCallLogAlertManager.isEmpty() ? 0 : 1); + } + + @Override + public int getItemViewType(int position) { + if (position == ALERT_POSITION && !mCallLogAlertManager.isEmpty()) { + return VIEW_TYPE_ALERT; + } + return VIEW_TYPE_CALLLOG; + } + + /** + * Retrieves an item at the specified position, taking into account the presence of a promo card. + * + * @param position The position to retrieve. + * @return The item at that position. + */ + @Override + public Object getItem(int position) { + return super.getItem(position - (mCallLogAlertManager.isEmpty() ? 0 : 1)); + } + + @Override + public long getItemId(int position) { + Cursor cursor = (Cursor) getItem(position); + if (cursor != null) { + return cursor.getLong(CallLogQuery.ID); + } else { + return 0; + } + } + + @Override + public int getGroupSize(int position) { + return super.getGroupSize(position - (mCallLogAlertManager.isEmpty() ? 0 : 1)); + } + + protected boolean isCallLogActivity() { + return mActivityType == ACTIVITY_TYPE_CALL_LOG; + } + + /** + * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user + * clicks the delete button, the deleted item is temporarily hidden from the list. If a user + * clicks delete on a second item before the first item's undo option has expired, the first item + * is immediately deleted so that only one item can be "undoed" at a time. + */ + @Override + public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) { + mHiddenRowIds.add(viewHolder.rowId); + // Save the new hidden item uri in case the activity is suspend before the undo has timed out. + mHiddenItemUris.add(uri); + + collapseExpandedCard(); + notifyItemChanged(viewHolder.getAdapterPosition()); + // The next item might have to update its day group label + notifyItemChanged(viewHolder.getAdapterPosition() + 1); + } + + private void collapseExpandedCard() { + mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; + mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + } + + /** When the list is changing all stored position is no longer valid. */ + public void invalidatePositions() { + mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + } + + /** When the user clicks "undo", the hidden item is unhidden. */ + @Override + public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) { + mHiddenItemUris.remove(uri); + mHiddenRowIds.remove(rowId); + notifyItemChanged(adapterPosition); + // The next item might have to update its day group label + notifyItemChanged(adapterPosition + 1); + } + + /** This callback signifies that a database deletion has completed. */ + @Override + public void onVoicemailDeletedInDatabase(long rowId, Uri uri) { + mHiddenItemUris.remove(uri); + } + + /** + * Retrieves the day group of the previous call in the call log. Used to determine if the day + * group has changed and to trigger display of the day group text. + * + * @param cursor The call log cursor. + * @return The previous day group, or DAY_GROUP_NONE if this is the first call. + */ + private int getPreviousDayGroup(Cursor cursor) { + // We want to restore the position in the cursor at the end. + int startingPosition = cursor.getPosition(); + moveToPreviousNonHiddenRow(cursor); + if (cursor.isBeforeFirst()) { + cursor.moveToPosition(startingPosition); + return CallLogGroupBuilder.DAY_GROUP_NONE; + } + int result = getDayGroupForCall(cursor.getLong(CallLogQuery.ID)); + cursor.moveToPosition(startingPosition); + return result; + } + + private void moveToPreviousNonHiddenRow(Cursor cursor) { + while (cursor.moveToPrevious() && mHiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {} + } + + /** + * Given a call Id, look up the day group that the call belongs to. The day group data is + * populated in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}. + * + * @param callId The call to retrieve the day group for. + * @return The day group for the call. + */ + @MainThread + private int getDayGroupForCall(long callId) { + Integer result = mDayGroups.get(callId); + if (result != null) { + return result; + } + return CallLogGroupBuilder.DAY_GROUP_NONE; + } + + /** + * Returns the call types for the given number of items in the cursor. + * + *

It uses the next {@code count} rows in the cursor to extract the types. + * + *

It position in the cursor is unchanged by this function. + */ + private static int[] getCallTypes(Cursor cursor, int count) { + int position = cursor.getPosition(); + int[] callTypes = new int[count]; + for (int index = 0; index < count; ++index) { + callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); + cursor.moveToNext(); + } + cursor.moveToPosition(position); + return callTypes; + } + + /** + * Determine the features which were enabled for any of the calls that make up a call log entry. + * + * @param cursor The cursor. + * @param count The number of calls for the current call log entry. + * @return The features. + */ + private int getCallFeatures(Cursor cursor, int count) { + int features = 0; + int position = cursor.getPosition(); + for (int index = 0; index < count; ++index) { + features |= cursor.getInt(CallLogQuery.FEATURES); + cursor.moveToNext(); + } + cursor.moveToPosition(position); + return features; + } + + /** + * Sets whether processing of requests for contact details should be enabled. + * + *

This method should be called in tests to disable such processing of requests when not + * needed. + */ + @VisibleForTesting + void disableRequestProcessingForTest() { + // TODO: Remove this and test the cache directly. + mContactInfoCache.disableRequestProcessing(); + } + + @VisibleForTesting + void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { + // TODO: Remove this and test the cache directly. + mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); + } + + /** + * Stores the day group associated with a call in the call log. + * + * @param rowId The row Id of the current call. + * @param dayGroup The day group the call belongs in. + */ + @Override + @MainThread + public void setDayGroup(long rowId, int dayGroup) { + if (!mDayGroups.containsKey(rowId)) { + mDayGroups.put(rowId, dayGroup); + } + } + + /** Clears the day group associations on re-bind of the call log. */ + @Override + @MainThread + public void clearDayGroups() { + mDayGroups.clear(); + } + + /** + * Retrieves the call Ids represented by the current call log row. + * + * @param cursor Call log cursor to retrieve call Ids from. + * @param groupSize Number of calls associated with the current call log row. + * @return Array of call Ids. + */ + private long[] getCallIds(final Cursor cursor, final int groupSize) { + // We want to restore the position in the cursor at the end. + int startingPosition = cursor.getPosition(); + long[] ids = new long[groupSize]; + // Copy the ids of the rows in the group. + for (int index = 0; index < groupSize; ++index) { + ids[index] = cursor.getLong(CallLogQuery.ID); + cursor.moveToNext(); + } + cursor.moveToPosition(startingPosition); + return ids; + } + + /** + * Determines the description for a day group. + * + * @param group The day group to retrieve the description for. + * @return The day group description. + */ + private CharSequence getGroupDescription(int group) { + if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { + return mActivity.getResources().getString(R.string.call_log_header_today); + } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { + return mActivity.getResources().getString(R.string.call_log_header_yesterday); + } else { + return mActivity.getResources().getString(R.string.call_log_header_other); + } + } + + @Override + public void onCapabilitiesUpdated() { + notifyDataSetChanged(); + } + + /** Interface used to initiate a refresh of the content. */ + public interface CallFetcher { + + void fetchCalls(); + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAlertManager.java b/java/com/android/dialer/app/calllog/CallLogAlertManager.java new file mode 100644 index 0000000000000000000000000000000000000000..40b30f001edd7d0a9e660cc459d36aeaf4014a3c --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAlertManager.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.app.alert.AlertManager; +import com.android.dialer.common.Assert; + +/** Manages "alerts" to be shown at the top of an call log to gain the user's attention. */ +public class CallLogAlertManager implements AlertManager { + + private final CallLogAdapter adapter; + private final View view; + private final LayoutInflater inflater; + private final ViewGroup parent; + private final ViewGroup container; + + public CallLogAlertManager(CallLogAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + this.adapter = adapter; + this.inflater = inflater; + this.parent = parent; + view = inflater.inflate(R.layout.call_log_alert_item, parent, false); + container = (ViewGroup) view.findViewById(R.id.container); + } + + @Override + public View inflate(int layoutId) { + return inflater.inflate(layoutId, container, false); + } + + public RecyclerView.ViewHolder createViewHolder(ViewGroup parent) { + Assert.checkArgument( + parent == this.parent, + "createViewHolder should be called with the same parent in constructor"); + return new AlertViewHolder(view); + } + + public boolean isEmpty() { + return container.getChildCount() == 0; + } + + public boolean contains(View view) { + return container.indexOfChild(view) != -1; + } + + @Override + public void clear() { + container.removeAllViews(); + adapter.notifyItemRemoved(CallLogAdapter.ALERT_POSITION); + } + + @Override + public void add(View view) { + if (contains(view)) { + return; + } + container.addView(view); + if (container.getChildCount() == 1) { + // Was empty before + adapter.notifyItemInserted(CallLogAdapter.ALERT_POSITION); + } + } + + /** + * Does nothing. The view this ViewHolder show is directly managed by {@link CallLogAlertManager} + */ + private static class AlertViewHolder extends RecyclerView.ViewHolder { + private AlertViewHolder(View view) { + super(view); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAsync.java b/java/com/android/dialer/app/calllog/CallLogAsync.java new file mode 100644 index 0000000000000000000000000000000000000000..f62deca890f1400a4d94080f705bc80dbf614237 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAsync.java @@ -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. + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.os.AsyncTask; +import android.provider.CallLog.Calls; +import com.android.dialer.common.Assert; + +/** + * Class to access the call log asynchronously to avoid carrying out database operations on the UI + * thread, using an {@link AsyncTask}. + * + *

 Typical usage: ==============
+ *
+ * // From an activity... String mLastNumber = "";
+ *
+ * CallLogAsync log = new CallLogAsync();
+ *
+ * CallLogAsync.GetLastOutgoingCallArgs lastCallArgs = new CallLogAsync.GetLastOutgoingCallArgs(
+ * this, new CallLogAsync.OnLastOutgoingCallComplete() { public void lastOutgoingCall(String number)
+ * { mLastNumber = number; } }); log.getLastOutgoingCall(lastCallArgs); 
+ */ +public class CallLogAsync { + + /** CallLog.getLastOutgoingCall(...) */ + public AsyncTask getLastOutgoingCall(GetLastOutgoingCallArgs args) { + Assert.isMainThread(); + return new GetLastOutgoingCallTask(args.callback).execute(args); + } + + /** Interface to retrieve the last dialed number asynchronously. */ + public interface OnLastOutgoingCallComplete { + + /** @param number The last dialed number or an empty string if none exists yet. */ + void lastOutgoingCall(String number); + } + + /** Parameter object to hold the args to get the last outgoing call from the call log DB. */ + public static class GetLastOutgoingCallArgs { + + public final Context context; + public final OnLastOutgoingCallComplete callback; + + public GetLastOutgoingCallArgs(Context context, OnLastOutgoingCallComplete callback) { + this.context = context; + this.callback = callback; + } + } + + /** AsyncTask to get the last outgoing call from the DB. */ + private class GetLastOutgoingCallTask extends AsyncTask { + + private final OnLastOutgoingCallComplete mCallback; + + public GetLastOutgoingCallTask(OnLastOutgoingCallComplete callback) { + mCallback = callback; + } + + // Happens on a background thread. We cannot run the callback + // here because only the UI thread can modify the view + // hierarchy (e.g enable/disable the dial button). The + // callback is ran rom the post execute method. + @Override + protected String doInBackground(GetLastOutgoingCallArgs... list) { + String number = ""; + for (GetLastOutgoingCallArgs args : list) { + // May block. Select only the last one. + number = Calls.getLastOutgoingCall(args.context); + } + return number; // passed to the onPostExecute method. + } + + // Happens on the UI thread, it is safe to run the callback + // that may do some work on the views. + @Override + protected void onPostExecute(String number) { + Assert.isMainThread(); + mCallback.lastOutgoingCall(number); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..b4e6fc5ada267e6d69d389aeaba0e72073d06a84 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.Manifest.permission; +import android.annotation.TargetApi; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.CallLog; +import android.provider.VoicemailContract.Voicemails; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.common.LogUtil; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; +import java.util.Arrays; + +@TargetApi(VERSION_CODES.M) +public class CallLogAsyncTaskUtil { + + private static final String TAG = "CallLogAsyncTaskUtil"; + private static AsyncTaskExecutor sAsyncTaskExecutor; + + private static void initTaskExecutor() { + sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); + } + + public static void getCallDetails( + @NonNull final Context context, + @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener, + @NonNull final Uri... callUris) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.GET_CALL_DETAILS, + new AsyncTask() { + @Override + public PhoneCallDetails[] doInBackground(Void... params) { + if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG) + != PackageManager.PERMISSION_GRANTED) { + LogUtil.w("CallLogAsyncTaskUtil.getCallDetails", "missing READ_CALL_LOG permission"); + return null; + } + // TODO: All calls correspond to the same person, so make a single lookup. + final int numCalls = callUris.length; + PhoneCallDetails[] details = new PhoneCallDetails[numCalls]; + try { + for (int index = 0; index < numCalls; ++index) { + details[index] = getPhoneCallDetailsForUri(context, callUris[index]); + } + return details; + } catch (IllegalArgumentException e) { + // Something went wrong reading in our primary data. + LogUtil.e( + "CallLogAsyncTaskUtil.getCallDetails", "invalid URI starting call details", e); + return null; + } + } + + @Override + public void onPostExecute(PhoneCallDetails[] phoneCallDetails) { + if (callLogAsyncTaskListener != null) { + callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails); + } + } + }); + } + + /** Return the phone call details for a given call log URI. */ + private static PhoneCallDetails getPhoneCallDetailsForUri( + @NonNull Context context, @NonNull Uri callUri) { + Cursor cursor = + context + .getContentResolver() + .query(callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null); + + try { + if (cursor == null || !cursor.moveToFirst()) { + throw new IllegalArgumentException("Cannot find content: " + callUri); + } + + // Read call log. + final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX); + final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX); + final String postDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) + ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS) + : ""; + final String viaNumber = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallDetailQuery.VIA_NUMBER) : ""; + final int numberPresentation = + cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX); + + final PhoneAccountHandle accountHandle = + PhoneAccountUtils.getAccount( + cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME), + cursor.getString(CallDetailQuery.ACCOUNT_ID)); + + // If this is not a regular number, there is no point in looking it up in the contacts. + ContactInfoHelper contactInfoHelper = + new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)); + boolean isVoicemail = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number); + boolean shouldLookupNumber = + PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) && !isVoicemail; + ContactInfo info = ContactInfo.EMPTY; + + if (shouldLookupNumber) { + ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso); + info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY; + } + + PhoneCallDetails details = new PhoneCallDetails(number, numberPresentation, postDialDigits); + details.updateDisplayNumber(context, info.formattedNumber, isVoicemail); + + details.viaNumber = viaNumber; + details.accountHandle = accountHandle; + details.contactUri = info.lookupUri; + details.namePrimary = info.name; + details.nameAlternative = info.nameAlternative; + details.numberType = info.type; + details.numberLabel = info.label; + details.photoUri = info.photoUri; + details.sourceType = info.sourceType; + details.objectId = info.objectId; + + details.callTypes = new int[] {cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX)}; + details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX); + details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX); + details.features = cursor.getInt(CallDetailQuery.FEATURES); + details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX); + details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX); + + details.countryIso = + !TextUtils.isEmpty(countryIso) ? countryIso : GeoUtil.getCurrentCountryIso(context); + + if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) { + details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE); + } + + return details; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Delete specified calls from the call log. + * + * @param context The context. + * @param callIds String of the callIds to delete from the call log, delimited by commas (","). + * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted. + */ + public static void deleteCalls( + @NonNull final Context context, + final String callIds, + @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.DELETE_CALL, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + context + .getContentResolver() + .delete( + TelecomUtil.getCallLogUri(context), + CallLog.Calls._ID + " IN (" + callIds + ")", + null); + return null; + } + + @Override + public void onPostExecute(Void result) { + if (callLogAsyncTaskListener != null) { + callLogAsyncTaskListener.onDeleteCall(); + } + } + }); + } + + public static void markVoicemailAsRead( + @NonNull final Context context, @NonNull final Uri voicemailUri) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.MARK_VOICEMAIL_READ, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + ContentValues values = new ContentValues(); + values.put(Voicemails.IS_READ, true); + context + .getContentResolver() + .update(voicemailUri, values, Voicemails.IS_READ + " = 0", null); + + Intent intent = new Intent(context, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + context.startService(intent); + return null; + } + }); + } + + public static void deleteVoicemail( + @NonNull final Context context, + final Uri voicemailUri, + @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.DELETE_VOICEMAIL, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + context.getContentResolver().delete(voicemailUri, null, null); + return null; + } + + @Override + public void onPostExecute(Void result) { + if (callLogAsyncTaskListener != null) { + callLogAsyncTaskListener.onDeleteVoicemail(); + } + } + }); + } + + public static void markCallAsRead(@NonNull final Context context, @NonNull final long[] callIds) { + if (!PermissionsUtil.hasPhonePermissions(context)) { + return; + } + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.MARK_CALL_READ, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + + StringBuilder where = new StringBuilder(); + where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE); + where.append(" AND "); + + Long[] callIdLongs = new Long[callIds.length]; + for (int i = 0; i < callIds.length; i++) { + callIdLongs[i] = callIds[i]; + } + where + .append(CallLog.Calls._ID) + .append(" IN (" + TextUtils.join(",", callIdLongs) + ")"); + + ContentValues values = new ContentValues(1); + values.put(CallLog.Calls.IS_READ, "1"); + context + .getContentResolver() + .update(CallLog.Calls.CONTENT_URI, values, where.toString(), null); + return null; + } + }); + } + + @VisibleForTesting + public static void resetForTest() { + sAsyncTaskExecutor = null; + } + + /** The enumeration of {@link AsyncTask} objects used in this class. */ + public enum Tasks { + DELETE_VOICEMAIL, + DELETE_CALL, + MARK_VOICEMAIL_READ, + MARK_CALL_READ, + GET_CALL_DETAILS, + UPDATE_DURATION, + } + + public interface CallLogAsyncTaskListener { + + void onDeleteCall(); + + void onDeleteVoicemail(); + + void onGetCallDetails(PhoneCallDetails[] details); + } + + private static final class CallDetailQuery { + + public static final String[] CALL_LOG_PROJECTION; + static final int DATE_COLUMN_INDEX = 0; + static final int DURATION_COLUMN_INDEX = 1; + static final int NUMBER_COLUMN_INDEX = 2; + static final int CALL_TYPE_COLUMN_INDEX = 3; + static final int COUNTRY_ISO_COLUMN_INDEX = 4; + static final int GEOCODED_LOCATION_COLUMN_INDEX = 5; + static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6; + static final int ACCOUNT_COMPONENT_NAME = 7; + static final int ACCOUNT_ID = 8; + static final int FEATURES = 9; + static final int DATA_USAGE = 10; + static final int TRANSCRIPTION_COLUMN_INDEX = 11; + static final int POST_DIAL_DIGITS = 12; + static final int VIA_NUMBER = 13; + private static final String[] CALL_LOG_PROJECTION_INTERNAL = + new String[] { + CallLog.Calls.DATE, + CallLog.Calls.DURATION, + CallLog.Calls.NUMBER, + CallLog.Calls.TYPE, + CallLog.Calls.COUNTRY_ISO, + CallLog.Calls.GEOCODED_LOCATION, + CallLog.Calls.NUMBER_PRESENTATION, + CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME, + CallLog.Calls.PHONE_ACCOUNT_ID, + CallLog.Calls.FEATURES, + CallLog.Calls.DATA_USAGE, + CallLog.Calls.TRANSCRIPTION + }; + + static { + ArrayList projectionList = new ArrayList<>(); + projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL)); + if (VERSION.SDK_INT >= VERSION_CODES.N) { + projectionList.add(CallLog.Calls.POST_DIAL_DIGITS); + projectionList.add(CallLog.Calls.VIA_NUMBER); + } + projectionList.trimToSize(); + CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogFragment.java b/java/com/android/dialer/app/calllog/CallLogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..1ae68cd650edd26e368cbacfa09315a114b23ed1 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogFragment.java @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import static android.Manifest.permission.READ_CALL_LOG; + +import android.app.Activity; +import android.app.Fragment; +import android.app.KeyguardManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.Bindings; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.contactinfo.ContactInfoCache.OnContactInfoChangedListener; +import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.app.list.ListsFragment.ListsPage; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.CallLogQueryHandler; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.util.PermissionsUtil; + +/** + * Displays a list of call log entries. To filter for a particular kind of call (all, missed or + * voicemails), specify it in the constructor. + */ +public class CallLogFragment extends Fragment + implements ListsPage, + CallLogQueryHandler.Listener, + CallLogAdapter.CallFetcher, + OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback, + CallLogModalAlertManager.Listener { + private static final String KEY_FILTER_TYPE = "filter_type"; + private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission"; + private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required"; + + private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1; + + private static final int EVENT_UPDATE_DISPLAY = 1; + + private static final long MILLIS_IN_MINUTE = 60 * 1000; + private final Handler mHandler = new Handler(); + // See issue 6363009 + private final ContentObserver mCallLogObserver = new CustomContentObserver(); + private final ContentObserver mContactsObserver = new CustomContentObserver(); + private RecyclerView mRecyclerView; + private LinearLayoutManager mLayoutManager; + private CallLogAdapter mAdapter; + private CallLogQueryHandler mCallLogQueryHandler; + private boolean mScrollToTop; + private EmptyContentView mEmptyListView; + private KeyguardManager mKeyguardManager; + private ContactInfoCache mContactInfoCache; + private final OnContactInfoChangedListener mOnContactInfoChangedListener = + new OnContactInfoChangedListener() { + @Override + public void onContactInfoChanged() { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + }; + private boolean mRefreshDataRequired; + private boolean mHasReadCallLogPermission; + // Exactly same variable is in Fragment as a package private. + private boolean mMenuVisible = true; + // Default to all calls. + protected int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; + + private final Handler mDisplayUpdateHandler = + new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_UPDATE_DISPLAY: + refreshData(); + rescheduleDisplayUpdate(); + break; + } + } + }; + protected CallLogModalAlertManager mModalAlertManager; + private ViewGroup mModalAlertView; + + @Override + public void onCreate(Bundle state) { + LogUtil.d("CallLogFragment.onCreate", toString()); + super.onCreate(state); + mRefreshDataRequired = true; + if (state != null) { + mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); + mHasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false); + mRefreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); + } + + final Activity activity = getActivity(); + final ContentResolver resolver = activity.getContentResolver(); + mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this); + mKeyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); + resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver); + resolver.registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); + setHasOptionsMenu(true); + } + + /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ + @Override + public boolean onCallsFetched(Cursor cursor) { + if (getActivity() == null || getActivity().isFinishing()) { + // Return false; we did not take ownership of the cursor + return false; + } + mAdapter.invalidatePositions(); + mAdapter.setLoading(false); + mAdapter.changeCursor(cursor); + // This will update the state of the "Clear call log" menu item. + getActivity().invalidateOptionsMenu(); + + if (cursor != null && cursor.getCount() > 0) { + mRecyclerView.setPaddingRelative( + mRecyclerView.getPaddingStart(), + 0, + mRecyclerView.getPaddingEnd(), + getResources().getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding)); + mEmptyListView.setVisibility(View.GONE); + } else { + mRecyclerView.setPaddingRelative( + mRecyclerView.getPaddingStart(), 0, mRecyclerView.getPaddingEnd(), 0); + mEmptyListView.setVisibility(View.VISIBLE); + } + if (mScrollToTop) { + // The smooth-scroll animation happens over a fixed time period. + // As a result, if it scrolls through a large portion of the list, + // each frame will jump so far from the previous one that the user + // will not experience the illusion of downward motion. Instead, + // if we're not already near the top of the list, we instantly jump + // near the top, and animate from there. + if (mLayoutManager.findFirstVisibleItemPosition() > 5) { + // TODO: Jump to near the top, then begin smooth scroll. + mRecyclerView.smoothScrollToPosition(0); + } + // Workaround for framework issue: the smooth-scroll doesn't + // occur if setSelection() is called immediately before. + mHandler.post( + new Runnable() { + @Override + public void run() { + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + mRecyclerView.smoothScrollToPosition(0); + } + }); + + mScrollToTop = false; + } + return true; + } + + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) {} + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) {} + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) {} + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + View view = inflater.inflate(R.layout.call_log_fragment, container, false); + setupView(view); + return view; + } + + protected void setupView(View view) { + mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + mRecyclerView.setHasFixedSize(true); + mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); + mEmptyListView.setImage(R.drawable.empty_call_log); + mEmptyListView.setActionClickedListener(this); + mModalAlertView = (ViewGroup) view.findViewById(R.id.modal_message_container); + mModalAlertManager = + new CallLogModalAlertManager(LayoutInflater.from(getContext()), mModalAlertView, this); + } + + protected void setupData() { + int activityType = CallLogAdapter.ACTIVITY_TYPE_DIALTACTS; + String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); + + mContactInfoCache = + new ContactInfoCache( + ExpirableCacheHeadlessFragment.attach((AppCompatActivity) getActivity()) + .getRetainedCache(), + new ContactInfoHelper(getActivity(), currentCountryIso), + mOnContactInfoChangedListener); + mAdapter = + Bindings.getLegacy(getActivity()) + .newCallLogAdapter( + getActivity(), + mRecyclerView, + this, + CallLogCache.getCallLogCache(getActivity()), + mContactInfoCache, + getVoicemailPlaybackPresenter(), + activityType); + mRecyclerView.setAdapter(mAdapter); + fetchCalls(); + } + + @Nullable + protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() { + return null; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setupData(); + mAdapter.onRestoreInstanceState(savedInstanceState); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + updateEmptyMessage(mCallTypeFilter); + } + + @Override + public void onResume() { + LogUtil.d("CallLogFragment.onResume", toString()); + super.onResume(); + final boolean hasReadCallLogPermission = + PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG); + if (!mHasReadCallLogPermission && hasReadCallLogPermission) { + // We didn't have the permission before, and now we do. Force a refresh of the call log. + // Note that this code path always happens on a fresh start, but mRefreshDataRequired + // is already true in that case anyway. + mRefreshDataRequired = true; + updateEmptyMessage(mCallTypeFilter); + } + + mHasReadCallLogPermission = hasReadCallLogPermission; + + /* + * Always clear the filtered numbers cache since users could have blocked/unblocked numbers + * from the settings page + */ + mAdapter.clearFilteredNumbersCache(); + refreshData(); + mAdapter.onResume(); + + rescheduleDisplayUpdate(); + } + + @Override + public void onPause() { + LogUtil.d("CallLogFragment.onPause", toString()); + cancelDisplayUpdate(); + mAdapter.onPause(); + super.onPause(); + } + + @Override + public void onStop() { + updateOnTransition(); + + super.onStop(); + mAdapter.onStop(); + } + + @Override + public void onDestroy() { + LogUtil.d("CallLogFragment.onDestroy", toString()); + mAdapter.changeCursor(null); + + getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); + getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); + outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, mHasReadCallLogPermission); + outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); + + mContactInfoCache.stop(); + + mAdapter.onSaveInstanceState(outState); + } + + @Override + public void fetchCalls() { + mCallLogQueryHandler.fetchCalls(mCallTypeFilter); + ((ListsFragment) getParentFragment()).updateTabUnreadCounts(); + } + + private void updateEmptyMessage(int filterType) { + final Context context = getActivity(); + if (context == null) { + return; + } + + if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) { + mEmptyListView.setDescription(R.string.permission_no_calllog); + mEmptyListView.setActionLabel(R.string.permission_single_turn_on); + return; + } + + final int messageId; + switch (filterType) { + case Calls.MISSED_TYPE: + messageId = R.string.call_log_missed_empty; + break; + case Calls.VOICEMAIL_TYPE: + messageId = R.string.call_log_voicemail_empty; + break; + case CallLogQueryHandler.CALL_TYPE_ALL: + messageId = R.string.call_log_all_empty; + break; + default: + throw new IllegalArgumentException( + "Unexpected filter type in CallLogFragment: " + filterType); + } + mEmptyListView.setDescription(messageId); + if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) { + mEmptyListView.setActionLabel(R.string.call_log_all_empty_action); + } + } + + public CallLogAdapter getAdapter() { + return mAdapter; + } + + @Override + public void setMenuVisibility(boolean menuVisible) { + super.setMenuVisibility(menuVisible); + if (mMenuVisible != menuVisible) { + mMenuVisible = menuVisible; + if (!menuVisible) { + updateOnTransition(); + } else if (isResumed()) { + refreshData(); + } + } + } + + /** Requests updates to the data to be shown. */ + private void refreshData() { + // Prevent unnecessary refresh. + if (mRefreshDataRequired) { + // Mark all entries in the contact info cache as out of date, so they will be looked up + // again once being shown. + mContactInfoCache.invalidate(); + mAdapter.setLoading(true); + + fetchCalls(); + mCallLogQueryHandler.fetchVoicemailStatus(); + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + updateOnTransition(); + mRefreshDataRequired = false; + } else { + // Refresh the display of the existing data to update the timestamp text descriptions. + mAdapter.notifyDataSetChanged(); + } + } + + /** + * Updates the voicemail notification state. + * + *

TODO: Move to CallLogActivity + */ + private void updateOnTransition() { + // We don't want to update any call data when keyguard is on because the user has likely not + // seen the new calls yet. + // This might be called before onCreate() and thus we need to check null explicitly. + if (mKeyguardManager != null + && !mKeyguardManager.inKeyguardRestrictedInputMode() + && mCallTypeFilter == Calls.VOICEMAIL_TYPE) { + CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); + } + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) { + FragmentCompat.requestPermissions( + this, new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE); + } else { + ((HostInterface) activity).showDialpad(); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) { + if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Force a refresh of the data since we were missing the permission before this. + mRefreshDataRequired = true; + } + } + } + + /** Schedules an update to the relative call times (X mins ago). */ + private void rescheduleDisplayUpdate() { + if (!mDisplayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) { + long time = System.currentTimeMillis(); + // This value allows us to change the display relatively close to when the time changes + // from one minute to the next. + long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE); + mDisplayUpdateHandler.sendEmptyMessageDelayed(EVENT_UPDATE_DISPLAY, millisUtilNextMinute); + } + } + + /** Cancels any pending update requests to update the relative call times (X mins ago). */ + private void cancelDisplayUpdate() { + mDisplayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY); + } + + @Override + @CallSuper + public void onPageResume(@Nullable Activity activity) { + LogUtil.d("CallLogFragment.onPageResume", "frag: %s", this); + if (activity != null) { + ((HostInterface) activity) + .enableFloatingButton(mModalAlertManager == null || mModalAlertManager.isEmpty()); + } + } + + @Override + @CallSuper + public void onPagePause(@Nullable Activity activity) { + LogUtil.d("CallLogFragment.onPagePause", "frag: %s", this); + } + + @Override + public void onShowModalAlert(boolean show) { + LogUtil.d( + "CallLogFragment.onShowModalAlert", + "show: %b, fragment: %s, isVisible: %b", + show, + this, + getUserVisibleHint()); + getAdapter().notifyDataSetChanged(); + HostInterface hostInterface = (HostInterface) getActivity(); + if (show) { + mRecyclerView.setVisibility(View.GONE); + mModalAlertView.setVisibility(View.VISIBLE); + if (hostInterface != null && getUserVisibleHint()) { + hostInterface.enableFloatingButton(false); + } + } else { + mRecyclerView.setVisibility(View.VISIBLE); + mModalAlertView.setVisibility(View.GONE); + if (hostInterface != null && getUserVisibleHint()) { + hostInterface.enableFloatingButton(true); + } + } + } + + public interface HostInterface { + + void showDialpad(); + + void enableFloatingButton(boolean enabled); + } + + protected class CustomContentObserver extends ContentObserver { + + public CustomContentObserver() { + super(mHandler); + } + + @Override + public void onChange(boolean selfChange) { + mRefreshDataRequired = true; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..45ff3783d504d7058ca9d96e448d3246005b683b --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.database.Cursor; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.text.format.Time; +import com.android.contacts.common.util.DateUtils; +import com.android.dialer.compat.AppCompatConstants; +import com.android.dialer.phonenumbercache.CallLogQuery; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import java.util.Objects; + +/** + * Groups together calls in the call log. The primary grouping attempts to group together calls to + * and from the same number into a single row on the call log. A secondary grouping assigns calls, + * grouped via the primary grouping, to "day groups". The day groups provide a means of identifying + * the calls which occurred "Today", "Yesterday", "Last week", or "Other". + * + *

This class is meant to be used in conjunction with {@link GroupingListAdapter}. + */ +public class CallLogGroupBuilder { + + /** + * Day grouping for call log entries used to represent no associated day group. Used primarily + * when retrieving the previous day group, but there is no previous day group (i.e. we are at the + * start of the list). + */ + public static final int DAY_GROUP_NONE = -1; + /** Day grouping for calls which occurred today. */ + public static final int DAY_GROUP_TODAY = 0; + /** Day grouping for calls which occurred yesterday. */ + public static final int DAY_GROUP_YESTERDAY = 1; + /** Day grouping for calls which occurred before last week. */ + public static final int DAY_GROUP_OTHER = 2; + /** Instance of the time object used for time calculations. */ + private static final Time TIME = new Time(); + /** The object on which the groups are created. */ + private final GroupCreator mGroupCreator; + + public CallLogGroupBuilder(GroupCreator groupCreator) { + mGroupCreator = groupCreator; + } + + /** + * Finds all groups of adjacent entries in the call log which should be grouped together and calls + * {@link GroupCreator#addGroup(int, int)} on {@link #mGroupCreator} for each of them. + * + *

For entries that are not grouped with others, we do not need to create a group of size one. + * + *

It assumes that the cursor will not change during its execution. + * + * @see GroupingListAdapter#addGroups(Cursor) + */ + public void addGroups(Cursor cursor) { + final int count = cursor.getCount(); + if (count == 0) { + return; + } + + // Clear any previous day grouping information. + mGroupCreator.clearDayGroups(); + + // Get current system time, used for calculating which day group calls belong to. + long currentTime = System.currentTimeMillis(); + cursor.moveToFirst(); + + // Determine the day group for the first call in the cursor. + final long firstDate = cursor.getLong(CallLogQuery.DATE); + final long firstRowId = cursor.getLong(CallLogQuery.ID); + int groupDayGroup = getDayGroup(firstDate, currentTime); + mGroupCreator.setDayGroup(firstRowId, groupDayGroup); + + // Instantiate the group values to those of the first call in the cursor. + String groupNumber = cursor.getString(CallLogQuery.NUMBER); + String groupPostDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : ""; + String groupViaNumbers = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; + int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE); + String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); + String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID); + int groupSize = 1; + + String number; + String numberPostDialDigits; + String numberViaNumbers; + int callType; + String accountComponentName; + String accountId; + + while (cursor.moveToNext()) { + // Obtain the values for the current call to group. + number = cursor.getString(CallLogQuery.NUMBER); + numberPostDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) + ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) + : ""; + numberViaNumbers = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; + callType = cursor.getInt(CallLogQuery.CALL_TYPE); + accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); + accountId = cursor.getString(CallLogQuery.ACCOUNT_ID); + + final boolean isSameNumber = equalNumbers(groupNumber, number); + final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits); + final boolean isSameViaNumbers = groupViaNumbers.equals(numberViaNumbers); + final boolean isSameAccount = + isSameAccount(groupAccountComponentName, accountComponentName, groupAccountId, accountId); + + // Group with the same number and account. Never group voicemails. Only group blocked + // calls with other blocked calls. + if (isSameNumber + && isSameAccount + && isSamePostDialDigits + && isSameViaNumbers + && areBothNotVoicemail(callType, groupCallType) + && (areBothNotBlocked(callType, groupCallType) + || areBothBlocked(callType, groupCallType))) { + // Increment the size of the group to include the current call, but do not create + // the group until finding a call that does not match. + groupSize++; + } else { + // The call group has changed. Determine the day group for the new call group. + final long date = cursor.getLong(CallLogQuery.DATE); + groupDayGroup = getDayGroup(date, currentTime); + + // Create a group for the previous group of calls, which does not include the + // current call. + mGroupCreator.addGroup(cursor.getPosition() - groupSize, groupSize); + + // Start a new group; it will include at least the current call. + groupSize = 1; + + // Update the group values to those of the current call. + groupNumber = number; + groupPostDialDigits = numberPostDialDigits; + groupViaNumbers = numberViaNumbers; + groupCallType = callType; + groupAccountComponentName = accountComponentName; + groupAccountId = accountId; + } + + // Save the day group associated with the current call. + final long currentCallId = cursor.getLong(CallLogQuery.ID); + mGroupCreator.setDayGroup(currentCallId, groupDayGroup); + } + + // Create a group for the last set of calls. + mGroupCreator.addGroup(count - groupSize, groupSize); + } + + @VisibleForTesting + boolean equalNumbers(@Nullable String number1, @Nullable String number2) { + if (PhoneNumberHelper.isUriNumber(number1) || PhoneNumberHelper.isUriNumber(number2)) { + return compareSipAddresses(number1, number2); + } else { + return PhoneNumberUtils.compare(number1, number2); + } + } + + private boolean isSameAccount(String name1, String name2, String id1, String id2) { + return TextUtils.equals(name1, name2) && TextUtils.equals(id1, id2); + } + + @VisibleForTesting + boolean compareSipAddresses(@Nullable String number1, @Nullable String number2) { + if (number1 == null || number2 == null) { + return Objects.equals(number1, number2); + } + + int index1 = number1.indexOf('@'); + final String userinfo1; + final String rest1; + if (index1 != -1) { + userinfo1 = number1.substring(0, index1); + rest1 = number1.substring(index1); + } else { + userinfo1 = number1; + rest1 = ""; + } + + int index2 = number2.indexOf('@'); + final String userinfo2; + final String rest2; + if (index2 != -1) { + userinfo2 = number2.substring(0, index2); + rest2 = number2.substring(index2); + } else { + userinfo2 = number2; + rest2 = ""; + } + + return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2); + } + + /** + * Given a call date and the current date, determine which date group the call belongs in. + * + * @param date The call date. + * @param now The current date. + * @return The date group the call belongs in. + */ + private int getDayGroup(long date, long now) { + int days = DateUtils.getDayDifference(TIME, date, now); + + if (days == 0) { + return DAY_GROUP_TODAY; + } else if (days == 1) { + return DAY_GROUP_YESTERDAY; + } else { + return DAY_GROUP_OTHER; + } + } + + private boolean areBothNotVoicemail(int callType, int groupCallType) { + return callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE + && groupCallType != AppCompatConstants.CALLS_VOICEMAIL_TYPE; + } + + private boolean areBothNotBlocked(int callType, int groupCallType) { + return callType != AppCompatConstants.CALLS_BLOCKED_TYPE + && groupCallType != AppCompatConstants.CALLS_BLOCKED_TYPE; + } + + private boolean areBothBlocked(int callType, int groupCallType) { + return callType == AppCompatConstants.CALLS_BLOCKED_TYPE + && groupCallType == AppCompatConstants.CALLS_BLOCKED_TYPE; + } + + public interface GroupCreator { + + /** + * Defines the interface for adding a group to the call log. The primary group for a call log + * groups the calls together based on the number which was dialed. + * + * @param cursorPosition The starting position of the group in the cursor. + * @param size The size of the group. + */ + void addGroup(int cursorPosition, int size); + + /** + * Defines the interface for tracking the day group each call belongs to. Calls in a call group + * are assigned the same day group as the first call in the group. The day group assigns calls + * to the buckets: Today, Yesterday, Last week, and Other + * + * @param rowId The row Id of the current call. + * @param dayGroup The day group the call belongs in. + */ + void setDayGroup(long rowId, int dayGroup); + + /** Defines the interface for clearing the day groupings information on rebind/regroup. */ + void clearDayGroups(); + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..ea2119c830ebcd9fc946063bb06d83adcd5f4c70 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.res.Resources; +import android.provider.CallLog.Calls; +import android.support.annotation.WorkerThread; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.util.Log; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.common.Assert; +import com.android.dialer.compat.AppCompatConstants; + +/** Helper class to fill in the views of a call log entry. */ +/* package */ class CallLogListItemHelper { + + private static final String TAG = "CallLogListItemHelper"; + + /** Helper for populating the details of a phone call. */ + private final PhoneCallDetailsHelper mPhoneCallDetailsHelper; + /** Resources to look up strings. */ + private final Resources mResources; + + private final CallLogCache mCallLogCache; + + /** + * Creates a new helper instance. + * + * @param phoneCallDetailsHelper used to set the details of a phone call + * @param resources The object from which resources can be retrieved + * @param callLogCache A cache for values retrieved from telecom/telephony + */ + public CallLogListItemHelper( + PhoneCallDetailsHelper phoneCallDetailsHelper, + Resources resources, + CallLogCache callLogCache) { + mPhoneCallDetailsHelper = phoneCallDetailsHelper; + mResources = resources; + mCallLogCache = callLogCache; + } + + /** + * Update phone call details. This is called before any drawing to avoid expensive operation on UI + * thread. + * + * @param details + */ + @WorkerThread + public void updatePhoneCallDetails(PhoneCallDetails details) { + Assert.isWorkerThread(); + details.callLocationAndDate = mPhoneCallDetailsHelper.getCallLocationAndDate(details); + details.callDescription = getCallDescription(details); + } + + /** + * Sets the name, label, and number for a contact. + * + * @param views the views to populate + * @param details the details of a phone call needed to fill in the data + */ + public void setPhoneCallDetails(CallLogListItemViewHolder views, PhoneCallDetails details) { + mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details); + + // Set the accessibility text for the contact badge + views.quickContactView.setContentDescription(getContactBadgeDescription(details)); + + // Set the primary action accessibility description + views.primaryActionView.setContentDescription(details.callDescription); + + // Cache name or number of caller. Used when setting the content descriptions of buttons + // when the actions ViewStub is inflated. + views.nameOrNumber = getNameOrNumber(details); + + // The call type or Location associated with the call. Use when setting text for a + // voicemail log's call button + views.callTypeOrLocation = mPhoneCallDetailsHelper.getCallTypeOrLocation(details); + + // Cache country iso. Used for number filtering. + views.countryIso = details.countryIso; + + views.updatePhoto(); + } + + /** + * Sets the accessibility descriptions for the action buttons in the action button ViewStub. + * + * @param views The views associated with the current call log entry. + */ + public void setActionContentDescriptions(CallLogListItemViewHolder views) { + if (views.nameOrNumber == null) { + Log.e(TAG, "setActionContentDescriptions; name or number is null."); + } + + // Calling expandTemplate with a null parameter will cause a NullPointerException. + // Although we don't expect a null name or number, it is best to protect against it. + CharSequence nameOrNumber = views.nameOrNumber == null ? "" : views.nameOrNumber; + + views.videoCallButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_video_call_action), nameOrNumber)); + + views.createNewContactButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_create_new_contact_action), nameOrNumber)); + + views.addToExistingContactButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_add_to_existing_contact_action), + nameOrNumber)); + + views.detailsButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_details_action), nameOrNumber)); + } + + /** + * Returns the accessibility description for the contact badge for a call log entry. + * + * @param details Details of call. + * @return Accessibility description. + */ + private CharSequence getContactBadgeDescription(PhoneCallDetails details) { + if (details.isSpam) { + return mResources.getString( + R.string.description_spam_contact_details, getNameOrNumber(details)); + } + return mResources.getString(R.string.description_contact_details, getNameOrNumber(details)); + } + + /** + * Returns the accessibility description of the "return call/call" action for a call log entry. + * Accessibility text is a combination of: {Voicemail Prefix}. {Number of Calls}. {Caller + * information} {Phone Account}. If most recent call is a voicemail, {Voicemail Prefix} is "New + * Voicemail.", otherwise "". + * + *

If more than one call for the caller, {Number of Calls} is: "{number of calls} calls.", + * otherwise "". + * + *

The {Caller Information} references the most recent call associated with the caller. For + * incoming calls: If missed call: Missed call from {Name/Number} {Call Type} {Call Time}. If + * answered call: Answered call from {Name/Number} {Call Type} {Call Time}. + * + *

For outgoing calls: If outgoing: Call to {Name/Number] {Call Type} {Call Time}. + * + *

Where: {Name/Number} is the name or number of the caller (as shown in call log). {Call type} + * is the contact phone number type (eg mobile) or location. {Call Time} is the time since the + * last call for the contact occurred. + * + *

The {Phone Account} refers to the account/SIM through which the call was placed or received + * in multi-SIM devices. + * + *

Examples: 3 calls. New Voicemail. Missed call from Joe Smith mobile 2 hours ago on SIM 1. + * + *

2 calls. Answered call from John Doe mobile 1 hour ago. + * + * @param context The application context. + * @param details Details of call. + * @return Return call action description. + */ + public CharSequence getCallDescription(PhoneCallDetails details) { + // Get the name or number of the caller. + final CharSequence nameOrNumber = getNameOrNumber(details); + + // Get the call type or location of the caller; null if not applicable + final CharSequence typeOrLocation = mPhoneCallDetailsHelper.getCallTypeOrLocation(details); + + // Get the time/date of the call + final CharSequence timeOfCall = mPhoneCallDetailsHelper.getCallDate(details); + + SpannableStringBuilder callDescription = new SpannableStringBuilder(); + + // Add number of calls if more than one. + if (details.callTypes.length > 1) { + callDescription.append( + mResources.getString(R.string.description_num_calls, details.callTypes.length)); + } + + // If call had video capabilities, add the "Video Call" string. + if ((details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { + callDescription.append(mResources.getString(R.string.description_video_call)); + } + + String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle); + CharSequence onAccountLabel = + PhoneCallDetails.createAccountLabelDescription(mResources, details.viaNumber, accountLabel); + + int stringID = getCallDescriptionStringID(details.callTypes, details.isRead); + callDescription.append( + TextUtils.expandTemplate( + mResources.getString(stringID), + nameOrNumber, + typeOrLocation == null ? "" : typeOrLocation, + timeOfCall, + onAccountLabel)); + + return callDescription; + } + + /** + * Determine the appropriate string ID to describe a call for accessibility purposes. + * + * @param callTypes The type of call corresponding to this entry or multiple if this entry + * represents multiple calls grouped together. + * @param isRead If the entry is a voicemail, {@code true} if the voicemail is read. + * @return String resource ID to use. + */ + public int getCallDescriptionStringID(int[] callTypes, boolean isRead) { + int lastCallType = getLastCallType(callTypes); + int stringID; + + if (lastCallType == AppCompatConstants.CALLS_MISSED_TYPE) { + //Message: Missed call from , , , + //. + stringID = R.string.description_incoming_missed_call; + } else if (lastCallType == AppCompatConstants.CALLS_INCOMING_TYPE) { + //Message: Answered call from , , , + //. + stringID = R.string.description_incoming_answered_call; + } else if (lastCallType == AppCompatConstants.CALLS_VOICEMAIL_TYPE) { + //Message: (Unread) [V/v]oicemail from , , , + //. + stringID = + isRead ? R.string.description_read_voicemail : R.string.description_unread_voicemail; + } else { + //Message: Call to , , , . + stringID = R.string.description_outgoing_call; + } + return stringID; + } + + /** + * Determine the call type for the most recent call. + * + * @param callTypes Call types to check. + * @return Call type. + */ + private int getLastCallType(int[] callTypes) { + if (callTypes.length > 0) { + return callTypes[0]; + } else { + return Calls.MISSED_TYPE; + } + } + + /** + * Return the name or number of the caller specified by the details. + * + * @param details Call details + * @return the name (if known) of the caller, otherwise the formatted number. + */ + private CharSequence getNameOrNumber(PhoneCallDetails details) { + final CharSequence recipient; + if (!TextUtils.isEmpty(details.getPreferredName())) { + recipient = details.getPreferredName(); + } else { + recipient = details.displayNumber + details.postDialDigits; + } + return recipient; + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..6abd36078ed3340bb70fe957668492dae9f72e76 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java @@ -0,0 +1,966 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.widget.CardView; +import android.support.v7.widget.RecyclerView; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.ContextMenu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewStub; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.contacts.common.ClipboardUtils; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.dialog.CallSubjectDialog; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.voicemail.VoicemailPlaybackLayout; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.blocking.BlockedNumbersMigrator; +import com.android.dialer.blocking.FilteredNumberCompat; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.callcomposer.CallComposerActivity; +import com.android.dialer.callcomposer.nano.CallComposerContact; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; + +/** + * This is an object containing references to views contained by the call log list item. This + * improves performance by reducing the frequency with which we need to find views by IDs. + * + *

This object also contains UI logic pertaining to the view, to isolate it from the + * CallLogAdapter. + */ +public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, + MenuItem.OnMenuItemClickListener, + View.OnCreateContextMenuListener { + private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed"; + + /** The root view of the call log list item */ + public final View rootView; + /** The quick contact badge for the contact. */ + public final QuickContactBadge quickContactView; + /** The primary action view of the entry. */ + public final View primaryActionView; + /** The details of the phone call. */ + public final PhoneCallDetailsViews phoneCallDetailsViews; + /** The text of the header for a day grouping. */ + public final TextView dayGroupHeader; + /** The view containing the details for the call log row, including the action buttons. */ + public final CardView callLogEntryView; + /** The actionable view which places a call to the number corresponding to the call log row. */ + public final ImageView primaryActionButtonView; + + private final Context mContext; + private final CallLogCache mCallLogCache; + private final CallLogListItemHelper mCallLogListItemHelper; + private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + private final OnClickListener mBlockReportListener; + private final int mPhotoSize; + /** Whether the data fields are populated by the worker thread, ready to be shown. */ + public boolean isLoaded; + /** The view containing call log item actions. Null until the ViewStub is inflated. */ + public View actionsView; + /** The button views below are assigned only when the action section is expanded. */ + public VoicemailPlaybackLayout voicemailPlaybackView; + + public View callButtonView; + public View videoCallButtonView; + public View createNewContactButtonView; + public View addToExistingContactButtonView; + public View sendMessageView; + public View blockReportView; + public View blockView; + public View unblockView; + public View reportNotSpamView; + public View detailsButtonView; + public View callWithNoteButtonView; + public View callComposeButtonView; + public View sendVoicemailButtonView; + public ImageView workIconView; + /** + * The row Id for the first call associated with the call log entry. Used as a key for the map + * used to track which call log entries have the action button section expanded. + */ + public long rowId; + /** + * The call Ids for the calls represented by the current call log entry. Used when the user + * deletes a call log entry. + */ + public long[] callIds; + /** + * The callable phone number for the current call log entry. Cached here as the call back intent + * is set only when the actions ViewStub is inflated. + */ + public String number; + /** The post-dial numbers that are dialed following the phone number. */ + public String postDialDigits; + /** The formatted phone number to display. */ + public String displayNumber; + /** + * The phone number presentation for the current call log entry. Cached here as the call back + * intent is set only when the actions ViewStub is inflated. + */ + public int numberPresentation; + /** The type of the phone number (e.g. main, work, etc). */ + public String numberType; + /** + * The country iso for the call. Cached here as the call back intent is set only when the actions + * ViewStub is inflated. + */ + public String countryIso; + /** + * The type of call for the current call log entry. Cached here as the call back intent is set + * only when the actions ViewStub is inflated. + */ + public int callType; + /** + * ID for blocked numbers database. Set when context menu is created, if the number is blocked. + */ + public Integer blockId; + /** + * The account for the current call log entry. Cached here as the call back intent is set only + * when the actions ViewStub is inflated. + */ + public PhoneAccountHandle accountHandle; + /** + * If the call has an associated voicemail message, the URI of the voicemail message for playback. + * Cached here as the voicemail intent is only set when the actions ViewStub is inflated. + */ + public String voicemailUri; + /** + * The name or number associated with the call. Cached here for use when setting content + * descriptions on buttons in the actions ViewStub when it is inflated. + */ + public CharSequence nameOrNumber; + /** + * The call type or Location associated with the call. Cached here for use when setting text for a + * voicemail log's call button + */ + public CharSequence callTypeOrLocation; + /** Whether this row is for a business or not. */ + public boolean isBusiness; + /** The contact info for the contact displayed in this list item. */ + public volatile ContactInfo info; + /** Whether spam feature is enabled, which affects UI. */ + public boolean isSpamFeatureEnabled; + /** Whether the current log entry is a spam number or not. */ + public boolean isSpam; + + public boolean isCallComposerCapable; + + private View.OnClickListener mExpandCollapseListener; + private boolean mVoicemailPrimaryActionButtonClicked; + + public int dayGroupHeaderVisibility; + public CharSequence dayGroupHeaderText; + public boolean isAttachedToWindow; + + public AsyncTask asyncTask; + + private CallLogListItemViewHolder( + Context context, + OnClickListener blockReportListener, + View.OnClickListener expandCollapseListener, + CallLogCache callLogCache, + CallLogListItemHelper callLogListItemHelper, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + View rootView, + QuickContactBadge quickContactView, + View primaryActionView, + PhoneCallDetailsViews phoneCallDetailsViews, + CardView callLogEntryView, + TextView dayGroupHeader, + ImageView primaryActionButtonView) { + super(rootView); + + mContext = context; + mExpandCollapseListener = expandCollapseListener; + mCallLogCache = callLogCache; + mCallLogListItemHelper = callLogListItemHelper; + mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; + mBlockReportListener = blockReportListener; + + this.rootView = rootView; + this.quickContactView = quickContactView; + this.primaryActionView = primaryActionView; + this.phoneCallDetailsViews = phoneCallDetailsViews; + this.callLogEntryView = callLogEntryView; + this.dayGroupHeader = dayGroupHeader; + this.primaryActionButtonView = primaryActionButtonView; + this.workIconView = (ImageView) rootView.findViewById(R.id.work_profile_icon); + mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); + + // Set text height to false on the TextViews so they don't have extra padding. + phoneCallDetailsViews.nameView.setElegantTextHeight(false); + phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false); + + quickContactView.setOverlay(null); + if (CompatUtils.hasPrioritizedMimeType()) { + quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + primaryActionButtonView.setOnClickListener(this); + primaryActionView.setOnClickListener(mExpandCollapseListener); + primaryActionView.setOnCreateContextMenuListener(this); + } + + public static CallLogListItemViewHolder create( + View view, + Context context, + OnClickListener blockReportListener, + View.OnClickListener expandCollapseListener, + CallLogCache callLogCache, + CallLogListItemHelper callLogListItemHelper, + VoicemailPlaybackPresenter voicemailPlaybackPresenter) { + + return new CallLogListItemViewHolder( + context, + blockReportListener, + expandCollapseListener, + callLogCache, + callLogListItemHelper, + voicemailPlaybackPresenter, + view, + (QuickContactBadge) view.findViewById(R.id.quick_contact_photo), + view.findViewById(R.id.primary_action_view), + PhoneCallDetailsViews.fromView(view), + (CardView) view.findViewById(R.id.call_log_row), + (TextView) view.findViewById(R.id.call_log_day_group_label), + (ImageView) view.findViewById(R.id.primary_action_button)); + } + + public static CallLogListItemViewHolder createForTest(Context context) { + Resources resources = context.getResources(); + CallLogCache callLogCache = CallLogCache.getCallLogCache(context); + PhoneCallDetailsHelper phoneCallDetailsHelper = + new PhoneCallDetailsHelper(context, resources, callLogCache); + + CallLogListItemViewHolder viewHolder = + new CallLogListItemViewHolder( + context, + null, + null /* expandCollapseListener */, + callLogCache, + new CallLogListItemHelper(phoneCallDetailsHelper, resources, callLogCache), + null /* voicemailPlaybackPresenter */, + new View(context), + new QuickContactBadge(context), + new View(context), + PhoneCallDetailsViews.createForTest(context), + new CardView(context), + new TextView(context), + new ImageView(context)); + viewHolder.detailsButtonView = new TextView(context); + viewHolder.actionsView = new View(context); + viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context); + viewHolder.workIconView = new ImageButton(context); + return viewHolder; + } + + @Override + public void onCreateContextMenu( + final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + if (TextUtils.isEmpty(number)) { + return; + } + + if (callType == CallLog.Calls.VOICEMAIL_TYPE) { + menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail)); + } else { + menu.setHeaderTitle( + PhoneNumberUtilsCompat.createTtsSpannable( + BidiFormatter.getInstance().unicodeWrap(number, TextDirectionHeuristics.LTR))); + } + + menu.add( + ContextMenu.NONE, + R.id.context_menu_copy_to_clipboard, + ContextMenu.NONE, + R.string.action_copy_number_text) + .setOnMenuItemClickListener(this); + + // The edit number before call does not show up if any of the conditions apply: + // 1) Number cannot be called + // 2) Number is the voicemail number + // 3) Number is a SIP address + + if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) + && !mCallLogCache.isVoicemailNumber(accountHandle, number) + && !PhoneNumberHelper.isSipNumber(number)) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_edit_before_call, + ContextMenu.NONE, + R.string.action_edit_number_before_call) + .setOnMenuItemClickListener(this); + } + + if (callType == CallLog.Calls.VOICEMAIL_TYPE + && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_copy_transcript_to_clipboard, + ContextMenu.NONE, + R.string.copy_transcript_text) + .setOnMenuItemClickListener(this); + } + + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number); + if (!isVoicemailNumber + && FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number) + && FilteredNumberCompat.canAttemptBlockOperations(mContext)) { + boolean isBlocked = blockId != null; + if (isBlocked) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_unblock, + ContextMenu.NONE, + R.string.call_log_action_unblock_number) + .setOnMenuItemClickListener(this); + } else { + if (isSpamFeatureEnabled) { + if (isSpam) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_report_not_spam, + ContextMenu.NONE, + R.string.call_log_action_remove_spam) + .setOnMenuItemClickListener(this); + menu.add( + ContextMenu.NONE, + R.id.context_menu_block, + ContextMenu.NONE, + R.string.call_log_action_block_number) + .setOnMenuItemClickListener(this); + } else { + menu.add( + ContextMenu.NONE, + R.id.context_menu_block_report_spam, + ContextMenu.NONE, + R.string.call_log_action_block_report_number) + .setOnMenuItemClickListener(this); + } + } else { + menu.add( + ContextMenu.NONE, + R.id.context_menu_block, + ContextMenu.NONE, + R.string.call_log_action_block_number) + .setOnMenuItemClickListener(this); + } + } + } + + Logger.get(mContext).logScreenView(ScreenEvent.Type.CALL_LOG_CONTEXT_MENU, (Activity) mContext); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + int resId = item.getItemId(); + if (resId == R.id.context_menu_copy_to_clipboard) { + ClipboardUtils.copyText(mContext, null, number, true); + return true; + } else if (resId == R.id.context_menu_copy_transcript_to_clipboard) { + ClipboardUtils.copyText( + mContext, null, phoneCallDetailsViews.voicemailTranscriptionView.getText(), true); + return true; + } else if (resId == R.id.context_menu_edit_before_call) { + final Intent intent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number)); + intent.setClass(mContext, DialtactsActivity.class); + DialerUtils.startActivityWithErrorToast(mContext, intent); + return true; + } else if (resId == R.id.context_menu_block_report_spam) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_REPORT_SPAM); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlockReportSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (resId == R.id.context_menu_block) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_NUMBER); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlock( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (resId == R.id.context_menu_unblock) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_UNBLOCK_NUMBER); + mBlockReportListener.onUnblock( + displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId); + } else if (resId == R.id.context_menu_report_not_spam) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_REPORT_AS_NOT_SPAM); + mBlockReportListener.onReportNotSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } + return false; + } + + /** + * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not inflated + * during initial binding, so click handlers, tags and accessibility text must be set here, if + * necessary. + */ + public void inflateActionViewStub() { + ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub); + if (stub != null) { + actionsView = stub.inflate(); + + voicemailPlaybackView = + (VoicemailPlaybackLayout) actionsView.findViewById(R.id.voicemail_playback_layout); + voicemailPlaybackView.setViewHolder(this); + + callButtonView = actionsView.findViewById(R.id.call_action); + callButtonView.setOnClickListener(this); + + videoCallButtonView = actionsView.findViewById(R.id.video_call_action); + videoCallButtonView.setOnClickListener(this); + + createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action); + createNewContactButtonView.setOnClickListener(this); + + addToExistingContactButtonView = + actionsView.findViewById(R.id.add_to_existing_contact_action); + addToExistingContactButtonView.setOnClickListener(this); + + sendMessageView = actionsView.findViewById(R.id.send_message_action); + sendMessageView.setOnClickListener(this); + + blockReportView = actionsView.findViewById(R.id.block_report_action); + blockReportView.setOnClickListener(this); + + blockView = actionsView.findViewById(R.id.block_action); + blockView.setOnClickListener(this); + + unblockView = actionsView.findViewById(R.id.unblock_action); + unblockView.setOnClickListener(this); + + reportNotSpamView = actionsView.findViewById(R.id.report_not_spam_action); + reportNotSpamView.setOnClickListener(this); + + detailsButtonView = actionsView.findViewById(R.id.details_action); + detailsButtonView.setOnClickListener(this); + + callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action); + callWithNoteButtonView.setOnClickListener(this); + + callComposeButtonView = actionsView.findViewById(R.id.call_compose_action); + callComposeButtonView.setOnClickListener(this); + + sendVoicemailButtonView = actionsView.findViewById(R.id.share_voicemail); + sendVoicemailButtonView.setOnClickListener(this); + } + } + + private void updatePrimaryActionButton(boolean isExpanded) { + + if (nameOrNumber == null) { + LogUtil.e("CallLogListItemViewHolder.updatePrimaryActionButton", "name or number is null"); + } + + // Calling expandTemplate with a null parameter will cause a NullPointerException. + CharSequence validNameOrNumber = nameOrNumber == null ? "" : nameOrNumber; + + if (!TextUtils.isEmpty(voicemailUri)) { + // Treat as voicemail list item; show play button if not expanded. + if (!isExpanded) { + primaryActionButtonView.setImageResource(R.drawable.ic_play_arrow_24dp); + primaryActionButtonView.setContentDescription( + TextUtils.expandTemplate( + mContext.getString(R.string.description_voicemail_action), validNameOrNumber)); + primaryActionButtonView.setVisibility(View.VISIBLE); + } else { + primaryActionButtonView.setVisibility(View.GONE); + } + } else { + // Treat as normal list item; show call button, if possible. + if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) { + boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number); + if (isVoicemailNumber) { + // Call to generic voicemail number, in case there are multiple accounts. + primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider()); + } else { + primaryActionButtonView.setTag( + IntentProvider.getReturnCallIntentProvider(number + postDialDigits)); + } + + primaryActionButtonView.setContentDescription( + TextUtils.expandTemplate( + mContext.getString(R.string.description_call_action), validNameOrNumber)); + primaryActionButtonView.setImageResource(R.drawable.ic_call_24dp); + primaryActionButtonView.setVisibility(View.VISIBLE); + } else { + primaryActionButtonView.setTag(null); + primaryActionButtonView.setVisibility(View.GONE); + } + } + } + + private static boolean isShareVoicemailAllowed(Context context) { + return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true); + } + + /** + * Binds text titles, click handlers and intents to the voicemail, details and callback action + * buttons. + */ + private void bindActionButtons() { + boolean canPlaceCallToNumber = PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation); + + if (isFullyUndialableVoicemail()) { + // Sometimes the voicemail server will report the message is from some non phone number + // source. If the number does not contains any dialable digit treat it as it is from a unknown + // number, remove all action buttons but still show the voicemail playback layout. + callButtonView.setVisibility(View.GONE); + videoCallButtonView.setVisibility(View.GONE); + detailsButtonView.setVisibility(View.GONE); + createNewContactButtonView.setVisibility(View.GONE); + addToExistingContactButtonView.setVisibility(View.GONE); + sendMessageView.setVisibility(View.GONE); + callWithNoteButtonView.setVisibility(View.GONE); + callComposeButtonView.setVisibility(View.GONE); + blockReportView.setVisibility(View.GONE); + blockView.setVisibility(View.GONE); + unblockView.setVisibility(View.GONE); + reportNotSpamView.setVisibility(View.GONE); + + if (isShareVoicemailAllowed(mContext)) { + sendVoicemailButtonView.setVisibility(View.VISIBLE); + } + voicemailPlaybackView.setVisibility(View.VISIBLE); + Uri uri = Uri.parse(voicemailUri); + mVoicemailPlaybackPresenter.setPlaybackView( + voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked); + mVoicemailPrimaryActionButtonClicked = false; + CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri); + return; + } + + if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) { + callButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number)); + ((TextView) callButtonView.findViewById(R.id.call_action_text)) + .setText( + TextUtils.expandTemplate( + mContext.getString(R.string.call_log_action_call), + nameOrNumber == null ? "" : nameOrNumber)); + TextView callTypeOrLocationView = + ((TextView) callButtonView.findViewById(R.id.call_type_or_location_text)); + if (callType == Calls.VOICEMAIL_TYPE && !TextUtils.isEmpty(callTypeOrLocation)) { + callTypeOrLocationView.setText(callTypeOrLocation); + callTypeOrLocationView.setVisibility(View.VISIBLE); + } else { + callTypeOrLocationView.setVisibility(View.GONE); + } + callButtonView.setVisibility(View.VISIBLE); + } else { + callButtonView.setVisibility(View.GONE); + } + + if (shouldShowVideoCallActionButton(canPlaceCallToNumber)) { + videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); + videoCallButtonView.setVisibility(View.VISIBLE); + } else { + videoCallButtonView.setVisibility(View.GONE); + } + + // For voicemail calls, show the voicemail playback layout; hide otherwise. + if (callType == Calls.VOICEMAIL_TYPE + && mVoicemailPlaybackPresenter != null + && !TextUtils.isEmpty(voicemailUri)) { + voicemailPlaybackView.setVisibility(View.VISIBLE); + if (isShareVoicemailAllowed(mContext)) { + Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE); + sendVoicemailButtonView.setVisibility(View.VISIBLE); + } + + Uri uri = Uri.parse(voicemailUri); + mVoicemailPlaybackPresenter.setPlaybackView( + voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked); + mVoicemailPrimaryActionButtonClicked = false; + CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri); + } else { + voicemailPlaybackView.setVisibility(View.GONE); + sendVoicemailButtonView.setVisibility(View.GONE); + } + + if (callType == Calls.VOICEMAIL_TYPE) { + detailsButtonView.setVisibility(View.GONE); + } else { + detailsButtonView.setVisibility(View.VISIBLE); + detailsButtonView.setTag(IntentProvider.getCallDetailIntentProvider(rowId, callIds, null)); + } + + boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam); + + if (!isBlockedOrSpam && info != null && UriUtils.isEncodedContactUri(info.lookupUri)) { + createNewContactButtonView.setTag( + IntentProvider.getAddContactIntentProvider( + info.lookupUri, info.name, info.number, info.type, true /* isNewContact */)); + createNewContactButtonView.setVisibility(View.VISIBLE); + + addToExistingContactButtonView.setTag( + IntentProvider.getAddContactIntentProvider( + info.lookupUri, info.name, info.number, info.type, false /* isNewContact */)); + addToExistingContactButtonView.setVisibility(View.VISIBLE); + } else { + createNewContactButtonView.setVisibility(View.GONE); + addToExistingContactButtonView.setVisibility(View.GONE); + } + + if (canPlaceCallToNumber && !isBlockedOrSpam) { + sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number)); + sendMessageView.setVisibility(View.VISIBLE); + } else { + sendMessageView.setVisibility(View.GONE); + } + + mCallLogListItemHelper.setActionContentDescriptions(this); + + boolean supportsCallSubject = mCallLogCache.doesAccountSupportCallSubject(accountHandle); + boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number); + callWithNoteButtonView.setVisibility( + supportsCallSubject && !isVoicemailNumber && info != null ? View.VISIBLE : View.GONE); + + callComposeButtonView.setVisibility(isCallComposerCapable ? View.VISIBLE : View.GONE); + + updateBlockReportActions(isVoicemailNumber); + } + + private boolean isFullyUndialableVoicemail() { + if (callType == Calls.VOICEMAIL_TYPE) { + if (!hasDialableChar(number)) { + return true; + } + } + return false; + } + + private static boolean hasDialableChar(CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + for (char c : number.toString().toCharArray()) { + if (PhoneNumberUtils.isDialable(c)) { + return true; + } + } + return false; + } + + private boolean shouldShowVideoCallActionButton(boolean canPlaceCallToNumber) { + return canPlaceCallToNumber && (hasPlacedVideoCall() || canSupportVideoCall()); + } + + private boolean hasPlacedVideoCall() { + return phoneCallDetailsViews.callTypeIcons.isVideoShown(); + } + + private boolean canSupportVideoCall() { + return mCallLogCache.canRelyOnVideoPresence() + && info != null + && (info.carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; + } + + /** + * Show or hide the action views, such as voicemail, details, and add contact. + * + *

If the action views have never been shown yet for this view, inflate the view stub. + */ + public void showActions(boolean show) { + showOrHideVoicemailTranscriptionView(show); + + if (show) { + if (!isLoaded) { + // b/31268128 for some unidentified reason showActions() can be called before the item is + // loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will + // be called again once the item is loaded. + LogUtil.e( + "CallLogListItemViewHolder.showActions", + "called before item is loaded", + new Exception()); + return; + } + + // Inflate the view stub if necessary, and wire up the event handlers. + inflateActionViewStub(); + bindActionButtons(); + actionsView.setVisibility(View.VISIBLE); + actionsView.setAlpha(1.0f); + } else { + // When recycling a view, it is possible the actionsView ViewStub was previously + // inflated so we should hide it in this case. + if (actionsView != null) { + actionsView.setVisibility(View.GONE); + } + } + + updatePrimaryActionButton(show); + } + + public void showOrHideVoicemailTranscriptionView(boolean isExpanded) { + if (callType != Calls.VOICEMAIL_TYPE) { + return; + } + + final TextView view = phoneCallDetailsViews.voicemailTranscriptionView; + if (!isExpanded || TextUtils.isEmpty(view.getText())) { + view.setVisibility(View.GONE); + return; + } + view.setVisibility(View.VISIBLE); + } + + public void updatePhoto() { + quickContactView.assignContactUri(info.lookupUri); + + if (isSpamFeatureEnabled && isSpam) { + quickContactView.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact)); + return; + } + final boolean isVoicemail = mCallLogCache.isVoicemailNumber(accountHandle, number); + int contactType = ContactPhotoManager.TYPE_DEFAULT; + if (isVoicemail) { + contactType = ContactPhotoManager.TYPE_VOICEMAIL; + } else if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } + + final String lookupKey = + info.lookupUri != null ? UriUtils.getLookupKeyFromUri(info.lookupUri) : null; + final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name; + final DefaultImageRequest request = + new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); + + if (info.photoId == 0 && info.photoUri != null) { + ContactPhotoManager.getInstance(mContext) + .loadPhoto( + quickContactView, + info.photoUri, + mPhotoSize, + false /* darkTheme */, + true /* isCircular */, + request); + } else { + ContactPhotoManager.getInstance(mContext) + .loadThumbnail( + quickContactView, + info.photoId, + false /* darkTheme */, + true /* isCircular */, + request); + } + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) { + Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_DIRECTLY); + mVoicemailPrimaryActionButtonClicked = true; + mExpandCollapseListener.onClick(primaryActionView); + } else if (view.getId() == R.id.call_with_note_action) { + CallSubjectDialog.start( + (Activity) mContext, + info.photoId, + info.photoUri, + info.lookupUri, + (String) nameOrNumber /* top line of contact view in call subject dialog */, + isBusiness, + number, + TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact + view in dialog. */ + numberType, /* phone number type (e.g. mobile) in second line of contact view */ + accountHandle); + } else if (view.getId() == R.id.block_report_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_REPORT_SPAM); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlockReportSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (view.getId() == R.id.block_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_NUMBER); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlock( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (view.getId() == R.id.unblock_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_UNBLOCK_NUMBER); + mBlockReportListener.onUnblock( + displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId); + } else if (view.getId() == R.id.report_not_spam_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_REPORT_AS_NOT_SPAM); + mBlockReportListener.onReportNotSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } else if (view.getId() == R.id.call_compose_action) { + LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed"); + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL); + CallComposerContact contact = new CallComposerContact(); + contact.photoId = info.photoId; + contact.photoUri = info.photoUri == null ? null : info.photoUri.toString(); + contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString(); + contact.nameOrNumber = (String) nameOrNumber; + contact.isBusiness = isBusiness; + contact.number = number; + /* second line of contact view. */ + contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber; + /* phone number type (e.g. mobile) in second line of contact view */ + contact.numberLabel = numberType; + Activity activity = (Activity) mContext; + activity.startActivityForResult( + CallComposerActivity.newIntent(activity, contact), + DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE); + } else if (view.getId() == R.id.share_voicemail) { + Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED); + mVoicemailPlaybackPresenter.shareVoicemail(); + } else { + logCallLogAction(view.getId()); + final IntentProvider intentProvider = (IntentProvider) view.getTag(); + if (intentProvider != null) { + final Intent intent = intentProvider.getIntent(mContext); + // See IntentProvider.getCallDetailIntentProvider() for why this may be null. + if (intent != null) { + DialerUtils.startActivityWithErrorToast(mContext, intent); + } + } + } + } + + private void logCallLogAction(int id) { + if (id == R.id.send_message_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE); + } else if (id == R.id.add_to_existing_contact_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_ADD_TO_CONTACT); + } else if (id == R.id.create_new_contact_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CREATE_NEW_CONTACT); + } + } + + private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) { + if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog( + mContext, ((Activity) mContext).getFragmentManager(), listener)) { + listener.onComplete(); + } + } + + private void updateBlockReportActions(boolean isVoicemailNumber) { + // Set block/spam actions. + blockReportView.setVisibility(View.GONE); + blockView.setVisibility(View.GONE); + unblockView.setVisibility(View.GONE); + reportNotSpamView.setVisibility(View.GONE); + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + if (isVoicemailNumber + || !FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number) + || !FilteredNumberCompat.canAttemptBlockOperations(mContext)) { + return; + } + boolean isBlocked = blockId != null; + if (isBlocked) { + unblockView.setVisibility(View.VISIBLE); + } else { + if (isSpamFeatureEnabled) { + if (isSpam) { + blockView.setVisibility(View.VISIBLE); + reportNotSpamView.setVisibility(View.VISIBLE); + } else { + blockReportView.setVisibility(View.VISIBLE); + } + } else { + blockView.setVisibility(View.VISIBLE); + } + } + } + + public interface OnClickListener { + + void onBlockReportSpam( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType); + + void onBlock( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType); + + void onUnblock( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType, + boolean isSpam, + Integer blockId); + + void onReportNotSpam( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType); + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java b/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java new file mode 100644 index 0000000000000000000000000000000000000000..9de260a0ae1ca636e0ba514b1977fcc1c821b180 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.app.alert.AlertManager; + +/** + * Alert manager controls modal view to show message in call log. When modal view is shown, regular + * call log will be hidden. + */ +public class CallLogModalAlertManager implements AlertManager { + + interface Listener { + void onShowModalAlert(boolean show); + } + + private final Listener listener; + private final ViewGroup parent; + private final ViewGroup container; + private final LayoutInflater inflater; + + public CallLogModalAlertManager(LayoutInflater inflater, ViewGroup parent, Listener listener) { + this.inflater = inflater; + this.parent = parent; + this.listener = listener; + container = (ViewGroup) parent.findViewById(R.id.modal_message_container); + } + + @Override + public View inflate(int layoutId) { + return inflater.inflate(layoutId, parent, false); + } + + @Override + public void add(View view) { + if (contains(view)) { + return; + } + container.addView(view); + listener.onShowModalAlert(true); + } + + @Override + public void clear() { + container.removeAllViews(); + listener.onShowModalAlert(false); + } + + public boolean isEmpty() { + return container.getChildCount() == 0; + } + + public boolean contains(View view) { + return container.indexOfChild(view) != -1; + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..8f664d1a41035b8255abc88e7deb4d05e48cacac --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.provider.CallLog.Calls; +import android.support.annotation.Nullable; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.R; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; +import java.util.List; + +/** Helper class operating on call log notifications. */ +public class CallLogNotificationsHelper { + + private static final String TAG = "CallLogNotifHelper"; + private static CallLogNotificationsHelper sInstance; + private final Context mContext; + private final NewCallsQuery mNewCallsQuery; + private final ContactInfoHelper mContactInfoHelper; + private final String mCurrentCountryIso; + + CallLogNotificationsHelper( + Context context, + NewCallsQuery newCallsQuery, + ContactInfoHelper contactInfoHelper, + String countryIso) { + mContext = context; + mNewCallsQuery = newCallsQuery; + mContactInfoHelper = contactInfoHelper; + mCurrentCountryIso = countryIso; + } + + /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */ + public static CallLogNotificationsHelper getInstance(Context context) { + if (sInstance == null) { + ContentResolver contentResolver = context.getContentResolver(); + String countryIso = GeoUtil.getCurrentCountryIso(context); + sInstance = + new CallLogNotificationsHelper( + context, + createNewCallsQuery(context, contentResolver), + new ContactInfoHelper(context, countryIso), + countryIso); + } + return sInstance; + } + + /** Removes the missed call notifications. */ + public static void removeMissedCallNotifications(Context context) { + TelecomUtil.cancelMissedCallsNotification(context); + } + + /** Update the voice mail notifications. */ + public static void updateVoicemailNotifications(Context context) { + CallLogNotificationsService.updateVoicemailNotifications(context, null); + } + + /** Create a new instance of {@link NewCallsQuery}. */ + public static NewCallsQuery createNewCallsQuery( + Context context, ContentResolver contentResolver) { + + return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver); + } + + /** + * Get all voicemails with the "new" flag set to 1. + * + * @return A list of NewCall objects where each object represents a new voicemail. + */ + @Nullable + public List getNewVoicemails() { + return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE); + } + + /** + * Get all missed calls with the "new" flag set to 1. + * + * @return A list of NewCall objects where each object represents a new missed call. + */ + @Nullable + public List getNewMissedCalls() { + return mNewCallsQuery.query(Calls.MISSED_TYPE); + } + + /** + * Given a number and number information (presentation and country ISO), get the best name for + * display. If the name is empty but we have a special presentation, display that. Otherwise + * attempt to look it up in the database or the cache. If that fails, fall back to displaying the + * number. + */ + public String getName( + @Nullable String number, int numberPresentation, @Nullable String countryIso) { + return getContactInfo(number, numberPresentation, countryIso).name; + } + + /** + * Given a number and number information (presentation and country ISO), get {@link ContactInfo}. + * If the name is empty but we have a special presentation, display that. Otherwise attempt to + * look it up in the cache. If that fails, fall back to displaying the number. + */ + public ContactInfo getContactInfo( + @Nullable String number, int numberPresentation, @Nullable String countryIso) { + if (countryIso == null) { + countryIso = mCurrentCountryIso; + } + + number = (number == null) ? "" : number; + ContactInfo contactInfo = new ContactInfo(); + contactInfo.number = number; + contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso); + // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo. + contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); + + // 1. Special number representation. + contactInfo.name = + PhoneNumberDisplayUtil.getDisplayName(mContext, number, numberPresentation, false) + .toString(); + if (!TextUtils.isEmpty(contactInfo.name)) { + return contactInfo; + } + + // 2. Look it up in the cache. + ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso); + + if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) { + return cachedContactInfo; + } + + if (!TextUtils.isEmpty(contactInfo.formattedNumber)) { + // 3. If we cannot lookup the contact, use the formatted number instead. + contactInfo.name = contactInfo.formattedNumber; + } else if (!TextUtils.isEmpty(number)) { + // 4. If number can't be formatted, use number. + contactInfo.name = number; + } else { + // 5. Otherwise, it's unknown number. + contactInfo.name = mContext.getResources().getString(R.string.unknown); + } + return contactInfo; + } + + /** Allows determining the new calls for which a notification should be generated. */ + public interface NewCallsQuery { + + /** Returns the new calls of a certain type for which a notification should be generated. */ + @Nullable + List query(int type); + } + + /** Information about a new voicemail. */ + public static final class NewCall { + + public final Uri callsUri; + public final Uri voicemailUri; + public final String number; + public final int numberPresentation; + public final String accountComponentName; + public final String accountId; + public final String transcription; + public final String countryIso; + public final long dateMs; + + public NewCall( + Uri callsUri, + Uri voicemailUri, + String number, + int numberPresentation, + String accountComponentName, + String accountId, + String transcription, + String countryIso, + long dateMs) { + this.callsUri = callsUri; + this.voicemailUri = voicemailUri; + this.number = number; + this.numberPresentation = numberPresentation; + this.accountComponentName = accountComponentName; + this.accountId = accountId; + this.transcription = transcription; + this.countryIso = countryIso; + this.dateMs = dateMs; + } + } + + /** + * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to notify + * about in the call log. + */ + private static final class DefaultNewCallsQuery implements NewCallsQuery { + + private static final String[] PROJECTION = { + Calls._ID, + Calls.NUMBER, + Calls.VOICEMAIL_URI, + Calls.NUMBER_PRESENTATION, + Calls.PHONE_ACCOUNT_COMPONENT_NAME, + Calls.PHONE_ACCOUNT_ID, + Calls.TRANSCRIPTION, + Calls.COUNTRY_ISO, + Calls.DATE + }; + private static final int ID_COLUMN_INDEX = 0; + private static final int NUMBER_COLUMN_INDEX = 1; + private static final int VOICEMAIL_URI_COLUMN_INDEX = 2; + private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3; + private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4; + private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5; + private static final int TRANSCRIPTION_COLUMN_INDEX = 6; + private static final int COUNTRY_ISO_COLUMN_INDEX = 7; + private static final int DATE_COLUMN_INDEX = 8; + + private final ContentResolver mContentResolver; + private final Context mContext; + + private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) { + mContext = context; + mContentResolver = contentResolver; + } + + @Override + @Nullable + @TargetApi(VERSION_CODES.M) + public List query(int type) { + if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) { + Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup."); + return null; + } + final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE); + final String[] selectionArgs = new String[] {Integer.toString(type)}; + try (Cursor cursor = + mContentResolver.query( + Calls.CONTENT_URI_WITH_VOICEMAIL, + PROJECTION, + selection, + selectionArgs, + Calls.DEFAULT_SORT_ORDER)) { + if (cursor == null) { + return null; + } + List newCalls = new ArrayList<>(); + while (cursor.moveToNext()) { + newCalls.add(createNewCallsFromCursor(cursor)); + } + return newCalls; + } catch (RuntimeException e) { + Log.w(TAG, "Exception when querying Contacts Provider for calls lookup"); + return null; + } + } + + /** Returns an instance of {@link NewCall} created by using the values of the cursor. */ + private NewCall createNewCallsFromCursor(Cursor cursor) { + String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX); + Uri callsUri = + ContentUris.withAppendedId( + Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX)); + Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString); + return new NewCall( + callsUri, + voicemailUri, + cursor.getString(NUMBER_COLUMN_INDEX), + cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX), + cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX), + cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX), + cursor.getString(TRANSCRIPTION_COLUMN_INDEX), + cursor.getString(COUNTRY_ISO_COLUMN_INDEX), + cursor.getLong(DATE_COLUMN_INDEX)); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java new file mode 100644 index 0000000000000000000000000000000000000000..8205281263f8361288781a99ccfc55043a7e4d4c --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import com.android.dialer.common.LogUtil; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.PermissionsUtil; +import me.leolin.shortcutbadger.ShortcutBadger; + +/** + * Provides operations for managing call-related notifications. + * + *

It handles the following actions: + * + *

    + *
  • Updating voicemail notifications + *
  • Marking new voicemails as old + *
  • Updating missed call notifications + *
  • Marking new missed calls as old + *
  • Calling back from a missed call + *
  • Sending an SMS from a missed call + *
+ */ +public class CallLogNotificationsService extends IntentService { + + /** Action to mark all the new voicemails as old. */ + public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD = + "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD"; + /** + * Action to update voicemail notifications. + * + *

May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}. + */ + public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS = + "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS"; + /** + * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new + * voicemail that triggered an update. + * + *

It must be a {@link Uri}. + */ + public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI"; + /** + * Action to update the missed call notifications. + * + *

Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and {@link + * #EXTRA_MISSED_CALL_COUNT}. + */ + public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS = + "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS"; + /** Action to mark all the new missed calls as old. */ + public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD = + "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD"; + /** Action to call back a missed call. */ + public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION = + "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION"; + + public static final String ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION = + "com.android.dialer.calllog.SEND_SMS_FROM_MISSED_CALL_NOTIFICATION"; + /** + * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS}, {@link + * #ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION} and {@link + * #ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION} to identify the number to display, call or + * text back. + * + *

It must be a {@link String}. + */ + public static final String EXTRA_MISSED_CALL_NUMBER = "MISSED_CALL_NUMBER"; + /** + * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS} to represent the + * number of missed calls. + * + *

It must be a {@link Integer} + */ + public static final String EXTRA_MISSED_CALL_COUNT = "MISSED_CALL_COUNT"; + + public static final int UNKNOWN_MISSED_CALL_COUNT = -1; + private VoicemailQueryHandler mVoicemailQueryHandler; + + public CallLogNotificationsService() { + super("CallLogNotificationsService"); + } + + /** + * Updates notifications for any new voicemails. + * + * @param context a valid context. + * @param voicemailUri The uri pointing to the voicemail to update the notification for. If {@code + * null}, then notifications for all new voicemails will be updated. + */ + public static void updateVoicemailNotifications(Context context, Uri voicemailUri) { + if (!TelecomUtil.isDefaultDialer(context)) { + LogUtil.i( + "CallLogNotificationsService.updateVoicemailNotifications", + "not default dialer, ignoring voicemail notifications"); + return; + } + if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS); + // If voicemailUri is null, then notifications for all voicemails will be updated. + if (voicemailUri != null) { + serviceIntent.putExtra(CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, voicemailUri); + } + context.startService(serviceIntent); + } + } + + /** + * Updates notifications for any new missed calls. + * + * @param context A valid context. + * @param count The number of new missed calls. + * @param number The phone number of the newest missed call. + */ + public static void updateMissedCallNotifications(Context context, int count, String number) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS); + serviceIntent.putExtra(EXTRA_MISSED_CALL_COUNT, count); + serviceIntent.putExtra(EXTRA_MISSED_CALL_NUMBER, number); + context.startService(serviceIntent); + } + + public static void markNewVoicemailsAsOld(Context context) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + context.startService(serviceIntent); + } + + public static boolean updateBadgeCount(Context context, int count) { + boolean success = ShortcutBadger.applyCount(context, count); + LogUtil.i( + "CallLogNotificationsService.updateBadgeCount", + "update badge count: %d success: %b", + count, + success); + return success; + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null) { + LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle null intent"); + return; + } + + if (!PermissionsUtil.hasPermission(this, android.Manifest.permission.READ_CALL_LOG)) { + return; + } + + String action = intent.getAction(); + switch (action) { + case ACTION_MARK_NEW_VOICEMAILS_AS_OLD: + if (mVoicemailQueryHandler == null) { + mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver()); + } + mVoicemailQueryHandler.markNewVoicemailsAsOld(); + break; + case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS: + Uri voicemailUri = intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI); + DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri); + break; + case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS: + int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT, UNKNOWN_MISSED_CALL_COUNT); + String number = intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER); + MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number); + updateBadgeCount(this, count); + break; + case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD: + CallLogNotificationsHelper.removeMissedCallNotifications(this); + break; + case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION: + MissedCallNotifier.getInstance(this) + .callBackFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + break; + case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION: + MissedCallNotifier.getInstance(this) + .sendSmsFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + break; + default: + LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent); + break; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..a781b0887f8907a5a176d6dfe499df2767a5a88c --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.provider.VoicemailContract; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.CallLogQueryHandler; + +/** + * Receiver for call log events. + * + *

It is currently used to handle {@link VoicemailContract#ACTION_NEW_VOICEMAIL} and {@link + * Intent#ACTION_BOOT_COMPLETED}. + */ +public class CallLogReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) { + checkVoicemailStatus(context); + CallLogNotificationsService.updateVoicemailNotifications(context, intent.getData()); + } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + CallLogNotificationsService.updateVoicemailNotifications(context, null); + } else { + LogUtil.w("CallLogReceiver.onReceive", "could not handle: " + intent); + } + } + + private static void checkVoicemailStatus(Context context) { + new CallLogQueryHandler( + context, + context.getContentResolver(), + new CallLogQueryHandler.Listener() { + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) { + VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus( + context, statusCursor, Source.Notification); + } + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public boolean onCallsFetched(Cursor combinedCursor) { + return false; + } + }) + .fetchVoicemailStatus(); + } +} diff --git a/java/com/android/dialer/app/calllog/CallTypeHelper.java b/java/com/android/dialer/app/calllog/CallTypeHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..f3c27a1ac9ebe5a4cec1540f468d99cd6c3223d2 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallTypeHelper.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.res.Resources; +import com.android.dialer.app.R; +import com.android.dialer.compat.AppCompatConstants; + +/** Helper class to perform operations related to call types. */ +public class CallTypeHelper { + + /** Name used to identify incoming calls. */ + private final CharSequence mIncomingName; + /** Name used to identify incoming calls which were transferred to another device. */ + private final CharSequence mIncomingPulledName; + /** Name used to identify outgoing calls. */ + private final CharSequence mOutgoingName; + /** Name used to identify outgoing calls which were transferred to another device. */ + private final CharSequence mOutgoingPulledName; + /** Name used to identify missed calls. */ + private final CharSequence mMissedName; + /** Name used to identify incoming video calls. */ + private final CharSequence mIncomingVideoName; + /** Name used to identify incoming video calls which were transferred to another device. */ + private final CharSequence mIncomingVideoPulledName; + /** Name used to identify outgoing video calls. */ + private final CharSequence mOutgoingVideoName; + /** Name used to identify outgoing video calls which were transferred to another device. */ + private final CharSequence mOutgoingVideoPulledName; + /** Name used to identify missed video calls. */ + private final CharSequence mMissedVideoName; + /** Name used to identify voicemail calls. */ + private final CharSequence mVoicemailName; + /** Name used to identify rejected calls. */ + private final CharSequence mRejectedName; + /** Name used to identify blocked calls. */ + private final CharSequence mBlockedName; + /** Name used to identify calls which were answered on another device. */ + private final CharSequence mAnsweredElsewhereName; + + public CallTypeHelper(Resources resources) { + // Cache these values so that we do not need to look them up each time. + mIncomingName = resources.getString(R.string.type_incoming); + mIncomingPulledName = resources.getString(R.string.type_incoming_pulled); + mOutgoingName = resources.getString(R.string.type_outgoing); + mOutgoingPulledName = resources.getString(R.string.type_outgoing_pulled); + mMissedName = resources.getString(R.string.type_missed); + mIncomingVideoName = resources.getString(R.string.type_incoming_video); + mIncomingVideoPulledName = resources.getString(R.string.type_incoming_video_pulled); + mOutgoingVideoName = resources.getString(R.string.type_outgoing_video); + mOutgoingVideoPulledName = resources.getString(R.string.type_outgoing_video_pulled); + mMissedVideoName = resources.getString(R.string.type_missed_video); + mVoicemailName = resources.getString(R.string.type_voicemail); + mRejectedName = resources.getString(R.string.type_rejected); + mBlockedName = resources.getString(R.string.type_blocked); + mAnsweredElsewhereName = resources.getString(R.string.type_answered_elsewhere); + } + + public static boolean isMissedCallType(int callType) { + return (callType != AppCompatConstants.CALLS_INCOMING_TYPE + && callType != AppCompatConstants.CALLS_OUTGOING_TYPE + && callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE + && callType != AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE); + } + + /** Returns the text used to represent the given call type. */ + public CharSequence getCallTypeText(int callType, boolean isVideoCall, boolean isPulledCall) { + switch (callType) { + case AppCompatConstants.CALLS_INCOMING_TYPE: + if (isVideoCall) { + if (isPulledCall) { + return mIncomingVideoPulledName; + } else { + return mIncomingVideoName; + } + } else { + if (isPulledCall) { + return mIncomingPulledName; + } else { + return mIncomingName; + } + } + + case AppCompatConstants.CALLS_OUTGOING_TYPE: + if (isVideoCall) { + if (isPulledCall) { + return mOutgoingVideoPulledName; + } else { + return mOutgoingVideoName; + } + } else { + if (isPulledCall) { + return mOutgoingPulledName; + } else { + return mOutgoingName; + } + } + + case AppCompatConstants.CALLS_MISSED_TYPE: + if (isVideoCall) { + return mMissedVideoName; + } else { + return mMissedName; + } + + case AppCompatConstants.CALLS_VOICEMAIL_TYPE: + return mVoicemailName; + + case AppCompatConstants.CALLS_REJECTED_TYPE: + return mRejectedName; + + case AppCompatConstants.CALLS_BLOCKED_TYPE: + return mBlockedName; + + case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE: + return mAnsweredElsewhereName; + + default: + return mMissedName; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallTypeIconsView.java b/java/com/android/dialer/app/calllog/CallTypeIconsView.java new file mode 100644 index 0000000000000000000000000000000000000000..cd5c5460c2493c4cb60cc55ac1d39d0e3685551d --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallTypeIconsView.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import com.android.contacts.common.util.BitmapUtil; +import com.android.dialer.app.R; +import com.android.dialer.compat.AppCompatConstants; +import java.util.ArrayList; +import java.util.List; + +/** + * View that draws one or more symbols for different types of calls (missed calls, outgoing etc). + * The symbols are set up horizontally. As this view doesn't create subviews, it is better suited + * for ListView-recycling that a regular LinearLayout using ImageViews. + */ +public class CallTypeIconsView extends View { + + private static Resources sResources; + private List mCallTypes = new ArrayList<>(3); + private boolean mShowVideo = false; + private int mWidth; + private int mHeight; + + public CallTypeIconsView(Context context) { + this(context, null); + } + + public CallTypeIconsView(Context context, AttributeSet attrs) { + super(context, attrs); + if (sResources == null) { + sResources = new Resources(context); + } + } + + public void clear() { + mCallTypes.clear(); + mWidth = 0; + mHeight = 0; + invalidate(); + } + + public void add(int callType) { + mCallTypes.add(callType); + + final Drawable drawable = getCallTypeDrawable(callType); + mWidth += drawable.getIntrinsicWidth() + sResources.iconMargin; + mHeight = Math.max(mHeight, drawable.getIntrinsicHeight()); + invalidate(); + } + + /** + * Determines whether the video call icon will be shown. + * + * @param showVideo True where the video icon should be shown. + */ + public void setShowVideo(boolean showVideo) { + mShowVideo = showVideo; + if (showVideo) { + mWidth += sResources.videoCall.getIntrinsicWidth(); + mHeight = Math.max(mHeight, sResources.videoCall.getIntrinsicHeight()); + invalidate(); + } + } + + /** + * Determines if the video icon should be shown. + * + * @return True if the video icon should be shown. + */ + public boolean isVideoShown() { + return mShowVideo; + } + + public int getCount() { + return mCallTypes.size(); + } + + public int getCallType(int index) { + return mCallTypes.get(index); + } + + private Drawable getCallTypeDrawable(int callType) { + switch (callType) { + case AppCompatConstants.CALLS_INCOMING_TYPE: + case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE: + return sResources.incoming; + case AppCompatConstants.CALLS_OUTGOING_TYPE: + return sResources.outgoing; + case AppCompatConstants.CALLS_MISSED_TYPE: + return sResources.missed; + case AppCompatConstants.CALLS_VOICEMAIL_TYPE: + return sResources.voicemail; + case AppCompatConstants.CALLS_BLOCKED_TYPE: + return sResources.blocked; + default: + // It is possible for users to end up with calls with unknown call types in their + // call history, possibly due to 3rd party call log implementations (e.g. to + // distinguish between rejected and missed calls). Instead of crashing, just + // assume that all unknown call types are missed calls. + return sResources.missed; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mWidth, mHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + int left = 0; + for (Integer callType : mCallTypes) { + final Drawable drawable = getCallTypeDrawable(callType); + final int right = left + drawable.getIntrinsicWidth(); + drawable.setBounds(left, 0, right, drawable.getIntrinsicHeight()); + drawable.draw(canvas); + left = right + sResources.iconMargin; + } + + // If showing the video call icon, draw it scaled appropriately. + if (mShowVideo) { + final Drawable drawable = sResources.videoCall; + final int right = left + sResources.videoCall.getIntrinsicWidth(); + drawable.setBounds(left, 0, right, sResources.videoCall.getIntrinsicHeight()); + drawable.draw(canvas); + } + } + + private static class Resources { + + // Drawable representing an incoming answered call. + public final Drawable incoming; + + // Drawable respresenting an outgoing call. + public final Drawable outgoing; + + // Drawable representing an incoming missed call. + public final Drawable missed; + + // Drawable representing a voicemail. + public final Drawable voicemail; + + // Drawable representing a blocked call. + public final Drawable blocked; + + // Drawable repesenting a video call. + public final Drawable videoCall; + + /** The margin to use for icons. */ + public final int iconMargin; + + /** + * Configures the call icon drawables. A single white call arrow which points down and left is + * used as a basis for all of the call arrow icons, applying rotation and colors as needed. + * + * @param context The current context. + */ + public Resources(Context context) { + final android.content.res.Resources r = context.getResources(); + + incoming = r.getDrawable(R.drawable.ic_call_arrow); + incoming.setColorFilter(r.getColor(R.color.answered_call), PorterDuff.Mode.MULTIPLY); + + // Create a rotated instance of the call arrow for outgoing calls. + outgoing = BitmapUtil.getRotatedDrawable(r, R.drawable.ic_call_arrow, 180f); + outgoing.setColorFilter(r.getColor(R.color.answered_call), PorterDuff.Mode.MULTIPLY); + + // Need to make a copy of the arrow drawable, otherwise the same instance colored + // above will be recolored here. + missed = r.getDrawable(R.drawable.ic_call_arrow).mutate(); + missed.setColorFilter(r.getColor(R.color.missed_call), PorterDuff.Mode.MULTIPLY); + + voicemail = r.getDrawable(R.drawable.quantum_ic_voicemail_white_18); + voicemail.setColorFilter( + r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY); + + blocked = getScaledBitmap(context, R.drawable.ic_block_24dp); + blocked.setColorFilter(r.getColor(R.color.blocked_call), PorterDuff.Mode.MULTIPLY); + + videoCall = getScaledBitmap(context, R.drawable.quantum_ic_videocam_white_24); + videoCall.setColorFilter( + r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY); + + iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin); + } + + // Gets the icon, scaled to the height of the call type icons. This helps display all the + // icons to be the same height, while preserving their width aspect ratio. + private Drawable getScaledBitmap(Context context, int resourceId) { + Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resourceId); + int scaledHeight = context.getResources().getDimensionPixelSize(R.dimen.call_type_icon_size); + int scaledWidth = + (int) ((float) icon.getWidth() * ((float) scaledHeight / (float) icon.getHeight())); + Bitmap scaledIcon = Bitmap.createScaledBitmap(icon, scaledWidth, scaledHeight, false); + return new BitmapDrawable(context.getResources(), scaledIcon); + } + } +} diff --git a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..0c9bd4b35f1d799ea7f376f8d20e4e9cbc7b1b63 --- /dev/null +++ b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.CallLog.Calls; +import com.android.dialer.app.R; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.PhoneNumberCache; + +/** Dialog that clears the call log after confirming with the user */ +public class ClearCallLogDialog extends DialogFragment { + + /** Preferred way to show this dialog */ + public static void show(FragmentManager fragmentManager) { + ClearCallLogDialog dialog = new ClearCallLogDialog(); + dialog.show(fragmentManager, "deleteCallLog"); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final ContentResolver resolver = getActivity().getContentResolver(); + final Context context = getActivity().getApplicationContext(); + final OnClickListener okListener = + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final ProgressDialog progressDialog = + ProgressDialog.show( + getActivity(), getString(R.string.clearCallLogProgress_title), "", true, false); + progressDialog.setOwnerActivity(getActivity()); + final AsyncTask task = + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + resolver.delete(Calls.CONTENT_URI, null, null); + CachedNumberLookupService cachedNumberLookupService = + PhoneNumberCache.get(context).getCachedNumberLookupService(); + if (cachedNumberLookupService != null) { + cachedNumberLookupService.clearAllCacheEntries(context); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + final Activity activity = progressDialog.getOwnerActivity(); + + if (activity == null || activity.isDestroyed() || activity.isFinishing()) { + return; + } + + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + } + }; + // TODO: Once we have the API, we should configure this ProgressDialog + // to only show up after a certain time (e.g. 150ms) + progressDialog.show(); + task.execute(); + } + }; + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.clearCallLogConfirmation_title) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setMessage(R.string.clearCallLogConfirmation) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, okListener) + .setCancelable(true) + .create(); + } +} diff --git a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java new file mode 100644 index 0000000000000000000000000000000000000000..651a0ccb85136eba38c9a0604774520bc8b77b6e --- /dev/null +++ b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.v4.util.Pair; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import com.android.contacts.common.compat.TelephonyManagerCompat; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.telecom.TelecomUtil; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** Shows a voicemail notification in the status bar. */ +public class DefaultVoicemailNotifier { + + public static final String TAG = "VoicemailNotifier"; + + /** The tag used to identify notifications from this class. */ + private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier"; + /** The identifier of the notification of new voicemails. */ + private static final int NOTIFICATION_ID = 1; + + /** The singleton instance of {@link DefaultVoicemailNotifier}. */ + private static DefaultVoicemailNotifier sInstance; + + private final Context mContext; + + private DefaultVoicemailNotifier(Context context) { + mContext = context; + } + + /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */ + public static DefaultVoicemailNotifier getInstance(Context context) { + if (sInstance == null) { + ContentResolver contentResolver = context.getContentResolver(); + sInstance = new DefaultVoicemailNotifier(context); + } + return sInstance; + } + + /** + * Updates the notification and notifies of the call with the given URI. + * + *

Clears the notification if there are no new voicemails, and notifies if the given URI + * corresponds to a new voicemail. + * + *

It is not safe to call this method from the main thread. + */ + public void updateNotification(Uri newCallUri) { + // Lookup the list of new voicemails to include in the notification. + // TODO: Move this into a service, to avoid holding the receiver up. + final List newCalls = + CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails(); + + if (newCalls == null) { + // Query failed, just return. + return; + } + + if (newCalls.isEmpty()) { + // No voicemails to notify about: clear the notification. + getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID); + return; + } + + Resources resources = mContext.getResources(); + + // This represents a list of names to include in the notification. + String callers = null; + + // Maps each number into a name: if a number is in the map, it has already left a more + // recent voicemail. + final Map names = new ArrayMap<>(); + + // Determine the call corresponding to the new voicemail we have to notify about. + NewCall callToNotify = null; + + // Iterate over the new voicemails to determine all the information above. + Iterator itr = newCalls.iterator(); + while (itr.hasNext()) { + NewCall newCall = itr.next(); + + // Skip notifying for numbers which are blocked. + if (FilteredNumbersUtil.shouldBlockVoicemail( + mContext, newCall.number, newCall.countryIso, newCall.dateMs)) { + itr.remove(); + + // Delete the voicemail. + mContext.getContentResolver().delete(newCall.voicemailUri, null, null); + continue; + } + + // Check if we already know the name associated with this number. + String name = names.get(newCall.number); + if (name == null) { + name = + CallLogNotificationsHelper.getInstance(mContext) + .getName(newCall.number, newCall.numberPresentation, newCall.countryIso); + names.put(newCall.number, name); + // This is a new caller. Add it to the back of the list of callers. + if (TextUtils.isEmpty(callers)) { + callers = name; + } else { + callers = + resources.getString(R.string.notification_voicemail_callers_list, callers, name); + } + } + // Check if this is the new call we need to notify about. + if (newCallUri != null + && newCall.voicemailUri != null + && ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) { + callToNotify = newCall; + } + } + + // All the potential new voicemails have been removed, e.g. if they were spam. + if (newCalls.isEmpty()) { + return; + } + + // If there is only one voicemail, set its transcription as the "long text". + String transcription = null; + if (newCalls.size() == 1) { + transcription = newCalls.get(0).transcription; + } + + if (newCallUri != null && callToNotify == null) { + Log.e(TAG, "The new call could not be found in the call log: " + newCallUri); + } + + // Determine the title of the notification and the icon for it. + final String title = + resources.getQuantityString( + R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size()); + // TODO: Use the photo of contact if all calls are from the same person. + final int icon = android.R.drawable.stat_notify_voicemail; + + Pair info = getNotificationInfo(callToNotify); + + Notification.Builder notificationBuilder = + new Notification.Builder(mContext) + .setSmallIcon(icon) + .setContentTitle(title) + .setContentText(callers) + .setColor(resources.getColor(R.color.dialer_theme_color)) + .setSound(info.first) + .setDefaults(info.second) + .setDeleteIntent(createMarkNewVoicemailsAsOldIntent()) + .setAutoCancel(true); + + if (!TextUtils.isEmpty(transcription)) { + notificationBuilder.setStyle(new Notification.BigTextStyle().bigText(transcription)); + } + + // Determine the intent to fire when the notification is clicked on. + final Intent contentIntent; + // Open the call log. + contentIntent = DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_VOICEMAIL); + contentIntent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); + notificationBuilder.setContentIntent( + PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT)); + + // The text to show in the ticker, describing the new event. + if (callToNotify != null) { + CharSequence msg = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + resources, + R.string.notification_new_voicemail_ticker, + names.get(callToNotify.number)); + notificationBuilder.setTicker(msg); + } + Log.i(TAG, "Creating voicemail notification"); + getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build()); + } + + /** + * Determines which ringtone Uri and Notification defaults to use when updating the notification + * for the given call. + */ + private Pair getNotificationInfo(@Nullable NewCall callToNotify) { + Log.v(TAG, "getNotificationInfo"); + if (callToNotify == null) { + Log.i(TAG, "callToNotify == null"); + return new Pair<>(null, 0); + } + PhoneAccountHandle accountHandle; + if (callToNotify.accountComponentName == null || callToNotify.accountId == null) { + Log.v(TAG, "accountComponentName == null || callToNotify.accountId == null"); + accountHandle = TelecomUtil.getDefaultOutgoingPhoneAccount(mContext, PhoneAccount.SCHEME_TEL); + if (accountHandle == null) { + Log.i(TAG, "No default phone account found, using default notification ringtone"); + return new Pair<>(null, Notification.DEFAULT_ALL); + } + + } else { + accountHandle = + new PhoneAccountHandle( + ComponentName.unflattenFromString(callToNotify.accountComponentName), + callToNotify.accountId); + } + if (accountHandle.getComponentName() != null) { + Log.v(TAG, "PhoneAccountHandle.ComponentInfo:" + accountHandle.getComponentName()); + } else { + Log.i(TAG, "PhoneAccountHandle.ComponentInfo: null"); + } + return new Pair<>( + TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle), + getNotificationDefaults(accountHandle)); + } + + private int getNotificationDefaults(PhoneAccountHandle accountHandle) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return TelephonyManagerCompat.isVoicemailVibrationEnabled( + getTelephonyManager(), accountHandle) + ? Notification.DEFAULT_VIBRATE + : 0; + } + return Notification.DEFAULT_ALL; + } + + /** Creates a pending intent that marks all new voicemails as old. */ + private PendingIntent createMarkNewVoicemailsAsOldIntent() { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + return PendingIntent.getService(mContext, 0, intent, 0); + } + + private NotificationManager getNotificationManager() { + return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + private TelephonyManager getTelephonyManager() { + return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + } +} diff --git a/java/com/android/dialer/app/calllog/GroupingListAdapter.java b/java/com/android/dialer/app/calllog/GroupingListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..d1157206fc0e88eec69b8d549991747794d0af00 --- /dev/null +++ b/java/com/android/dialer/app/calllog/GroupingListAdapter.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.support.v7.widget.RecyclerView; +import android.util.SparseIntArray; + +/** + * Maintains a list that groups items into groups of consecutive elements which are disjoint, that + * is, an item can only belong to one group. This is leveraged for grouping calls in the call log + * received from or made to the same phone number. + * + *

There are two integers stored as metadata for every list item in the adapter. + */ +abstract class GroupingListAdapter extends RecyclerView.Adapter { + + protected ContentObserver mChangeObserver = + new ContentObserver(new Handler()) { + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + }; + protected DataSetObserver mDataSetObserver = + new DataSetObserver() { + @Override + public void onChanged() { + notifyDataSetChanged(); + } + }; + private Cursor mCursor; + /** + * SparseIntArray, which maps the cursor position of the first element of a group to the size of + * the group. The index of a key in this map corresponds to the list position of that group. + */ + private SparseIntArray mGroupMetadata; + + private int mItemCount; + + public GroupingListAdapter() { + reset(); + } + + /** + * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for each of them. + */ + protected abstract void addGroups(Cursor cursor); + + protected abstract void onContentChanged(); + + public void changeCursor(Cursor cursor) { + if (cursor == mCursor) { + return; + } + + if (mCursor != null) { + mCursor.unregisterContentObserver(mChangeObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.close(); + } + + // Reset whenever the cursor is changed. + reset(); + mCursor = cursor; + + if (cursor != null) { + addGroups(mCursor); + + // Calculate the item count by subtracting group child counts from the cursor count. + mItemCount = mGroupMetadata.size(); + + cursor.registerContentObserver(mChangeObserver); + cursor.registerDataSetObserver(mDataSetObserver); + notifyDataSetChanged(); + } + } + + /** + * Records information about grouping in the list. Should be called by the overridden {@link + * #addGroups} method. + */ + public void addGroup(int cursorPosition, int groupSize) { + int lastIndex = mGroupMetadata.size() - 1; + if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) { + mGroupMetadata.put(cursorPosition, groupSize); + } else { + // Optimization to avoid binary search if adding groups in ascending cursor position. + mGroupMetadata.append(cursorPosition, groupSize); + } + } + + @Override + public int getItemCount() { + return mItemCount; + } + + /** + * Given the position of a list item, returns the size of the group of items corresponding to that + * position. + */ + public int getGroupSize(int listPosition) { + if (listPosition < 0 || listPosition >= mGroupMetadata.size()) { + return 0; + } + + return mGroupMetadata.valueAt(listPosition); + } + + /** + * Given the position of a list item, returns the the first item in the group of items + * corresponding to that position. + */ + public Object getItem(int listPosition) { + if (mCursor == null || listPosition < 0 || listPosition >= mGroupMetadata.size()) { + return null; + } + + int cursorPosition = mGroupMetadata.keyAt(listPosition); + if (mCursor.moveToPosition(cursorPosition)) { + return mCursor; + } else { + return null; + } + } + + private void reset() { + mItemCount = 0; + mGroupMetadata = new SparseIntArray(); + } +} diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..879ac353dcbbabf503273c41c1b0655b984c7a88 --- /dev/null +++ b/java/com/android/dialer/app/calllog/IntentProvider.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.ContactsContract; +import android.telecom.PhoneAccountHandle; +import com.android.contacts.common.model.Contact; +import com.android.contacts.common.model.ContactLoader; +import com.android.dialer.app.CallDetailActivity; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.IntentUtil; +import java.util.ArrayList; + +/** + * Used to create an intent to attach to an action in the call log. + * + *

The intent is constructed lazily with the given information. + */ +public abstract class IntentProvider { + + private static final String TAG = IntentProvider.class.getSimpleName(); + + public static IntentProvider getReturnCallIntentProvider(final String number) { + return getReturnCallIntentProvider(number, null); + } + + public static IntentProvider getReturnCallIntentProvider( + final String number, final PhoneAccountHandle accountHandle) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG) + .setPhoneAccountHandle(accountHandle) + .build(); + } + }; + } + + public static IntentProvider getReturnVideoCallIntentProvider(final String number) { + return getReturnVideoCallIntentProvider(number, null); + } + + public static IntentProvider getReturnVideoCallIntentProvider( + final String number, final PhoneAccountHandle accountHandle) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG) + .setPhoneAccountHandle(accountHandle) + .setIsVideoCall(true) + .build(); + } + }; + } + + public static IntentProvider getReturnVoicemailCallIntentProvider() { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return new CallIntentBuilder(CallUtil.getVoicemailUri(), CallInitiationType.Type.CALL_LOG) + .build(); + } + }; + } + + public static IntentProvider getSendSmsIntentProvider(final String number) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return IntentUtil.getSendSmsIntent(number); + } + }; + } + + /** + * Retrieves the call details intent provider for an entry in the call log. + * + * @param id The call ID of the first call in the call group. + * @param extraIds The call ID of the other calls grouped together with the call. + * @param voicemailUri If call log entry is for a voicemail, the voicemail URI. + * @return The call details intent provider. + */ + public static IntentProvider getCallDetailIntentProvider( + final long id, final long[] extraIds, final String voicemailUri) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + Intent intent = new Intent(context, CallDetailActivity.class); + // Check if the first item is a voicemail. + if (voicemailUri != null) { + intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri)); + } + + if (extraIds != null && extraIds.length > 0) { + intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, extraIds); + } else { + // If there is a single item, use the direct URI for it. + intent.setData(ContentUris.withAppendedId(TelecomUtil.getCallLogUri(context), id)); + } + return intent; + } + }; + } + + /** Retrieves an add contact intent for the given contact and phone call details. */ + public static IntentProvider getAddContactIntentProvider( + final Uri lookupUri, + final CharSequence name, + final CharSequence number, + final int numberType, + final boolean isNewContact) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + Contact contactToSave = null; + + if (lookupUri != null) { + contactToSave = ContactLoader.parseEncodedContactEntity(lookupUri); + } + + if (contactToSave != null) { + // Populate the intent with contact information stored in the lookup URI. + // Note: This code mirrors code in Contacts/QuickContactsActivity. + final Intent intent; + if (isNewContact) { + intent = IntentUtil.getNewContactIntent(); + } else { + intent = IntentUtil.getAddToExistingContactIntent(); + } + + ArrayList values = contactToSave.getContentValues(); + // Only pre-fill the name field if the provided display name is an nickname + // or better (e.g. structured name, nickname) + if (contactToSave.getDisplayNameSource() + >= ContactsContract.DisplayNameSources.NICKNAME) { + intent.putExtra(ContactsContract.Intents.Insert.NAME, contactToSave.getDisplayName()); + } else if (contactToSave.getDisplayNameSource() + == ContactsContract.DisplayNameSources.ORGANIZATION) { + // This is probably an organization. Instead of copying the organization + // name into a name entry, copy it into the organization entry. This + // way we will still consider the contact an organization. + final ContentValues organization = new ContentValues(); + organization.put( + ContactsContract.CommonDataKinds.Organization.COMPANY, + contactToSave.getDisplayName()); + organization.put( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE); + values.add(organization); + } + + // Last time used and times used are aggregated values from the usage stat + // table. They need to be removed from data values so the SQL table can insert + // properly + for (ContentValues value : values) { + value.remove(ContactsContract.Data.LAST_TIME_USED); + value.remove(ContactsContract.Data.TIMES_USED); + } + + intent.putExtra(ContactsContract.Intents.Insert.DATA, values); + + return intent; + } else { + // If no lookup uri is provided, rely on the available phone number and name. + if (isNewContact) { + return IntentUtil.getNewContactIntent(name, number, numberType); + } else { + return IntentUtil.getAddToExistingContactIntent(name, number, numberType); + } + } + } + }; + } + + public abstract Intent getIntent(Context context); +} diff --git a/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java b/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..3a202034e4f6202bc38b2e575c697ad8891f8d25 --- /dev/null +++ b/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.calllog; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Receives broadcasts that should trigger a refresh of the missed call notification. This includes + * both an explicit broadcast from Telecom and a reboot. + */ +public class MissedCallNotificationReceiver extends BroadcastReceiver { + + //TODO: Use compat class for these methods. + public static final String ACTION_SHOW_MISSED_CALLS_NOTIFICATION = + "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"; + + public static final String EXTRA_NOTIFICATION_COUNT = "android.telecom.extra.NOTIFICATION_COUNT"; + + public static final String EXTRA_NOTIFICATION_PHONE_NUMBER = + "android.telecom.extra.NOTIFICATION_PHONE_NUMBER"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (!ACTION_SHOW_MISSED_CALLS_NOTIFICATION.equals(action)) { + return; + } + + int count = + intent.getIntExtra( + EXTRA_NOTIFICATION_COUNT, CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT); + String number = intent.getStringExtra(EXTRA_NOTIFICATION_PHONE_NUMBER); + CallLogNotificationsService.updateMissedCallNotifications(context, count, number); + } +} diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java new file mode 100644 index 0000000000000000000000000000000000000000..2fa3dae65890fbd5f5f997ae9f97947077d7a2aa --- /dev/null +++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.calllog; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.AsyncTask; +import android.provider.CallLog.Calls; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.os.UserManagerCompat; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall; +import com.android.dialer.app.contactinfo.ContactPhotoLoader; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import java.util.List; + +/** Creates a notification for calls that the user missed (neither answered nor rejected). */ +public class MissedCallNotifier { + + /** The tag used to identify notifications from this class. */ + private static final String NOTIFICATION_TAG = "MissedCallNotifier"; + /** The identifier of the notification of new missed calls. */ + private static final int NOTIFICATION_ID = 1; + + private static MissedCallNotifier sInstance; + private Context mContext; + private CallLogNotificationsHelper mCalllogNotificationsHelper; + + @VisibleForTesting + MissedCallNotifier(Context context, CallLogNotificationsHelper callLogNotificationsHelper) { + mContext = context; + mCalllogNotificationsHelper = callLogNotificationsHelper; + } + + /** Returns the singleton instance of the {@link MissedCallNotifier}. */ + public static MissedCallNotifier getInstance(Context context) { + if (sInstance == null) { + CallLogNotificationsHelper callLogNotificationsHelper = + CallLogNotificationsHelper.getInstance(context); + sInstance = new MissedCallNotifier(context, callLogNotificationsHelper); + } + return sInstance; + } + + /** + * Creates a missed call notification with a post call message if there are no existing missed + * calls. + */ + public void createPostCallMessageNotification(String number, String message) { + int count = CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT; + if (ConfigProviderBindings.get(mContext).getBoolean("enable_call_compose", false)) { + updateMissedCallNotification(count, number, message); + } else { + updateMissedCallNotification(count, number, null); + } + } + + /** Creates a missed call notification. */ + public void updateMissedCallNotification(int count, String number) { + updateMissedCallNotification(count, number, null); + } + + private void updateMissedCallNotification( + int count, String number, @Nullable String postCallMessage) { + final int titleResId; + CharSequence expandedText; // The text in the notification's line 1 and 2. + + final List newCalls = mCalllogNotificationsHelper.getNewMissedCalls(); + + if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) { + if (newCalls == null) { + // If the intent did not contain a count, and we are unable to get a count from the + // call log, then no notification can be shown. + return; + } + count = newCalls.size(); + } + + if (count == 0) { + // No voicemails to notify about: clear the notification. + clearMissedCalls(); + return; + } + + // The call log has been updated, use that information preferentially. + boolean useCallLog = newCalls != null && newCalls.size() == count; + NewCall newestCall = useCallLog ? newCalls.get(0) : null; + long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis(); + String missedNumber = useCallLog ? newestCall.number : number; + + Notification.Builder builder = new Notification.Builder(mContext); + // Display the first line of the notification: + // 1 missed call: + // More than 1 missed call: + "missed calls" + if (count == 1) { + //TODO: look up caller ID that is not in contacts. + ContactInfo contactInfo = + mCalllogNotificationsHelper.getContactInfo( + missedNumber, + useCallLog ? newestCall.numberPresentation : Calls.PRESENTATION_ALLOWED, + useCallLog ? newestCall.countryIso : null); + + titleResId = + contactInfo.userType == ContactsUtils.USER_TYPE_WORK + ? R.string.notification_missedWorkCallTitle + : R.string.notification_missedCallTitle; + if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) + || TextUtils.equals(contactInfo.name, contactInfo.number)) { + expandedText = + PhoneNumberUtilsCompat.createTtsSpannable( + BidiFormatter.getInstance() + .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR)); + } else { + expandedText = contactInfo.name; + } + + if (!TextUtils.isEmpty(postCallMessage)) { + // Ex. "John Doe: Hey dude" + expandedText = + mContext.getString( + R.string.post_call_notification_message, expandedText, postCallMessage); + } + ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo); + Bitmap photoIcon = loader.loadPhotoIcon(); + if (photoIcon != null) { + builder.setLargeIcon(photoIcon); + } + } else { + titleResId = R.string.notification_missedCallsTitle; + expandedText = mContext.getString(R.string.notification_missedCallsMsg, count); + } + + // Create a public viewable version of the notification, suitable for display when sensitive + // notification content is hidden. + Notification.Builder publicBuilder = new Notification.Builder(mContext); + publicBuilder + .setSmallIcon(android.R.drawable.stat_notify_missed_call) + .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) + // Show "Phone" for notification title. + .setContentTitle(mContext.getText(R.string.userCallActivityLabel)) + // Notification details shows that there are missed call(s), but does not reveal + // the missed caller information. + .setContentText(mContext.getText(titleResId)) + .setContentIntent(createCallLogPendingIntent()) + .setAutoCancel(true) + .setWhen(timeMs) + .setShowWhen(true) + .setDeleteIntent(createClearMissedCallsPendingIntent()); + + // Create the notification suitable for display when sensitive information is showing. + builder + .setSmallIcon(android.R.drawable.stat_notify_missed_call) + .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) + .setContentTitle(mContext.getText(titleResId)) + .setContentText(expandedText) + .setContentIntent(createCallLogPendingIntent()) + .setAutoCancel(true) + .setWhen(timeMs) + .setShowWhen(true) + .setDefaults(Notification.DEFAULT_VIBRATE) + .setDeleteIntent(createClearMissedCallsPendingIntent()) + // Include a public version of the notification to be shown when the missed call + // notification is shown on the user's lock screen and they have chosen to hide + // sensitive notification information. + .setPublicVersion(publicBuilder.build()); + + // Add additional actions when there is only 1 missed call and the user isn't locked + if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) { + if (!TextUtils.isEmpty(missedNumber) + && !TextUtils.equals(missedNumber, mContext.getString(R.string.handle_restricted))) { + builder.addAction( + R.drawable.ic_phone_24dp, + mContext.getString(R.string.notification_missedCall_call_back), + createCallBackPendingIntent(missedNumber)); + + if (!PhoneNumberHelper.isUriNumber(missedNumber)) { + builder.addAction( + R.drawable.ic_message_24dp, + mContext.getString(R.string.notification_missedCall_message), + createSendSmsFromNotificationPendingIntent(missedNumber)); + } + } + } + + Notification notification = builder.build(); + configureLedOnNotification(notification); + + LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); + getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification); + } + + private void clearMissedCalls() { + AsyncTask.execute( + new Runnable() { + @Override + public void run() { + // Call log is only accessible when unlocked. If that's the case, clear the list of + // new missed calls from the call log. + if (UserManagerCompat.isUserUnlocked(mContext)) { + ContentValues values = new ContentValues(); + values.put(Calls.NEW, 0); + values.put(Calls.IS_READ, 1); + StringBuilder where = new StringBuilder(); + where.append(Calls.NEW); + where.append(" = 1 AND "); + where.append(Calls.TYPE); + where.append(" = ?"); + try { + mContext + .getContentResolver() + .update( + Calls.CONTENT_URI, + values, + where.toString(), + new String[] {Integer.toString(Calls.MISSED_TYPE)}); + } catch (IllegalArgumentException e) { + LogUtil.e( + "MissedCallNotifier.clearMissedCalls", + "contacts provider update command failed", + e); + } + } + getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID); + } + }); + } + + /** Trigger an intent to make a call from a missed call number. */ + public void callBackFromMissedCall(String number) { + closeSystemDialogs(mContext); + CallLogNotificationsHelper.removeMissedCallNotifications(mContext); + DialerUtils.startActivityWithErrorToast( + mContext, + new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION) + .build() + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + /** Trigger an intent to send an sms from a missed call number. */ + public void sendSmsFromMissedCall(String number) { + closeSystemDialogs(mContext); + CallLogNotificationsHelper.removeMissedCallNotifications(mContext); + DialerUtils.startActivityWithErrorToast( + mContext, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + /** + * Creates a new pending intent that sends the user to the call log. + * + * @return The pending intent. + */ + private PendingIntent createCallLogPendingIntent() { + Intent contentIntent = + DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_HISTORY); + return PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** Creates a pending intent that marks all new missed calls as old. */ + private PendingIntent createClearMissedCallsPendingIntent() { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD); + return PendingIntent.getService(mContext, 0, intent, 0); + } + + private PendingIntent createCallBackPendingIntent(String number) { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION); + intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number); + // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new + // extra. + return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private PendingIntent createSendSmsFromNotificationPendingIntent(String number) { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION); + intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number); + // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new + // extra. + return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** Configures a notification to emit the blinky notification light. */ + private void configureLedOnNotification(Notification notification) { + notification.flags |= Notification.FLAG_SHOW_LIGHTS; + notification.defaults |= Notification.DEFAULT_LIGHTS; + } + + /** Closes open system dialogs and the notification shade. */ + private void closeSystemDialogs(Context context) { + context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + + private NotificationManager getNotificationMgr() { + return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java b/java/com/android/dialer/app/calllog/PhoneAccountUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c6d94d341e819432c7cfce4c1b60a0e065d362b6 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneAccountUtils.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.content.ComponentName; +import android.content.Context; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.dialer.telecom.TelecomUtil; +import java.util.ArrayList; +import java.util.List; + +/** Methods to help extract {@code PhoneAccount} information from database and Telecomm sources. */ +public class PhoneAccountUtils { + + /** Return a list of phone accounts that are subscription/SIM accounts. */ + public static List getSubscriptionPhoneAccounts(Context context) { + List subscriptionAccountHandles = new ArrayList(); + final List accountHandles = + TelecomUtil.getCallCapablePhoneAccounts(context); + for (PhoneAccountHandle accountHandle : accountHandles) { + PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle); + if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { + subscriptionAccountHandles.add(accountHandle); + } + } + return subscriptionAccountHandles; + } + + /** Compose PhoneAccount object from component name and account id. */ + @Nullable + public static PhoneAccountHandle getAccount( + @Nullable String componentString, @Nullable String accountId) { + if (TextUtils.isEmpty(componentString) || TextUtils.isEmpty(accountId)) { + return null; + } + final ComponentName componentName = ComponentName.unflattenFromString(componentString); + if (componentName == null) { + return null; + } + return new PhoneAccountHandle(componentName, accountId); + } + + /** Extract account label from PhoneAccount object. */ + @Nullable + public static String getAccountLabel( + Context context, @Nullable PhoneAccountHandle accountHandle) { + PhoneAccount account = getAccountOrNull(context, accountHandle); + if (account != null && account.getLabel() != null) { + return account.getLabel().toString(); + } + return null; + } + + /** Extract account color from PhoneAccount object. */ + public static int getAccountColor(Context context, @Nullable PhoneAccountHandle accountHandle) { + final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle); + + // For single-sim devices the PhoneAccount will be NO_HIGHLIGHT_COLOR by default, so it is + // safe to always use the account highlight color. + return account == null ? PhoneAccount.NO_HIGHLIGHT_COLOR : account.getHighlightColor(); + } + + /** + * Determine whether a phone account supports call subjects. + * + * @return {@code true} if call subjects are supported, {@code false} otherwise. + */ + public static boolean getAccountSupportsCallSubject( + Context context, @Nullable PhoneAccountHandle accountHandle) { + final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle); + + return account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT); + } + + /** + * Retrieve the account metadata, but if the account does not exist or the device has only a + * single registered and enabled account, return null. + */ + @Nullable + private static PhoneAccount getAccountOrNull( + Context context, @Nullable PhoneAccountHandle accountHandle) { + if (TelecomUtil.getCallCapablePhoneAccounts(context).size() <= 1) { + return null; + } + return TelecomUtil.getPhoneAccount(context, accountHandle); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..b18270bb3639c218a7e5bf35482d59dddbdb96f9 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v4.content.ContextCompat; +import android.telecom.PhoneAccount; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.TextView; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.DialerUtils; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.concurrent.TimeUnit; + +/** Helper class to fill in the views in {@link PhoneCallDetailsViews}. */ +public class PhoneCallDetailsHelper { + + /** The maximum number of icons will be shown to represent the call types in a group. */ + private static final int MAX_CALL_TYPE_ICONS = 3; + + private final Context mContext; + private final Resources mResources; + private final CallLogCache mCallLogCache; + /** Calendar used to construct dates */ + private final Calendar mCalendar; + /** The injected current time in milliseconds since the epoch. Used only by tests. */ + private Long mCurrentTimeMillisForTest; + + private CharSequence mPhoneTypeLabelForTest; + /** List of items to be concatenated together for accessibility descriptions */ + private ArrayList mDescriptionItems = new ArrayList<>(); + + /** + * Creates a new instance of the helper. + * + *

Generally you should have a single instance of this helper in any context. + * + * @param resources used to look up strings + */ + public PhoneCallDetailsHelper(Context context, Resources resources, CallLogCache callLogCache) { + mContext = context; + mResources = resources; + mCallLogCache = callLogCache; + mCalendar = Calendar.getInstance(); + } + + /** Fills the call details views with content. */ + public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details) { + // Display up to a given number of icons. + views.callTypeIcons.clear(); + int count = details.callTypes.length; + boolean isVoicemail = false; + for (int index = 0; index < count && index < MAX_CALL_TYPE_ICONS; ++index) { + views.callTypeIcons.add(details.callTypes[index]); + if (index == 0) { + isVoicemail = details.callTypes[index] == Calls.VOICEMAIL_TYPE; + } + } + + // Show the video icon if the call had video enabled. + views.callTypeIcons.setShowVideo( + (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO); + views.callTypeIcons.requestLayout(); + views.callTypeIcons.setVisibility(View.VISIBLE); + + // Show the total call count only if there are more than the maximum number of icons. + final Integer callCount; + if (count > MAX_CALL_TYPE_ICONS) { + callCount = count; + } else { + callCount = null; + } + + // Set the call count, location, date and if voicemail, set the duration. + setDetailText(views, callCount, details); + + // Set the account label if it exists. + String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle); + if (!TextUtils.isEmpty(details.viaNumber)) { + if (!TextUtils.isEmpty(accountLabel)) { + accountLabel = + mResources.getString( + R.string.call_log_via_number_phone_account, accountLabel, details.viaNumber); + } else { + accountLabel = mResources.getString(R.string.call_log_via_number, details.viaNumber); + } + } + if (!TextUtils.isEmpty(accountLabel)) { + views.callAccountLabel.setVisibility(View.VISIBLE); + views.callAccountLabel.setText(accountLabel); + int color = mCallLogCache.getAccountColor(details.accountHandle); + if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) { + int defaultColor = R.color.dialer_secondary_text_color; + views.callAccountLabel.setTextColor(mContext.getResources().getColor(defaultColor)); + } else { + views.callAccountLabel.setTextColor(color); + } + } else { + views.callAccountLabel.setVisibility(View.GONE); + } + + final CharSequence nameText; + final CharSequence displayNumber = details.displayNumber; + if (TextUtils.isEmpty(details.getPreferredName())) { + nameText = displayNumber; + // We have a real phone number as "nameView" so make it always LTR + views.nameView.setTextDirection(View.TEXT_DIRECTION_LTR); + } else { + nameText = details.getPreferredName(); + } + + views.nameView.setText(nameText); + + if (isVoicemail) { + views.voicemailTranscriptionView.setText( + TextUtils.isEmpty(details.transcription) ? null : details.transcription); + } + + // Bold if not read + Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD; + views.nameView.setTypeface(typeface); + views.voicemailTranscriptionView.setTypeface(typeface); + views.callLocationAndDate.setTypeface(typeface); + views.callLocationAndDate.setTextColor( + ContextCompat.getColor( + mContext, + details.isRead ? R.color.call_log_detail_color : R.color.call_log_unread_text_color)); + } + + /** + * Builds a string containing the call location and date. For voicemail logs only the call date is + * returned because location information is displayed in the call action button + * + * @param details The call details. + * @return The call location and date string. + */ + public CharSequence getCallLocationAndDate(PhoneCallDetails details) { + mDescriptionItems.clear(); + + if (details.callTypes[0] != Calls.VOICEMAIL_TYPE) { + // Get type of call (ie mobile, home, etc) if known, or the caller's location. + CharSequence callTypeOrLocation = getCallTypeOrLocation(details); + + // Only add the call type or location if its not empty. It will be empty for unknown + // callers. + if (!TextUtils.isEmpty(callTypeOrLocation)) { + mDescriptionItems.add(callTypeOrLocation); + } + } + + // The date of this call + mDescriptionItems.add(getCallDate(details)); + + // Create a comma separated list from the call type or location, and call date. + return DialerUtils.join(mDescriptionItems); + } + + /** + * For a call, if there is an associated contact for the caller, return the known call type (e.g. + * mobile, home, work). If there is no associated contact, attempt to use the caller's location if + * known. + * + * @param details Call details to use. + * @return Type of call (mobile/home) if known, or the location of the caller (if known). + */ + public CharSequence getCallTypeOrLocation(PhoneCallDetails details) { + if (details.isSpam) { + return mResources.getString(R.string.spam_number_call_log_label); + } else if (details.isBlocked) { + return mResources.getString(R.string.blocked_number_call_log_label); + } + + CharSequence numberFormattedLabel = null; + // Only show a label if the number is shown and it is not a SIP address. + if (!TextUtils.isEmpty(details.number) + && !PhoneNumberHelper.isUriNumber(details.number.toString()) + && !mCallLogCache.isVoicemailNumber(details.accountHandle, details.number)) { + + if (TextUtils.isEmpty(details.namePrimary) && !TextUtils.isEmpty(details.geocode)) { + numberFormattedLabel = details.geocode; + } else if (!(details.numberType == Phone.TYPE_CUSTOM + && TextUtils.isEmpty(details.numberLabel))) { + // Get type label only if it will not be "Custom" because of an empty number label. + numberFormattedLabel = + mPhoneTypeLabelForTest != null + ? mPhoneTypeLabelForTest + : Phone.getTypeLabel(mResources, details.numberType, details.numberLabel); + } + } + + if (!TextUtils.isEmpty(details.namePrimary) && TextUtils.isEmpty(numberFormattedLabel)) { + numberFormattedLabel = details.displayNumber; + } + return numberFormattedLabel; + } + + public void setPhoneTypeLabelForTest(CharSequence phoneTypeLabel) { + this.mPhoneTypeLabelForTest = phoneTypeLabel; + } + + /** + * Get the call date/time of the call. For the call log this is relative to the current time. e.g. + * 3 minutes ago. For voicemail, see {@link #getGranularDateTime(PhoneCallDetails)} + * + * @param details Call details to use. + * @return String representing when the call occurred. + */ + public CharSequence getCallDate(PhoneCallDetails details) { + if (details.callTypes[0] == Calls.VOICEMAIL_TYPE) { + return getGranularDateTime(details); + } + + return DateUtils.getRelativeTimeSpanString( + details.date, + getCurrentTimeMillis(), + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE); + } + + /** + * Get the granular version of the call date/time of the call. The result is always in the form + * 'DATE at TIME'. The date value changes based on when the call was created. + * + *

If created today, DATE is 'Today' If created this year, DATE is 'MMM dd' Otherwise, DATE is + * 'MMM dd, yyyy' + * + *

TIME is the localized time format, e.g. 'hh:mm a' or 'HH:mm' + * + * @param details Call details to use + * @return String representing when the call occurred + */ + public CharSequence getGranularDateTime(PhoneCallDetails details) { + return mResources.getString( + R.string.voicemailCallLogDateTimeFormat, + getGranularDate(details.date), + DateUtils.formatDateTime(mContext, details.date, DateUtils.FORMAT_SHOW_TIME)); + } + + /** + * Get the granular version of the call date. See {@link #getGranularDateTime(PhoneCallDetails)} + */ + private String getGranularDate(long date) { + if (DateUtils.isToday(date)) { + return mResources.getString(R.string.voicemailCallLogToday); + } + return DateUtils.formatDateTime( + mContext, + date, + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_MONTH + | (shouldShowYear(date) ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR)); + } + + /** + * Determines whether the year should be shown for the given date + * + * @return {@code true} if date is within the current year, {@code false} otherwise + */ + private boolean shouldShowYear(long date) { + mCalendar.setTimeInMillis(getCurrentTimeMillis()); + int currentYear = mCalendar.get(Calendar.YEAR); + mCalendar.setTimeInMillis(date); + return currentYear != mCalendar.get(Calendar.YEAR); + } + + /** Sets the text of the header view for the details page of a phone call. */ + public void setCallDetailsHeader(TextView nameView, PhoneCallDetails details) { + final CharSequence nameText; + if (!TextUtils.isEmpty(details.namePrimary)) { + nameText = details.namePrimary; + } else if (!TextUtils.isEmpty(details.displayNumber)) { + nameText = details.displayNumber; + } else { + nameText = mResources.getString(R.string.unknown); + } + + nameView.setText(nameText); + } + + public void setCurrentTimeForTest(long currentTimeMillis) { + mCurrentTimeMillisForTest = currentTimeMillis; + } + + /** + * Returns the current time in milliseconds since the epoch. + * + *

It can be injected in tests using {@link #setCurrentTimeForTest(long)}. + */ + private long getCurrentTimeMillis() { + if (mCurrentTimeMillisForTest == null) { + return System.currentTimeMillis(); + } else { + return mCurrentTimeMillisForTest; + } + } + + /** Sets the call count, date, and if it is a voicemail, sets the duration. */ + private void setDetailText( + PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details) { + // Combine the count (if present) and the date. + CharSequence dateText = details.callLocationAndDate; + final CharSequence text; + if (callCount != null) { + text = mResources.getString(R.string.call_log_item_count_and_date, callCount, dateText); + } else { + text = dateText; + } + + if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) { + views.callLocationAndDate.setText( + mResources.getString( + R.string.voicemailCallLogDateTimeFormatWithDuration, + text, + getVoicemailDuration(details))); + } else { + views.callLocationAndDate.setText(text); + } + } + + private String getVoicemailDuration(PhoneCallDetails details) { + long minutes = TimeUnit.SECONDS.toMinutes(details.duration); + long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes); + if (minutes > 99) { + minutes = 99; + } + return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java new file mode 100644 index 0000000000000000000000000000000000000000..476996826417c69083029b17c3600914d5d76bb6 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; +import com.android.dialer.app.R; + +/** Encapsulates the views that are used to display the details of a phone call in the call log. */ +public final class PhoneCallDetailsViews { + + public final TextView nameView; + public final View callTypeView; + public final CallTypeIconsView callTypeIcons; + public final TextView callLocationAndDate; + public final TextView voicemailTranscriptionView; + public final TextView callAccountLabel; + + private PhoneCallDetailsViews( + TextView nameView, + View callTypeView, + CallTypeIconsView callTypeIcons, + TextView callLocationAndDate, + TextView voicemailTranscriptionView, + TextView callAccountLabel) { + this.nameView = nameView; + this.callTypeView = callTypeView; + this.callTypeIcons = callTypeIcons; + this.callLocationAndDate = callLocationAndDate; + this.voicemailTranscriptionView = voicemailTranscriptionView; + this.callAccountLabel = callAccountLabel; + } + + /** + * Create a new instance by extracting the elements from the given view. + * + *

The view should contain three text views with identifiers {@code R.id.name}, {@code + * R.id.date}, and {@code R.id.number}, and a linear layout with identifier {@code + * R.id.call_types}. + */ + public static PhoneCallDetailsViews fromView(View view) { + return new PhoneCallDetailsViews( + (TextView) view.findViewById(R.id.name), + view.findViewById(R.id.call_type), + (CallTypeIconsView) view.findViewById(R.id.call_type_icons), + (TextView) view.findViewById(R.id.call_location_and_date), + (TextView) view.findViewById(R.id.voicemail_transcription), + (TextView) view.findViewById(R.id.call_account_label)); + } + + public static PhoneCallDetailsViews createForTest(Context context) { + return new PhoneCallDetailsViews( + new TextView(context), + new View(context), + new CallTypeIconsView(context), + new TextView(context), + new TextView(context), + new TextView(context)); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java b/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..410d4cc3713487906660b1a61006c1b1c5380a3f --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.provider.CallLog.Calls; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.dialer.app.R; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; + +/** Helper for formatting and managing the display of phone numbers. */ +public class PhoneNumberDisplayUtil { + + /** Returns the string to display for the given phone number if there is no matching contact. */ + /* package */ + static CharSequence getDisplayName( + Context context, CharSequence number, int presentation, boolean isVoicemail) { + if (presentation == Calls.PRESENTATION_UNKNOWN) { + return context.getResources().getString(R.string.unknown); + } + if (presentation == Calls.PRESENTATION_RESTRICTED) { + return PhoneNumberHelper.getDisplayNameForRestrictedNumber(context); + } + if (presentation == Calls.PRESENTATION_PAYPHONE) { + return context.getResources().getString(R.string.payphone); + } + if (isVoicemail) { + return context.getResources().getString(R.string.voicemail); + } + if (PhoneNumberHelper.isLegacyUnknownNumbers(number)) { + return context.getResources().getString(R.string.unknown); + } + return ""; + } + + /** + * Returns the string to display for the given phone number. + * + * @param number the number to display + * @param formattedNumber the formatted number if available, may be null + */ + public static CharSequence getDisplayNumber( + Context context, + CharSequence number, + int presentation, + CharSequence formattedNumber, + CharSequence postDialDigits, + boolean isVoicemail) { + final CharSequence displayName = getDisplayName(context, number, presentation, isVoicemail); + if (!TextUtils.isEmpty(displayName)) { + return getTtsSpannableLtrNumber(displayName); + } + + if (!TextUtils.isEmpty(formattedNumber)) { + return getTtsSpannableLtrNumber(formattedNumber); + } else if (!TextUtils.isEmpty(number)) { + return getTtsSpannableLtrNumber(number.toString() + postDialDigits); + } else { + return context.getResources().getString(R.string.unknown); + } + } + + /** Returns number annotated as phone number in LTR direction. */ + public static CharSequence getTtsSpannableLtrNumber(CharSequence number) { + return PhoneNumberUtilsCompat.createTtsSpannable( + BidiFormatter.getInstance().unicodeWrap(number.toString(), TextDirectionHeuristics.LTR)); + } +} diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..e539ceef638269a26b6f677c418347ce1beaf0d7 --- /dev/null +++ b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.app.Activity; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.os.Bundle; +import android.provider.CallLog; +import android.provider.VoicemailContract; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.app.voicemail.VoicemailAudioManager; +import com.android.dialer.app.voicemail.VoicemailErrorManager; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.common.LogUtil; + +public class VisualVoicemailCallLogFragment extends CallLogFragment { + + private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); + private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + + private VoicemailErrorManager mVoicemailAlertManager; + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + mCallTypeFilter = CallLog.Calls.VOICEMAIL_TYPE; + mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), state); + getActivity() + .getContentResolver() + .registerContentObserver( + VoicemailContract.Status.CONTENT_URI, true, mVoicemailStatusObserver); + } + + @Override + protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() { + return mVoicemailPlaybackPresenter; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mVoicemailAlertManager = + new VoicemailErrorManager(getContext(), getAdapter().getAlertManager(), mModalAlertManager); + getActivity() + .getContentResolver() + .registerContentObserver( + VoicemailContract.Status.CONTENT_URI, + true, + mVoicemailAlertManager.getContentObserver()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + View view = inflater.inflate(R.layout.call_log_fragment, container, false); + setupView(view); + return view; + } + + @Override + public void onResume() { + super.onResume(); + mVoicemailPlaybackPresenter.onResume(); + mVoicemailAlertManager.onResume(); + } + + @Override + public void onPause() { + mVoicemailPlaybackPresenter.onPause(); + mVoicemailAlertManager.onPause(); + super.onPause(); + } + + @Override + public void onDestroy() { + getActivity() + .getContentResolver() + .unregisterContentObserver(mVoicemailAlertManager.getContentObserver()); + mVoicemailPlaybackPresenter.onDestroy(); + getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mVoicemailPlaybackPresenter.onSaveInstanceState(outState); + } + + @Override + public void fetchCalls() { + super.fetchCalls(); + ((ListsFragment) getParentFragment()).updateTabUnreadCounts(); + } + + @Override + public void onPageResume(@Nullable Activity activity) { + LogUtil.d("VisualVoicemailCallLogFragment.onPageResume", null); + super.onPageResume(activity); + if (activity != null) { + activity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM); + } + } + + @Override + public void onPagePause(@Nullable Activity activity) { + LogUtil.d("VisualVoicemailCallLogFragment.onPagePause", null); + super.onPagePause(activity); + if (activity != null) { + activity.setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); + } + } +} diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d8354ec63c20679da63abf8aacd17361866e41 --- /dev/null +++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.calllog; + +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.provider.CallLog.Calls; +import android.util.Log; + +/** Handles asynchronous queries to the call log for voicemail. */ +public class VoicemailQueryHandler extends AsyncQueryHandler { + + private static final String TAG = "VoicemailQueryHandler"; + + /** The token for the query to mark all new voicemails as old. */ + private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 50; + + private Context mContext; + + public VoicemailQueryHandler(Context context, ContentResolver contentResolver) { + super(contentResolver); + mContext = context; + } + + /** Updates all new voicemails to mark them as old. */ + public void markNewVoicemailsAsOld() { + // Mark all "new" voicemails as not new anymore. + StringBuilder where = new StringBuilder(); + where.append(Calls.NEW); + where.append(" = 1 AND "); + where.append(Calls.TYPE); + where.append(" = ?"); + + ContentValues values = new ContentValues(1); + values.put(Calls.NEW, "0"); + + startUpdate( + UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, + null, + Calls.CONTENT_URI_WITH_VOICEMAIL, + values, + where.toString(), + new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)}); + } + + @Override + protected void onUpdateComplete(int token, Object cookie, int result) { + if (token == UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN) { + if (mContext != null) { + Intent serviceIntent = new Intent(mContext, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS); + mContext.startService(serviceIntent); + } else { + Log.w(TAG, "Unknown update completed: ignoring: " + token); + } + } + } +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java new file mode 100644 index 0000000000000000000000000000000000000000..7645a333e23290ff58175393d7f37ff051a4ffa5 --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import com.android.dialer.app.calllog.CallLogAdapter; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.CallUtil; + +/** + * This is the base class for the CallLogCaches. + * + *

Keeps a cache of recently made queries to the Telecom/Telephony processes. The aim of this + * cache is to reduce the number of cross-process requests to TelecomManager, which can negatively + * affect performance. + * + *

This is designed with the specific use case of the {@link CallLogAdapter} in mind. + */ +public abstract class CallLogCache { + // TODO: Dialer should be fixed so as not to check isVoicemail() so often but at the time of + // this writing, that was a much larger undertaking than creating this cache. + + protected final Context mContext; + + private boolean mHasCheckedForVideoAvailability; + private int mVideoAvailability; + + public CallLogCache(Context context) { + mContext = context; + } + + /** Return the most compatible version of the TelecomCallLogCache. */ + public static CallLogCache getCallLogCache(Context context) { + if (CompatUtils.isClassAvailable("android.telecom.PhoneAccountHandle")) { + return new CallLogCacheLollipopMr1(context); + } + return new CallLogCacheLollipop(context); + } + + public void reset() { + mHasCheckedForVideoAvailability = false; + mVideoAvailability = 0; + } + + /** + * Returns true if the given number is the number of the configured voicemail. To be able to + * mock-out this, it is not a static method. + */ + public abstract boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number); + + /** + * Returns {@code true} when the current sim supports video calls, regardless of the value in a + * contact's {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} + * column. + */ + public boolean isVideoEnabled() { + if (!mHasCheckedForVideoAvailability) { + mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext); + mHasCheckedForVideoAvailability = true; + } + return (mVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) != 0; + } + + /** + * Returns {@code true} when the current sim supports checking video calling capabilities via the + * {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} column. + */ + public boolean canRelyOnVideoPresence() { + if (!mHasCheckedForVideoAvailability) { + mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext); + mHasCheckedForVideoAvailability = true; + } + return (mVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE) != 0; + } + + /** Extract account label from PhoneAccount object. */ + public abstract String getAccountLabel(PhoneAccountHandle accountHandle); + + /** Extract account color from PhoneAccount object. */ + public abstract int getAccountColor(PhoneAccountHandle accountHandle); + + /** + * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note) + * for outgoing calls. + * + * @param accountHandle The PhoneAccount handle. + * @return {@code true} if calling with a note is supported, {@code false} otherwise. + */ + public abstract boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle); +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java new file mode 100644 index 0000000000000000000000000000000000000000..78aaa4193fd4f3bd268b3c850857da284af394a4 --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; + +/** + * This is a compatibility class for the CallLogCache for versions of dialer before Lollipop Mr1 + * (the introduction of phone accounts). + * + *

This class should not be initialized directly and instead be acquired from {@link + * CallLogCache#getCallLogCache}. + */ +class CallLogCacheLollipop extends CallLogCache { + + private String mVoicemailNumber; + + /* package */ CallLogCacheLollipop(Context context) { + super(context); + } + + @Override + public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + String numberString = number.toString(); + + if (!TextUtils.isEmpty(mVoicemailNumber)) { + return PhoneNumberUtils.compare(numberString, mVoicemailNumber); + } + + if (PhoneNumberUtils.isVoiceMailNumber(numberString)) { + mVoicemailNumber = numberString; + return true; + } + + return false; + } + + @Override + public String getAccountLabel(PhoneAccountHandle accountHandle) { + return null; + } + + @Override + public int getAccountColor(PhoneAccountHandle accountHandle) { + return PhoneAccount.NO_HIGHLIGHT_COLOR; + } + + @Override + public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { + return false; + } +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java new file mode 100644 index 0000000000000000000000000000000000000000..c342b7e3bbf72a98f8055cb217dae6607a100fbc --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.Pair; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This is the CallLogCache for versions of dialer Lollipop Mr1 and above with support for multi-SIM + * devices. + * + *

This class should not be initialized directly and instead be acquired from {@link + * CallLogCache#getCallLogCache}. + */ +class CallLogCacheLollipopMr1 extends CallLogCache { + + /* + * Maps from a phone-account/number pair to a boolean because multiple numbers could return true + * for the voicemail number if those numbers are not pre-normalized. Access must be synchronzied + * as it's used in the background thread in CallLogAdapter. {@see CallLogAdapter#loadData} + */ + @VisibleForTesting + final Map, Boolean> mVoicemailQueryCache = + new ConcurrentHashMap<>(); + + private final Map mPhoneAccountLabelCache = new HashMap<>(); + private final Map mPhoneAccountColorCache = new HashMap<>(); + private final Map mPhoneAccountCallWithNoteCache = new HashMap<>(); + + /* package */ CallLogCacheLollipopMr1(Context context) { + super(context); + } + + @Override + public void reset() { + mVoicemailQueryCache.clear(); + mPhoneAccountLabelCache.clear(); + mPhoneAccountColorCache.clear(); + mPhoneAccountCallWithNoteCache.clear(); + + super.reset(); + } + + @Override + public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + Pair key = new Pair<>(accountHandle, number); + Boolean value = mVoicemailQueryCache.get(key); + if (value != null) { + return value; + } + boolean isVoicemail = + PhoneNumberHelper.isVoicemailNumber(mContext, accountHandle, number.toString()); + mVoicemailQueryCache.put(key, isVoicemail); + return isVoicemail; + } + + @Override + public String getAccountLabel(PhoneAccountHandle accountHandle) { + if (mPhoneAccountLabelCache.containsKey(accountHandle)) { + return mPhoneAccountLabelCache.get(accountHandle); + } else { + String label = PhoneAccountUtils.getAccountLabel(mContext, accountHandle); + mPhoneAccountLabelCache.put(accountHandle, label); + return label; + } + } + + @Override + public int getAccountColor(PhoneAccountHandle accountHandle) { + if (mPhoneAccountColorCache.containsKey(accountHandle)) { + return mPhoneAccountColorCache.get(accountHandle); + } else { + Integer color = PhoneAccountUtils.getAccountColor(mContext, accountHandle); + mPhoneAccountColorCache.put(accountHandle, color); + return color; + } + } + + @Override + public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { + if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) { + return mPhoneAccountCallWithNoteCache.get(accountHandle); + } else { + Boolean supportsCallWithNote = + PhoneAccountUtils.getAccountSupportsCallSubject(mContext, accountHandle); + mPhoneAccountCallWithNoteCache.put(accountHandle, supportsCallWithNote); + return supportsCallWithNote; + } + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java new file mode 100644 index 0000000000000000000000000000000000000000..4135cb7b848b19b8f02d341cd17c9f6f25f8d303 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.contactinfo; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.util.ExpirableCache; +import java.lang.ref.WeakReference; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; + +/** + * This is a cache of contact details for the phone numbers in the c all log. The key is the phone + * number with the country in which teh call was placed or received. The content of the cache is + * expired (but not purged) whenever the application comes to the foreground. + * + *

This cache queues request for information and queries for information on a background thread, + * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction + * as needed. + * + *

TODO: Explore whether there is a pattern to remove external dependencies for starting and + * stopping the query thread. + */ +public class ContactInfoCache { + + private static final int REDRAW = 1; + private static final int START_THREAD = 2; + private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000; + + private final ExpirableCache mCache; + private final ContactInfoHelper mContactInfoHelper; + private final OnContactInfoChangedListener mOnContactInfoChangedListener; + private final BlockingQueue mUpdateRequests; + private final Handler mHandler; + private QueryThread mContactInfoQueryThread; + private volatile boolean mRequestProcessingDisabled = false; + + private static class InnerHandler extends Handler { + + private final WeakReference contactInfoCacheWeakReference; + + public InnerHandler(WeakReference contactInfoCacheWeakReference) { + this.contactInfoCacheWeakReference = contactInfoCacheWeakReference; + } + + @Override + public void handleMessage(Message msg) { + ContactInfoCache reference = contactInfoCacheWeakReference.get(); + if (reference == null) { + return; + } + switch (msg.what) { + case REDRAW: + reference.mOnContactInfoChangedListener.onContactInfoChanged(); + break; + case START_THREAD: + reference.startRequestProcessing(); + } + } + } + + public ContactInfoCache( + @NonNull ExpirableCache internalCache, + @NonNull ContactInfoHelper contactInfoHelper, + @NonNull OnContactInfoChangedListener listener) { + mCache = internalCache; + mContactInfoHelper = contactInfoHelper; + mOnContactInfoChangedListener = listener; + mUpdateRequests = new PriorityBlockingQueue<>(); + mHandler = new InnerHandler(new WeakReference<>(this)); + } + + public ContactInfo getValue( + String number, + String countryIso, + ContactInfo callLogContactInfo, + boolean remoteLookupIfNotFoundLocally) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + ExpirableCache.CachedValue cachedInfo = mCache.getCachedValue(numberCountryIso); + ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); + if (cachedInfo == null) { + mCache.put(numberCountryIso, ContactInfo.EMPTY); + // Use the cached contact info from the call log. + info = callLogContactInfo; + // The db request should happen on a non-UI thread. + // Request the contact details immediately since they are currently missing. + int requestType = + remoteLookupIfNotFoundLocally + ? ContactInfoRequest.TYPE_LOCAL_AND_REMOTE + : ContactInfoRequest.TYPE_LOCAL; + enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ true, requestType); + // We will format the phone number when we make the background request. + } else { + if (cachedInfo.isExpired()) { + // The contact info is no longer up to date, we should request it. However, we + // do not need to request them immediately. + enqueueRequest( + number, + countryIso, + callLogContactInfo, /* immediate */ + false, + ContactInfoRequest.TYPE_LOCAL); + } else if (!callLogInfoMatches(callLogContactInfo, info)) { + // The call log information does not match the one we have, look it up again. + // We could simply update the call log directly, but that needs to be done in a + // background thread, so it is easier to simply request a new lookup, which will, as + // a side-effect, update the call log. + enqueueRequest( + number, + countryIso, + callLogContactInfo, /* immediate */ + false, + ContactInfoRequest.TYPE_LOCAL); + } + + if (info == ContactInfo.EMPTY) { + // Use the cached contact info from the call log. + info = callLogContactInfo; + } + } + return info; + } + + /** + * Queries the appropriate content provider for the contact associated with the number. + * + *

Upon completion it also updates the cache in the call log, if it is different from {@code + * callLogInfo}. + * + *

The number might be either a SIP address or a phone number. + * + *

It returns true if it updated the content of the cache and we should therefore tell the view + * to update its content. + */ + private boolean queryContactInfo(ContactInfoRequest request) { + ContactInfo info; + if (request.isLocalRequest()) { + info = mContactInfoHelper.lookupNumber(request.number, request.countryIso); + if (request.type == ContactInfoRequest.TYPE_LOCAL_AND_REMOTE) { + if (!mContactInfoHelper.hasName(info)) { + enqueueRequest( + request.number, + request.countryIso, + request.callLogInfo, + true, + ContactInfoRequest.TYPE_REMOTE); + return false; + } + } + } else { + info = mContactInfoHelper.lookupNumberInRemoteDirectory(request.number, request.countryIso); + } + + if (info == null) { + // The lookup failed, just return without requesting to update the view. + return false; + } + + // Check the existing entry in the cache: only if it has changed we should update the + // view. + NumberWithCountryIso numberCountryIso = + new NumberWithCountryIso(request.number, request.countryIso); + ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso); + + final boolean isRemoteSource = info.sourceType != 0; + + // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} + // to avoid updating the data set for every new row that is scrolled into view. + + // Exception: Photo uris for contacts from remote sources are not cached in the call log + // cache, so we have to force a redraw for these contacts regardless. + boolean updated = + (existingInfo != ContactInfo.EMPTY || isRemoteSource) && !info.equals(existingInfo); + + // Store the data in the cache so that the UI thread can use to display it. Store it + // even if it has not changed so that it is marked as not expired. + mCache.put(numberCountryIso, info); + + // Update the call log even if the cache it is up-to-date: it is possible that the cache + // contains the value from a different call log entry. + mContactInfoHelper.updateCallLogContactInfo( + request.number, request.countryIso, info, request.callLogInfo); + if (!request.isLocalRequest()) { + mContactInfoHelper.updateCachedNumberLookupService(info); + } + return updated; + } + + /** + * After a delay, start the thread to begin processing requests. We perform lookups on a + * background thread, but this must be called to indicate the thread should be running. + */ + public void start() { + // Schedule a thread-creation message if the thread hasn't been created yet, as an + // optimization to queue fewer messages. + if (mContactInfoQueryThread == null) { + // TODO: Check whether this delay before starting to process is necessary. + mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS); + } + } + + /** + * Stops the thread and clears the queue of messages to process. This cleans up the thread for + * lookups so that it is not perpetually running. + */ + public void stop() { + stopRequestProcessing(); + } + + /** + * Starts a background thread to process contact-lookup requests, unless one has already been + * started. + */ + private synchronized void startRequestProcessing() { + // For unit-testing. + if (mRequestProcessingDisabled) { + return; + } + + // If a thread is already started, don't start another. + if (mContactInfoQueryThread != null) { + return; + } + + mContactInfoQueryThread = new QueryThread(); + mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY); + mContactInfoQueryThread.start(); + } + + public void invalidate() { + mCache.expireAll(); + stopRequestProcessing(); + } + + /** + * Stops the background thread that processes updates and cancels any pending requests to start + * it. + */ + private synchronized void stopRequestProcessing() { + // Remove any pending requests to start the processing thread. + mHandler.removeMessages(START_THREAD); + if (mContactInfoQueryThread != null) { + // Stop the thread; we are finished with it. + mContactInfoQueryThread.stopProcessing(); + mContactInfoQueryThread.interrupt(); + mContactInfoQueryThread = null; + } + } + + /** + * Enqueues a request to look up the contact details for the given phone number. + * + *

It also provides the current contact info stored in the call log for this number. + * + *

If the {@code immediate} parameter is true, it will start immediately the thread that looks + * up the contact information (if it has not been already started). Otherwise, it will be started + * with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MS}. + */ + private void enqueueRequest( + String number, + String countryIso, + ContactInfo callLogInfo, + boolean immediate, + @ContactInfoRequest.TYPE int type) { + ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo, type); + if (!mUpdateRequests.contains(request)) { + mUpdateRequests.offer(request); + } + + if (immediate) { + startRequestProcessing(); + } + } + + /** Checks whether the contact info from the call log matches the one from the contacts db. */ + private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { + // The call log only contains a subset of the fields in the contacts db. Only check those. + return TextUtils.equals(callLogInfo.name, info.name) + && callLogInfo.type == info.type + && TextUtils.equals(callLogInfo.label, info.label); + } + + /** Sets whether processing of requests for contact details should be enabled. */ + public void disableRequestProcessing() { + mRequestProcessingDisabled = true; + } + + @VisibleForTesting + public void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + mCache.put(numberCountryIso, contactInfo); + } + + public interface OnContactInfoChangedListener { + + void onContactInfoChanged(); + } + + /* + * Handles requests for contact name and number type. + */ + private class QueryThread extends Thread { + + private volatile boolean mDone = false; + + public QueryThread() { + super("ContactInfoCache.QueryThread"); + } + + public void stopProcessing() { + mDone = true; + } + + @Override + public void run() { + boolean shouldRedraw = false; + while (true) { + // Check if thread is finished, and if so return immediately. + if (mDone) { + return; + } + + try { + ContactInfoRequest request = mUpdateRequests.take(); + shouldRedraw |= queryContactInfo(request); + if (shouldRedraw + && (mUpdateRequests.isEmpty() + || request.isLocalRequest() && !mUpdateRequests.peek().isLocalRequest())) { + shouldRedraw = false; + mHandler.sendEmptyMessage(REDRAW); + } + } catch (InterruptedException e) { + // Ignore and attempt to continue processing requests + } + } + } + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..5c2eb1dbb024366730edc7a7250c9c8de05343b6 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.contactinfo; + +import android.support.annotation.IntDef; +import android.text.TextUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +/** A request for contact details for the given number, used by the ContactInfoCache. */ +public final class ContactInfoRequest implements Comparable { + + private static final AtomicLong NEXT_SEQUENCE_NUMBER = new AtomicLong(0); + + private final long sequenceNumber; + + /** The number to look-up. */ + public final String number; + /** The country in which a call to or from this number was placed or received. */ + public final String countryIso; + /** The cached contact information stored in the call log. */ + public final ContactInfo callLogInfo; + + /** Is the request a remote lookup. Remote requests are treated as lower priority. */ + @TYPE public final int type; + + /** Specifies the type of the request is. */ + @IntDef( + value = { + TYPE_LOCAL, + TYPE_LOCAL_AND_REMOTE, + TYPE_REMOTE, + } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface TYPE {} + + public static final int TYPE_LOCAL = 0; + /** If cannot find the contact locally, do remote lookup later. */ + public static final int TYPE_LOCAL_AND_REMOTE = 1; + + public static final int TYPE_REMOTE = 2; + + public ContactInfoRequest( + String number, String countryIso, ContactInfo callLogInfo, @TYPE int type) { + this.sequenceNumber = NEXT_SEQUENCE_NUMBER.getAndIncrement(); + this.number = number; + this.countryIso = countryIso; + this.callLogInfo = callLogInfo; + this.type = type; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ContactInfoRequest)) { + return false; + } + + ContactInfoRequest other = (ContactInfoRequest) obj; + + if (!TextUtils.equals(number, other.number)) { + return false; + } + if (!TextUtils.equals(countryIso, other.countryIso)) { + return false; + } + if (!Objects.equals(callLogInfo, other.callLogInfo)) { + return false; + } + + if (type != other.type) { + return false; + } + + return true; + } + + public boolean isLocalRequest() { + return type == TYPE_LOCAL || type == TYPE_LOCAL_AND_REMOTE; + } + + @Override + public int hashCode() { + return Objects.hash(sequenceNumber, number, countryIso, callLogInfo, type); + } + + @Override + public int compareTo(ContactInfoRequest other) { + // Local query always comes first. + if (isLocalRequest() && !other.isLocalRequest()) { + return -1; + } + if (!isLocalRequest() && other.isLocalRequest()) { + return 1; + } + // First come first served. + return sequenceNumber < other.sequenceNumber ? -1 : 1; + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..a8c7185028e66d40365bee4fbad9d50e395857d7 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.contactinfo; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.lettertiles.LetterTileDrawable; +import com.android.dialer.app.R; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +/** + * Class to create the appropriate contact icon from a ContactInfo. This class is for synchronous, + * blocking calls to generate bitmaps, while ContactCommons.ContactPhotoManager is to cache, manage + * and update a ImageView asynchronously. + */ +public class ContactPhotoLoader { + + private final Context mContext; + private final ContactInfo mContactInfo; + + public ContactPhotoLoader(Context context, ContactInfo contactInfo) { + mContext = Objects.requireNonNull(context); + mContactInfo = Objects.requireNonNull(contactInfo); + } + + private static Bitmap drawableToBitmap(Drawable drawable, int width, int height) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** Create a contact photo icon bitmap appropriate for the ContactInfo. */ + public Bitmap loadPhotoIcon() { + Assert.isWorkerThread(); + int photoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); + return drawableToBitmap(getIcon(), photoSize, photoSize); + } + + @VisibleForTesting + Drawable getIcon() { + Drawable drawable = createPhotoIconDrawable(); + if (drawable == null) { + drawable = createLetterTileDrawable(); + } + return drawable; + } + + /** + * @return a {@link Drawable} of circular photo icon if the photo can be loaded, {@code null} + * otherwise. + */ + @Nullable + private Drawable createPhotoIconDrawable() { + if (mContactInfo.photoUri == null) { + return null; + } + try { + InputStream input = mContext.getContentResolver().openInputStream(mContactInfo.photoUri); + if (input == null) { + LogUtil.w( + "ContactPhotoLoader.createPhotoIconDrawable", + "createPhotoIconDrawable: InputStream is null"); + return null; + } + Bitmap bitmap = BitmapFactory.decodeStream(input); + input.close(); + + if (bitmap == null) { + LogUtil.w( + "ContactPhotoLoader.createPhotoIconDrawable", + "createPhotoIconDrawable: Bitmap is null"); + return null; + } + final RoundedBitmapDrawable drawable = + RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap); + drawable.setAntiAlias(true); + drawable.setCornerRadius(bitmap.getHeight() / 2); + return drawable; + } catch (IOException e) { + LogUtil.e("ContactPhotoLoader.createPhotoIconDrawable", e.toString()); + return null; + } + } + + /** @return a {@link LetterTileDrawable} based on the ContactInfo. */ + private Drawable createLetterTileDrawable() { + ContactInfoHelper helper = + new ContactInfoHelper(mContext, GeoUtil.getCurrentCountryIso(mContext)); + LetterTileDrawable drawable = new LetterTileDrawable(mContext.getResources()); + drawable.setCanonicalDialerLetterTileDetails( + mContactInfo.name, + mContactInfo.lookupKey, + LetterTileDrawable.SHAPE_CIRCLE, + helper.isBusiness(mContactInfo.sourceType) + ? LetterTileDrawable.TYPE_BUSINESS + : LetterTileDrawable.TYPE_DEFAULT); + return drawable; + } +} diff --git a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..aed51b5071c1c7ecd300875f715d1b7a729047ec --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.contactinfo; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v7.app.AppCompatActivity; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.util.ExpirableCache; + +/** + * Fragment without any UI whose purpose is to retain an instance of {@link ExpirableCache} across + * configuration change through the use of {@link #setRetainInstance(boolean)}. This is done as + * opposed to implementing {@link android.os.Parcelable} as it is a less widespread change. + */ +public class ExpirableCacheHeadlessFragment extends Fragment { + + private static final String FRAGMENT_TAG = "ExpirableCacheHeadlessFragment"; + private static final int CONTACT_INFO_CACHE_SIZE = 100; + + private ExpirableCache retainedCache; + + @NonNull + public static ExpirableCacheHeadlessFragment attach(@NonNull AppCompatActivity parentActivity) { + return attach(parentActivity.getSupportFragmentManager()); + } + + @NonNull + private static ExpirableCacheHeadlessFragment attach(FragmentManager fragmentManager) { + ExpirableCacheHeadlessFragment fragment = + (ExpirableCacheHeadlessFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (fragment == null) { + fragment = new ExpirableCacheHeadlessFragment(); + // Allowing state loss since in rare cases this is called after activity's state is saved and + // it's fine if the cache is lost. + fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG).commitNowAllowingStateLoss(); + } + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + retainedCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); + setRetainInstance(true); + } + + public ExpirableCache getRetainedCache() { + return retainedCache; + } +} diff --git a/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java new file mode 100644 index 0000000000000000000000000000000000000000..a005c447db52b14de25bf71eff24450c57f05382 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.contactinfo; + +import android.text.TextUtils; + +/** + * Stores a phone number of a call with the country code where it originally occurred. This object + * is used as a key in the {@code ContactInfoCache}. + * + *

The country does not necessarily specify the country of the phone number itself, but rather it + * is the country in which the user was in when the call was placed or received. + */ +public final class NumberWithCountryIso { + + public final String number; + public final String countryIso; + + public NumberWithCountryIso(String number, String countryIso) { + this.number = number; + this.countryIso = countryIso; + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (!(o instanceof NumberWithCountryIso)) { + return false; + } + NumberWithCountryIso other = (NumberWithCountryIso) o; + return TextUtils.equals(number, other.number) && TextUtils.equals(countryIso, other.countryIso); + } + + @Override + public int hashCode() { + int numberHashCode = number == null ? 0 : number.hashCode(); + int countryHashCode = countryIso == null ? 0 : countryIso.hashCode(); + + return numberHashCode ^ countryHashCode; + } +} diff --git a/java/com/android/dialer/app/dialpad/DialpadFragment.java b/java/com/android/dialer/app/dialpad/DialpadFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..18bb250cefdf901c070c817931025328d0894117 --- /dev/null +++ b/java/com/android/dialer/app/dialpad/DialpadFragment.java @@ -0,0 +1,1689 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.dialpad; + +import android.Manifest.permission; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.net.Uri; +import android.os.Bundle; +import android.os.Trace; +import android.provider.Contacts.People; +import android.provider.Contacts.Phones; +import android.provider.Contacts.PhonesColumns; +import android.provider.Settings; +import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberFormattingTextWatcher; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.RelativeLayout; +import android.widget.TextView; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.dialog.CallSubjectDialog; +import com.android.contacts.common.util.StopWatch; +import com.android.contacts.common.widget.FloatingActionButtonController; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.SpecialCharSequenceMgr; +import com.android.dialer.app.calllog.CallLogAsync; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.LogUtil; +import com.android.dialer.dialpadview.DialpadKeyButton; +import com.android.dialer.dialpadview.DialpadView; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.PermissionsUtil; +import java.util.HashSet; +import java.util.List; + +/** Fragment that displays a twelve-key phone dialpad. */ +public class DialpadFragment extends Fragment + implements View.OnClickListener, + View.OnLongClickListener, + View.OnKeyListener, + AdapterView.OnItemClickListener, + TextWatcher, + PopupMenu.OnMenuItemClickListener, + DialpadKeyButton.OnPressedListener { + + private static final String TAG = "DialpadFragment"; + private static final boolean DEBUG = DialtactsActivity.DEBUG; + private static final String EMPTY_NUMBER = ""; + private static final char PAUSE = ','; + private static final char WAIT = ';'; + /** The length of DTMF tones in milliseconds */ + private static final int TONE_LENGTH_MS = 150; + + private static final int TONE_LENGTH_INFINITE = -1; + /** The DTMF tone volume relative to other sounds in the stream */ + private static final int TONE_RELATIVE_VOLUME = 80; + /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */ + private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF; + /** Identifier for the "Add Call" intent extra. */ + private static final String ADD_CALL_MODE_KEY = "add_call_mode"; + /** + * Identifier for intent extra for sending an empty Flash message for CDMA networks. This message + * is used by the network to simulate a press/depress of the "hookswitch" of a landline phone. Aka + * "empty flash". + * + *

TODO: Using an intent extra to tell the phone to send this flash is a temporary measure. To + * be replaced with an Telephony/TelecomManager call in the future. TODO: Keep in sync with the + * string defined in OutgoingCallBroadcaster.java in Phone app until this is replaced with the + * Telephony/Telecom API. + */ + private static final String EXTRA_SEND_EMPTY_FLASH = "com.android.phone.extra.SEND_EMPTY_FLASH"; + + private static final String PREF_DIGITS_FILLED_BY_INTENT = "pref_digits_filled_by_intent"; + private final Object mToneGeneratorLock = new Object(); + /** Set of dialpad keys that are currently being pressed */ + private final HashSet mPressedDialpadKeys = new HashSet(12); + // Last number dialed, retrieved asynchronously from the call DB + // in onCreate. This number is displayed when the user hits the + // send key and cleared in onPause. + private final CallLogAsync mCallLog = new CallLogAsync(); + private OnDialpadQueryChangedListener mDialpadQueryListener; + private DialpadView mDialpadView; + private EditText mDigits; + private int mDialpadSlideInDuration; + /** Remembers if we need to clear digits field when the screen is completely gone. */ + private boolean mClearDigitsOnStop; + + private View mOverflowMenuButton; + private PopupMenu mOverflowPopupMenu; + private View mDelete; + private ToneGenerator mToneGenerator; + private View mSpacer; + private FloatingActionButtonController mFloatingActionButtonController; + private ListView mDialpadChooser; + private DialpadChooserAdapter mDialpadChooserAdapter; + /** Regular expression prohibiting manual phone call. Can be empty, which means "no rule". */ + private String mProhibitedPhoneNumberRegexp; + + private PseudoEmergencyAnimator mPseudoEmergencyAnimator; + private String mLastNumberDialed = EMPTY_NUMBER; + + // determines if we want to playback local DTMF tones. + private boolean mDTMFToneEnabled; + private String mCurrentCountryIso; + private CallStateReceiver mCallStateReceiver; + private boolean mWasEmptyBeforeTextChange; + /** + * This field is set to true while processing an incoming DIAL intent, in order to make sure that + * SpecialCharSequenceMgr actions can be triggered by user input but *not* by a tel: URI passed by + * some other app. It will be set to false when all digits are cleared. + */ + private boolean mDigitsFilledByIntent; + + private boolean mStartedFromNewIntent = false; + private boolean mFirstLaunch = false; + private boolean mAnimate = false; + + /** + * Determines whether an add call operation is requested. + * + * @param intent The intent. + * @return {@literal true} if add call operation was requested. {@literal false} otherwise. + */ + public static boolean isAddCallMode(Intent intent) { + if (intent == null) { + return false; + } + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { + // see if we are "adding a call" from the InCallScreen; false by default. + return intent.getBooleanExtra(ADD_CALL_MODE_KEY, false); + } else { + return false; + } + } + + /** + * Format the provided string of digits into one that represents a properly formatted phone + * number. + * + * @param dialString String of characters to format + * @param normalizedNumber the E164 format number whose country code is used if the given + * phoneNumber doesn't have the country code. + * @param countryIso The country code representing the format to use if the provided normalized + * number is null or invalid. + * @return the provided string of digits as a formatted phone number, retaining any post-dial + * portion of the string. + */ + @VisibleForTesting + static String getFormattedDigits(String dialString, String normalizedNumber, String countryIso) { + String number = PhoneNumberUtils.extractNetworkPortion(dialString); + // Also retrieve the post dial portion of the provided data, so that the entire dial + // string can be reconstituted later. + final String postDial = PhoneNumberUtils.extractPostDialPortion(dialString); + + if (TextUtils.isEmpty(number)) { + return postDial; + } + + number = PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso); + + if (TextUtils.isEmpty(postDial)) { + return number; + } + + return number.concat(postDial); + } + + /** + * Returns true of the newDigit parameter can be added at the current selection point, otherwise + * returns false. Only prevents input of WAIT and PAUSE digits at an unsupported position. Fails + * early if start == -1 or start is larger than end. + */ + @VisibleForTesting + /* package */ static boolean canAddDigit(CharSequence digits, int start, int end, char newDigit) { + if (newDigit != WAIT && newDigit != PAUSE) { + throw new IllegalArgumentException( + "Should not be called for anything other than PAUSE & WAIT"); + } + + // False if no selection, or selection is reversed (end < start) + if (start == -1 || end < start) { + return false; + } + + // unsupported selection-out-of-bounds state + if (start > digits.length() || end > digits.length()) { + return false; + } + + // Special digit cannot be the first digit + if (start == 0) { + return false; + } + + if (newDigit == WAIT) { + // preceding char is ';' (WAIT) + if (digits.charAt(start - 1) == WAIT) { + return false; + } + + // next char is ';' (WAIT) + if ((digits.length() > end) && (digits.charAt(end) == WAIT)) { + return false; + } + } + + return true; + } + + private TelephonyManager getTelephonyManager() { + return (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); + } + + @Override + public Context getContext() { + return getActivity(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + mWasEmptyBeforeTextChange = TextUtils.isEmpty(s); + } + + @Override + public void onTextChanged(CharSequence input, int start, int before, int changeCount) { + if (mWasEmptyBeforeTextChange != TextUtils.isEmpty(input)) { + final Activity activity = getActivity(); + if (activity != null) { + activity.invalidateOptionsMenu(); + updateMenuOverflowButton(mWasEmptyBeforeTextChange); + } + } + + // DTMF Tones do not need to be played here any longer - + // the DTMF dialer handles that functionality now. + } + + @Override + public void afterTextChanged(Editable input) { + // When DTMF dialpad buttons are being pressed, we delay SpecialCharSequenceMgr sequence, + // since some of SpecialCharSequenceMgr's behavior is too abrupt for the "touch-down" + // behavior. + if (!mDigitsFilledByIntent + && SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), mDigits)) { + // A special sequence was entered, clear the digits + mDigits.getText().clear(); + } + + if (isDigitsEmpty()) { + mDigitsFilledByIntent = false; + mDigits.setCursorVisible(false); + } + + if (mDialpadQueryListener != null) { + mDialpadQueryListener.onDialpadQueryChanged(mDigits.getText().toString()); + } + + updateDeleteButtonEnabledState(); + } + + @Override + public void onCreate(Bundle state) { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(state); + + mFirstLaunch = state == null; + + mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); + + mProhibitedPhoneNumberRegexp = + getResources().getString(R.string.config_prohibited_phone_number_regexp); + + if (state != null) { + mDigitsFilledByIntent = state.getBoolean(PREF_DIGITS_FILLED_BY_INTENT); + } + + mDialpadSlideInDuration = getResources().getInteger(R.integer.dialpad_slide_in_duration); + + if (mCallStateReceiver == null) { + IntentFilter callStateIntentFilter = + new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED); + mCallStateReceiver = new CallStateReceiver(); + getActivity().registerReceiver(mCallStateReceiver, callStateIntentFilter); + } + Trace.endSection(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + Trace.beginSection(TAG + " onCreateView"); + Trace.beginSection(TAG + " inflate view"); + final View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false); + Trace.endSection(); + Trace.beginSection(TAG + " buildLayer"); + fragmentView.buildLayer(); + Trace.endSection(); + + Trace.beginSection(TAG + " setup views"); + + mDialpadView = (DialpadView) fragmentView.findViewById(R.id.dialpad_view); + mDialpadView.setCanDigitsBeEdited(true); + mDigits = mDialpadView.getDigits(); + mDigits.setKeyListener(UnicodeDialerKeyListener.INSTANCE); + mDigits.setOnClickListener(this); + mDigits.setOnKeyListener(this); + mDigits.setOnLongClickListener(this); + mDigits.addTextChangedListener(this); + mDigits.setElegantTextHeight(false); + + PhoneNumberFormattingTextWatcher watcher = + new PhoneNumberFormattingTextWatcher(GeoUtil.getCurrentCountryIso(getActivity())); + mDigits.addTextChangedListener(watcher); + + // Check for the presence of the keypad + View oneButton = fragmentView.findViewById(R.id.one); + if (oneButton != null) { + configureKeypadListeners(fragmentView); + } + + mDelete = mDialpadView.getDeleteButton(); + + if (mDelete != null) { + mDelete.setOnClickListener(this); + mDelete.setOnLongClickListener(this); + } + + mSpacer = fragmentView.findViewById(R.id.spacer); + mSpacer.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (isDigitsEmpty()) { + if (getActivity() != null) { + return ((HostInterface) getActivity()).onDialpadSpacerTouchWithEmptyQuery(); + } + return true; + } + return false; + } + }); + + mDigits.setCursorVisible(false); + + // Set up the "dialpad chooser" UI; see showDialpadChooser(). + mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser); + mDialpadChooser.setOnItemClickListener(this); + + final View floatingActionButtonContainer = + fragmentView.findViewById(R.id.dialpad_floating_action_button_container); + final ImageButton floatingActionButton = + (ImageButton) fragmentView.findViewById(R.id.dialpad_floating_action_button); + floatingActionButton.setOnClickListener(this); + mFloatingActionButtonController = + new FloatingActionButtonController( + getActivity(), floatingActionButtonContainer, floatingActionButton); + Trace.endSection(); + Trace.endSection(); + return fragmentView; + } + + private boolean isLayoutReady() { + return mDigits != null; + } + + @VisibleForTesting + public EditText getDigitsWidget() { + return mDigits; + } + + /** @return true when {@link #mDigits} is actually filled by the Intent. */ + private boolean fillDigitsIfNecessary(Intent intent) { + // Only fills digits from an intent if it is a new intent. + // Otherwise falls back to the previously used number. + if (!mFirstLaunch && !mStartedFromNewIntent) { + return false; + } + + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { + Uri uri = intent.getData(); + if (uri != null) { + if (PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) { + // Put the requested number into the input area + String data = uri.getSchemeSpecificPart(); + // Remember it is filled via Intent. + mDigitsFilledByIntent = true; + final String converted = + PhoneNumberUtils.convertKeypadLettersToDigits( + PhoneNumberUtils.replaceUnicodeDigits(data)); + setFormattedDigits(converted, null); + return true; + } else { + if (!PermissionsUtil.hasContactsPermissions(getActivity())) { + return false; + } + String type = intent.getType(); + if (People.CONTENT_ITEM_TYPE.equals(type) || Phones.CONTENT_ITEM_TYPE.equals(type)) { + // Query the phone number + Cursor c = + getActivity() + .getContentResolver() + .query( + intent.getData(), + new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY}, + null, + null, + null); + if (c != null) { + try { + if (c.moveToFirst()) { + // Remember it is filled via Intent. + mDigitsFilledByIntent = true; + // Put the number into the input area + setFormattedDigits(c.getString(0), c.getString(1)); + return true; + } + } finally { + c.close(); + } + } + } + } + } + } + return false; + } + + /** + * Checks the given Intent and changes dialpad's UI state. For example, if the Intent requires the + * screen to enter "Add Call" mode, this method will show correct UI for the mode. + */ + private void configureScreenFromIntent(Activity parent) { + // If we were not invoked with a DIAL intent, + if (!(parent instanceof DialtactsActivity)) { + setStartedFromNewIntent(false); + return; + } + // See if we were invoked with a DIAL intent. If we were, fill in the appropriate + // digits in the dialer field. + Intent intent = parent.getIntent(); + + if (!isLayoutReady()) { + // This happens typically when parent's Activity#onNewIntent() is called while + // Fragment#onCreateView() isn't called yet, and thus we cannot configure Views at + // this point. onViewCreate() should call this method after preparing layouts, so + // just ignore this call now. + LogUtil.i( + "DialpadFragment.configureScreenFromIntent", + "Screen configuration is requested before onCreateView() is called. Ignored"); + return; + } + + boolean needToShowDialpadChooser = false; + + // Be sure *not* to show the dialpad chooser if this is an + // explicit "Add call" action, though. + final boolean isAddCallMode = isAddCallMode(intent); + if (!isAddCallMode) { + + // Don't show the chooser when called via onNewIntent() and phone number is present. + // i.e. User clicks a telephone link from gmail for example. + // In this case, we want to show the dialpad with the phone number. + final boolean digitsFilled = fillDigitsIfNecessary(intent); + if (!(mStartedFromNewIntent && digitsFilled)) { + + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) + || Intent.ACTION_VIEW.equals(action) + || Intent.ACTION_MAIN.equals(action)) { + // If there's already an active call, bring up an intermediate UI to + // make the user confirm what they really want to do. + if (isPhoneInUse()) { + needToShowDialpadChooser = true; + } + } + } + } + showDialpadChooser(needToShowDialpadChooser); + setStartedFromNewIntent(false); + } + + public void setStartedFromNewIntent(boolean value) { + mStartedFromNewIntent = value; + } + + public void clearCallRateInformation() { + setCallRateInformation(null, null); + } + + public void setCallRateInformation(String countryName, String displayRate) { + mDialpadView.setCallRateInformation(countryName, displayRate); + } + + /** Sets formatted digits to digits field. */ + private void setFormattedDigits(String data, String normalizedNumber) { + final String formatted = getFormattedDigits(data, normalizedNumber, mCurrentCountryIso); + if (!TextUtils.isEmpty(formatted)) { + Editable digits = mDigits.getText(); + digits.replace(0, digits.length(), formatted); + // for some reason this isn't getting called in the digits.replace call above.. + // but in any case, this will make sure the background drawable looks right + afterTextChanged(digits); + } + } + + private void configureKeypadListeners(View fragmentView) { + final int[] buttonIds = + new int[] { + R.id.one, + R.id.two, + R.id.three, + R.id.four, + R.id.five, + R.id.six, + R.id.seven, + R.id.eight, + R.id.nine, + R.id.star, + R.id.zero, + R.id.pound + }; + + DialpadKeyButton dialpadKey; + + for (int i = 0; i < buttonIds.length; i++) { + dialpadKey = (DialpadKeyButton) fragmentView.findViewById(buttonIds[i]); + dialpadKey.setOnPressedListener(this); + } + + // Long-pressing one button will initiate Voicemail. + final DialpadKeyButton one = (DialpadKeyButton) fragmentView.findViewById(R.id.one); + one.setOnLongClickListener(this); + + // Long-pressing zero button will enter '+' instead. + final DialpadKeyButton zero = (DialpadKeyButton) fragmentView.findViewById(R.id.zero); + zero.setOnLongClickListener(this); + } + + @Override + public void onStart() { + Trace.beginSection(TAG + " onStart"); + super.onStart(); + // if the mToneGenerator creation fails, just continue without it. It is + // a local audio signal, and is not as important as the dtmf tone itself. + final long start = System.currentTimeMillis(); + synchronized (mToneGeneratorLock) { + if (mToneGenerator == null) { + try { + mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME); + } catch (RuntimeException e) { + LogUtil.e( + "DialpadFragment.onStart", + "Exception caught while creating local tone generator: " + e); + mToneGenerator = null; + } + } + } + final long total = System.currentTimeMillis() - start; + if (total > 50) { + LogUtil.i("DialpadFragment.onStart", "Time for ToneGenerator creation: " + total); + } + Trace.endSection(); + } + + @Override + public void onResume() { + Trace.beginSection(TAG + " onResume"); + super.onResume(); + + final DialtactsActivity activity = (DialtactsActivity) getActivity(); + mDialpadQueryListener = activity; + + final StopWatch stopWatch = StopWatch.start("Dialpad.onResume"); + + // Query the last dialed number. Do it first because hitting + // the DB is 'slow'. This call is asynchronous. + queryLastOutgoingCall(); + + stopWatch.lap("qloc"); + + final ContentResolver contentResolver = activity.getContentResolver(); + + // retrieve the DTMF tone play back setting. + mDTMFToneEnabled = + Settings.System.getInt(contentResolver, Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; + + stopWatch.lap("dtwd"); + + stopWatch.lap("hptc"); + + mPressedDialpadKeys.clear(); + + configureScreenFromIntent(getActivity()); + + stopWatch.lap("fdin"); + + if (!isPhoneInUse()) { + // A sanity-check: the "dialpad chooser" UI should not be visible if the phone is idle. + showDialpadChooser(false); + } + + stopWatch.lap("hnt"); + + updateDeleteButtonEnabledState(); + + stopWatch.lap("bes"); + + stopWatch.stopAndLog(TAG, 50); + + // Populate the overflow menu in onResume instead of onCreate, so that if the SMS activity + // is disabled while Dialer is paused, the "Send a text message" option can be correctly + // removed when resumed. + mOverflowMenuButton = mDialpadView.getOverflowMenuButton(); + mOverflowPopupMenu = buildOptionsMenu(mOverflowMenuButton); + mOverflowMenuButton.setOnTouchListener(mOverflowPopupMenu.getDragToOpenListener()); + mOverflowMenuButton.setOnClickListener(this); + mOverflowMenuButton.setVisibility(isDigitsEmpty() ? View.INVISIBLE : View.VISIBLE); + + if (mFirstLaunch) { + // The onHiddenChanged callback does not get called the first time the fragment is + // attached, so call it ourselves here. + onHiddenChanged(false); + } + + mFirstLaunch = false; + Trace.endSection(); + } + + @Override + public void onPause() { + super.onPause(); + + // Make sure we don't leave this activity with a tone still playing. + stopTone(); + mPressedDialpadKeys.clear(); + + // TODO: I wonder if we should not check if the AsyncTask that + // lookup the last dialed number has completed. + mLastNumberDialed = EMPTY_NUMBER; // Since we are going to query again, free stale number. + + SpecialCharSequenceMgr.cleanup(); + } + + @Override + public void onStop() { + super.onStop(); + + synchronized (mToneGeneratorLock) { + if (mToneGenerator != null) { + mToneGenerator.release(); + mToneGenerator = null; + } + } + + if (mClearDigitsOnStop) { + mClearDigitsOnStop = false; + clearDialpad(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(PREF_DIGITS_FILLED_BY_INTENT, mDigitsFilledByIntent); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mPseudoEmergencyAnimator != null) { + mPseudoEmergencyAnimator.destroy(); + mPseudoEmergencyAnimator = null; + } + getActivity().unregisterReceiver(mCallStateReceiver); + } + + private void keyPressed(int keyCode) { + if (getView() == null || getView().getTranslationY() != 0) { + return; + } + switch (keyCode) { + case KeyEvent.KEYCODE_1: + playTone(ToneGenerator.TONE_DTMF_1, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_2: + playTone(ToneGenerator.TONE_DTMF_2, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_3: + playTone(ToneGenerator.TONE_DTMF_3, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_4: + playTone(ToneGenerator.TONE_DTMF_4, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_5: + playTone(ToneGenerator.TONE_DTMF_5, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_6: + playTone(ToneGenerator.TONE_DTMF_6, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_7: + playTone(ToneGenerator.TONE_DTMF_7, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_8: + playTone(ToneGenerator.TONE_DTMF_8, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_9: + playTone(ToneGenerator.TONE_DTMF_9, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_0: + playTone(ToneGenerator.TONE_DTMF_0, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_POUND: + playTone(ToneGenerator.TONE_DTMF_P, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_STAR: + playTone(ToneGenerator.TONE_DTMF_S, TONE_LENGTH_INFINITE); + break; + default: + break; + } + + getView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); + mDigits.onKeyDown(keyCode, event); + + // If the cursor is at the end of the text we hide it. + final int length = mDigits.length(); + if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) { + mDigits.setCursorVisible(false); + } + } + + @Override + public boolean onKey(View view, int keyCode, KeyEvent event) { + if (view.getId() == R.id.digits) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + handleDialButtonPressed(); + return true; + } + } + return false; + } + + /** + * When a key is pressed, we start playing DTMF tone, do vibration, and enter the digit + * immediately. When a key is released, we stop the tone. Note that the "key press" event will be + * delivered by the system with certain amount of delay, it won't be synced with user's actual + * "touch-down" behavior. + */ + @Override + public void onPressed(View view, boolean pressed) { + if (DEBUG) { + LogUtil.d("DialpadFragment.onPressed", "view: " + view + ", pressed: " + pressed); + } + if (pressed) { + int resId = view.getId(); + if (resId == R.id.one) { + keyPressed(KeyEvent.KEYCODE_1); + } else if (resId == R.id.two) { + keyPressed(KeyEvent.KEYCODE_2); + } else if (resId == R.id.three) { + keyPressed(KeyEvent.KEYCODE_3); + } else if (resId == R.id.four) { + keyPressed(KeyEvent.KEYCODE_4); + } else if (resId == R.id.five) { + keyPressed(KeyEvent.KEYCODE_5); + } else if (resId == R.id.six) { + keyPressed(KeyEvent.KEYCODE_6); + } else if (resId == R.id.seven) { + keyPressed(KeyEvent.KEYCODE_7); + } else if (resId == R.id.eight) { + keyPressed(KeyEvent.KEYCODE_8); + } else if (resId == R.id.nine) { + keyPressed(KeyEvent.KEYCODE_9); + } else if (resId == R.id.zero) { + keyPressed(KeyEvent.KEYCODE_0); + } else if (resId == R.id.pound) { + keyPressed(KeyEvent.KEYCODE_POUND); + } else if (resId == R.id.star) { + keyPressed(KeyEvent.KEYCODE_STAR); + } else { + LogUtil.e( + "DialpadFragment.onPressed", "Unexpected onTouch(ACTION_DOWN) event from: " + view); + } + mPressedDialpadKeys.add(view); + } else { + mPressedDialpadKeys.remove(view); + if (mPressedDialpadKeys.isEmpty()) { + stopTone(); + } + } + } + + /** + * Called by the containing Activity to tell this Fragment to build an overflow options menu for + * display by the container when appropriate. + * + * @param invoker the View that invoked the options menu, to act as an anchor location. + */ + private PopupMenu buildOptionsMenu(View invoker) { + final PopupMenu popupMenu = + new PopupMenu(getActivity(), invoker) { + @Override + public void show() { + final Menu menu = getMenu(); + + boolean enable = !isDigitsEmpty(); + for (int i = 0; i < menu.size(); i++) { + MenuItem item = menu.getItem(i); + item.setEnabled(enable); + if (item.getItemId() == R.id.menu_call_with_note) { + item.setVisible(CallUtil.isCallWithSubjectSupported(getContext())); + } + } + super.show(); + } + }; + popupMenu.inflate(R.menu.dialpad_options); + popupMenu.setOnMenuItemClickListener(this); + return popupMenu; + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.dialpad_floating_action_button) { + view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + handleDialButtonPressed(); + } else if (resId == R.id.deleteButton) { + keyPressed(KeyEvent.KEYCODE_DEL); + } else if (resId == R.id.digits) { + if (!isDigitsEmpty()) { + mDigits.setCursorVisible(true); + } + } else if (resId == R.id.dialpad_overflow) { + mOverflowPopupMenu.show(); + } else { + LogUtil.w("DialpadFragment.onClick", "Unexpected event from: " + view); + return; + } + } + + @Override + public boolean onLongClick(View view) { + final Editable digits = mDigits.getText(); + final int id = view.getId(); + if (id == R.id.deleteButton) { + digits.clear(); + return true; + } else if (id == R.id.one) { + if (isDigitsEmpty() || TextUtils.equals(mDigits.getText(), "1")) { + // We'll try to initiate voicemail and thus we want to remove irrelevant string. + removePreviousDigitIfPossible('1'); + + List subscriptionAccountHandles = + PhoneAccountUtils.getSubscriptionPhoneAccounts(getActivity()); + boolean hasUserSelectedDefault = + subscriptionAccountHandles.contains( + TelecomUtil.getDefaultOutgoingPhoneAccount( + getActivity(), PhoneAccount.SCHEME_VOICEMAIL)); + boolean needsAccountDisambiguation = + subscriptionAccountHandles.size() > 1 && !hasUserSelectedDefault; + + if (needsAccountDisambiguation || isVoicemailAvailable()) { + // On a multi-SIM phone, if the user has not selected a default + // subscription, initiate a call to voicemail so they can select an account + // from the "Call with" dialog. + callVoicemail(); + } else if (getActivity() != null) { + // Voicemail is unavailable maybe because Airplane mode is turned on. + // Check the current status and show the most appropriate error message. + final boolean isAirplaneModeOn = + Settings.System.getInt( + getActivity().getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) + != 0; + if (isAirplaneModeOn) { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_voicemail_airplane_mode_message); + dialogFragment.show(getFragmentManager(), "voicemail_request_during_airplane_mode"); + } else { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_voicemail_not_ready_message); + dialogFragment.show(getFragmentManager(), "voicemail_not_ready"); + } + } + return true; + } + return false; + } else if (id == R.id.zero) { + if (mPressedDialpadKeys.contains(view)) { + // If the zero key is currently pressed, then the long press occurred by touch + // (and not via other means like certain accessibility input methods). + // Remove the '0' that was input when the key was first pressed. + removePreviousDigitIfPossible('0'); + } + keyPressed(KeyEvent.KEYCODE_PLUS); + stopTone(); + mPressedDialpadKeys.remove(view); + return true; + } else if (id == R.id.digits) { + mDigits.setCursorVisible(true); + return false; + } + return false; + } + + /** + * Remove the digit just before the current position of the cursor, iff the following conditions + * are true: 1) The cursor is not positioned at index 0. 2) The digit before the current cursor + * position matches the current digit. + * + * @param digit to remove from the digits view. + */ + private void removePreviousDigitIfPossible(char digit) { + final int currentPosition = mDigits.getSelectionStart(); + if (currentPosition > 0 && digit == mDigits.getText().charAt(currentPosition - 1)) { + mDigits.setSelection(currentPosition); + mDigits.getText().delete(currentPosition - 1, currentPosition); + } + } + + public void callVoicemail() { + DialerUtils.startActivityWithErrorToast( + getActivity(), + new CallIntentBuilder(CallUtil.getVoicemailUri(), CallInitiationType.Type.DIALPAD).build()); + hideAndClearDialpad(false); + } + + private void hideAndClearDialpad(boolean animate) { + ((DialtactsActivity) getActivity()).hideDialpadFragment(animate, true); + } + + /** + * In most cases, when the dial button is pressed, there is a number in digits area. Pack it in + * the intent, start the outgoing call broadcast as a separate task and finish this activity. + * + *

When there is no digit and the phone is CDMA and off hook, we're sending a blank flash for + * CDMA. CDMA networks use Flash messages when special processing needs to be done, mainly for + * 3-way or call waiting scenarios. Presumably, here we're in a special 3-way scenario where the + * network needs a blank flash before being able to add the new participant. (This is not the case + * with all 3-way calls, just certain CDMA infrastructures.) + * + *

Otherwise, there is no digit, display the last dialed number. Don't finish since the user + * may want to edit it. The user needs to press the dial button again, to dial it (general case + * described above). + */ + private void handleDialButtonPressed() { + if (isDigitsEmpty()) { // No number entered. + handleDialButtonClickWithEmptyDigits(); + } else { + final String number = mDigits.getText().toString(); + + // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated + // test equipment. + // TODO: clean it up. + if (number != null + && !TextUtils.isEmpty(mProhibitedPhoneNumberRegexp) + && number.matches(mProhibitedPhoneNumberRegexp)) { + LogUtil.i( + "DialpadFragment.handleDialButtonPressed", + "The phone number is prohibited explicitly by a rule."); + if (getActivity() != null) { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message); + dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog"); + } + + // Clear the digits just in case. + clearDialpad(); + } else { + final Intent intent = + new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD).build(); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + hideAndClearDialpad(false); + } + } + } + + public void clearDialpad() { + if (mDigits != null) { + mDigits.getText().clear(); + } + } + + private void handleDialButtonClickWithEmptyDigits() { + if (phoneIsCdma() && isPhoneInUse()) { + // TODO: Move this logic into services/Telephony + // + // This is really CDMA specific. On GSM is it possible + // to be off hook and wanted to add a 3rd party using + // the redial feature. + startActivity(newFlashIntent()); + } else { + if (!TextUtils.isEmpty(mLastNumberDialed)) { + // Recall the last number dialed. + mDigits.setText(mLastNumberDialed); + + // ...and move the cursor to the end of the digits string, + // so you'll be able to delete digits using the Delete + // button (just as if you had typed the number manually.) + // + // Note we use mDigits.getText().length() here, not + // mLastNumberDialed.length(), since the EditText widget now + // contains a *formatted* version of mLastNumberDialed (due to + // mTextWatcher) and its length may have changed. + mDigits.setSelection(mDigits.getText().length()); + } else { + // There's no "last number dialed" or the + // background query is still running. There's + // nothing useful for the Dial button to do in + // this case. Note: with a soft dial button, this + // can never happens since the dial button is + // disabled under these conditons. + playTone(ToneGenerator.TONE_PROP_NACK); + } + } + } + + /** Plays the specified tone for TONE_LENGTH_MS milliseconds. */ + private void playTone(int tone) { + playTone(tone, TONE_LENGTH_MS); + } + + /** + * Play the specified tone for the specified milliseconds + * + *

The tone is played locally, using the audio stream for phone calls. Tones are played only if + * the "Audible touch tones" user preference is checked, and are NOT played if the device is in + * silent mode. + * + *

The tone length can be -1, meaning "keep playing the tone." If the caller does so, it should + * call stopTone() afterward. + * + * @param tone a tone code from {@link ToneGenerator} + * @param durationMs tone length. + */ + private void playTone(int tone, int durationMs) { + // if local tone playback is disabled, just return. + if (!mDTMFToneEnabled) { + return; + } + + // Also do nothing if the phone is in silent mode. + // We need to re-check the ringer mode for *every* playTone() + // call, rather than keeping a local flag that's updated in + // onResume(), since it's possible to toggle silent mode without + // leaving the current activity (via the ENDCALL-longpress menu.) + AudioManager audioManager = + (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE); + int ringerMode = audioManager.getRingerMode(); + if ((ringerMode == AudioManager.RINGER_MODE_SILENT) + || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) { + return; + } + + synchronized (mToneGeneratorLock) { + if (mToneGenerator == null) { + LogUtil.w("DialpadFragment.playTone", "mToneGenerator == null, tone: " + tone); + return; + } + + // Start the new tone (will stop any playing tone) + mToneGenerator.startTone(tone, durationMs); + } + } + + /** Stop the tone if it is played. */ + private void stopTone() { + // if local tone playback is disabled, just return. + if (!mDTMFToneEnabled) { + return; + } + synchronized (mToneGeneratorLock) { + if (mToneGenerator == null) { + LogUtil.w("DialpadFragment.stopTone", "mToneGenerator == null"); + return; + } + mToneGenerator.stopTone(); + } + } + + /** + * Brings up the "dialpad chooser" UI in place of the usual Dialer elements (the textfield/button + * and the dialpad underneath). + * + *

We show this UI if the user brings up the Dialer while a call is already in progress, since + * there's a good chance we got here accidentally (and the user really wanted the in-call dialpad + * instead). So in this situation we display an intermediate UI that lets the user explicitly + * choose between the in-call dialpad ("Use touch tone keypad") and the regular Dialer ("Add + * call"). (Or, the option "Return to call in progress" just goes back to the in-call UI with no + * dialpad at all.) + * + * @param enabled If true, show the "dialpad chooser" instead of the regular Dialer UI + */ + private void showDialpadChooser(boolean enabled) { + if (getActivity() == null) { + return; + } + // Check if onCreateView() is already called by checking one of View objects. + if (!isLayoutReady()) { + return; + } + + if (enabled) { + LogUtil.i("DialpadFragment.showDialpadChooser", "Showing dialpad chooser!"); + if (mDialpadView != null) { + mDialpadView.setVisibility(View.GONE); + } + + mFloatingActionButtonController.setVisible(false); + mDialpadChooser.setVisibility(View.VISIBLE); + + // Instantiate the DialpadChooserAdapter and hook it up to the + // ListView. We do this only once. + if (mDialpadChooserAdapter == null) { + mDialpadChooserAdapter = new DialpadChooserAdapter(getActivity()); + } + mDialpadChooser.setAdapter(mDialpadChooserAdapter); + } else { + LogUtil.i("DialpadFragment.showDialpadChooser", "Displaying normal Dialer UI."); + if (mDialpadView != null) { + mDialpadView.setVisibility(View.VISIBLE); + } else { + mDigits.setVisibility(View.VISIBLE); + } + + // mFloatingActionButtonController must also be 'scaled in', in order to be visible after + // 'scaleOut()' hidden method. + if (!mFloatingActionButtonController.isVisible()) { + // Just call 'scaleIn()' method if the mFloatingActionButtonController was not already + // previously visible. + mFloatingActionButtonController.scaleIn(0); + mFloatingActionButtonController.setVisible(true); + } + mDialpadChooser.setVisibility(View.GONE); + } + } + + /** @return true if we're currently showing the "dialpad chooser" UI. */ + private boolean isDialpadChooserVisible() { + return mDialpadChooser.getVisibility() == View.VISIBLE; + } + + /** Handle clicks from the dialpad chooser. */ + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + DialpadChooserAdapter.ChoiceItem item = + (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position); + int itemId = item.id; + if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD) { + // Fire off an intent to go back to the in-call UI + // with the dialpad visible. + returnToInCallScreen(true); + } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL) { + // Fire off an intent to go back to the in-call UI + // (with the dialpad hidden). + returnToInCallScreen(false); + } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL) { + // Ok, guess the user really did want to be here (in the + // regular Dialer) after all. Bring back the normal Dialer UI. + showDialpadChooser(false); + } else { + LogUtil.w("DialpadFragment.onItemClick", "Unexpected itemId: " + itemId); + } + } + + /** + * Returns to the in-call UI (where there's presumably a call in progress) in response to the user + * selecting "use touch tone keypad" or "return to call" from the dialpad chooser. + */ + private void returnToInCallScreen(boolean showDialpad) { + TelecomUtil.showInCallScreen(getActivity(), showDialpad); + + // Finally, finish() ourselves so that we don't stay on the + // activity stack. + // Note that we do this whether or not the showCallScreenWithDialpad() + // call above had any effect or not! (That call is a no-op if the + // phone is idle, which can happen if the current call ends while + // the dialpad chooser is up. In this case we can't show the + // InCallScreen, and there's no point staying here in the Dialer, + // so we just take the user back where he came from...) + getActivity().finish(); + } + + /** + * @return true if the phone is "in use", meaning that at least one line is active (ie. off hook + * or ringing or dialing, or on hold). + */ + private boolean isPhoneInUse() { + final Context context = getActivity(); + if (context != null) { + return TelecomUtil.isInCall(context); + } + return false; + } + + /** @return true if the phone is a CDMA phone type */ + private boolean phoneIsCdma() { + return getTelephonyManager().getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + int resId = item.getItemId(); + if (resId == R.id.menu_2s_pause) { + updateDialString(PAUSE); + return true; + } else if (resId == R.id.menu_add_wait) { + updateDialString(WAIT); + return true; + } else if (resId == R.id.menu_call_with_note) { + CallSubjectDialog.start(getActivity(), mDigits.getText().toString()); + hideAndClearDialpad(false); + return true; + } else { + return false; + } + } + + /** + * Updates the dial string (mDigits) after inserting a Pause character (,) or Wait character (;). + */ + private void updateDialString(char newDigit) { + if (newDigit != WAIT && newDigit != PAUSE) { + throw new IllegalArgumentException("Not expected for anything other than PAUSE & WAIT"); + } + + int selectionStart; + int selectionEnd; + + // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText()); + int anchor = mDigits.getSelectionStart(); + int point = mDigits.getSelectionEnd(); + + selectionStart = Math.min(anchor, point); + selectionEnd = Math.max(anchor, point); + + if (selectionStart == -1) { + selectionStart = selectionEnd = mDigits.length(); + } + + Editable digits = mDigits.getText(); + + if (canAddDigit(digits, selectionStart, selectionEnd, newDigit)) { + digits.replace(selectionStart, selectionEnd, Character.toString(newDigit)); + + if (selectionStart != selectionEnd) { + // Unselect: back to a regular cursor, just pass the character inserted. + mDigits.setSelection(selectionStart + 1); + } + } + } + + /** Update the enabledness of the "Dial" and "Backspace" buttons if applicable. */ + private void updateDeleteButtonEnabledState() { + if (getActivity() == null) { + return; + } + final boolean digitsNotEmpty = !isDigitsEmpty(); + mDelete.setEnabled(digitsNotEmpty); + } + + /** + * Handle transitions for the menu button depending on the state of the digits edit text. + * Transition out when going from digits to no digits and transition in when the first digit is + * pressed. + * + * @param transitionIn True if transitioning in, False if transitioning out + */ + private void updateMenuOverflowButton(boolean transitionIn) { + mOverflowMenuButton = mDialpadView.getOverflowMenuButton(); + if (transitionIn) { + AnimUtils.fadeIn(mOverflowMenuButton, AnimUtils.DEFAULT_DURATION); + } else { + AnimUtils.fadeOut(mOverflowMenuButton, AnimUtils.DEFAULT_DURATION); + } + } + + /** + * Check if voicemail is enabled/accessible. + * + * @return true if voicemail is enabled and accessible. Note that this can be false "temporarily" + * after the app boot. + */ + private boolean isVoicemailAvailable() { + try { + PhoneAccountHandle defaultUserSelectedAccount = + TelecomUtil.getDefaultOutgoingPhoneAccount(getActivity(), PhoneAccount.SCHEME_VOICEMAIL); + if (defaultUserSelectedAccount == null) { + // In a single-SIM phone, there is no default outgoing phone account selected by + // the user, so just call TelephonyManager#getVoicemailNumber directly. + return !TextUtils.isEmpty(getTelephonyManager().getVoiceMailNumber()); + } else { + return !TextUtils.isEmpty( + TelecomUtil.getVoicemailNumber(getActivity(), defaultUserSelectedAccount)); + } + } catch (SecurityException se) { + // Possibly no READ_PHONE_STATE privilege. + LogUtil.w( + "DialpadFragment.isVoicemailAvailable", + "SecurityException is thrown. Maybe privilege isn't sufficient."); + } + return false; + } + + /** @return true if the widget with the phone number digits is empty. */ + private boolean isDigitsEmpty() { + return mDigits.length() == 0; + } + + /** + * Starts the asyn query to get the last dialed/outgoing number. When the background query + * finishes, mLastNumberDialed is set to the last dialed number or an empty string if none exists + * yet. + */ + private void queryLastOutgoingCall() { + mLastNumberDialed = EMPTY_NUMBER; + if (ContextCompat.checkSelfPermission(getActivity(), permission.READ_CALL_LOG) + != PackageManager.PERMISSION_GRANTED) { + return; + } + CallLogAsync.GetLastOutgoingCallArgs lastCallArgs = + new CallLogAsync.GetLastOutgoingCallArgs( + getActivity(), + new CallLogAsync.OnLastOutgoingCallComplete() { + @Override + public void lastOutgoingCall(String number) { + // TODO: Filter out emergency numbers if + // the carrier does not want redial for + // these. + // If the fragment has already been detached since the last time + // we called queryLastOutgoingCall in onResume there is no point + // doing anything here. + if (getActivity() == null) { + return; + } + mLastNumberDialed = number; + updateDeleteButtonEnabledState(); + } + }); + mCallLog.getLastOutgoingCall(lastCallArgs); + } + + private Intent newFlashIntent() { + Intent intent = new CallIntentBuilder(EMPTY_NUMBER, CallInitiationType.Type.DIALPAD).build(); + intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true); + return intent; + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + final DialtactsActivity activity = (DialtactsActivity) getActivity(); + final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view); + if (activity == null) { + return; + } + if (!hidden && !isDialpadChooserVisible()) { + if (mAnimate) { + dialpadView.animateShow(); + } + mFloatingActionButtonController.setVisible(false); + mFloatingActionButtonController.scaleIn(mAnimate ? mDialpadSlideInDuration : 0); + activity.onDialpadShown(); + mDigits.requestFocus(); + } + if (hidden) { + if (mAnimate) { + mFloatingActionButtonController.scaleOut(); + } else { + mFloatingActionButtonController.setVisible(false); + } + } + } + + public boolean getAnimate() { + return mAnimate; + } + + public void setAnimate(boolean value) { + mAnimate = value; + } + + public void setYFraction(float yFraction) { + ((DialpadSlidingRelativeLayout) getView()).setYFraction(yFraction); + } + + public int getDialpadHeight() { + if (mDialpadView == null) { + return 0; + } + return mDialpadView.getHeight(); + } + + public void process_quote_emergency_unquote(String query) { + if (PseudoEmergencyAnimator.PSEUDO_EMERGENCY_NUMBER.equals(query)) { + if (mPseudoEmergencyAnimator == null) { + mPseudoEmergencyAnimator = + new PseudoEmergencyAnimator( + new PseudoEmergencyAnimator.ViewProvider() { + @Override + public View getView() { + return DialpadFragment.this.getView(); + } + }); + } + mPseudoEmergencyAnimator.start(); + } else { + if (mPseudoEmergencyAnimator != null) { + mPseudoEmergencyAnimator.end(); + } + } + } + + public interface OnDialpadQueryChangedListener { + + void onDialpadQueryChanged(String query); + } + + public interface HostInterface { + + /** + * Notifies the parent activity that the space above the dialpad has been tapped with no query + * in the dialpad present. In most situations this will cause the dialpad to be dismissed, + * unless there happens to be content showing. + */ + boolean onDialpadSpacerTouchWithEmptyQuery(); + } + + /** + * LinearLayout with getter and setter methods for the translationY property using floats, for + * animation purposes. + */ + public static class DialpadSlidingRelativeLayout extends RelativeLayout { + + public DialpadSlidingRelativeLayout(Context context) { + super(context); + } + + public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @UsedByReflection(value = "dialpad_fragment.xml") + public float getYFraction() { + final int height = getHeight(); + if (height == 0) { + return 0; + } + return getTranslationY() / height; + } + + @UsedByReflection(value = "dialpad_fragment.xml") + public void setYFraction(float yFraction) { + setTranslationY(yFraction * getHeight()); + } + } + + public static class ErrorDialogFragment extends DialogFragment { + + private static final String ARG_TITLE_RES_ID = "argTitleResId"; + private static final String ARG_MESSAGE_RES_ID = "argMessageResId"; + private int mTitleResId; + private int mMessageResId; + + public static ErrorDialogFragment newInstance(int messageResId) { + return newInstance(0, messageResId); + } + + public static ErrorDialogFragment newInstance(int titleResId, int messageResId) { + final ErrorDialogFragment fragment = new ErrorDialogFragment(); + final Bundle args = new Bundle(); + args.putInt(ARG_TITLE_RES_ID, titleResId); + args.putInt(ARG_MESSAGE_RES_ID, messageResId); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID); + mMessageResId = getArguments().getInt(ARG_MESSAGE_RES_ID); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + if (mTitleResId != 0) { + builder.setTitle(mTitleResId); + } + if (mMessageResId != 0) { + builder.setMessage(mMessageResId); + } + builder.setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + return builder.create(); + } + } + + /** + * Simple list adapter, binding to an icon + text label for each item in the "dialpad chooser" + * list. + */ + private static class DialpadChooserAdapter extends BaseAdapter { + + // IDs for the possible "choices": + static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101; + static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102; + static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103; + private static final int NUM_ITEMS = 3; + private LayoutInflater mInflater; + private ChoiceItem[] mChoiceItems = new ChoiceItem[NUM_ITEMS]; + + public DialpadChooserAdapter(Context context) { + // Cache the LayoutInflate to avoid asking for a new one each time. + mInflater = LayoutInflater.from(context); + + // Initialize the possible choices. + // TODO: could this be specified entirely in XML? + + // - "Use touch tone keypad" + mChoiceItems[0] = + new ChoiceItem( + context.getString(R.string.dialer_useDtmfDialpad), + BitmapFactory.decodeResource( + context.getResources(), R.drawable.ic_dialer_fork_tt_keypad), + DIALPAD_CHOICE_USE_DTMF_DIALPAD); + + // - "Return to call in progress" + mChoiceItems[1] = + new ChoiceItem( + context.getString(R.string.dialer_returnToInCallScreen), + BitmapFactory.decodeResource( + context.getResources(), R.drawable.ic_dialer_fork_current_call), + DIALPAD_CHOICE_RETURN_TO_CALL); + + // - "Add call" + mChoiceItems[2] = + new ChoiceItem( + context.getString(R.string.dialer_addAnotherCall), + BitmapFactory.decodeResource( + context.getResources(), R.drawable.ic_dialer_fork_add_call), + DIALPAD_CHOICE_ADD_NEW_CALL); + } + + @Override + public int getCount() { + return NUM_ITEMS; + } + + /** Return the ChoiceItem for a given position. */ + @Override + public Object getItem(int position) { + return mChoiceItems[position]; + } + + /** Return a unique ID for each possible choice. */ + @Override + public long getItemId(int position) { + return position; + } + + /** Make a view for each row. */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // When convertView is non-null, we can reuse it (there's no need + // to reinflate it.) + if (convertView == null) { + convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null); + } + + TextView text = (TextView) convertView.findViewById(R.id.text); + text.setText(mChoiceItems[position].text); + + ImageView icon = (ImageView) convertView.findViewById(R.id.icon); + icon.setImageBitmap(mChoiceItems[position].icon); + + return convertView; + } + + // Simple struct for a single "choice" item. + static class ChoiceItem { + + String text; + Bitmap icon; + int id; + + public ChoiceItem(String s, Bitmap b, int i) { + text = s; + icon = b; + id = i; + } + } + } + + private class CallStateReceiver extends BroadcastReceiver { + + /** + * Receive call state changes so that we can take down the "dialpad chooser" if the phone + * becomes idle while the chooser UI is visible. + */ + @Override + public void onReceive(Context context, Intent intent) { + String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); + if ((TextUtils.equals(state, TelephonyManager.EXTRA_STATE_IDLE) + || TextUtils.equals(state, TelephonyManager.EXTRA_STATE_OFFHOOK)) + && isDialpadChooserVisible()) { + // Note there's a race condition in the UI here: the + // dialpad chooser could conceivably disappear (on its + // own) at the exact moment the user was trying to select + // one of the choices, which would be confusing. (But at + // least that's better than leaving the dialpad chooser + // onscreen, but useless...) + showDialpadChooser(false); + } + } + } +} diff --git a/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java b/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java new file mode 100644 index 0000000000000000000000000000000000000000..2ffacb6d8755ccf8b475478264e6b30ee5f42a2e --- /dev/null +++ b/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.dialpad; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LightingColorFilter; +import android.os.Handler; +import android.os.Vibrator; +import android.view.View; +import com.android.dialer.app.R; + +/** Animates the dial button on "emergency" phone numbers. */ +public class PseudoEmergencyAnimator { + + public static final String PSEUDO_EMERGENCY_NUMBER = "01189998819991197253"; + private static final int VIBRATE_LENGTH_MILLIS = 200; + private static final int ITERATION_LENGTH_MILLIS = 1000; + private static final int ANIMATION_ITERATION_COUNT = 6; + private ViewProvider mViewProvider; + private ValueAnimator mPseudoEmergencyColorAnimator; + + PseudoEmergencyAnimator(ViewProvider viewProvider) { + mViewProvider = viewProvider; + } + + public void destroy() { + end(); + mViewProvider = null; + } + + public void start() { + if (mPseudoEmergencyColorAnimator == null) { + Integer colorFrom = Color.BLUE; + Integer colorTo = Color.RED; + mPseudoEmergencyColorAnimator = + ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + + mPseudoEmergencyColorAnimator.addUpdateListener( + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + try { + int color = (int) animator.getAnimatedValue(); + ColorFilter colorFilter = new LightingColorFilter(Color.BLACK, color); + + View floatingActionButtonContainer = + getView().findViewById(R.id.dialpad_floating_action_button_container); + if (floatingActionButtonContainer != null) { + floatingActionButtonContainer.getBackground().setColorFilter(colorFilter); + } + } catch (Exception e) { + animator.cancel(); + } + } + }); + + mPseudoEmergencyColorAnimator.addListener( + new AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) { + try { + vibrate(VIBRATE_LENGTH_MILLIS); + } catch (Exception e) { + animation.cancel(); + } + } + + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationEnd(Animator animation) { + try { + View floatingActionButtonContainer = + getView().findViewById(R.id.dialpad_floating_action_button_container); + if (floatingActionButtonContainer != null) { + floatingActionButtonContainer.getBackground().clearColorFilter(); + } + + new Handler() + .postDelayed( + new Runnable() { + @Override + public void run() { + try { + vibrate(VIBRATE_LENGTH_MILLIS); + } catch (Exception e) { + // ignored + } + } + }, + ITERATION_LENGTH_MILLIS); + } catch (Exception e) { + animation.cancel(); + } + } + }); + + mPseudoEmergencyColorAnimator.setDuration(VIBRATE_LENGTH_MILLIS); + mPseudoEmergencyColorAnimator.setRepeatMode(ValueAnimator.REVERSE); + mPseudoEmergencyColorAnimator.setRepeatCount(ANIMATION_ITERATION_COUNT); + } + if (!mPseudoEmergencyColorAnimator.isStarted()) { + mPseudoEmergencyColorAnimator.start(); + } + } + + public void end() { + if (mPseudoEmergencyColorAnimator != null && mPseudoEmergencyColorAnimator.isStarted()) { + mPseudoEmergencyColorAnimator.end(); + } + } + + private View getView() { + return mViewProvider == null ? null : mViewProvider.getView(); + } + + private Context getContext() { + View view = getView(); + return view != null ? view.getContext() : null; + } + + private void vibrate(long milliseconds) { + Context context = getContext(); + if (context != null) { + Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + if (vibrator != null) { + vibrator.vibrate(milliseconds); + } + } + } + + public interface ViewProvider { + + View getView(); + } +} diff --git a/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java b/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..f3a93f916f4781686359493f0c90980c66eac42f --- /dev/null +++ b/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.dialpad; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.util.Log; +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; +import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; + +/** Implements a Loader class to asynchronously load SmartDial search results. */ +public class SmartDialCursorLoader extends AsyncTaskLoader { + + private static final String TAG = "SmartDialCursorLoader"; + private static final boolean DEBUG = false; + + private final Context mContext; + + private Cursor mCursor; + + private String mQuery; + private SmartDialNameMatcher mNameMatcher; + + private ForceLoadContentObserver mObserver; + + private boolean mShowEmptyListForNullQuery = true; + + public SmartDialCursorLoader(Context context) { + super(context); + mContext = context; + } + + /** + * Configures the query string to be used to find SmartDial matches. + * + * @param query The query string user typed. + */ + public void configureQuery(String query) { + if (DEBUG) { + Log.v(TAG, "Configure new query to be " + query); + } + mQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialPrefix.getMap()); + + /** Constructs a name matcher object for matching names. */ + mNameMatcher = new SmartDialNameMatcher(mQuery, SmartDialPrefix.getMap()); + mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery); + } + + /** + * Queries the SmartDial database and loads results in background. + * + * @return Cursor of contacts that matches the SmartDial query. + */ + @Override + public Cursor loadInBackground() { + if (DEBUG) { + Log.v(TAG, "Load in background " + mQuery); + } + + if (!PermissionsUtil.hasContactsPermissions(mContext)) { + return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + } + + /** Loads results from the database helper. */ + final DialerDatabaseHelper dialerDatabaseHelper = + Database.get(mContext).getDatabaseHelper(mContext); + final ArrayList allMatches = + dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher); + + if (DEBUG) { + Log.v(TAG, "Loaded matches " + String.valueOf(allMatches.size())); + } + + /** Constructs a cursor for the returned array of results. */ + final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; + for (ContactNumber contact : allMatches) { + row[PhoneQuery.PHONE_ID] = contact.dataId; + row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; + row[PhoneQuery.CONTACT_ID] = contact.id; + row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; + row[PhoneQuery.PHOTO_ID] = contact.photoId; + row[PhoneQuery.DISPLAY_NAME] = contact.displayName; + row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; + cursor.addRow(row); + } + return cursor; + } + + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + /** The Loader has been reset; ignore the result and invalidate the data. */ + releaseResources(cursor); + return; + } + + /** Hold a reference to the old data so it doesn't get garbage collected. */ + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (mObserver == null) { + mObserver = new ForceLoadContentObserver(); + mContext + .getContentResolver() + .registerContentObserver(DialerDatabaseHelper.SMART_DIAL_UPDATED_URI, true, mObserver); + } + + if (isStarted()) { + /** If the Loader is in a started state, deliver the results to the client. */ + super.deliverResult(cursor); + } + + /** Invalidate the old data as we don't need it any more. */ + if (oldCursor != null && oldCursor != cursor) { + releaseResources(oldCursor); + } + } + + @Override + protected void onStartLoading() { + if (mCursor != null) { + /** Deliver any previously loaded data immediately. */ + deliverResult(mCursor); + } + if (mCursor == null) { + /** Force loads every time as our results change with queries. */ + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ + cancelLoad(); + } + + @Override + protected void onReset() { + /** Ensure the loader has been stopped. */ + onStopLoading(); + + if (mObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + + /** Release all previously saved query results. */ + if (mCursor != null) { + releaseResources(mCursor); + mCursor = null; + } + } + + @Override + public void onCanceled(Cursor cursor) { + super.onCanceled(cursor); + + if (mObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + + /** The load has been canceled, so we should release the resources associated with 'data'. */ + releaseResources(cursor); + } + + private void releaseResources(Cursor cursor) { + if (cursor != null) { + cursor.close(); + } + } + + public void setShowEmptyListForNullQuery(boolean show) { + mShowEmptyListForNullQuery = show; + if (mNameMatcher != null) { + mNameMatcher.setShouldMatchEmptyQuery(!show); + } + } +} diff --git a/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java b/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java new file mode 100644 index 0000000000000000000000000000000000000000..051daf46e189cf13b648c909fc5786edb4f22b6c --- /dev/null +++ b/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.dialpad; + +import android.telephony.PhoneNumberUtils; +import android.text.Spanned; +import android.text.method.DialerKeyListener; + +/** + * {@link DialerKeyListener} with Unicode support. Converts any Unicode(e.g. Arabic) characters that + * represent digits into digits before filtering the results so that we can support pasted digits + * from Unicode languages. + */ +public class UnicodeDialerKeyListener extends DialerKeyListener { + + public static final UnicodeDialerKeyListener INSTANCE = new UnicodeDialerKeyListener(); + + @Override + public CharSequence filter( + CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + final String converted = + PhoneNumberUtils.convertKeypadLettersToDigits( + PhoneNumberUtils.replaceUnicodeDigits(source.toString())); + // PhoneNumberUtils.replaceUnicodeDigits performs a character for character replacement, + // so we can assume that start and end positions should remain unchanged. + CharSequence result = super.filter(converted, start, end, dest, dstart, dend); + if (result == null) { + if (source.equals(converted)) { + // There was no conversion or filtering performed. Just return null according to + // the behavior of DialerKeyListener. + return null; + } else { + // filter returns null if the charsequence is to be returned unchanged/unfiltered. + // But in this case we do want to return a modified character string (even if + // none of the characters in the modified string are filtered). So if + // result == null we return the unfiltered but converted numeric string instead. + return converted.subSequence(start, end); + } + } + return result; + } +} diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..b9381331c0a6dc6a016fb987e4cfb5d6b870ffe2 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.filterednumber; + +import android.app.FragmentManager; +import android.content.Context; +import android.database.Cursor; +import android.telephony.PhoneNumberUtils; +import android.view.View; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.R; +import com.android.dialer.blocking.BlockNumberDialogFragment; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.InteractionEvent; +import com.android.dialer.phonenumbercache.ContactInfoHelper; + +public class BlockedNumbersAdapter extends NumbersAdapter { + + private BlockedNumbersAdapter( + Context context, + FragmentManager fragmentManager, + ContactInfoHelper contactInfoHelper, + ContactPhotoManager contactPhotoManager) { + super(context, fragmentManager, contactInfoHelper, contactPhotoManager); + } + + public static BlockedNumbersAdapter newBlockedNumbersAdapter( + Context context, FragmentManager fragmentManager) { + return new BlockedNumbersAdapter( + context, + fragmentManager, + new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)), + ContactPhotoManager.getInstance(context)); + } + + @Override + public void bindView(View view, final Context context, Cursor cursor) { + super.bindView(view, context, cursor); + final Integer id = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); + final String countryIso = + cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.COUNTRY_ISO)); + final String number = cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER)); + final String normalizedNumber = + cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NORMALIZED_NUMBER)); + + final View deleteButton = view.findViewById(R.id.delete_button); + deleteButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + BlockNumberDialogFragment.show( + id, + number, + countryIso, + PhoneNumberUtils.formatNumber(number, countryIso), + R.id.blocked_numbers_activity_container, + getFragmentManager(), + new BlockNumberDialogFragment.Callback() { + @Override + public void onFilterNumberSuccess() {} + + @Override + public void onUnfilterNumberSuccess() { + Logger.get(context) + .logInteraction(InteractionEvent.Type.UNBLOCK_NUMBER_MANAGEMENT_SCREEN); + } + + @Override + public void onChangeFilteredNumberUndo() {} + }); + } + }); + + updateView(view, number, countryIso); + } + + @Override + public boolean isEmpty() { + // Always return false, so that the header with blocking-related options always shows. + return false; + } +} diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..f53a45840bf2d47b40c60e0ca926dad77f5f8cbf --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.filterednumber; + +import android.app.ListFragment; +import android.app.LoaderManager; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.contacts.common.lettertiles.LetterTileDrawable; +import com.android.dialer.app.R; +import com.android.dialer.blocking.BlockedNumbersMigrator; +import com.android.dialer.blocking.BlockedNumbersMigrator.Listener; +import com.android.dialer.blocking.FilteredNumberCompat; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.blocking.FilteredNumbersUtil.CheckForSendToVoicemailContactListener; +import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener; +import com.android.dialer.database.FilteredNumberContract; +import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker; + +public class BlockedNumbersFragment extends ListFragment + implements LoaderManager.LoaderCallbacks, + View.OnClickListener, + VisualVoicemailEnabledChecker.Callback { + + private static final char ADD_BLOCKED_NUMBER_ICON_LETTER = '+'; + protected View migratePromoView; + private BlockedNumbersMigrator blockedNumbersMigratorForTest; + private TextView blockedNumbersText; + private TextView footerText; + private BlockedNumbersAdapter mAdapter; + private VisualVoicemailEnabledChecker mVoicemailEnabledChecker; + private View mImportSettings; + private View mBlockedNumbersDisabledForEmergency; + private View mBlockedNumberListDivider; + + @Override + public Context getContext() { + return getActivity(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + LayoutInflater inflater = + (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + getListView().addHeaderView(inflater.inflate(R.layout.blocked_number_header, null)); + getListView().addFooterView(inflater.inflate(R.layout.blocked_number_footer, null)); + //replace the icon for add number with LetterTileDrawable(), so it will have identical style + ImageView addNumberIcon = (ImageView) getActivity().findViewById(R.id.add_number_icon); + LetterTileDrawable drawable = new LetterTileDrawable(getResources()); + drawable.setLetter(ADD_BLOCKED_NUMBER_ICON_LETTER); + drawable.setColor( + ActivityCompat.getColor(getActivity(), R.color.add_blocked_number_icon_color)); + drawable.setIsCircular(true); + addNumberIcon.setImageDrawable(drawable); + + if (mAdapter == null) { + mAdapter = + BlockedNumbersAdapter.newBlockedNumbersAdapter( + getContext(), getActivity().getFragmentManager()); + } + setListAdapter(mAdapter); + + blockedNumbersText = (TextView) getListView().findViewById(R.id.blocked_number_text_view); + migratePromoView = getListView().findViewById(R.id.migrate_promo); + getListView().findViewById(R.id.migrate_promo_allow_button).setOnClickListener(this); + mImportSettings = getListView().findViewById(R.id.import_settings); + mBlockedNumbersDisabledForEmergency = + getListView().findViewById(R.id.blocked_numbers_disabled_for_emergency); + mBlockedNumberListDivider = getActivity().findViewById(R.id.blocked_number_list_divider); + getListView().findViewById(R.id.import_button).setOnClickListener(this); + getListView().findViewById(R.id.view_numbers_button).setOnClickListener(this); + getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(this); + + footerText = (TextView) getActivity().findViewById(R.id.blocked_number_footer_textview); + mVoicemailEnabledChecker = new VisualVoicemailEnabledChecker(getContext(), this); + mVoicemailEnabledChecker.asyncUpdate(); + updateActiveVoicemailProvider(); + } + + @Override + public void onDestroy() { + setListAdapter(null); + super.onDestroy(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getLoaderManager().initLoader(0, null, this); + } + + @Override + public void onResume() { + super.onResume(); + + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + ColorDrawable backgroundDrawable = + new ColorDrawable(ActivityCompat.getColor(getActivity(), R.color.dialer_theme_color)); + actionBar.setBackgroundDrawable(backgroundDrawable); + actionBar.setDisplayShowCustomEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setTitle(R.string.manage_blocked_numbers_label); + + // If the device can use the framework blocking solution, users should not be able to add + // new blocked numbers from the Blocked Management UI. They will be shown a promo card + // asking them to migrate to new blocking instead. + if (FilteredNumberCompat.canUseNewFiltering()) { + migratePromoView.setVisibility(View.VISIBLE); + blockedNumbersText.setVisibility(View.GONE); + getListView().findViewById(R.id.add_number_linear_layout).setVisibility(View.GONE); + getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(null); + mBlockedNumberListDivider.setVisibility(View.GONE); + mImportSettings.setVisibility(View.GONE); + getListView().findViewById(R.id.import_button).setOnClickListener(null); + getListView().findViewById(R.id.view_numbers_button).setOnClickListener(null); + mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE); + footerText.setVisibility(View.GONE); + } else { + FilteredNumbersUtil.checkForSendToVoicemailContact( + getActivity(), + new CheckForSendToVoicemailContactListener() { + @Override + public void onComplete(boolean hasSendToVoicemailContact) { + final int visibility = hasSendToVoicemailContact ? View.VISIBLE : View.GONE; + mImportSettings.setVisibility(visibility); + } + }); + } + + // All views except migrate and the block list are hidden when new filtering is available + if (!FilteredNumberCompat.canUseNewFiltering() + && FilteredNumbersUtil.hasRecentEmergencyCall(getContext())) { + mBlockedNumbersDisabledForEmergency.setVisibility(View.VISIBLE); + } else { + mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE); + } + + mVoicemailEnabledChecker.asyncUpdate(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.blocked_number_fragment, container, false); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + final String[] projection = { + FilteredNumberContract.FilteredNumberColumns._ID, + FilteredNumberContract.FilteredNumberColumns.COUNTRY_ISO, + FilteredNumberContract.FilteredNumberColumns.NUMBER, + FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER + }; + final String selection = + FilteredNumberContract.FilteredNumberColumns.TYPE + + "=" + + FilteredNumberContract.FilteredNumberTypes.BLOCKED_NUMBER; + return new CursorLoader( + getContext(), + FilteredNumberContract.FilteredNumber.CONTENT_URI, + projection, + selection, + null, + null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mAdapter.swapCursor(data); + if (FilteredNumberCompat.canUseNewFiltering() || data.getCount() == 0) { + mBlockedNumberListDivider.setVisibility(View.INVISIBLE); + } else { + mBlockedNumberListDivider.setVisibility(View.VISIBLE); + } + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.swapCursor(null); + } + + @Override + public void onClick(final View view) { + final BlockedNumbersSettingsActivity activity = (BlockedNumbersSettingsActivity) getActivity(); + if (activity == null) { + return; + } + + int resId = view.getId(); + if (resId == R.id.add_number_linear_layout) { + activity.showSearchUi(); + } else if (resId == R.id.view_numbers_button) { + activity.showNumbersToImportPreviewUi(); + } else if (resId == R.id.import_button) { + FilteredNumbersUtil.importSendToVoicemailContacts( + activity, + new ImportSendToVoicemailContactsListener() { + @Override + public void onImportComplete() { + mImportSettings.setVisibility(View.GONE); + } + }); + } else if (resId == R.id.migrate_promo_allow_button) { + view.setEnabled(false); + (blockedNumbersMigratorForTest != null + ? blockedNumbersMigratorForTest + : new BlockedNumbersMigrator(getContext())) + .migrate( + new Listener() { + @Override + public void onComplete() { + getContext() + .startActivity( + FilteredNumberCompat.createManageBlockedNumbersIntent(getContext())); + // Remove this activity from the backstack + activity.finish(); + } + }); + } + } + + @Override + public void onVisualVoicemailEnabledStatusChanged(boolean newStatus) { + updateActiveVoicemailProvider(); + } + + private void updateActiveVoicemailProvider() { + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + if (mVoicemailEnabledChecker.isVisualVoicemailEnabled()) { + footerText.setText(R.string.block_number_footer_message_vvm); + } else { + footerText.setText(R.string.block_number_footer_message_no_vvm); + } + } + + void setBlockedNumbersMigratorForTest(BlockedNumbersMigrator blockedNumbersMigrator) { + blockedNumbersMigratorForTest = blockedNumbersMigrator; + } +} diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..eef92071044cc4fbce0739ef79565b3e1c321a33 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.filterednumber; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.view.MenuItem; +import com.android.dialer.app.R; +import com.android.dialer.app.list.BlockedListSearchFragment; +import com.android.dialer.app.list.SearchFragment; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.ScreenEvent; + +public class BlockedNumbersSettingsActivity extends AppCompatActivity + implements SearchFragment.HostInterface { + + private static final String TAG_BLOCKED_MANAGEMENT_FRAGMENT = "blocked_management"; + private static final String TAG_BLOCKED_SEARCH_FRAGMENT = "blocked_search"; + private static final String TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT = "view_numbers_to_import"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.blocked_numbers_activity); + + // If savedInstanceState != null, the Activity will automatically restore the last fragment. + if (savedInstanceState == null) { + showManagementUi(); + } + } + + /** Shows fragment with the list of currently blocked numbers and settings related to blocking. */ + public void showManagementUi() { + BlockedNumbersFragment fragment = + (BlockedNumbersFragment) + getFragmentManager().findFragmentByTag(TAG_BLOCKED_MANAGEMENT_FRAGMENT); + if (fragment == null) { + fragment = new BlockedNumbersFragment(); + } + + getFragmentManager() + .beginTransaction() + .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_MANAGEMENT_FRAGMENT) + .commit(); + + Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_MANAGEMENT, this); + } + + /** Shows fragment with search UI for browsing/finding numbers to block. */ + public void showSearchUi() { + BlockedListSearchFragment fragment = + (BlockedListSearchFragment) + getFragmentManager().findFragmentByTag(TAG_BLOCKED_SEARCH_FRAGMENT); + if (fragment == null) { + fragment = new BlockedListSearchFragment(); + fragment.setHasOptionsMenu(false); + fragment.setShowEmptyListForNullQuery(true); + fragment.setDirectorySearchEnabled(false); + } + + getFragmentManager() + .beginTransaction() + .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_SEARCH_FRAGMENT) + .addToBackStack(null) + .commit(); + + Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_ADD_NUMBER, this); + } + + /** + * Shows fragment with UI to preview the numbers of contacts currently marked as send-to-voicemail + * in Contacts. These numbers can be imported into Dialer's blocked number list. + */ + public void showNumbersToImportPreviewUi() { + ViewNumbersToImportFragment fragment = + (ViewNumbersToImportFragment) + getFragmentManager().findFragmentByTag(TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT); + if (fragment == null) { + fragment = new ViewNumbersToImportFragment(); + } + + getFragmentManager() + .beginTransaction() + .replace( + R.id.blocked_numbers_activity_container, fragment, TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT) + .addToBackStack(null) + .commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return false; + } + + @Override + public void onBackPressed() { + // TODO: Achieve back navigation without overriding onBackPressed. + if (getFragmentManager().getBackStackEntryCount() > 0) { + getFragmentManager().popBackStack(); + } else { + super.onBackPressed(); + } + } + + @Override + public boolean isActionBarShowing() { + return false; + } + + @Override + public boolean isDialpadShown() { + return false; + } + + @Override + public int getDialpadHeight() { + return 0; + } + + @Override + public int getActionBarHideOffset() { + return 0; + } + + @Override + public int getActionBarHeight() { + return 0; + } +} diff --git a/java/com/android/dialer/app/filterednumber/NumbersAdapter.java b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..f71517a44dc3a8191bbba1f8f7e4b7c3d56ab4a3 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.filterednumber; + +import android.app.FragmentManager; +import android.content.Context; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.View; +import android.widget.QuickContactBadge; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.R; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; + +public class NumbersAdapter extends SimpleCursorAdapter { + + private Context mContext; + private FragmentManager mFragmentManager; + private ContactInfoHelper mContactInfoHelper; + private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); + private ContactPhotoManager mContactPhotoManager; + + public NumbersAdapter( + Context context, + FragmentManager fragmentManager, + ContactInfoHelper contactInfoHelper, + ContactPhotoManager contactPhotoManager) { + super(context, R.layout.blocked_number_item, null, new String[] {}, new int[] {}, 0); + mContext = context; + mFragmentManager = fragmentManager; + mContactInfoHelper = contactInfoHelper; + mContactPhotoManager = contactPhotoManager; + } + + public void updateView(View view, String number, String countryIso) { + final TextView callerName = (TextView) view.findViewById(R.id.caller_name); + final TextView callerNumber = (TextView) view.findViewById(R.id.caller_number); + final QuickContactBadge quickContactBadge = + (QuickContactBadge) view.findViewById(R.id.quick_contact_photo); + quickContactBadge.setOverlay(null); + if (CompatUtils.hasPrioritizedMimeType()) { + quickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + + ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); + if (info == null) { + info = new ContactInfo(); + info.number = number; + } + final CharSequence locationOrType = getNumberTypeOrLocation(info); + final String displayNumber = getDisplayNumber(info); + final String displayNumberStr = + mBidiFormatter.unicodeWrap(displayNumber, TextDirectionHeuristics.LTR); + + String nameForDefaultImage; + if (!TextUtils.isEmpty(info.name)) { + nameForDefaultImage = info.name; + callerName.setText(info.name); + callerNumber.setText(locationOrType + " " + displayNumberStr); + } else { + nameForDefaultImage = displayNumber; + callerName.setText(displayNumberStr); + if (!TextUtils.isEmpty(locationOrType)) { + callerNumber.setText(locationOrType); + callerNumber.setVisibility(View.VISIBLE); + } else { + callerNumber.setVisibility(View.GONE); + } + } + loadContactPhoto(info, nameForDefaultImage, quickContactBadge); + } + + private void loadContactPhoto(ContactInfo info, String displayName, QuickContactBadge badge) { + final String lookupKey = + info.lookupUri == null ? null : UriUtils.getLookupKeyFromUri(info.lookupUri); + final int contactType = + mContactInfoHelper.isBusiness(info.sourceType) + ? ContactPhotoManager.TYPE_BUSINESS + : ContactPhotoManager.TYPE_DEFAULT; + final DefaultImageRequest request = + new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); + badge.assignContactUri(info.lookupUri); + badge.setContentDescription( + mContext.getResources().getString(R.string.description_contact_details, displayName)); + mContactPhotoManager.loadDirectoryPhoto( + badge, info.photoUri, false /* darkTheme */, true /* isCircular */, request); + } + + private String getDisplayNumber(ContactInfo info) { + if (!TextUtils.isEmpty(info.formattedNumber)) { + return info.formattedNumber; + } else if (!TextUtils.isEmpty(info.number)) { + return info.number; + } else { + return ""; + } + } + + private CharSequence getNumberTypeOrLocation(ContactInfo info) { + if (!TextUtils.isEmpty(info.name)) { + return ContactsContract.CommonDataKinds.Phone.getTypeLabel( + mContext.getResources(), info.type, info.label); + } else { + return PhoneNumberHelper.getGeoDescription(mContext, info.number); + } + } + + protected Context getContext() { + return mContext; + } + + protected FragmentManager getFragmentManager() { + return mFragmentManager; + } +} diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..5228a1d7901a830440d623c43f7f2fcad9725163 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.filterednumber; + +import android.app.FragmentManager; +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.R; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.phonenumbercache.ContactInfoHelper; + +public class ViewNumbersToImportAdapter extends NumbersAdapter { + + private ViewNumbersToImportAdapter( + Context context, + FragmentManager fragmentManager, + ContactInfoHelper contactInfoHelper, + ContactPhotoManager contactPhotoManager) { + super(context, fragmentManager, contactInfoHelper, contactPhotoManager); + } + + public static ViewNumbersToImportAdapter newViewNumbersToImportAdapter( + Context context, FragmentManager fragmentManager) { + return new ViewNumbersToImportAdapter( + context, + fragmentManager, + new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)), + ContactPhotoManager.getInstance(context)); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + super.bindView(view, context, cursor); + + final String number = cursor.getString(FilteredNumbersUtil.PhoneQuery.NUMBER_COLUMN_INDEX); + + view.findViewById(R.id.delete_button).setVisibility(View.GONE); + updateView(view, number, null /* countryIso */); + } +} diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..d45f61ed7c68604080a7a35e4232f15f9d4971f1 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.filterednumber; + +import android.app.ListFragment; +import android.app.LoaderManager; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener; + +public class ViewNumbersToImportFragment extends ListFragment + implements LoaderManager.LoaderCallbacks, View.OnClickListener { + + private ViewNumbersToImportAdapter mAdapter; + + @Override + public Context getContext() { + return getActivity(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (mAdapter == null) { + mAdapter = + ViewNumbersToImportAdapter.newViewNumbersToImportAdapter( + getContext(), getActivity().getFragmentManager()); + } + setListAdapter(mAdapter); + } + + @Override + public void onDestroy() { + setListAdapter(null); + super.onDestroy(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getLoaderManager().initLoader(0, null, this); + } + + @Override + public void onResume() { + super.onResume(); + + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + actionBar.setTitle(R.string.import_send_to_voicemail_numbers_label); + actionBar.setDisplayShowCustomEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + + getActivity().findViewById(R.id.cancel_button).setOnClickListener(this); + getActivity().findViewById(R.id.import_button).setOnClickListener(this); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.view_numbers_to_import_fragment, container, false); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + final CursorLoader cursorLoader = + new CursorLoader( + getContext(), + Phone.CONTENT_URI, + FilteredNumbersUtil.PhoneQuery.PROJECTION, + FilteredNumbersUtil.PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE, + null, + null); + return cursorLoader; + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.swapCursor(null); + } + + @Override + public void onClick(final View view) { + if (view.getId() == R.id.import_button) { + FilteredNumbersUtil.importSendToVoicemailContacts( + getContext(), + new ImportSendToVoicemailContactsListener() { + @Override + public void onImportComplete() { + if (getActivity() != null) { + getActivity().onBackPressed(); + } + } + }); + } else if (view.getId() == R.id.cancel_button) { + getActivity().onBackPressed(); + } + } +} diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java new file mode 100644 index 0000000000000000000000000000000000000000..2125a1524f7c1ab692716ffffc6c999837f896a9 --- /dev/null +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.legacybindings; + +import android.app.Activity; +import android.view.ViewGroup; +import com.android.dialer.app.calllog.CallLogAdapter; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.list.RegularSearchFragment; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; + +/** + * These are old bindings between Dialer and the container application. All new bindings should be + * added to the bindings module and not here. + */ +public interface DialerLegacyBindings { + + /** + * activityType must be one of following constants: CallLogAdapter.ACTIVITY_TYPE_CALL_LOG, or + * CallLogAdapter.ACTIVITY_TYPE_DIALTACTS. + */ + CallLogAdapter newCallLogAdapter( + Activity activity, + ViewGroup alertContainer, + CallLogAdapter.CallFetcher callFetcher, + CallLogCache callLogCache, + ContactInfoCache contactInfoCache, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + int activityType); + + RegularSearchFragment newRegularSearchFragment(); +} diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..70d379c9fbe98a9f4572887632e13e53e0dbbe57 --- /dev/null +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.legacybindings; + +/** + * This interface should be implementated by the Application subclass. It allows the dialer module + * to get references to the DialerLegacyBindings. + */ +public interface DialerLegacyBindingsFactory { + + DialerLegacyBindings newDialerLegacyBindings(); +} diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java new file mode 100644 index 0000000000000000000000000000000000000000..f01df78f8bcd30352109a8cc7de04efcb2e76198 --- /dev/null +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.legacybindings; + +import android.app.Activity; +import android.view.ViewGroup; +import com.android.dialer.app.calllog.CallLogAdapter; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.list.RegularSearchFragment; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; + +/** Default implementation for dialer legacy bindings. */ +public class DialerLegacyBindingsStub implements DialerLegacyBindings { + + @Override + public CallLogAdapter newCallLogAdapter( + Activity activity, + ViewGroup alertContainer, + CallLogAdapter.CallFetcher callFetcher, + CallLogCache callLogCache, + ContactInfoCache contactInfoCache, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + int activityType) { + return new CallLogAdapter( + activity, + alertContainer, + callFetcher, + callLogCache, + contactInfoCache, + voicemailPlaybackPresenter, + activityType); + } + + @Override + public RegularSearchFragment newRegularSearchFragment() { + return new RegularSearchFragment(); + } +} diff --git a/java/com/android/dialer/app/list/AllContactsFragment.java b/java/com/android/dialer/app/list/AllContactsFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..093e8f38423884e027d4241467f7c66e75801b4e --- /dev/null +++ b/java/com/android/dialer/app/list/AllContactsFragment.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.list; + +import static android.Manifest.permission.READ_CONTACTS; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.Loader; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.QuickContact; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.ContactEntryListFragment; +import com.android.contacts.common.list.ContactListFilter; +import com.android.contacts.common.list.DefaultContactListAdapter; +import com.android.contacts.common.util.FabUtil; +import com.android.dialer.app.R; +import com.android.dialer.app.list.ListsFragment.ListsPage; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PermissionsUtil; + +/** Fragments to show all contacts with phone numbers. */ +public class AllContactsFragment extends ContactEntryListFragment + implements ListsPage, + OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; + + private EmptyContentView mEmptyListView; + + /** + * Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS + * permission is granted via the UI in another fragment. + */ + private BroadcastReceiver mReadContactsPermissionGrantedReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reloadData(); + } + }; + + public AllContactsFragment() { + setQuickContactEnabled(false); + setAdjustSelectionBoundsEnabled(true); + setPhotoLoaderEnabled(true); + setSectionHeaderDisplayEnabled(true); + setDarkTheme(false); + setVisibleScrollbarEnabled(true); + } + + @Override + public void onViewCreated(View view, android.os.Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); + mEmptyListView.setImage(R.drawable.empty_contacts); + mEmptyListView.setDescription(R.string.all_contacts_empty); + mEmptyListView.setActionClickedListener(this); + getListView().setEmptyView(mEmptyListView); + mEmptyListView.setVisibility(View.GONE); + + FabUtil.addBottomPaddingToListViewForFab(getListView(), getResources()); + } + + @Override + public void onStart() { + super.onStart(); + PermissionsUtil.registerPermissionReceiver( + getActivity(), mReadContactsPermissionGrantedReceiver, READ_CONTACTS); + } + + @Override + public void onStop() { + PermissionsUtil.unregisterPermissionReceiver( + getActivity(), mReadContactsPermissionGrantedReceiver); + super.onStop(); + } + + @Override + protected void startLoading() { + if (PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) { + super.startLoading(); + mEmptyListView.setDescription(R.string.all_contacts_empty); + mEmptyListView.setActionLabel(R.string.all_contacts_empty_add_contact_action); + } else { + mEmptyListView.setDescription(R.string.permission_no_contacts); + mEmptyListView.setActionLabel(R.string.permission_single_turn_on); + mEmptyListView.setVisibility(View.VISIBLE); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + super.onLoadFinished(loader, data); + + if (data == null || data.getCount() == 0) { + mEmptyListView.setVisibility(View.VISIBLE); + } + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + final DefaultContactListAdapter adapter = + new DefaultContactListAdapter(getActivity()) { + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + itemView.setTag(this.getContactUri(partition, cursor)); + } + }; + adapter.setDisplayPhotos(true); + adapter.setFilter( + ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_DEFAULT)); + adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled()); + return adapter; + } + + @Override + protected View inflateView(LayoutInflater inflater, ViewGroup container) { + return inflater.inflate(R.layout.all_contacts_fragment, null); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Uri uri = (Uri) view.getTag(); + if (uri != null) { + if (CompatUtils.hasPrioritizedMimeType()) { + QuickContact.showQuickContact(getContext(), view, uri, null, Phone.CONTENT_ITEM_TYPE); + } else { + QuickContact.showQuickContact(getActivity(), view, uri, QuickContact.MODE_LARGE, null); + } + } + } + + @Override + protected void onItemClick(int position, long id) { + // Do nothing. Implemented to satisfy ContactEntryListFragment. + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) { + FragmentCompat.requestPermissions( + this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE); + } else { + // Add new contact + DialerUtils.startActivityWithErrorToast( + activity, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) { + if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Force a refresh of the data since we were missing the permission before this. + reloadData(); + } + } + } + + @Override + public void onPageResume(@Nullable Activity activity) { + LogUtil.i("AllContactsFragment.onPageResume", null); + } + + @Override + public void onPagePause(@Nullable Activity activity) { + LogUtil.i("AllContactsFragment.onPagePause", null); + } +} diff --git a/java/com/android/dialer/app/list/BlockedListSearchAdapter.java b/java/com/android/dialer/app/list/BlockedListSearchAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..a90ce7a0df114985f972fc9b9c628ac86e0aa719 --- /dev/null +++ b/java/com/android/dialer/app/list/BlockedListSearchAdapter.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.view.View; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.list.ContactListItemView; +import com.android.dialer.app.R; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; + +/** List adapter to display search results for adding a blocked number. */ +public class BlockedListSearchAdapter extends RegularSearchListAdapter { + + private Resources mResources; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + + public BlockedListSearchAdapter(Context context) { + super(context); + mResources = context.getResources(); + disableAllShortcuts(); + setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, true); + + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(context); + } + + @Override + protected boolean isChanged(boolean showNumberShortcuts) { + return setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, showNumberShortcuts || mIsQuerySipAddress); + } + + public void setViewBlocked(ContactListItemView view, Integer id) { + view.setTag(R.id.block_id, id); + final int textColor = mResources.getColor(R.color.blocked_number_block_color); + view.getDataView().setTextColor(textColor); + view.getLabelView().setTextColor(textColor); + //TODO: Add icon + } + + public void setViewUnblocked(ContactListItemView view) { + view.setTag(R.id.block_id, null); + final int textColor = mResources.getColor(R.color.dialer_secondary_text_color); + view.getDataView().setTextColor(textColor); + view.getLabelView().setTextColor(textColor); + //TODO: Remove icon + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + + final ContactListItemView view = (ContactListItemView) itemView; + // Reset view state to unblocked. + setViewUnblocked(view); + + final String number = getPhoneNumber(position); + final String countryIso = GeoUtil.getCurrentCountryIso(mContext); + final FilteredNumberAsyncQueryHandler.OnCheckBlockedListener onCheckListener = + new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() { + @Override + public void onCheckComplete(Integer id) { + if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) { + setViewBlocked(view, id); + } + } + }; + mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso); + } +} diff --git a/java/com/android/dialer/app/list/BlockedListSearchFragment.java b/java/com/android/dialer/app/list/BlockedListSearchFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..2129981c037026b92de4636dd2518744b3f65691 --- /dev/null +++ b/java/com/android/dialer/app/list/BlockedListSearchFragment.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.telephony.PhoneNumberUtils; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.Toast; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.R; +import com.android.dialer.app.widget.SearchEditTextLayout; +import com.android.dialer.blocking.BlockNumberDialogFragment; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.InteractionEvent; + +public class BlockedListSearchFragment extends RegularSearchFragment + implements BlockNumberDialogFragment.Callback { + + private static final String TAG = BlockedListSearchFragment.class.getSimpleName(); + + private final TextWatcher mPhoneSearchQueryTextListener = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + setQueryString(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) {} + }; + private final SearchEditTextLayout.Callback mSearchLayoutCallback = + new SearchEditTextLayout.Callback() { + @Override + public void onBackButtonClicked() { + getActivity().onBackPressed(); + } + + @Override + public void onSearchViewClicked() {} + }; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + private EditText mSearchView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setShowEmptyListForNullQuery(true); + /* + * Pass in the empty string here so ContactEntryListFragment#setQueryString interprets it as + * an empty search query, rather than as an uninitalized value. In the latter case, the + * adapter returned by #createListAdapter is used, which populates the view with contacts. + * Passing in the empty string forces ContactEntryListFragment to interpret it as an empty + * query, which results in showing an empty view + */ + setQueryString(getQueryString() == null ? "" : getQueryString()); + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(getContext()); + } + + @Override + public void onResume() { + super.onResume(); + + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + actionBar.setCustomView(R.layout.search_edittext); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setDisplayShowHomeEnabled(false); + + final SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container); + searchEditTextLayout.expand(false, true); + searchEditTextLayout.setCallback(mSearchLayoutCallback); + searchEditTextLayout.setBackgroundDrawable(null); + + mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); + mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); + mSearchView.setHint(R.string.block_number_search_hint); + + searchEditTextLayout + .findViewById(R.id.search_box_expanded) + .setBackgroundColor(getContext().getResources().getColor(android.R.color.white)); + + if (!TextUtils.isEmpty(getQueryString())) { + mSearchView.setText(getQueryString()); + } + + // TODO: Don't set custom text size; use default search text size. + mSearchView.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.blocked_number_search_text_size)); + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + BlockedListSearchAdapter adapter = new BlockedListSearchAdapter(getActivity()); + adapter.setDisplayPhotos(true); + // Don't show SIP addresses. + adapter.setUseCallableUri(false); + // Keep in sync with the queryString set in #onCreate + adapter.setQueryString(getQueryString() == null ? "" : getQueryString()); + return adapter; + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + super.onItemClick(parent, view, position, id); + final int adapterPosition = position - getListView().getHeaderViewsCount(); + final BlockedListSearchAdapter adapter = (BlockedListSearchAdapter) getAdapter(); + final int shortcutType = adapter.getShortcutTypeFromPosition(adapterPosition); + final Integer blockId = (Integer) view.getTag(R.id.block_id); + final String number; + switch (shortcutType) { + case DialerPhoneNumberListAdapter.SHORTCUT_INVALID: + // Handles click on a search result, either contact or nearby places result. + number = adapter.getPhoneNumber(adapterPosition); + blockContactNumber(number, blockId); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_BLOCK_NUMBER: + // Handles click on 'Block number' shortcut to add the user query as a number. + number = adapter.getQueryString(); + blockNumber(number); + break; + default: + Log.w(TAG, "Ignoring unsupported shortcut type: " + shortcutType); + break; + } + } + + @Override + protected void onItemClick(int position, long id) { + // Prevent SearchFragment.onItemClicked from being called. + } + + private void blockNumber(final String number) { + final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); + final OnCheckBlockedListener onCheckListener = + new OnCheckBlockedListener() { + @Override + public void onCheckComplete(Integer id) { + if (id == null) { + BlockNumberDialogFragment.show( + id, + number, + countryIso, + PhoneNumberUtils.formatNumber(number, countryIso), + R.id.blocked_numbers_activity_container, + getFragmentManager(), + BlockedListSearchFragment.this); + } else if (id == FilteredNumberAsyncQueryHandler.INVALID_ID) { + Toast.makeText( + getContext(), + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.invalidNumber, number), + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText( + getContext(), + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.alreadyBlocked, number), + Toast.LENGTH_SHORT) + .show(); + } + } + }; + mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso); + } + + @Override + public void onFilterNumberSuccess() { + Logger.get(getContext()).logInteraction(InteractionEvent.Type.BLOCK_NUMBER_MANAGEMENT_SCREEN); + goBack(); + } + + @Override + public void onUnfilterNumberSuccess() { + Log.wtf(TAG, "Unblocked a number from the BlockedListSearchFragment"); + goBack(); + } + + private void goBack() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + activity.onBackPressed(); + } + + @Override + public void onChangeFilteredNumberUndo() { + getAdapter().notifyDataSetChanged(); + } + + private void blockContactNumber(final String number, final Integer blockId) { + if (blockId != null) { + Toast.makeText( + getContext(), + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.alreadyBlocked, number), + Toast.LENGTH_SHORT) + .show(); + return; + } + + BlockNumberDialogFragment.show( + blockId, + number, + GeoUtil.getCurrentCountryIso(getContext()), + number, + R.id.blocked_numbers_activity_container, + getFragmentManager(), + this); + } +} diff --git a/java/com/android/dialer/app/list/ContentChangedFilter.java b/java/com/android/dialer/app/list/ContentChangedFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..663846da555c5224268b534993c37fbffbb8c78e --- /dev/null +++ b/java/com/android/dialer/app/list/ContentChangedFilter.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.list; + +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * AccessibilityDelegate that will filter out TYPE_WINDOW_CONTENT_CHANGED Used to suppress "Showing + * items x of y" from firing of ListView whenever it's content changes. AccessibilityEvent can only + * be rejected at a view's parent once it is generated, use addToParent() to add this delegate to + * the parent. + */ +public class ContentChangedFilter extends AccessibilityDelegate { + + //the view we don't want TYPE_WINDOW_CONTENT_CHANGED to fire. + private View mView; + + private ContentChangedFilter(View view) { + super(); + mView = view; + } + + /** Add this delegate to the parent of @param view to filter out TYPE_WINDOW_CONTENT_CHANGED */ + public static void addToParent(View view) { + View parent = (View) view.getParent(); + parent.setAccessibilityDelegate(new ContentChangedFilter(view)); + } + + @Override + public boolean onRequestSendAccessibilityEvent( + ViewGroup host, View child, AccessibilityEvent event) { + if (child == mView) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + return false; + } + } + return super.onRequestSendAccessibilityEvent(host, child, event); + } +} diff --git a/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java b/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..7e2525f246cf1fe77abcda7c5c584b0cdc67675b --- /dev/null +++ b/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.list; + +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.telephony.PhoneNumberUtils; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.list.ContactListItemView; +import com.android.contacts.common.list.PhoneNumberListAdapter; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.R; +import com.android.dialer.util.CallUtil; + +/** + * {@link PhoneNumberListAdapter} with the following added shortcuts, that are displayed as list + * items: 1) Directly calling the phone number query 2) Adding the phone number query to a contact + * + *

These shortcuts can be enabled or disabled to toggle whether or not they show up in the list. + */ +public class DialerPhoneNumberListAdapter extends PhoneNumberListAdapter { + + public static final int SHORTCUT_INVALID = -1; + public static final int SHORTCUT_DIRECT_CALL = 0; + public static final int SHORTCUT_CREATE_NEW_CONTACT = 1; + public static final int SHORTCUT_ADD_TO_EXISTING_CONTACT = 2; + public static final int SHORTCUT_SEND_SMS_MESSAGE = 3; + public static final int SHORTCUT_MAKE_VIDEO_CALL = 4; + public static final int SHORTCUT_BLOCK_NUMBER = 5; + public static final int SHORTCUT_COUNT = 6; + private final boolean[] mShortcutEnabled = new boolean[SHORTCUT_COUNT]; + private final BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); + private String mFormattedQueryString; + private String mCountryIso; + private boolean mVideoCallingEnabled = false; + + public DialerPhoneNumberListAdapter(Context context) { + super(context); + + mCountryIso = GeoUtil.getCurrentCountryIso(context); + mVideoCallingEnabled = CallUtil.isVideoEnabled(context); + } + + @Override + public int getCount() { + return super.getCount() + getShortcutCount(); + } + + /** @return The number of enabled shortcuts. Ranges from 0 to a maximum of SHORTCUT_COUNT */ + public int getShortcutCount() { + int count = 0; + for (int i = 0; i < mShortcutEnabled.length; i++) { + if (mShortcutEnabled[i]) { + count++; + } + } + return count; + } + + public void disableAllShortcuts() { + for (int i = 0; i < mShortcutEnabled.length; i++) { + mShortcutEnabled[i] = false; + } + } + + @Override + public int getItemViewType(int position) { + final int shortcut = getShortcutTypeFromPosition(position); + if (shortcut >= 0) { + // shortcutPos should always range from 1 to SHORTCUT_COUNT + return super.getViewTypeCount() + shortcut; + } else { + return super.getItemViewType(position); + } + } + + @Override + public int getViewTypeCount() { + // Number of item view types in the super implementation + 2 for the 2 new shortcuts + return super.getViewTypeCount() + SHORTCUT_COUNT; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final int shortcutType = getShortcutTypeFromPosition(position); + if (shortcutType >= 0) { + if (convertView != null) { + assignShortcutToView((ContactListItemView) convertView, shortcutType); + return convertView; + } else { + final ContactListItemView v = + new ContactListItemView(getContext(), null, mVideoCallingEnabled); + assignShortcutToView(v, shortcutType); + return v; + } + } else { + return super.getView(position, convertView, parent); + } + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + final ContactListItemView view = super.newView(context, partition, cursor, position, parent); + + view.setSupportVideoCallIcon(mVideoCallingEnabled); + return view; + } + + /** + * @param position The position of the item + * @return The enabled shortcut type matching the given position if the item is a shortcut, -1 + * otherwise + */ + public int getShortcutTypeFromPosition(int position) { + int shortcutCount = position - super.getCount(); + if (shortcutCount >= 0) { + // Iterate through the array of shortcuts, looking only for shortcuts where + // mShortcutEnabled[i] is true + for (int i = 0; shortcutCount >= 0 && i < mShortcutEnabled.length; i++) { + if (mShortcutEnabled[i]) { + shortcutCount--; + if (shortcutCount < 0) { + return i; + } + } + } + throw new IllegalArgumentException( + "Invalid position - greater than cursor count " + " but not a shortcut."); + } + return SHORTCUT_INVALID; + } + + @Override + public boolean isEmpty() { + return getShortcutCount() == 0 && super.isEmpty(); + } + + @Override + public boolean isEnabled(int position) { + final int shortcutType = getShortcutTypeFromPosition(position); + if (shortcutType >= 0) { + return true; + } else { + return super.isEnabled(position); + } + } + + private void assignShortcutToView(ContactListItemView v, int shortcutType) { + final CharSequence text; + final int drawableId; + final Resources resources = getContext().getResources(); + final String number = getFormattedQueryString(); + switch (shortcutType) { + case SHORTCUT_DIRECT_CALL: + text = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + resources, + R.string.search_shortcut_call_number, + mBidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR)); + drawableId = R.drawable.ic_search_phone; + break; + case SHORTCUT_CREATE_NEW_CONTACT: + text = resources.getString(R.string.search_shortcut_create_new_contact); + drawableId = R.drawable.ic_search_add_contact; + break; + case SHORTCUT_ADD_TO_EXISTING_CONTACT: + text = resources.getString(R.string.search_shortcut_add_to_contact); + drawableId = R.drawable.ic_person_24dp; + break; + case SHORTCUT_SEND_SMS_MESSAGE: + text = resources.getString(R.string.search_shortcut_send_sms_message); + drawableId = R.drawable.ic_message_24dp; + break; + case SHORTCUT_MAKE_VIDEO_CALL: + text = resources.getString(R.string.search_shortcut_make_video_call); + drawableId = R.drawable.ic_videocam; + break; + case SHORTCUT_BLOCK_NUMBER: + text = resources.getString(R.string.search_shortcut_block_number); + drawableId = R.drawable.ic_not_interested_googblue_24dp; + break; + default: + throw new IllegalArgumentException("Invalid shortcut type"); + } + v.setDrawableResource(drawableId); + v.setDisplayName(text); + v.setPhotoPosition(super.getPhotoPosition()); + v.setAdjustSelectionBoundsEnabled(false); + } + + /** @return True if the shortcut state (disabled vs enabled) was changed by this operation */ + public boolean setShortcutEnabled(int shortcutType, boolean visible) { + final boolean changed = mShortcutEnabled[shortcutType] != visible; + mShortcutEnabled[shortcutType] = visible; + return changed; + } + + public String getFormattedQueryString() { + return mFormattedQueryString; + } + + @Override + public void setQueryString(String queryString) { + mFormattedQueryString = + PhoneNumberUtils.formatNumber(PhoneNumberUtils.normalizeNumber(queryString), mCountryIso); + super.setQueryString(queryString); + } +} diff --git a/java/com/android/dialer/app/list/DragDropController.java b/java/com/android/dialer/app/list/DragDropController.java new file mode 100644 index 0000000000000000000000000000000000000000..c22dd13187e79fff20efdb8bae0aa2aaaa49f06c --- /dev/null +++ b/java/com/android/dialer/app/list/DragDropController.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.list; + +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.View; +import java.util.ArrayList; +import java.util.List; + +/** + * Class that handles and combines drag events generated from multiple views, and then fires off + * events to any OnDragDropListeners that have registered for callbacks. + */ +public class DragDropController { + + private final List mOnDragDropListeners = new ArrayList(); + private final DragItemContainer mDragItemContainer; + private final int[] mLocationOnScreen = new int[2]; + + public DragDropController(DragItemContainer dragItemContainer) { + mDragItemContainer = dragItemContainer; + } + + /** @return True if the drag is started, false if the drag is cancelled for some reason. */ + boolean handleDragStarted(View v, int x, int y) { + int screenX = x; + int screenY = y; + // The coordinates in dragEvent of DragEvent.ACTION_DRAG_STARTED before NYC is window-related. + // This is fixed in NYC. + if (VERSION.SDK_INT >= VERSION_CODES.N) { + v.getLocationOnScreen(mLocationOnScreen); + screenX = x + mLocationOnScreen[0]; + screenY = y + mLocationOnScreen[1]; + } + final PhoneFavoriteSquareTileView tileView = + mDragItemContainer.getViewForLocation(screenX, screenY); + if (tileView == null) { + return false; + } + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDragStarted(screenX, screenY, tileView); + } + + return true; + } + + public void handleDragHovered(View v, int x, int y) { + v.getLocationOnScreen(mLocationOnScreen); + final int screenX = x + mLocationOnScreen[0]; + final int screenY = y + mLocationOnScreen[1]; + final PhoneFavoriteSquareTileView view = + mDragItemContainer.getViewForLocation(screenX, screenY); + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDragHovered(screenX, screenY, view); + } + } + + public void handleDragFinished(int x, int y, boolean isRemoveView) { + if (isRemoveView) { + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDroppedOnRemove(); + } + } + + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDragFinished(x, y); + } + } + + public void addOnDragDropListener(OnDragDropListener listener) { + if (!mOnDragDropListeners.contains(listener)) { + mOnDragDropListeners.add(listener); + } + } + + public void removeOnDragDropListener(OnDragDropListener listener) { + if (mOnDragDropListeners.contains(listener)) { + mOnDragDropListeners.remove(listener); + } + } + + /** + * Callback interface used to retrieve views based on the current touch coordinates of the drag + * event. The {@link DragItemContainer} houses the draggable views that this {@link + * DragDropController} controls. + */ + public interface DragItemContainer { + + PhoneFavoriteSquareTileView getViewForLocation(int x, int y); + } +} diff --git a/java/com/android/dialer/app/list/ListsFragment.java b/java/com/android/dialer/app/list/ListsFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..725ad3001064aec6196e6b18549d5b3e764068b7 --- /dev/null +++ b/java/com/android/dialer/app/list/ListsFragment.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Trace; +import android.preference.PreferenceManager; +import android.provider.VoicemailContract; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.list.ViewPagerTabs; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogFragment; +import com.android.dialer.app.calllog.CallLogNotificationsHelper; +import com.android.dialer.app.calllog.VisualVoicemailCallLogFragment; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source; +import com.android.dialer.app.widget.ActionBarController; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.CallLogQueryHandler; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.util.ViewUtil; +import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker; +import com.android.dialer.voicemailstatus.VoicemailStatusHelper; +import com.android.dialer.voicemailstatus.VoicemailStatusHelperImpl; +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment that is used as the main screen of the Dialer. + * + *

Contains a ViewPager that contains various contact lists like the Speed Dial list and the All + * Contacts list. This will also eventually contain the logic that allows sliding the ViewPager + * containing the lists up above the search bar and pin it against the top of the screen. + */ +public class ListsFragment extends Fragment + implements ViewPager.OnPageChangeListener, CallLogQueryHandler.Listener { + + /** Every fragment in the list show implement this interface. */ + public interface ListsPage { + + /** + * Called when the page is resumed, including selecting the page or activity resume. Note: This + * is called before the page fragment is attached to a activity. + * + * @param activity the activity hosting the ListFragment + */ + void onPageResume(@Nullable Activity activity); + + /** + * Called when the page is paused, including selecting another page or activity pause. Note: + * This is called after the page fragment is detached from a activity. + * + * @param activity the activity hosting the ListFragment + */ + void onPagePause(@Nullable Activity activity); + } + + public static final int TAB_INDEX_SPEED_DIAL = 0; + public static final int TAB_INDEX_HISTORY = 1; + public static final int TAB_INDEX_ALL_CONTACTS = 2; + public static final int TAB_INDEX_VOICEMAIL = 3; + public static final int TAB_COUNT_DEFAULT = 3; + public static final int TAB_COUNT_WITH_VOICEMAIL = 4; + private static final String TAG = "ListsFragment"; + private ActionBar mActionBar; + private ViewPager mViewPager; + private ViewPagerTabs mViewPagerTabs; + private ViewPagerAdapter mViewPagerAdapter; + private RemoveView mRemoveView; + private View mRemoveViewContent; + private SpeedDialFragment mSpeedDialFragment; + private CallLogFragment mHistoryFragment; + private AllContactsFragment mAllContactsFragment; + private CallLogFragment mVoicemailFragment; + private ListsPage mCurrentPage; + private SharedPreferences mPrefs; + private boolean mHasActiveVoicemailProvider; + private boolean mHasFetchedVoicemailStatus; + private boolean mShowVoicemailTabAfterVoicemailStatusIsFetched; + private VoicemailStatusHelper mVoicemailStatusHelper; + private ArrayList mOnPageChangeListeners = + new ArrayList(); + private String[] mTabTitles; + private int[] mTabIcons; + /** The position of the currently selected tab. */ + private int mTabIndex = TAB_INDEX_SPEED_DIAL; + + private CallLogQueryHandler mCallLogQueryHandler; + + private final ContentObserver mVoicemailStatusObserver = + new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mCallLogQueryHandler.fetchVoicemailStatus(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + LogUtil.d("ListsFragment.onCreate", null); + Trace.beginSection(TAG + " onCreate"); + super.onCreate(savedInstanceState); + + mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); + mHasFetchedVoicemailStatus = false; + + mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + mHasActiveVoicemailProvider = + mPrefs.getBoolean( + VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, false); + + Trace.endSection(); + } + + @Override + public void onResume() { + LogUtil.d("ListsFragment.onResume", null); + Trace.beginSection(TAG + " onResume"); + super.onResume(); + + mActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (getUserVisibleHint()) { + sendScreenViewForCurrentPosition(); + } + + // Fetch voicemail status to determine if we should show the voicemail tab. + mCallLogQueryHandler = + new CallLogQueryHandler(getActivity(), getActivity().getContentResolver(), this); + mCallLogQueryHandler.fetchVoicemailStatus(); + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + Trace.endSection(); + mCurrentPage = getListsPage(mViewPager.getCurrentItem()); + if (mCurrentPage != null) { + mCurrentPage.onPageResume(getActivity()); + } + } + + @Override + public void onPause() { + LogUtil.d("ListsFragment.onPause", null); + if (mCurrentPage != null) { + mCurrentPage.onPagePause(getActivity()); + } + super.onPause(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mViewPager.removeOnPageChangeListener(this); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + LogUtil.d("ListsFragment.onCreateView", null); + Trace.beginSection(TAG + " onCreateView"); + Trace.beginSection(TAG + " inflate view"); + final View parentView = inflater.inflate(R.layout.lists_fragment, container, false); + Trace.endSection(); + Trace.beginSection(TAG + " setup views"); + mViewPager = (ViewPager) parentView.findViewById(R.id.lists_pager); + mViewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager()); + mViewPager.setAdapter(mViewPagerAdapter); + mViewPager.setOffscreenPageLimit(TAB_COUNT_WITH_VOICEMAIL - 1); + mViewPager.addOnPageChangeListener(this); + showTab(TAB_INDEX_SPEED_DIAL); + + mTabTitles = new String[TAB_COUNT_WITH_VOICEMAIL]; + mTabTitles[TAB_INDEX_SPEED_DIAL] = getResources().getString(R.string.tab_speed_dial); + mTabTitles[TAB_INDEX_HISTORY] = getResources().getString(R.string.tab_history); + mTabTitles[TAB_INDEX_ALL_CONTACTS] = getResources().getString(R.string.tab_all_contacts); + mTabTitles[TAB_INDEX_VOICEMAIL] = getResources().getString(R.string.tab_voicemail); + + mTabIcons = new int[TAB_COUNT_WITH_VOICEMAIL]; + mTabIcons[TAB_INDEX_SPEED_DIAL] = R.drawable.ic_grade_24dp; + mTabIcons[TAB_INDEX_HISTORY] = R.drawable.ic_schedule_24dp; + mTabIcons[TAB_INDEX_ALL_CONTACTS] = R.drawable.ic_people_24dp; + mTabIcons[TAB_INDEX_VOICEMAIL] = R.drawable.ic_voicemail_24dp; + + mViewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header); + mViewPagerTabs.configureTabIcons(mTabIcons); + mViewPagerTabs.setViewPager(mViewPager); + addOnPageChangeListener(mViewPagerTabs); + + mRemoveView = (RemoveView) parentView.findViewById(R.id.remove_view); + mRemoveViewContent = parentView.findViewById(R.id.remove_view_content); + + getActivity() + .getContentResolver() + .registerContentObserver( + VoicemailContract.Status.CONTENT_URI, true, mVoicemailStatusObserver); + + Trace.endSection(); + Trace.endSection(); + return parentView; + } + + @Override + public void onDestroy() { + getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); + super.onDestroy(); + } + + public void addOnPageChangeListener(OnPageChangeListener onPageChangeListener) { + if (!mOnPageChangeListeners.contains(onPageChangeListener)) { + mOnPageChangeListeners.add(onPageChangeListener); + } + } + + /** + * Shows the tab with the specified index. If the voicemail tab index is specified, but the + * voicemail status hasn't been fetched, it will try to show the tab after the voicemail status + * has been fetched. + */ + public void showTab(int index) { + if (index == TAB_INDEX_VOICEMAIL) { + if (mHasActiveVoicemailProvider) { + Logger.get(getContext()).logImpression(DialerImpression.Type.VVM_TAB_VISIBLE); + mViewPager.setCurrentItem(getRtlPosition(TAB_INDEX_VOICEMAIL)); + } else if (!mHasFetchedVoicemailStatus) { + // Try to show the voicemail tab after the voicemail status returns. + mShowVoicemailTabAfterVoicemailStatusIsFetched = true; + } + } else if (index < getTabCount()) { + mViewPager.setCurrentItem(getRtlPosition(index)); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mTabIndex = getRtlPosition(position); + + final int count = mOnPageChangeListeners.size(); + for (int i = 0; i < count; i++) { + mOnPageChangeListeners.get(i).onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + LogUtil.i("ListsFragment.onPageSelected", "position: %d", position); + mTabIndex = getRtlPosition(position); + + // Show the tab which has been selected instead. + mShowVoicemailTabAfterVoicemailStatusIsFetched = false; + + final int count = mOnPageChangeListeners.size(); + for (int i = 0; i < count; i++) { + mOnPageChangeListeners.get(i).onPageSelected(position); + } + sendScreenViewForCurrentPosition(); + + if (mCurrentPage != null) { + mCurrentPage.onPagePause(getActivity()); + } + mCurrentPage = getListsPage(position); + if (mCurrentPage != null) { + mCurrentPage.onPageResume(getActivity()); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + final int count = mOnPageChangeListeners.size(); + for (int i = 0; i < count; i++) { + mOnPageChangeListeners.get(i).onPageScrollStateChanged(state); + } + } + + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) { + mHasFetchedVoicemailStatus = true; + + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + + VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus( + getContext(), statusCursor, Source.Activity); + + // Update mHasActiveVoicemailProvider, which controls the number of tabs displayed. + boolean hasActiveVoicemailProvider = + mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0; + if (hasActiveVoicemailProvider != mHasActiveVoicemailProvider) { + mHasActiveVoicemailProvider = hasActiveVoicemailProvider; + mViewPagerAdapter.notifyDataSetChanged(); + + if (hasActiveVoicemailProvider) { + mViewPagerTabs.updateTab(TAB_INDEX_VOICEMAIL); + } else { + mViewPagerTabs.removeTab(TAB_INDEX_VOICEMAIL); + removeVoicemailFragment(); + } + + mPrefs + .edit() + .putBoolean( + VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, + hasActiveVoicemailProvider) + .commit(); + } + + if (hasActiveVoicemailProvider) { + mCallLogQueryHandler.fetchVoicemailUnreadCount(); + } + + if (mHasActiveVoicemailProvider && mShowVoicemailTabAfterVoicemailStatusIsFetched) { + mShowVoicemailTabAfterVoicemailStatusIsFetched = false; + showTab(TAB_INDEX_VOICEMAIL); + } + } + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) { + if (getActivity() == null || getActivity().isFinishing() || cursor == null) { + return; + } + + int count = 0; + try { + count = cursor.getCount(); + } finally { + cursor.close(); + } + + mViewPagerTabs.setUnreadCount(count, TAB_INDEX_VOICEMAIL); + mViewPagerTabs.updateTab(TAB_INDEX_VOICEMAIL); + } + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + if (getActivity() == null || getActivity().isFinishing() || cursor == null) { + return; + } + + int count = 0; + try { + count = cursor.getCount(); + } finally { + cursor.close(); + } + + mViewPagerTabs.setUnreadCount(count, TAB_INDEX_HISTORY); + mViewPagerTabs.updateTab(TAB_INDEX_HISTORY); + } + + @Override + public boolean onCallsFetched(Cursor statusCursor) { + // Return false; did not take ownership of cursor + return false; + } + + public int getCurrentTabIndex() { + return mTabIndex; + } + + /** + * External method to update unread count because the unread count changes when the user expands a + * voicemail in the call log or when the user expands an unread call in the call history tab. + */ + public void updateTabUnreadCounts() { + if (mCallLogQueryHandler != null) { + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + if (mHasActiveVoicemailProvider) { + mCallLogQueryHandler.fetchVoicemailUnreadCount(); + } + } + } + + /** External method to mark all missed calls as read. */ + public void markMissedCallsAsReadAndRemoveNotifications() { + if (mCallLogQueryHandler != null) { + mCallLogQueryHandler.markMissedCallsAsRead(); + CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); + } + } + + public void showRemoveView(boolean show) { + mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE); + mRemoveView.setAlpha(show ? 0 : 1); + mRemoveView.animate().alpha(show ? 1 : 0).start(); + } + + public boolean shouldShowActionBar() { + // TODO: Update this based on scroll state. + return mActionBar != null; + } + + public SpeedDialFragment getSpeedDialFragment() { + return mSpeedDialFragment; + } + + public RemoveView getRemoveView() { + return mRemoveView; + } + + public int getTabCount() { + return mViewPagerAdapter.getCount(); + } + + private int getRtlPosition(int position) { + if (ViewUtil.isRtl()) { + return mViewPagerAdapter.getCount() - 1 - position; + } + return position; + } + + public void sendScreenViewForCurrentPosition() { + if (!isResumed()) { + return; + } + + int screenType; + switch (getCurrentTabIndex()) { + case TAB_INDEX_SPEED_DIAL: + screenType = ScreenEvent.Type.SPEED_DIAL; + break; + case TAB_INDEX_HISTORY: + screenType = ScreenEvent.Type.CALL_LOG; + break; + case TAB_INDEX_ALL_CONTACTS: + screenType = ScreenEvent.Type.ALL_CONTACTS; + break; + case TAB_INDEX_VOICEMAIL: + screenType = ScreenEvent.Type.VOICEMAIL_LOG; + break; + default: + return; + } + Logger.get(getActivity()).logScreenView(screenType, getActivity()); + } + + private void removeVoicemailFragment() { + if (mVoicemailFragment != null) { + getChildFragmentManager() + .beginTransaction() + .remove(mVoicemailFragment) + .commitAllowingStateLoss(); + mVoicemailFragment = null; + } + } + + private ListsPage getListsPage(int position) { + switch (getRtlPosition(position)) { + case TAB_INDEX_SPEED_DIAL: + return mSpeedDialFragment; + case TAB_INDEX_HISTORY: + return mHistoryFragment; + case TAB_INDEX_ALL_CONTACTS: + return mAllContactsFragment; + case TAB_INDEX_VOICEMAIL: + return mVoicemailFragment; + } + throw new IllegalStateException("No fragment at position " + position); + } + + public interface HostInterface { + + ActionBarController getActionBarController(); + } + + public class ViewPagerAdapter extends FragmentPagerAdapter { + + private final List mFragments = new ArrayList<>(); + + public ViewPagerAdapter(FragmentManager fm) { + super(fm); + for (int i = 0; i < TAB_COUNT_WITH_VOICEMAIL; i++) { + mFragments.add(null); + } + } + + @Override + public long getItemId(int position) { + return getRtlPosition(position); + } + + @Override + public Fragment getItem(int position) { + LogUtil.d("ViewPagerAdapter.getItem", "position: %d", position); + switch (getRtlPosition(position)) { + case TAB_INDEX_SPEED_DIAL: + if (mSpeedDialFragment == null) { + mSpeedDialFragment = new SpeedDialFragment(); + } + return mSpeedDialFragment; + case TAB_INDEX_HISTORY: + if (mHistoryFragment == null) { + mHistoryFragment = new CallLogFragment(); + } + return mHistoryFragment; + case TAB_INDEX_ALL_CONTACTS: + if (mAllContactsFragment == null) { + mAllContactsFragment = new AllContactsFragment(); + } + return mAllContactsFragment; + case TAB_INDEX_VOICEMAIL: + if (mVoicemailFragment == null) { + mVoicemailFragment = new VisualVoicemailCallLogFragment(); + LogUtil.v( + "ViewPagerAdapter.getItem", + "new VisualVoicemailCallLogFragment: %s", + mVoicemailFragment); + } + return mVoicemailFragment; + } + throw new IllegalStateException("No fragment at position " + position); + } + + @Override + public Fragment instantiateItem(ViewGroup container, int position) { + LogUtil.d("ViewPagerAdapter.instantiateItem", "position: %d", position); + // On rotation the FragmentManager handles rotation. Therefore getItem() isn't called. + // Copy the fragments that the FragmentManager finds so that we can store them in + // instance variables for later. + final Fragment fragment = (Fragment) super.instantiateItem(container, position); + if (fragment instanceof SpeedDialFragment) { + mSpeedDialFragment = (SpeedDialFragment) fragment; + } else if (fragment instanceof CallLogFragment && position == TAB_INDEX_HISTORY) { + mHistoryFragment = (CallLogFragment) fragment; + } else if (fragment instanceof AllContactsFragment) { + mAllContactsFragment = (AllContactsFragment) fragment; + } else if (fragment instanceof CallLogFragment && position == TAB_INDEX_VOICEMAIL) { + mVoicemailFragment = (CallLogFragment) fragment; + LogUtil.v("ViewPagerAdapter.instantiateItem", mVoicemailFragment.toString()); + } + mFragments.set(position, fragment); + return fragment; + } + + /** + * When {@link android.support.v4.view.PagerAdapter#notifyDataSetChanged} is called, this method + * is called on all pages to determine whether they need to be recreated. When the voicemail tab + * is removed, the view needs to be recreated by returning POSITION_NONE. If + * notifyDataSetChanged is called for some other reason, the voicemail tab is recreated only if + * it is active. All other tabs do not need to be recreated and POSITION_UNCHANGED is returned. + */ + @Override + public int getItemPosition(Object object) { + return !mHasActiveVoicemailProvider && mFragments.indexOf(object) == TAB_INDEX_VOICEMAIL + ? POSITION_NONE + : POSITION_UNCHANGED; + } + + @Override + public int getCount() { + return mHasActiveVoicemailProvider ? TAB_COUNT_WITH_VOICEMAIL : TAB_COUNT_DEFAULT; + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabTitles[position]; + } + } +} diff --git a/java/com/android/dialer/app/list/OnDragDropListener.java b/java/com/android/dialer/app/list/OnDragDropListener.java new file mode 100644 index 0000000000000000000000000000000000000000..b71c7fef6e5c9c9e0d9074b7a858fd1232a63247 --- /dev/null +++ b/java/com/android/dialer/app/list/OnDragDropListener.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.list; + +/** + * Classes that want to receive callbacks in response to drag events should implement this + * interface. + */ +public interface OnDragDropListener { + + /** + * Called when a drag is started. + * + * @param x X-coordinate of the drag event + * @param y Y-coordinate of the drag event + * @param view The contact tile which the drag was started on + */ + void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view); + + /** + * Called when a drag is in progress and the user moves the dragged contact to a location. + * + * @param x X-coordinate of the drag event + * @param y Y-coordinate of the drag event + * @param view Contact tile in the ListView which is currently being displaced by the dragged + * contact + */ + void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view); + + /** + * Called when a drag is completed (whether by dropping it somewhere or simply by dragging the + * contact off the screen) + * + * @param x X-coordinate of the drag event + * @param y Y-coordinate of the drag event + */ + void onDragFinished(int x, int y); + + /** + * Called when a contact has been dropped on the remove view, indicating that the user wants to + * remove this contact. + */ + void onDroppedOnRemove(); +} diff --git a/src/com/android/dialer/list/OnListFragmentScrolledListener.java b/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java similarity index 78% rename from src/com/android/dialer/list/OnListFragmentScrolledListener.java rename to java/com/android/dialer/app/list/OnListFragmentScrolledListener.java index 5ed3a64340e69e47958cc6b01c64923d658f98ba..a76f3b527c7c0d39eaa3237a543584bedf083921 100644 --- a/src/com/android/dialer/list/OnListFragmentScrolledListener.java +++ b/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java @@ -14,13 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.dialer.list; +package com.android.dialer.app.list; /* * Interface to provide callback to activity when a child fragment is scrolled */ public interface OnListFragmentScrolledListener { - public void onListFragmentScrollStateChange(int scrollState); - public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, - int totalItemCount); + + void onListFragmentScrollStateChange(int scrollState); + + void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount); } diff --git a/java/com/android/dialer/app/list/PhoneFavoriteListView.java b/java/com/android/dialer/app/list/PhoneFavoriteListView.java new file mode 100644 index 0000000000000000000000000000000000000000..9516f061158f3427d4e073bef457261636308cfc --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoriteListView.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2012 Google Inc. + * Licensed to The Android Open Source Project. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.dialer.app.list; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.DragEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.GridView; +import android.widget.ImageView; +import com.android.dialer.app.R; +import com.android.dialer.app.list.DragDropController.DragItemContainer; + +/** Viewgroup that presents the user's speed dial contacts in a grid. */ +public class PhoneFavoriteListView extends GridView + implements OnDragDropListener, DragItemContainer { + + public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName(); + final int[] mLocationOnScreen = new int[2]; + private final long SCROLL_HANDLER_DELAY_MILLIS = 5; + private final int DRAG_SCROLL_PX_UNIT = 25; + private final float DRAG_SHADOW_ALPHA = 0.7f; + /** + * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be offseted to the top / bottom by + * {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels. + */ + private final float BOUND_GAP_RATIO = 0.2f; + + private float mTouchSlop; + private int mTopScrollBound; + private int mBottomScrollBound; + private int mLastDragY; + private Handler mScrollHandler; + private final Runnable mDragScroller = + new Runnable() { + @Override + public void run() { + if (mLastDragY <= mTopScrollBound) { + smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); + } else if (mLastDragY >= mBottomScrollBound) { + smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); + } + mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS); + } + }; + private boolean mIsDragScrollerRunning = false; + private int mTouchDownForDragStartX; + private int mTouchDownForDragStartY; + private Bitmap mDragShadowBitmap; + private ImageView mDragShadowOverlay; + private final AnimatorListenerAdapter mDragShadowOverAnimatorListener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mDragShadowBitmap != null) { + mDragShadowBitmap.recycle(); + mDragShadowBitmap = null; + } + mDragShadowOverlay.setVisibility(GONE); + mDragShadowOverlay.setImageBitmap(null); + } + }; + private View mDragShadowParent; + private int mAnimationDuration; + // X and Y offsets inside the item from where the user grabbed to the + // child's left coordinate. This is used to aid in the drawing of the drag shadow. + private int mTouchOffsetToChildLeft; + private int mTouchOffsetToChildTop; + private int mDragShadowLeft; + private int mDragShadowTop; + private DragDropController mDragDropController = new DragDropController(this); + + public PhoneFavoriteListView(Context context) { + this(context, null); + } + + public PhoneFavoriteListView(Context context, AttributeSet attrs) { + this(context, attrs, -1); + } + + public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration); + mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); + mDragDropController.addOnDragDropListener(this); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); + } + + /** + * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should be + * cleaned up and removed once drag to remove becomes the only way to remove contacts. + */ + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mTouchDownForDragStartX = (int) ev.getX(); + mTouchDownForDragStartY = (int) ev.getY(); + } + + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onDragEvent(DragEvent event) { + final int action = event.getAction(); + final int eX = (int) event.getX(); + final int eY = (int) event.getY(); + switch (action) { + case DragEvent.ACTION_DRAG_STARTED: + { + if (!PhoneFavoriteTileView.DRAG_PHONE_FAVORITE_TILE.equals(event.getLocalState())) { + // Ignore any drag events that were not propagated by long pressing + // on a {@link PhoneFavoriteTileView} + return false; + } + if (!mDragDropController.handleDragStarted(this, eX, eY)) { + return false; + } + break; + } + case DragEvent.ACTION_DRAG_LOCATION: + mLastDragY = eY; + mDragDropController.handleDragHovered(this, eX, eY); + // Kick off {@link #mScrollHandler} if it's not started yet. + if (!mIsDragScrollerRunning + && + // And if the distance traveled while dragging exceeds the touch slop + (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) { + mIsDragScrollerRunning = true; + ensureScrollHandler(); + mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS); + } + break; + case DragEvent.ACTION_DRAG_ENTERED: + final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO); + mTopScrollBound = (getTop() + boundGap); + mBottomScrollBound = (getBottom() - boundGap); + break; + case DragEvent.ACTION_DRAG_EXITED: + case DragEvent.ACTION_DRAG_ENDED: + case DragEvent.ACTION_DROP: + ensureScrollHandler(); + mScrollHandler.removeCallbacks(mDragScroller); + mIsDragScrollerRunning = false; + // Either a successful drop or it's ended with out drop. + if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) { + mDragDropController.handleDragFinished(eX, eY, false); + } + break; + default: + break; + } + // This ListView will consume the drag events on behalf of its children. + return true; + } + + public void setDragShadowOverlay(ImageView overlay) { + mDragShadowOverlay = overlay; + mDragShadowParent = (View) mDragShadowOverlay.getParent(); + } + + /** Find the view under the pointer. */ + private View getViewAtPosition(int x, int y) { + final int count = getChildCount(); + View child; + for (int childIdx = 0; childIdx < count; childIdx++) { + child = getChildAt(childIdx); + if (y >= child.getTop() + && y <= child.getBottom() + && x >= child.getLeft() + && x <= child.getRight()) { + return child; + } + } + return null; + } + + private void ensureScrollHandler() { + if (mScrollHandler == null) { + mScrollHandler = getHandler(); + } + } + + public DragDropController getDragDropController() { + return mDragDropController; + } + + @Override + public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) { + if (mDragShadowOverlay == null) { + return; + } + + mDragShadowOverlay.clearAnimation(); + mDragShadowBitmap = createDraggedChildBitmap(tileView); + if (mDragShadowBitmap == null) { + return; + } + + tileView.getLocationOnScreen(mLocationOnScreen); + mDragShadowLeft = mLocationOnScreen[0]; + mDragShadowTop = mLocationOnScreen[1]; + + // x and y are the coordinates of the on-screen touch event. Using these + // and the on-screen location of the tileView, calculate the difference between + // the position of the user's finger and the position of the tileView. These will + // be used to offset the location of the drag shadow so that it appears that the + // tileView is positioned directly under the user's finger. + mTouchOffsetToChildLeft = x - mDragShadowLeft; + mTouchOffsetToChildTop = y - mDragShadowTop; + + mDragShadowParent.getLocationOnScreen(mLocationOnScreen); + mDragShadowLeft -= mLocationOnScreen[0]; + mDragShadowTop -= mLocationOnScreen[1]; + + mDragShadowOverlay.setImageBitmap(mDragShadowBitmap); + mDragShadowOverlay.setVisibility(VISIBLE); + mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA); + + mDragShadowOverlay.setX(mDragShadowLeft); + mDragShadowOverlay.setY(mDragShadowTop); + } + + @Override + public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) { + // Update the drag shadow location. + mDragShadowParent.getLocationOnScreen(mLocationOnScreen); + mDragShadowLeft = x - mTouchOffsetToChildLeft - mLocationOnScreen[0]; + mDragShadowTop = y - mTouchOffsetToChildTop - mLocationOnScreen[1]; + // Draw the drag shadow at its last known location if the drag shadow exists. + if (mDragShadowOverlay != null) { + mDragShadowOverlay.setX(mDragShadowLeft); + mDragShadowOverlay.setY(mDragShadowTop); + } + } + + @Override + public void onDragFinished(int x, int y) { + if (mDragShadowOverlay != null) { + mDragShadowOverlay.clearAnimation(); + mDragShadowOverlay + .animate() + .alpha(0.0f) + .setDuration(mAnimationDuration) + .setListener(mDragShadowOverAnimatorListener) + .start(); + } + } + + @Override + public void onDroppedOnRemove() {} + + private Bitmap createDraggedChildBitmap(View view) { + view.setDrawingCacheEnabled(true); + final Bitmap cache = view.getDrawingCache(); + + Bitmap bitmap = null; + if (cache != null) { + try { + bitmap = cache.copy(Bitmap.Config.ARGB_8888, false); + } catch (final OutOfMemoryError e) { + Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e); + bitmap = null; + } + } + + view.destroyDrawingCache(); + view.setDrawingCacheEnabled(false); + + return bitmap; + } + + @Override + public PhoneFavoriteSquareTileView getViewForLocation(int x, int y) { + getLocationOnScreen(mLocationOnScreen); + // Calculate the X and Y coordinates of the drag event relative to the view + final int viewX = x - mLocationOnScreen[0]; + final int viewY = y - mLocationOnScreen[1]; + final View child = getViewAtPosition(viewX, viewY); + + if (!(child instanceof PhoneFavoriteSquareTileView)) { + return null; + } + + return (PhoneFavoriteSquareTileView) child; + } +} diff --git a/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java new file mode 100644 index 0000000000000000000000000000000000000000..5a18d039bb3d298f12c2f8da411e16146d7a557b --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java @@ -0,0 +1,119 @@ +/* + +* Copyright (C) 2011 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package com.android.dialer.app.list; + +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.QuickContact; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.TextView; +import com.android.contacts.common.list.ContactEntry; +import com.android.dialer.app.R; +import com.android.dialer.compat.CompatUtils; + +/** Displays the contact's picture overlaid with their name and number type in a tile. */ +public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView { + + private static final String TAG = PhoneFavoriteSquareTileView.class.getSimpleName(); + + private final float mHeightToWidthRatio; + + private ImageButton mSecondaryButton; + + private ContactEntry mContactEntry; + + public PhoneFavoriteSquareTileView(Context context, AttributeSet attrs) { + super(context, attrs); + + mHeightToWidthRatio = + getResources().getFraction(R.dimen.contact_tile_height_to_width_ratio, 1, 1); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + final TextView nameView = (TextView) findViewById(R.id.contact_tile_name); + nameView.setElegantTextHeight(false); + final TextView phoneTypeView = (TextView) findViewById(R.id.contact_tile_phone_type); + phoneTypeView.setElegantTextHeight(false); + mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button); + } + + @Override + protected int getApproximateImageSize() { + // The picture is the full size of the tile (minus some padding, but we can be generous) + return getWidth(); + } + + private void launchQuickContact() { + if (CompatUtils.hasPrioritizedMimeType()) { + QuickContact.showQuickContact( + getContext(), + PhoneFavoriteSquareTileView.this, + getLookupUri(), + null, + Phone.CONTENT_ITEM_TYPE); + } else { + QuickContact.showQuickContact( + getContext(), + PhoneFavoriteSquareTileView.this, + getLookupUri(), + QuickContact.MODE_LARGE, + null); + } + } + + @Override + public void loadFromContact(ContactEntry entry) { + super.loadFromContact(entry); + if (entry != null) { + mSecondaryButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + launchQuickContact(); + } + }); + } + mContactEntry = entry; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + final int height = (int) (mHeightToWidthRatio * width); + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + getChildAt(i) + .measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + setMeasuredDimension(width, height); + } + + @Override + protected String getNameForView(ContactEntry contactEntry) { + return contactEntry.getPreferredDisplayName(); + } + + public ContactEntry getContactEntry() { + return mContactEntry; + } +} diff --git a/java/com/android/dialer/app/list/PhoneFavoriteTileView.java b/java/com/android/dialer/app/list/PhoneFavoriteTileView.java new file mode 100644 index 0000000000000000000000000000000000000000..db89cf3dcdd98412cc6c9a30ade9f2b99b4ab08b --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoriteTileView.java @@ -0,0 +1,155 @@ +/* + +* Copyright (C) 2011 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package com.android.dialer.app.list; + +import android.content.ClipData; +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.list.ContactEntry; +import com.android.contacts.common.list.ContactTileView; +import com.android.dialer.app.R; + +/** + * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in + * Dialtacts for frequently called contacts. Slightly different behavior from superclass when you + * tap it, you want to call the frequently-called number for the contact, even if that is not the + * default number for that contact. This abstract class is the super class to both the row and tile + * view. + */ +public abstract class PhoneFavoriteTileView extends ContactTileView { + + // Constant to pass to the drag event so that the drag action only happens when a phone favorite + // tile is long pressed. + static final String DRAG_PHONE_FAVORITE_TILE = "PHONE_FAVORITE_TILE"; + private static final String TAG = PhoneFavoriteTileView.class.getSimpleName(); + private static final boolean DEBUG = false; + // These parameters instruct the photo manager to display the default image/letter at 70% of + // its normal size, and vertically offset upwards 12% towards the top of the letter tile, to + // make room for the contact name and number label at the bottom of the image. + private static final float DEFAULT_IMAGE_LETTER_OFFSET = -0.12f; + private static final float DEFAULT_IMAGE_LETTER_SCALE = 0.70f; + // Dummy clip data object that is attached to drag shadows so that text views + // don't crash with an NPE if the drag shadow is released in their bounds + private static final ClipData EMPTY_CLIP_DATA = ClipData.newPlainText("", ""); + /** View that contains the transparent shadow that is overlaid on top of the contact image. */ + private View mShadowOverlay; + /** Users' most frequent phone number. */ + private String mPhoneNumberString; + + public PhoneFavoriteTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mShadowOverlay = findViewById(R.id.shadow_overlay); + + setOnLongClickListener( + new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + final PhoneFavoriteTileView view = (PhoneFavoriteTileView) v; + // NOTE The drag shadow is handled in the ListView. + view.startDrag( + EMPTY_CLIP_DATA, new View.DragShadowBuilder(), DRAG_PHONE_FAVORITE_TILE, 0); + return true; + } + }); + } + + @Override + public void loadFromContact(ContactEntry entry) { + super.loadFromContact(entry); + // Set phone number to null in case we're reusing the view. + mPhoneNumberString = null; + if (entry != null) { + // Grab the phone-number to call directly. See {@link onClick()}. + mPhoneNumberString = entry.phoneNumber; + + // If this is a blank entry, don't show anything. + // TODO krelease: Just hide the view for now. For this to truly look like an empty row + // the entire ContactTileRow needs to be hidden. + if (entry == ContactEntry.BLANK_ENTRY) { + setVisibility(View.INVISIBLE); + } else { + final ImageView starIcon = (ImageView) findViewById(R.id.contact_star_icon); + starIcon.setVisibility(entry.isFavorite ? View.VISIBLE : View.GONE); + setVisibility(View.VISIBLE); + } + } + } + + @Override + protected boolean isDarkTheme() { + return false; + } + + @Override + protected OnClickListener createClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mListener == null) { + return; + } + if (TextUtils.isEmpty(mPhoneNumberString)) { + // Copy "superclass" implementation + mListener.onContactSelected( + getLookupUri(), MoreContactUtils.getTargetRectFromView(PhoneFavoriteTileView.this)); + } else { + // When you tap a frequently-called contact, you want to + // call them at the number that you usually talk to them + // at (i.e. the one displayed in the UI), regardless of + // whether that's their default number. + mListener.onCallNumberDirectly(mPhoneNumberString); + } + } + }; + } + + @Override + protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) { + return new DefaultImageRequest( + displayName, + lookupKey, + ContactPhotoManager.TYPE_DEFAULT, + DEFAULT_IMAGE_LETTER_SCALE, + DEFAULT_IMAGE_LETTER_OFFSET, + false); + } + + @Override + protected void configureViewForImage(boolean isDefaultImage) { + // Hide the shadow overlay if the image is a default image (i.e. colored letter tile) + if (mShadowOverlay != null) { + mShadowOverlay.setVisibility(isDefaultImage ? View.GONE : View.VISIBLE); + } + } + + @Override + protected boolean isContactPhotoCircular() { + // Unlike Contacts' tiles, the Dialer's favorites tiles are square. + return false; + } +} diff --git a/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java b/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..c692ecac775ffc44f0068622adae3f43fd6f119f --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java @@ -0,0 +1,627 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import android.content.ContentProviderOperation; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.PinnedPositions; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Log; +import android.util.LongSparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactTileLoaderFactory; +import com.android.contacts.common.list.ContactEntry; +import com.android.contacts.common.list.ContactTileView; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.dialer.app.R; +import com.android.dialer.shortcuts.ShortcutRefresher; +import com.google.common.collect.ComparisonChain; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.PriorityQueue; + +/** Also allows for a configurable number of columns as well as a maximum row of tiled contacts. */ +public class PhoneFavoritesTileAdapter extends BaseAdapter implements OnDragDropListener { + + // Pinned positions start from 1, so there are a total of 20 maximum pinned contacts + private static final int PIN_LIMIT = 21; + private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + /** + * The soft limit on how many contact tiles to show. NOTE This soft limit would not restrict the + * number of starred contacts to show, rather 1. If the count of starred contacts is less than + * this limit, show 20 tiles total. 2. If the count of starred contacts is more than or equal to + * this limit, show all starred tiles and no frequents. + */ + private static final int TILES_SOFT_LIMIT = 20; + /** Contact data stored in cache. This is used to populate the associated view. */ + private ArrayList mContactEntries = null; + + private int mNumFrequents; + private int mNumStarred; + + private ContactTileView.Listener mListener; + private OnDataSetChangedForAnimationListener mDataSetChangedListener; + private Context mContext; + private Resources mResources; + private ContactsPreferences mContactsPreferences; + private final Comparator mContactEntryComparator = + new Comparator() { + @Override + public int compare(ContactEntry lhs, ContactEntry rhs) { + return ComparisonChain.start() + .compare(lhs.pinned, rhs.pinned) + .compare(getPreferredSortName(lhs), getPreferredSortName(rhs)) + .result(); + } + + private String getPreferredSortName(ContactEntry contactEntry) { + if (mContactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY + || TextUtils.isEmpty(contactEntry.nameAlternative)) { + return contactEntry.namePrimary; + } + return contactEntry.nameAlternative; + } + }; + /** Back up of the temporarily removed Contact during dragging. */ + private ContactEntry mDraggedEntry = null; + /** Position of the temporarily removed contact in the cache. */ + private int mDraggedEntryIndex = -1; + /** New position of the temporarily removed contact in the cache. */ + private int mDropEntryIndex = -1; + /** New position of the temporarily entered contact in the cache. */ + private int mDragEnteredEntryIndex = -1; + + private boolean mAwaitingRemove = false; + private boolean mDelayCursorUpdates = false; + private ContactPhotoManager mPhotoManager; + + /** Indicates whether a drag is in process. */ + private boolean mInDragging = false; + + public PhoneFavoritesTileAdapter( + Context context, + ContactTileView.Listener listener, + OnDataSetChangedForAnimationListener dataSetChangedListener) { + mDataSetChangedListener = dataSetChangedListener; + mListener = listener; + mContext = context; + mResources = context.getResources(); + mContactsPreferences = new ContactsPreferences(mContext); + mNumFrequents = 0; + mContactEntries = new ArrayList<>(); + } + + void setPhotoLoader(ContactPhotoManager photoLoader) { + mPhotoManager = photoLoader; + } + + /** + * Indicates whether a drag is in process. + * + * @param inDragging Boolean variable indicating whether there is a drag in process. + */ + private void setInDragging(boolean inDragging) { + mDelayCursorUpdates = inDragging; + mInDragging = inDragging; + } + + void refreshContactsPreferences() { + mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); + mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY); + } + + /** + * Gets the number of frequents from the passed in cursor. + * + *

This methods is needed so the GroupMemberTileAdapter can override this. + * + * @param cursor The cursor to get number of frequents from. + */ + private void saveNumFrequentsFromCursor(Cursor cursor) { + mNumFrequents = cursor.getCount() - mNumStarred; + } + + /** + * Creates {@link ContactTileView}s for each item in {@link Cursor}. + * + *

Else use {@link ContactTileLoaderFactory} + */ + void setContactCursor(Cursor cursor) { + if (!mDelayCursorUpdates && cursor != null && !cursor.isClosed()) { + mNumStarred = getNumStarredContacts(cursor); + if (mAwaitingRemove) { + mDataSetChangedListener.cacheOffsetsForDatasetChange(); + } + + saveNumFrequentsFromCursor(cursor); + saveCursorToCache(cursor); + // cause a refresh of any views that rely on this data + notifyDataSetChanged(); + // about to start redraw + mDataSetChangedListener.onDataSetChangedForAnimation(); + } + } + + /** + * Saves the cursor data to the cache, to speed up UI changes. + * + * @param cursor Returned cursor from {@link ContactTileLoaderFactory} with data to populate the + * view. + */ + private void saveCursorToCache(Cursor cursor) { + mContactEntries.clear(); + + if (cursor == null) { + return; + } + + final LongSparseArray duplicates = new LongSparseArray<>(cursor.getCount()); + + // Track the length of {@link #mContactEntries} and compare to {@link #TILES_SOFT_LIMIT}. + int counter = 0; + + // The cursor should not be closed since this is invoked from a CursorLoader. + if (cursor.moveToFirst()) { + int starredColumn = cursor.getColumnIndexOrThrow(Contacts.STARRED); + int contactIdColumn = cursor.getColumnIndexOrThrow(Phone.CONTACT_ID); + int photoUriColumn = cursor.getColumnIndexOrThrow(Contacts.PHOTO_URI); + int lookupKeyColumn = cursor.getColumnIndexOrThrow(Contacts.LOOKUP_KEY); + int pinnedColumn = cursor.getColumnIndexOrThrow(Contacts.PINNED); + int nameColumn = cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_PRIMARY); + int nameAlternativeColumn = cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_ALTERNATIVE); + int isDefaultNumberColumn = cursor.getColumnIndexOrThrow(Phone.IS_SUPER_PRIMARY); + int phoneTypeColumn = cursor.getColumnIndexOrThrow(Phone.TYPE); + int phoneLabelColumn = cursor.getColumnIndexOrThrow(Phone.LABEL); + int phoneNumberColumn = cursor.getColumnIndexOrThrow(Phone.NUMBER); + do { + final int starred = cursor.getInt(starredColumn); + final long id; + + // We display a maximum of TILES_SOFT_LIMIT contacts, or the total number of starred + // whichever is greater. + if (starred < 1 && counter >= TILES_SOFT_LIMIT) { + break; + } else { + id = cursor.getLong(contactIdColumn); + } + + final ContactEntry existing = (ContactEntry) duplicates.get(id); + if (existing != null) { + // Check if the existing number is a default number. If not, clear the phone number + // and label fields so that the disambiguation dialog will show up. + if (!existing.isDefaultNumber) { + existing.phoneLabel = null; + existing.phoneNumber = null; + } + continue; + } + + final String photoUri = cursor.getString(photoUriColumn); + final String lookupKey = cursor.getString(lookupKeyColumn); + final int pinned = cursor.getInt(pinnedColumn); + final String name = cursor.getString(nameColumn); + final String nameAlternative = cursor.getString(nameAlternativeColumn); + final boolean isStarred = cursor.getInt(starredColumn) > 0; + final boolean isDefaultNumber = cursor.getInt(isDefaultNumberColumn) > 0; + + final ContactEntry contact = new ContactEntry(); + + contact.id = id; + contact.namePrimary = + (!TextUtils.isEmpty(name)) ? name : mResources.getString(R.string.missing_name); + contact.nameAlternative = + (!TextUtils.isEmpty(nameAlternative)) + ? nameAlternative + : mResources.getString(R.string.missing_name); + contact.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); + contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); + contact.lookupKey = lookupKey; + contact.lookupUri = + ContentUris.withAppendedId( + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); + contact.isFavorite = isStarred; + contact.isDefaultNumber = isDefaultNumber; + + // Set phone number and label + final int phoneNumberType = cursor.getInt(phoneTypeColumn); + final String phoneNumberCustomLabel = cursor.getString(phoneLabelColumn); + contact.phoneLabel = + (String) Phone.getTypeLabel(mResources, phoneNumberType, phoneNumberCustomLabel); + contact.phoneNumber = cursor.getString(phoneNumberColumn); + + contact.pinned = pinned; + mContactEntries.add(contact); + + duplicates.put(id, contact); + + counter++; + } while (cursor.moveToNext()); + } + + mAwaitingRemove = false; + + arrangeContactsByPinnedPosition(mContactEntries); + + ShortcutRefresher.refresh(mContext, mContactEntries); + notifyDataSetChanged(); + } + + /** Iterates over the {@link Cursor} Returns position of the first NON Starred Contact */ + private int getNumStarredContacts(Cursor cursor) { + if (cursor == null) { + return 0; + } + + if (cursor.moveToFirst()) { + int starredColumn = cursor.getColumnIndex(Contacts.STARRED); + do { + if (cursor.getInt(starredColumn) == 0) { + return cursor.getPosition(); + } + } while (cursor.moveToNext()); + } + // There are not NON Starred contacts in cursor + // Set divider position to end + return cursor.getCount(); + } + + /** Returns the number of frequents that will be displayed in the list. */ + int getNumFrequents() { + return mNumFrequents; + } + + @Override + public int getCount() { + if (mContactEntries == null) { + return 0; + } + + return mContactEntries.size(); + } + + /** + * Returns an ArrayList of the {@link ContactEntry}s that are to appear on the row for the given + * position. + */ + @Override + public ContactEntry getItem(int position) { + return mContactEntries.get(position); + } + + /** + * For the top row of tiled contacts, the item id is the position of the row of contacts. For + * frequent contacts, the item id is the maximum number of rows of tiled contacts + the actual + * contact id. Since contact ids are always greater than 0, this guarantees that all items within + * this adapter will always have unique ids. + */ + @Override + public long getItemId(int position) { + return getItem(position).id; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return getCount() > 0; + } + + @Override + public void notifyDataSetChanged() { + if (DEBUG) { + Log.v(TAG, "notifyDataSetChanged"); + } + super.notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (DEBUG) { + Log.v(TAG, "get view for " + String.valueOf(position)); + } + + PhoneFavoriteTileView tileView = null; + + if (convertView instanceof PhoneFavoriteTileView) { + tileView = (PhoneFavoriteTileView) convertView; + } + + if (tileView == null) { + tileView = + (PhoneFavoriteTileView) View.inflate(mContext, R.layout.phone_favorite_tile_view, null); + } + tileView.setPhotoManager(mPhotoManager); + tileView.setListener(mListener); + tileView.loadFromContact(getItem(position)); + return tileView; + } + + @Override + public int getViewTypeCount() { + return ViewTypes.COUNT; + } + + @Override + public int getItemViewType(int position) { + return ViewTypes.TILE; + } + + /** + * Temporarily removes a contact from the list for UI refresh. Stores data for this contact in the + * back-up variable. + * + * @param index Position of the contact to be removed. + */ + private void popContactEntry(int index) { + if (isIndexInBound(index)) { + mDraggedEntry = mContactEntries.get(index); + mDraggedEntryIndex = index; + mDragEnteredEntryIndex = index; + markDropArea(mDragEnteredEntryIndex); + } + } + + /** + * @param itemIndex Position of the contact in {@link #mContactEntries}. + * @return True if the given index is valid for {@link #mContactEntries}. + */ + boolean isIndexInBound(int itemIndex) { + return itemIndex >= 0 && itemIndex < mContactEntries.size(); + } + + /** + * Mark the tile as drop area by given the item index in {@link #mContactEntries}. + * + * @param itemIndex Position of the contact in {@link #mContactEntries}. + */ + private void markDropArea(int itemIndex) { + if (mDraggedEntry != null + && isIndexInBound(mDragEnteredEntryIndex) + && isIndexInBound(itemIndex)) { + mDataSetChangedListener.cacheOffsetsForDatasetChange(); + // Remove the old placeholder item and place the new placeholder item. + mContactEntries.remove(mDragEnteredEntryIndex); + mDragEnteredEntryIndex = itemIndex; + mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY); + ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id; + mDataSetChangedListener.onDataSetChangedForAnimation(); + notifyDataSetChanged(); + } + } + + /** Drops the temporarily removed contact to the desired location in the list. */ + private void handleDrop() { + boolean changed = false; + if (mDraggedEntry != null) { + if (isIndexInBound(mDragEnteredEntryIndex) && mDragEnteredEntryIndex != mDraggedEntryIndex) { + // Don't add the ContactEntry here (to prevent a double animation from occuring). + // When we receive a new cursor the list of contact entries will automatically be + // populated with the dragged ContactEntry at the correct spot. + mDropEntryIndex = mDragEnteredEntryIndex; + mContactEntries.set(mDropEntryIndex, mDraggedEntry); + mDataSetChangedListener.cacheOffsetsForDatasetChange(); + changed = true; + } else if (isIndexInBound(mDraggedEntryIndex)) { + // If {@link #mDragEnteredEntryIndex} is invalid, + // falls back to the original position of the contact. + mContactEntries.remove(mDragEnteredEntryIndex); + mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); + mDropEntryIndex = mDraggedEntryIndex; + notifyDataSetChanged(); + } + + if (changed && mDropEntryIndex < PIN_LIMIT) { + final ArrayList operations = + getReflowedPinningOperations(mContactEntries, mDraggedEntryIndex, mDropEntryIndex); + if (!operations.isEmpty()) { + // update the database here with the new pinned positions + try { + mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Exception thrown when pinning contacts", e); + } + } + } + mDraggedEntry = null; + } + } + + /** + * Used when a contact is removed from speeddial. This will both unstar and set pinned position of + * the contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites list. + */ + private void unstarAndUnpinContact(Uri contactUri) { + final ContentValues values = new ContentValues(2); + values.put(Contacts.STARRED, false); + values.put(Contacts.PINNED, PinnedPositions.DEMOTED); + mContext.getContentResolver().update(contactUri, values, null, null); + } + + /** + * Given a list of contacts that each have pinned positions, rearrange the list (destructive) such + * that all pinned contacts are in their defined pinned positions, and unpinned contacts take the + * spaces between those pinned contacts. Demoted contacts should not appear in the resulting list. + * + *

This method also updates the pinned positions of pinned contacts so that they are all unique + * positive integers within range from 0 to toArrange.size() - 1. This is because when the contact + * entries are read from the database, it is possible for them to have overlapping pin positions + * due to sync or modifications by third party apps. + */ + @VisibleForTesting + private void arrangeContactsByPinnedPosition(ArrayList toArrange) { + final PriorityQueue pinnedQueue = + new PriorityQueue<>(PIN_LIMIT, mContactEntryComparator); + + final List unpinnedContacts = new LinkedList<>(); + + final int length = toArrange.size(); + for (int i = 0; i < length; i++) { + final ContactEntry contact = toArrange.get(i); + // Decide whether the contact is hidden(demoted), pinned, or unpinned + if (contact.pinned > PIN_LIMIT || contact.pinned == PinnedPositions.UNPINNED) { + unpinnedContacts.add(contact); + } else if (contact.pinned > PinnedPositions.DEMOTED) { + // Demoted or contacts with negative pinned positions are ignored. + // Pinned contacts go into a priority queue where they are ranked by pinned + // position. This is required because the contacts provider does not return + // contacts ordered by pinned position. + pinnedQueue.add(contact); + } + } + + final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size()); + + toArrange.clear(); + for (int i = 1; i < maxToPin + 1; i++) { + if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) { + final ContactEntry toPin = pinnedQueue.poll(); + toPin.pinned = i; + toArrange.add(toPin); + } else if (!unpinnedContacts.isEmpty()) { + toArrange.add(unpinnedContacts.remove(0)); + } + } + + // If there are still contacts in pinnedContacts at this point, it means that the pinned + // positions of these pinned contacts exceed the actual number of contacts in the list. + // For example, the user had 10 frequents, starred and pinned one of them at the last spot, + // and then cleared frequents. Contacts in this situation should become unpinned. + while (!pinnedQueue.isEmpty()) { + final ContactEntry entry = pinnedQueue.poll(); + entry.pinned = PinnedPositions.UNPINNED; + toArrange.add(entry); + } + + // Any remaining unpinned contacts that weren't in the gaps between the pinned contacts + // now just get appended to the end of the list. + toArrange.addAll(unpinnedContacts); + } + + /** + * Given an existing list of contact entries and a single entry that is to be pinned at a + * particular position, return a list of {@link ContentProviderOperation}s that contains new + * pinned positions for all contacts that are forced to be pinned at new positions, trying as much + * as possible to keep pinned contacts at their original location. + * + *

At this point in time the pinned position of each contact in the list has already been + * updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned + * positions(within {@link #PIN_LIMIT} are unique positive integers. + */ + @VisibleForTesting + private ArrayList getReflowedPinningOperations( + ArrayList list, int oldPos, int newPinPos) { + final ArrayList positions = new ArrayList<>(); + final int lowerBound = Math.min(oldPos, newPinPos); + final int upperBound = Math.max(oldPos, newPinPos); + for (int i = lowerBound; i <= upperBound; i++) { + final ContactEntry entry = list.get(i); + + // Pinned positions in the database start from 1 instead of being zero-indexed like + // arrays, so offset by 1. + final int databasePinnedPosition = i + 1; + if (entry.pinned == databasePinnedPosition) { + continue; + } + + final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, String.valueOf(entry.id)); + final ContentValues values = new ContentValues(); + values.put(Contacts.PINNED, databasePinnedPosition); + positions.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); + } + return positions; + } + + @Override + public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { + setInDragging(true); + final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); + popContactEntry(itemIndex); + } + + @Override + public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) { + if (view == null) { + // The user is hovering over a view that is not a contact tile, no need to do + // anything here. + return; + } + final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); + if (mInDragging + && mDragEnteredEntryIndex != itemIndex + && isIndexInBound(itemIndex) + && itemIndex < PIN_LIMIT + && itemIndex >= 0) { + markDropArea(itemIndex); + } + } + + @Override + public void onDragFinished(int x, int y) { + setInDragging(false); + // A contact has been dragged to the RemoveView in order to be unstarred, so simply wait + // for the new contact cursor which will cause the UI to be refreshed without the unstarred + // contact. + if (!mAwaitingRemove) { + handleDrop(); + } + } + + @Override + public void onDroppedOnRemove() { + if (mDraggedEntry != null) { + unstarAndUnpinContact(mDraggedEntry.lookupUri); + mAwaitingRemove = true; + } + } + + interface OnDataSetChangedForAnimationListener { + + void onDataSetChangedForAnimation(long... idsInPlace); + + void cacheOffsetsForDatasetChange(); + } + + private static class ViewTypes { + + static final int TILE = 0; + static final int COUNT = 1; + } +} diff --git a/java/com/android/dialer/app/list/RegularSearchFragment.java b/java/com/android/dialer/app/list/RegularSearchFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..26959539bb7aebe58604702307eb1ce056e2285a --- /dev/null +++ b/java/com/android/dialer/app/list/RegularSearchFragment.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import static android.Manifest.permission.READ_CONTACTS; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.support.v13.app.FragmentCompat; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.PinnedHeaderListView; +import com.android.dialer.app.R; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.PhoneNumberCache; +import com.android.dialer.util.PermissionsUtil; + +public class RegularSearchFragment extends SearchFragment + implements OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + public static final int PERMISSION_REQUEST_CODE = 1; + + private static final int SEARCH_DIRECTORY_RESULT_LIMIT = 5; + protected String mPermissionToRequest; + + public RegularSearchFragment() { + configureDirectorySearch(); + } + + public void configureDirectorySearch() { + setDirectorySearchEnabled(true); + setDirectoryResultLimit(SEARCH_DIRECTORY_RESULT_LIMIT); + } + + @Override + protected void onCreateView(LayoutInflater inflater, ViewGroup container) { + super.onCreateView(inflater, container); + ((PinnedHeaderListView) getListView()).setScrollToSectionOnHeaderTouch(true); + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + RegularSearchListAdapter adapter = new RegularSearchListAdapter(getActivity()); + adapter.setDisplayPhotos(true); + adapter.setUseCallableUri(usesCallableUri()); + adapter.setListener(this); + return adapter; + } + + @Override + protected void cacheContactInfo(int position) { + CachedNumberLookupService cachedNumberLookupService = + PhoneNumberCache.get(getContext()).getCachedNumberLookupService(); + if (cachedNumberLookupService != null) { + final RegularSearchListAdapter adapter = (RegularSearchListAdapter) getAdapter(); + cachedNumberLookupService.addContact( + getContext(), adapter.getContactInfo(cachedNumberLookupService, position)); + } + } + + @Override + protected void setupEmptyView() { + if (mEmptyView != null && getActivity() != null) { + final int imageResource; + final int actionLabelResource; + final int descriptionResource; + final OnEmptyViewActionButtonClickedListener listener; + if (!PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) { + imageResource = R.drawable.empty_contacts; + actionLabelResource = R.string.permission_single_turn_on; + descriptionResource = R.string.permission_no_search; + listener = this; + mPermissionToRequest = READ_CONTACTS; + } else { + imageResource = EmptyContentView.NO_IMAGE; + actionLabelResource = EmptyContentView.NO_LABEL; + descriptionResource = EmptyContentView.NO_LABEL; + listener = null; + mPermissionToRequest = null; + } + + mEmptyView.setImage(imageResource); + mEmptyView.setActionLabel(actionLabelResource); + mEmptyView.setDescription(descriptionResource); + if (listener != null) { + mEmptyView.setActionClickedListener(listener); + } + } + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (READ_CONTACTS.equals(mPermissionToRequest)) { + FragmentCompat.requestPermissions( + this, new String[] {mPermissionToRequest}, PERMISSION_REQUEST_CODE); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PERMISSION_REQUEST_CODE) { + setupEmptyView(); + if (grantResults != null + && grantResults.length == 1 + && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + PermissionsUtil.notifyPermissionGranted(getActivity(), permissions[0]); + } + } + } + + @Override + protected int getCallInitiationType(boolean isRemoteDirectory) { + return isRemoteDirectory + ? CallInitiationType.Type.REMOTE_DIRECTORY + : CallInitiationType.Type.REGULAR_SEARCH; + } + + public interface CapabilityChecker { + + boolean isNearbyPlacesSearchEnabled(); + } +} diff --git a/java/com/android/dialer/app/list/RegularSearchListAdapter.java b/java/com/android/dialer/app/list/RegularSearchListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..94544d2dbab66e741ddff062934a8e78b2dc937d --- /dev/null +++ b/java/com/android/dialer/app/list/RegularSearchListAdapter.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.text.TextUtils; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.list.DirectoryPartition; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.CallUtil; + +/** List adapter to display regular search results. */ +public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter { + + protected boolean mIsQuerySipAddress; + + public RegularSearchListAdapter(Context context) { + super(context); + setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, false); + setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, false); + } + + public CachedContactInfo getContactInfo(CachedNumberLookupService lookupService, int position) { + ContactInfo info = new ContactInfo(); + CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info); + final Cursor item = (Cursor) getItem(position); + if (item != null) { + final DirectoryPartition partition = + (DirectoryPartition) getPartition(getPartitionForPosition(position)); + final long directoryId = partition.getDirectoryId(); + final boolean isExtendedDirectory = isExtendedDirectory(directoryId); + + info.name = item.getString(PhoneQuery.DISPLAY_NAME); + info.type = item.getInt(PhoneQuery.PHONE_TYPE); + info.label = item.getString(PhoneQuery.PHONE_LABEL); + info.number = item.getString(PhoneQuery.PHONE_NUMBER); + final String photoUriStr = item.getString(PhoneQuery.PHOTO_URI); + info.photoUri = photoUriStr == null ? null : Uri.parse(photoUriStr); + /* + * An extended directory is custom directory in the app, but not a directory provided by + * framework. So it can't be USER_TYPE_WORK. + * + * When a search result is selected, RegularSearchFragment calls getContactInfo and + * cache the resulting @{link ContactInfo} into local db. Set usertype to USER_TYPE_WORK + * only if it's NOT extended directory id and is enterprise directory. + */ + info.userType = + !isExtendedDirectory && DirectoryCompat.isEnterpriseDirectoryId(directoryId) + ? ContactsUtils.USER_TYPE_WORK + : ContactsUtils.USER_TYPE_CURRENT; + + cacheInfo.setLookupKey(item.getString(PhoneQuery.LOOKUP_KEY)); + + final String sourceName = partition.getLabel(); + if (isExtendedDirectory) { + cacheInfo.setExtendedSource(sourceName, directoryId); + } else { + cacheInfo.setDirectorySource(sourceName, directoryId); + } + } + return cacheInfo; + } + + @Override + public String getFormattedQueryString() { + if (mIsQuerySipAddress) { + // Return unnormalized SIP address + return getQueryString(); + } + return super.getFormattedQueryString(); + } + + @Override + public void setQueryString(String queryString) { + // Don't show actions if the query string contains a letter. + final boolean showNumberShortcuts = + !TextUtils.isEmpty(getFormattedQueryString()) && hasDigitsInQueryString(); + mIsQuerySipAddress = PhoneNumberHelper.isUriNumber(queryString); + + if (isChanged(showNumberShortcuts)) { + notifyDataSetChanged(); + } + super.setQueryString(queryString); + } + + protected boolean isChanged(boolean showNumberShortcuts) { + boolean changed = false; + changed |= setShortcutEnabled(SHORTCUT_DIRECT_CALL, showNumberShortcuts || mIsQuerySipAddress); + changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts); + changed |= + setShortcutEnabled( + SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext())); + return changed; + } + + /** Whether there is at least one digit in the query string. */ + private boolean hasDigitsInQueryString() { + String queryString = getQueryString(); + int length = queryString.length(); + for (int i = 0; i < length; i++) { + if (Character.isDigit(queryString.charAt(i))) { + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/app/list/RemoveView.java b/java/com/android/dialer/app/list/RemoveView.java new file mode 100644 index 0000000000000000000000000000000000000000..3b917db43e6982549820ea447b145e498f83380f --- /dev/null +++ b/java/com/android/dialer/app/list/RemoveView.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.list; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.DragEvent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.dialer.app.R; + +public class RemoveView extends FrameLayout { + + DragDropController mDragDropController; + TextView mRemoveText; + ImageView mRemoveIcon; + int mUnhighlightedColor; + int mHighlightedColor; + Drawable mRemoveDrawable; + + public RemoveView(Context context) { + super(context); + } + + public RemoveView(Context context, AttributeSet attrs) { + this(context, attrs, -1); + } + + public RemoveView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + mRemoveText = (TextView) findViewById(R.id.remove_view_text); + mRemoveIcon = (ImageView) findViewById(R.id.remove_view_icon); + final Resources r = getResources(); + mUnhighlightedColor = r.getColor(R.color.remove_text_color); + mHighlightedColor = r.getColor(R.color.remove_highlighted_text_color); + mRemoveDrawable = r.getDrawable(R.drawable.ic_remove); + } + + public void setDragDropController(DragDropController controller) { + mDragDropController = controller; + } + + @Override + public boolean onDragEvent(DragEvent event) { + final int action = event.getAction(); + switch (action) { + case DragEvent.ACTION_DRAG_ENTERED: + // TODO: This is temporary solution and should be removed once accessibility for + // drag and drop is supported by framework(b/26871588). + sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT); + setAppearanceHighlighted(); + break; + case DragEvent.ACTION_DRAG_EXITED: + setAppearanceNormal(); + break; + case DragEvent.ACTION_DRAG_LOCATION: + if (mDragDropController != null) { + mDragDropController.handleDragHovered(this, (int) event.getX(), (int) event.getY()); + } + break; + case DragEvent.ACTION_DROP: + sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT); + if (mDragDropController != null) { + mDragDropController.handleDragFinished((int) event.getX(), (int) event.getY(), true); + } + setAppearanceNormal(); + break; + } + return true; + } + + private void setAppearanceNormal() { + mRemoveText.setTextColor(mUnhighlightedColor); + mRemoveIcon.setColorFilter(mUnhighlightedColor); + invalidate(); + } + + private void setAppearanceHighlighted() { + mRemoveText.setTextColor(mHighlightedColor); + mRemoveIcon.setColorFilter(mHighlightedColor); + invalidate(); + } +} diff --git a/java/com/android/dialer/app/list/SearchFragment.java b/java/com/android/dialer/app/list/SearchFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..4a7d48ae4c90b48d03c3636a08f9c93ca09f23de --- /dev/null +++ b/java/com/android/dialer/app/list/SearchFragment.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.app.DialogFragment; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.Space; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.ContactListItemView; +import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; +import com.android.contacts.common.list.PhoneNumberPickerFragment; +import com.android.contacts.common.util.FabUtil; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.app.R; +import com.android.dialer.app.dialpad.DialpadFragment.ErrorDialogFragment; +import com.android.dialer.app.widget.DialpadSearchEmptyContentView; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PermissionsUtil; + +public class SearchFragment extends PhoneNumberPickerFragment { + + protected EmptyContentView mEmptyView; + private OnListFragmentScrolledListener mActivityScrollListener; + private View.OnTouchListener mActivityOnTouchListener; + /* + * Stores the untouched user-entered string that is used to populate the add to contacts + * intent. + */ + private String mAddToContactNumber; + private int mActionBarHeight; + private int mShadowHeight; + private int mPaddingTop; + private int mShowDialpadDuration; + private int mHideDialpadDuration; + /** + * Used to resize the list view containing search results so that it fits the available space + * above the dialpad. Does not have a user-visible effect in regular touch usage (since the + * dialpad hides that portion of the ListView anyway), but improves usability in accessibility + * mode. + */ + private Space mSpacer; + + private HostInterface mActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + setQuickContactEnabled(true); + setAdjustSelectionBoundsEnabled(false); + setDarkTheme(false); + setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(false /* opposite */)); + setUseCallableUri(true); + + try { + mActivityScrollListener = (OnListFragmentScrolledListener) activity; + } catch (ClassCastException e) { + LogUtil.v( + "SearchFragment.onAttach", + activity.toString() + + " doesn't implement OnListFragmentScrolledListener. " + + "Ignoring."); + } + } + + @Override + public void onStart() { + super.onStart(); + if (isSearchMode()) { + getAdapter().setHasHeader(0, false); + } + + mActivity = (HostInterface) getActivity(); + + final Resources res = getResources(); + mActionBarHeight = mActivity.getActionBarHeight(); + mShadowHeight = res.getDrawable(R.drawable.search_shadow).getIntrinsicHeight(); + mPaddingTop = res.getDimensionPixelSize(R.dimen.search_list_padding_top); + mShowDialpadDuration = res.getInteger(R.integer.dialpad_slide_in_duration); + mHideDialpadDuration = res.getInteger(R.integer.dialpad_slide_out_duration); + + final ListView listView = getListView(); + + if (mEmptyView == null) { + if (this instanceof SmartDialSearchFragment) { + mEmptyView = new DialpadSearchEmptyContentView(getActivity()); + } else { + mEmptyView = new EmptyContentView(getActivity()); + } + ((ViewGroup) getListView().getParent()).addView(mEmptyView); + getListView().setEmptyView(mEmptyView); + setupEmptyView(); + } + + listView.setBackgroundColor(res.getColor(R.color.background_dialer_results)); + listView.setClipToPadding(false); + setVisibleScrollbarEnabled(false); + + //Turn of accessibility live region as the list constantly update itself and spam messages. + listView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE); + ContentChangedFilter.addToParent(listView); + + listView.setOnScrollListener( + new OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mActivityScrollListener != null) { + mActivityScrollListener.onListFragmentScrollStateChange(scrollState); + } + } + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} + }); + if (mActivityOnTouchListener != null) { + listView.setOnTouchListener(mActivityOnTouchListener); + } + + updatePosition(false /* animate */); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + FabUtil.addBottomPaddingToListViewForFab(getListView(), getResources()); + } + + @Override + public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { + Animator animator = null; + if (nextAnim != 0) { + animator = AnimatorInflater.loadAnimator(getActivity(), nextAnim); + } + if (animator != null) { + final View view = getView(); + final int oldLayerType = view.getLayerType(); + animator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setLayerType(oldLayerType, null); + } + }); + } + return animator; + } + + @Override + protected void setSearchMode(boolean flag) { + super.setSearchMode(flag); + // This hides the "All contacts with phone numbers" header in the search fragment + final ContactEntryListAdapter adapter = getAdapter(); + if (adapter != null) { + adapter.setHasHeader(0, false); + } + } + + public void setAddToContactNumber(String addToContactNumber) { + mAddToContactNumber = addToContactNumber; + } + + /** + * Return true if phone number is prohibited by a value - + * (R.string.config_prohibited_phone_number_regexp) in the config files. False otherwise. + */ + public boolean checkForProhibitedPhoneNumber(String number) { + // Regular expression prohibiting manual phone call. Can be empty i.e. "no rule". + String prohibitedPhoneNumberRegexp = + getResources().getString(R.string.config_prohibited_phone_number_regexp); + + // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated + // test equipment. + if (number != null + && !TextUtils.isEmpty(prohibitedPhoneNumberRegexp) + && number.matches(prohibitedPhoneNumberRegexp)) { + LogUtil.i( + "SearchFragment.checkForProhibitedPhoneNumber", + "the phone number is prohibited explicitly by a rule"); + if (getActivity() != null) { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message); + dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog"); + } + + return true; + } + return false; + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + DialerPhoneNumberListAdapter adapter = new DialerPhoneNumberListAdapter(getActivity()); + adapter.setDisplayPhotos(true); + adapter.setUseCallableUri(super.usesCallableUri()); + adapter.setListener(this); + return adapter; + } + + @Override + protected void onItemClick(int position, long id) { + final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); + final int shortcutType = adapter.getShortcutTypeFromPosition(position); + final OnPhoneNumberPickerActionListener listener; + final Intent intent; + final String number; + + LogUtil.i("SearchFragment.onItemClick", "shortcutType: " + shortcutType); + + switch (shortcutType) { + case DialerPhoneNumberListAdapter.SHORTCUT_INVALID: + super.onItemClick(position, id); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_DIRECT_CALL: + number = adapter.getQueryString(); + listener = getOnPhoneNumberPickerListener(); + if (listener != null && !checkForProhibitedPhoneNumber(number)) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = + getCallInitiationType(false /* isRemoteDirectory */); + callSpecificAppData.positionOfSelectedSearchResult = position; + callSpecificAppData.charactersInSearchString = + getQueryString() == null ? 0 : getQueryString().length(); + listener.onPickPhoneNumber(number, false /* isVideoCall */, callSpecificAppData); + } + break; + case DialerPhoneNumberListAdapter.SHORTCUT_CREATE_NEW_CONTACT: + number = + TextUtils.isEmpty(mAddToContactNumber) + ? adapter.getFormattedQueryString() + : mAddToContactNumber; + intent = IntentUtil.getNewContactIntent(number); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_ADD_TO_EXISTING_CONTACT: + number = + TextUtils.isEmpty(mAddToContactNumber) + ? adapter.getFormattedQueryString() + : mAddToContactNumber; + intent = IntentUtil.getAddToExistingContactIntent(number); + DialerUtils.startActivityWithErrorToast( + getActivity(), intent, R.string.add_contact_not_available); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_SEND_SMS_MESSAGE: + number = adapter.getFormattedQueryString(); + intent = IntentUtil.getSendSmsIntent(number); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_MAKE_VIDEO_CALL: + number = + TextUtils.isEmpty(mAddToContactNumber) ? adapter.getQueryString() : mAddToContactNumber; + listener = getOnPhoneNumberPickerListener(); + if (listener != null && !checkForProhibitedPhoneNumber(number)) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = + getCallInitiationType(false /* isRemoteDirectory */); + callSpecificAppData.positionOfSelectedSearchResult = position; + callSpecificAppData.charactersInSearchString = + getQueryString() == null ? 0 : getQueryString().length(); + listener.onPickPhoneNumber(number, true /* isVideoCall */, callSpecificAppData); + } + break; + } + } + + /** + * Updates the position and padding of the search fragment, depending on whether the dialpad is + * shown. This can be optionally animated. + */ + public void updatePosition(boolean animate) { + if (mActivity == null) { + // Activity will be set in onStart, and this method will be called again + return; + } + + // Use negative shadow height instead of 0 to account for the 9-patch's shadow. + int startTranslationValue = + mActivity.isDialpadShown() ? mActionBarHeight - mShadowHeight : -mShadowHeight; + int endTranslationValue = 0; + // Prevents ListView from being translated down after a rotation when the ActionBar is up. + if (animate || mActivity.isActionBarShowing()) { + endTranslationValue = mActivity.isDialpadShown() ? 0 : mActionBarHeight - mShadowHeight; + } + if (animate) { + // If the dialpad will be shown, then this animation involves sliding the list up. + final boolean slideUp = mActivity.isDialpadShown(); + + Interpolator interpolator = slideUp ? AnimUtils.EASE_IN : AnimUtils.EASE_OUT; + int duration = slideUp ? mShowDialpadDuration : mHideDialpadDuration; + getView().setTranslationY(startTranslationValue); + getView() + .animate() + .translationY(endTranslationValue) + .setInterpolator(interpolator) + .setDuration(duration) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (!slideUp) { + resizeListView(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (slideUp) { + resizeListView(); + } + } + }); + + } else { + getView().setTranslationY(endTranslationValue); + resizeListView(); + } + + // There is padding which should only be applied when the dialpad is not shown. + int paddingTop = mActivity.isDialpadShown() ? 0 : mPaddingTop; + final ListView listView = getListView(); + listView.setPaddingRelative( + listView.getPaddingStart(), + paddingTop, + listView.getPaddingEnd(), + listView.getPaddingBottom()); + } + + public void resizeListView() { + if (mSpacer == null) { + return; + } + int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0; + if (spacerHeight != mSpacer.getHeight()) { + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpacer.getLayoutParams(); + lp.height = spacerHeight; + mSpacer.setLayoutParams(lp); + } + } + + @Override + protected void startLoading() { + if (getActivity() == null) { + return; + } + + if (PermissionsUtil.hasContactsPermissions(getActivity())) { + super.startLoading(); + } else if (TextUtils.isEmpty(getQueryString())) { + // Clear out any existing call shortcuts. + final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); + adapter.disableAllShortcuts(); + } else { + // The contact list is not going to change (we have no results since permissions are + // denied), but the shortcuts might because of the different query, so update the + // list. + getAdapter().notifyDataSetChanged(); + } + + setupEmptyView(); + } + + public void setOnTouchListener(View.OnTouchListener onTouchListener) { + mActivityOnTouchListener = onTouchListener; + } + + @Override + protected View inflateView(LayoutInflater inflater, ViewGroup container) { + final LinearLayout parent = (LinearLayout) super.inflateView(inflater, container); + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + mSpacer = new Space(getActivity()); + parent.addView( + mSpacer, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0)); + } + return parent; + } + + protected void setupEmptyView() {} + + public interface HostInterface { + + boolean isActionBarShowing(); + + boolean isDialpadShown(); + + int getDialpadHeight(); + + int getActionBarHideOffset(); + + int getActionBarHeight(); + } +} diff --git a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..566a15d53d3812275eeca562d1bdc82fb49ba6a1 --- /dev/null +++ b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.list.ContactListItemView; +import com.android.dialer.app.dialpad.SmartDialCursorLoader; +import com.android.dialer.smartdial.SmartDialMatchPosition; +import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.util.CallUtil; +import java.util.ArrayList; + +/** List adapter to display the SmartDial search results. */ +public class SmartDialNumberListAdapter extends DialerPhoneNumberListAdapter { + + private static final String TAG = SmartDialNumberListAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + + @NonNull private final SmartDialNameMatcher mNameMatcher; + + public SmartDialNumberListAdapter(Context context) { + super(context); + mNameMatcher = new SmartDialNameMatcher("", SmartDialPrefix.getMap()); + setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false); + + if (DEBUG) { + Log.v(TAG, "Constructing List Adapter"); + } + } + + /** Sets query for the SmartDialCursorLoader. */ + public void configureLoader(SmartDialCursorLoader loader) { + if (DEBUG) { + Log.v(TAG, "Configure Loader with query" + getQueryString()); + } + + if (getQueryString() == null) { + loader.configureQuery(""); + mNameMatcher.setQuery(""); + } else { + loader.configureQuery(getQueryString()); + mNameMatcher.setQuery(PhoneNumberUtils.normalizeNumber(getQueryString())); + } + } + + /** + * Sets highlight options for a List item in the SmartDial search results. + * + * @param view ContactListItemView where the result will be displayed. + * @param cursor Object containing information of the associated List item. + */ + @Override + protected void setHighlight(ContactListItemView view, Cursor cursor) { + view.clearHighlightSequences(); + + if (mNameMatcher.matches(cursor.getString(PhoneQuery.DISPLAY_NAME))) { + final ArrayList nameMatches = mNameMatcher.getMatchPositions(); + for (SmartDialMatchPosition match : nameMatches) { + view.addNameHighlightSequence(match.start, match.end); + if (DEBUG) { + Log.v( + TAG, + cursor.getString(PhoneQuery.DISPLAY_NAME) + + " " + + mNameMatcher.getQuery() + + " " + + String.valueOf(match.start)); + } + } + } + + final SmartDialMatchPosition numberMatch = + mNameMatcher.matchesNumber(cursor.getString(PhoneQuery.PHONE_NUMBER)); + if (numberMatch != null) { + view.addNumberHighlightSequence(numberMatch.start, numberMatch.end); + } + } + + @Override + public void setQueryString(String queryString) { + final boolean showNumberShortcuts = !TextUtils.isEmpty(getFormattedQueryString()); + boolean changed = false; + changed |= setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts); + changed |= + setShortcutEnabled( + SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext())); + if (changed) { + notifyDataSetChanged(); + } + super.setQueryString(queryString); + } + + public void setShowEmptyListForNullQuery(boolean show) { + mNameMatcher.setShouldMatchEmptyQuery(!show); + } +} diff --git a/java/com/android/dialer/app/list/SmartDialSearchFragment.java b/java/com/android/dialer/app/list/SmartDialSearchFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..c783d3ac36764515f8642e68aae8327d6457216f --- /dev/null +++ b/java/com/android/dialer/app/list/SmartDialSearchFragment.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import static android.Manifest.permission.CALL_PHONE; + +import android.app.Activity; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v13.app.FragmentCompat; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.dialer.app.R; +import com.android.dialer.app.dialpad.SmartDialCursorLoader; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.util.PermissionsUtil; + +/** Implements a fragment to load and display SmartDial search results. */ +public class SmartDialSearchFragment extends SearchFragment + implements EmptyContentView.OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + private static final String TAG = SmartDialSearchFragment.class.getSimpleName(); + + private static final int CALL_PHONE_PERMISSION_REQUEST_CODE = 1; + + /** Creates a SmartDialListAdapter to display and operate on search results. */ + @Override + protected ContactEntryListAdapter createListAdapter() { + SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity()); + adapter.setUseCallableUri(super.usesCallableUri()); + adapter.setQuickContactEnabled(true); + adapter.setShowEmptyListForNullQuery(getShowEmptyListForNullQuery()); + // Set adapter's query string to restore previous instance state. + adapter.setQueryString(getQueryString()); + adapter.setListener(this); + return adapter; + } + + /** Creates a SmartDialCursorLoader object to load query results. */ + @Override + public Loader onCreateLoader(int id, Bundle args) { + // Smart dialing does not support Directory Load, falls back to normal search instead. + if (id == getDirectoryLoaderId()) { + return super.onCreateLoader(id, args); + } else { + final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter(); + SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext()); + loader.setShowEmptyListForNullQuery(getShowEmptyListForNullQuery()); + adapter.configureLoader(loader); + return loader; + } + } + + @Override + protected void setupEmptyView() { + if (mEmptyView != null && getActivity() != null) { + if (!PermissionsUtil.hasPermission(getActivity(), CALL_PHONE)) { + mEmptyView.setImage(R.drawable.empty_contacts); + mEmptyView.setActionLabel(R.string.permission_single_turn_on); + mEmptyView.setDescription(R.string.permission_place_call); + mEmptyView.setActionClickedListener(this); + } else { + mEmptyView.setImage(EmptyContentView.NO_IMAGE); + mEmptyView.setActionLabel(EmptyContentView.NO_LABEL); + mEmptyView.setDescription(EmptyContentView.NO_LABEL); + } + } + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + FragmentCompat.requestPermissions( + this, new String[] {CALL_PHONE}, CALL_PHONE_PERMISSION_REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == CALL_PHONE_PERMISSION_REQUEST_CODE) { + setupEmptyView(); + } + } + + @Override + protected int getCallInitiationType(boolean isRemoteDirectory) { + return CallInitiationType.Type.SMART_DIAL; + } + + public boolean isShowingPermissionRequest() { + return mEmptyView != null && mEmptyView.isShowingContent(); + } + + @Override + public void setShowEmptyListForNullQuery(boolean show) { + if (getAdapter() != null) { + ((SmartDialNumberListAdapter) getAdapter()).setShowEmptyListForNullQuery(show); + } + super.setShowEmptyListForNullQuery(show); + } +} diff --git a/java/com/android/dialer/app/list/SpeedDialFragment.java b/java/com/android/dialer/app/list/SpeedDialFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0f89028a2aa7eee70880a2cf27d66f050d3952 --- /dev/null +++ b/java/com/android/dialer/app/list/SpeedDialFragment.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dialer.app.list; + +import static android.Manifest.permission.READ_CONTACTS; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.Fragment; +import android.app.LoaderManager; +import android.content.CursorLoader; +import android.content.Loader; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Bundle; +import android.os.Trace; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentCompat; +import android.support.v4.util.LongSparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.LayoutAnimationController; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; +import android.widget.ListView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactTileLoaderFactory; +import com.android.contacts.common.list.ContactTileView; +import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; +import com.android.dialer.app.R; +import com.android.dialer.app.list.ListsFragment.ListsPage; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.util.ViewUtil; +import java.util.ArrayList; + +/** This fragment displays the user's favorite/frequent contacts in a grid. */ +public class SpeedDialFragment extends Fragment + implements ListsPage, + OnItemClickListener, + PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener, + EmptyContentView.OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; + + /** + * By default, the animation code assumes that all items in a list view are of the same height + * when animating new list items into view (e.g. from the bottom of the screen into view). This + * can cause incorrect translation offsets when a item that is larger or smaller than other list + * item is removed from the list. This key is used to provide the actual height of the removed + * object so that the actual translation appears correct to the user. + */ + private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE; + + private static final String TAG = "SpeedDialFragment"; + private static final boolean DEBUG = false; + /** Used with LoaderManager. */ + private static final int LOADER_ID_CONTACT_TILE = 1; + + private final LongSparseArray mItemIdTopMap = new LongSparseArray<>(); + private final LongSparseArray mItemIdLeftMap = new LongSparseArray<>(); + private final ContactTileView.Listener mContactTileAdapterListener = + new ContactTileAdapterListener(); + private final LoaderManager.LoaderCallbacks mContactTileLoaderListener = + new ContactTileLoaderListener(); + private final ScrollListener mScrollListener = new ScrollListener(); + private int mAnimationDuration; + private OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener; + private OnListFragmentScrolledListener mActivityScrollListener; + private PhoneFavoritesTileAdapter mContactTileAdapter; + private View mParentView; + private PhoneFavoriteListView mListView; + private View mContactTileFrame; + /** Layout used when there are no favorites. */ + private EmptyContentView mEmptyView; + + @Override + public void onCreate(Bundle savedState) { + if (DEBUG) { + LogUtil.d("SpeedDialFragment.onCreate", null); + } + Trace.beginSection(TAG + " onCreate"); + super.onCreate(savedState); + + // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter. + // We don't construct the resultant adapter at this moment since it requires LayoutInflater + // that will be available on onCreateView(). + mContactTileAdapter = + new PhoneFavoritesTileAdapter(getActivity(), mContactTileAdapterListener, this); + mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(getActivity())); + mAnimationDuration = getResources().getInteger(R.integer.fade_duration); + Trace.endSection(); + } + + @Override + public void onResume() { + Trace.beginSection(TAG + " onResume"); + super.onResume(); + if (mContactTileAdapter != null) { + mContactTileAdapter.refreshContactsPreferences(); + } + if (PermissionsUtil.hasContactsPermissions(getActivity())) { + if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) { + getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); + + } else { + getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); + } + + mEmptyView.setDescription(R.string.speed_dial_empty); + mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action); + } else { + mEmptyView.setDescription(R.string.permission_no_speeddial); + mEmptyView.setActionLabel(R.string.permission_single_turn_on); + } + Trace.endSection(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreateView"); + mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false); + + mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list); + mListView.setOnItemClickListener(this); + mListView.setVerticalScrollBarEnabled(false); + mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); + mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); + mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter); + + final ImageView dragShadowOverlay = + (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay); + mListView.setDragShadowOverlay(dragShadowOverlay); + + mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view); + mEmptyView.setImage(R.drawable.empty_speed_dial); + mEmptyView.setActionClickedListener(this); + + mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame); + + final LayoutAnimationController controller = + new LayoutAnimationController( + AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); + controller.setDelay(0); + mListView.setLayoutAnimation(controller); + mListView.setAdapter(mContactTileAdapter); + + mListView.setOnScrollListener(mScrollListener); + mListView.setFastScrollEnabled(false); + mListView.setFastScrollAlwaysVisible(false); + + //prevent content changes of the list from firing accessibility events. + mListView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE); + ContentChangedFilter.addToParent(mListView); + + Trace.endSection(); + return mParentView; + } + + public boolean hasFrequents() { + if (mContactTileAdapter == null) { + return false; + } + return mContactTileAdapter.getNumFrequents() > 0; + } + + /* package */ void setEmptyViewVisibility(final boolean visible) { + final int previousVisibility = mEmptyView.getVisibility(); + final int emptyViewVisibility = visible ? View.VISIBLE : View.GONE; + final int listViewVisibility = visible ? View.GONE : View.VISIBLE; + + if (previousVisibility != emptyViewVisibility) { + final FrameLayout.LayoutParams params = (LayoutParams) mContactTileFrame.getLayoutParams(); + params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; + mContactTileFrame.setLayoutParams(params); + mEmptyView.setVisibility(emptyViewVisibility); + mListView.setVisibility(listViewVisibility); + } + } + + @Override + public void onStart() { + super.onStart(); + + final Activity activity = getActivity(); + + try { + mActivityScrollListener = (OnListFragmentScrolledListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException( + activity.toString() + " must implement OnListFragmentScrolledListener"); + } + + try { + OnDragDropListener listener = (OnDragDropListener) activity; + mListView.getDragDropController().addOnDragDropListener(listener); + ((HostInterface) activity).setDragDropController(mListView.getDragDropController()); + } catch (ClassCastException e) { + throw new ClassCastException( + activity.toString() + " must implement OnDragDropListener and HostInterface"); + } + + try { + mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException( + activity.toString() + " must implement PhoneFavoritesFragment.listener"); + } + + // Use initLoader() instead of restartLoader() to refraining unnecessary reload. + // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will + // be called, on which we'll check if "all" contacts should be reloaded again or not. + if (PermissionsUtil.hasContactsPermissions(activity)) { + getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); + } else { + setEmptyViewVisibility(true); + } + } + + /** + * {@inheritDoc} + * + *

This is only effective for elements provided by {@link #mContactTileAdapter}. {@link + * #mContactTileAdapter} has its own logic for click events. + */ + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final int contactTileAdapterCount = mContactTileAdapter.getCount(); + if (position <= contactTileAdapterCount) { + LogUtil.e( + "SpeedDialFragment.onItemClick", + "event for unexpected position. The position " + + position + + " is before \"all\" section. Ignored."); + } + } + + /** + * Cache the current view offsets into memory. Once a relayout of views in the ListView has + * happened due to a dataset change, the cached offsets are used to create animations that slide + * views from their previous positions to their new ones, to give the appearance that the views + * are sliding into their new positions. + */ + private void saveOffsets(int removedItemHeight) { + final int firstVisiblePosition = mListView.getFirstVisiblePosition(); + if (DEBUG) { + LogUtil.d("SpeedDialFragment.saveOffsets", "Child count : " + mListView.getChildCount()); + } + for (int i = 0; i < mListView.getChildCount(); i++) { + final View child = mListView.getChildAt(i); + final int position = firstVisiblePosition + i; + // Since we are getting the position from mListView and then querying + // mContactTileAdapter, its very possible that things are out of sync + // and we might index out of bounds. Let's make sure that this doesn't happen. + if (!mContactTileAdapter.isIndexInBound(position)) { + continue; + } + final long itemId = mContactTileAdapter.getItemId(position); + if (DEBUG) { + LogUtil.d( + "SpeedDialFragment.saveOffsets", + "Saving itemId: " + itemId + " for listview child " + i + " Top: " + child.getTop()); + } + mItemIdTopMap.put(itemId, child.getTop()); + mItemIdLeftMap.put(itemId, child.getLeft()); + } + mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight); + } + + /* + * Performs animations for the gridView + */ + private void animateGridView(final long... idsInPlace) { + if (mItemIdTopMap.size() == 0) { + // Don't do animations if the database is being queried for the first time and + // the previous item offsets have not been cached, or the user hasn't done anything + // (dragging, swiping etc) that requires an animation. + return; + } + + ViewUtil.doOnPreDraw( + mListView, + true, + new Runnable() { + @Override + public void run() { + + final int firstVisiblePosition = mListView.getFirstVisiblePosition(); + final AnimatorSet animSet = new AnimatorSet(); + final ArrayList animators = new ArrayList(); + for (int i = 0; i < mListView.getChildCount(); i++) { + final View child = mListView.getChildAt(i); + int position = firstVisiblePosition + i; + + // Since we are getting the position from mListView and then querying + // mContactTileAdapter, its very possible that things are out of sync + // and we might index out of bounds. Let's make sure that this doesn't happen. + if (!mContactTileAdapter.isIndexInBound(position)) { + continue; + } + + final long itemId = mContactTileAdapter.getItemId(position); + + if (containsId(idsInPlace, itemId)) { + animators.add(ObjectAnimator.ofFloat(child, "alpha", 0.0f, 1.0f)); + break; + } else { + Integer startTop = mItemIdTopMap.get(itemId); + Integer startLeft = mItemIdLeftMap.get(itemId); + final int top = child.getTop(); + final int left = child.getLeft(); + int deltaX = 0; + int deltaY = 0; + + if (startLeft != null) { + if (startLeft != left) { + deltaX = startLeft - left; + animators.add(ObjectAnimator.ofFloat(child, "translationX", deltaX, 0.0f)); + } + } + + if (startTop != null) { + if (startTop != top) { + deltaY = startTop - top; + animators.add(ObjectAnimator.ofFloat(child, "translationY", deltaY, 0.0f)); + } + } + + if (DEBUG) { + LogUtil.d( + "SpeedDialFragment.onPreDraw", + "Found itemId: " + + itemId + + " for listview child " + + i + + " Top: " + + top + + " Delta: " + + deltaY); + } + } + } + + if (animators.size() > 0) { + animSet.setDuration(mAnimationDuration).playTogether(animators); + animSet.start(); + } + + mItemIdTopMap.clear(); + mItemIdLeftMap.clear(); + } + }); + } + + private boolean containsId(long[] ids, long target) { + // Linear search on array is fine because this is typically only 0-1 elements long + for (int i = 0; i < ids.length; i++) { + if (ids[i] == target) { + return true; + } + } + return false; + } + + @Override + public void onDataSetChangedForAnimation(long... idsInPlace) { + animateGridView(idsInPlace); + } + + @Override + public void cacheOffsetsForDatasetChange() { + saveOffsets(0); + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) { + FragmentCompat.requestPermissions( + this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE); + } else { + // Switch tabs + ((HostInterface) activity).showAllContactsTab(); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) { + if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS); + } + } + } + + @Override + public void onPageResume(@Nullable Activity activity) { + LogUtil.i("SpeedDialFragment.onPageResume", null); + } + + @Override + public void onPagePause(@Nullable Activity activity) { + LogUtil.i("SpeedDialFragment.onPagePause", null); + } + + public interface HostInterface { + + void setDragDropController(DragDropController controller); + + void showAllContactsTab(); + } + + private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks { + + @Override + public CursorLoader onCreateLoader(int id, Bundle args) { + if (DEBUG) { + LogUtil.d("ContactTileLoaderListener.onCreateLoader", null); + } + return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (DEBUG) { + LogUtil.d("ContactTileLoaderListener.onLoadFinished", null); + } + mContactTileAdapter.setContactCursor(data); + setEmptyViewVisibility(mContactTileAdapter.getCount() == 0); + } + + @Override + public void onLoaderReset(Loader loader) { + if (DEBUG) { + LogUtil.d("ContactTileLoaderListener.onLoaderReset", null); + } + } + } + + private class ContactTileAdapterListener implements ContactTileView.Listener { + + @Override + public void onContactSelected(Uri contactUri, Rect targetRect) { + if (mPhoneNumberPickerActionListener != null) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL; + mPhoneNumberPickerActionListener.onPickDataUri( + contactUri, false /* isVideoCall */, callSpecificAppData); + } + } + + @Override + public void onCallNumberDirectly(String phoneNumber) { + if (mPhoneNumberPickerActionListener != null) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL; + mPhoneNumberPickerActionListener.onPickPhoneNumber( + phoneNumber, false /* isVideoCall */, callSpecificAppData); + } + } + } + + private class ScrollListener implements ListView.OnScrollListener { + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mActivityScrollListener != null) { + mActivityScrollListener.onListFragmentScroll( + firstVisibleItem, visibleItemCount, totalItemCount); + } + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mActivityScrollListener.onListFragmentScrollStateChange(scrollState); + } + } +} diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..247b34f4cd851f11fcab10505d2bcda7b42fd84b --- /dev/null +++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/color/settings_text_color_primary.xml b/java/com/android/dialer/app/res/color/settings_text_color_primary.xml similarity index 83% rename from res/color/settings_text_color_primary.xml rename to java/com/android/dialer/app/res/color/settings_text_color_primary.xml index 862d8a2c39e71dce0d49aca66120c4fa899933a2..ba259088a642f4ccc5c1a6a29d53b42d69c6e270 100644 --- a/res/color/settings_text_color_primary.xml +++ b/java/com/android/dialer/app/res/color/settings_text_color_primary.xml @@ -18,6 +18,6 @@ --> - - + + diff --git a/res/color/settings_text_color_secondary.xml b/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml similarity index 83% rename from res/color/settings_text_color_secondary.xml rename to java/com/android/dialer/app/res/color/settings_text_color_secondary.xml index 0b00e468875073e808113657575dbdab41ccefbb..2f7899272e47d517f5c62963ee5aa5fef8121458 100644 --- a/res/color/settings_text_color_secondary.xml +++ b/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml @@ -18,6 +18,6 @@ --> - - + + diff --git a/res/drawable-hdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png similarity index 100% rename from res/drawable-hdpi/empty_call_log.png rename to java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png diff --git a/res/drawable-hdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png similarity index 100% rename from res/drawable-hdpi/empty_contacts.png rename to java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png diff --git a/res/drawable-hdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png similarity index 100% rename from res/drawable-hdpi/empty_speed_dial.png rename to java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png diff --git a/res/drawable-hdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png similarity index 100% rename from res/drawable-hdpi/fab_ic_dial.png rename to java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png diff --git a/res/drawable-hdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_archive_white_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png diff --git a/res/drawable-hdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png similarity index 100% rename from res/drawable-hdpi/ic_call_arrow.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png diff --git a/res/drawable-hdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_content_copy_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png diff --git a/res/drawable-hdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_delete_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png diff --git a/res/drawable-hdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png old mode 100755 new mode 100644 similarity index 100% rename from res/drawable-hdpi/ic_dialer_fork_add_call.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png diff --git a/res/drawable-hdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png old mode 100755 new mode 100644 similarity index 100% rename from res/drawable-hdpi/ic_dialer_fork_current_call.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png diff --git a/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png old mode 100755 new mode 100644 similarity index 100% rename from res/drawable-hdpi/ic_dialer_fork_tt_keypad.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png diff --git a/res/drawable-hdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_grade_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png diff --git a/res/drawable-hdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png similarity index 100% rename from res/drawable-hdpi/ic_handle.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png diff --git a/res/drawable-hdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png similarity index 100% rename from res/drawable-hdpi/ic_menu_history_lt.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png diff --git a/res/drawable-hdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png similarity index 100% rename from res/drawable-hdpi/ic_mic_grey600.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png diff --git a/res/drawable-hdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_more_vert_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png diff --git a/res/drawable-hdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_not_interested_googblue_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png new file mode 100644 index 0000000000000000000000000000000000000000..bf413f9122c095517884ab0e3ca46775f5c4b8bf Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2ea05c462291e4a4f8bd30856a25ad33fd420f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ff698afc0f44ec4628bb0b9dd60af34ae62863e6 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png differ diff --git a/res/drawable-hdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_phone_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png diff --git a/res/drawable-hdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_play_arrow_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png diff --git a/res/drawable-hdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png similarity index 100% rename from res/drawable-hdpi/ic_remove.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png diff --git a/res/drawable-hdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png similarity index 100% rename from res/drawable-hdpi/ic_results_phone.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png diff --git a/res/drawable-hdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_schedule_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png diff --git a/res/drawable-hdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_share_white_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png diff --git a/res/drawable-hdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png similarity index 100% rename from res/drawable-hdpi/ic_star.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_star.png diff --git a/res/drawable-hdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png similarity index 100% rename from res/drawable-hdpi/ic_unblock.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png diff --git a/res/drawable-hdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png similarity index 100% rename from res/drawable-hdpi/ic_vm_sound_off_dis.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png diff --git a/res/drawable-hdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png similarity index 100% rename from res/drawable-hdpi/ic_vm_sound_off_dk.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png diff --git a/res/drawable-hdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png similarity index 100% rename from res/drawable-hdpi/ic_vm_sound_on_dis.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png diff --git a/res/drawable-hdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png similarity index 100% rename from res/drawable-hdpi/ic_vm_sound_on_dk.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..03a62e15f90fc0cbfc0d743228e74a8ab242c781 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png differ diff --git a/res/drawable-hdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png similarity index 100% rename from res/drawable-hdpi/ic_volume_down_24dp.png rename to java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..57d787163e90cc34c16eca4e924adca106deb12c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png differ diff --git a/res/drawable-hdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png similarity index 100% rename from res/drawable-hdpi/search_shadow.9.png rename to java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png diff --git a/res/drawable-hdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png similarity index 100% rename from res/drawable-hdpi/shadow_contact_photo.png rename to java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png diff --git a/res/drawable-mdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png similarity index 100% rename from res/drawable-mdpi/empty_call_log.png rename to java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png diff --git a/res/drawable-mdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png similarity index 100% rename from res/drawable-mdpi/empty_contacts.png rename to java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png diff --git a/res/drawable-mdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png similarity index 100% rename from res/drawable-mdpi/empty_speed_dial.png rename to java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png diff --git a/res/drawable-mdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png similarity index 100% rename from res/drawable-mdpi/fab_ic_dial.png rename to java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png diff --git a/res/drawable-mdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_archive_white_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png diff --git a/res/drawable-mdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png similarity index 100% rename from res/drawable-mdpi/ic_call_arrow.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png diff --git a/res/drawable-mdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_content_copy_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png diff --git a/res/drawable-mdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_delete_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png diff --git a/res/drawable-mdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png similarity index 100% rename from res/drawable-mdpi/ic_dialer_fork_add_call.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png diff --git a/res/drawable-mdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png similarity index 100% rename from res/drawable-mdpi/ic_dialer_fork_current_call.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png diff --git a/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png similarity index 100% rename from res/drawable-mdpi/ic_dialer_fork_tt_keypad.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png diff --git a/res/drawable-mdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_grade_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png diff --git a/res/drawable-mdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png similarity index 100% rename from res/drawable-mdpi/ic_handle.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png diff --git a/res/drawable-mdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png similarity index 100% rename from res/drawable-mdpi/ic_menu_history_lt.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png diff --git a/res/drawable-mdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png similarity index 100% rename from res/drawable-mdpi/ic_mic_grey600.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png diff --git a/res/drawable-mdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_more_vert_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png diff --git a/res/drawable-mdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_not_interested_googblue_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f1c7efe88d4fc8f14e8cd85d2f98176bb65d54 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png differ diff --git a/res/drawable-mdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_pause_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png diff --git a/res/drawable-mdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_people_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png diff --git a/res/drawable-mdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_phone_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png diff --git a/res/drawable-mdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_play_arrow_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png diff --git a/res/drawable-mdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png similarity index 100% rename from res/drawable-mdpi/ic_remove.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png diff --git a/res/drawable-mdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png similarity index 100% rename from res/drawable-mdpi/ic_results_phone.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png diff --git a/res/drawable-mdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_schedule_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png diff --git a/res/drawable-mdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_share_white_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png diff --git a/res/drawable-mdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png similarity index 100% rename from res/drawable-mdpi/ic_star.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_star.png diff --git a/res/drawable-mdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png similarity index 100% rename from res/drawable-mdpi/ic_unblock.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png diff --git a/res/drawable-mdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png similarity index 100% rename from res/drawable-mdpi/ic_vm_sound_off_dis.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png diff --git a/res/drawable-mdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png similarity index 100% rename from res/drawable-mdpi/ic_vm_sound_off_dk.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png diff --git a/res/drawable-mdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png similarity index 100% rename from res/drawable-mdpi/ic_vm_sound_on_dis.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png diff --git a/res/drawable-mdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png similarity index 100% rename from res/drawable-mdpi/ic_vm_sound_on_dk.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png diff --git a/res/drawable-mdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_voicemail_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png diff --git a/res/drawable-mdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_volume_down_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png diff --git a/res/drawable-mdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png similarity index 100% rename from res/drawable-mdpi/ic_volume_up_24dp.png rename to java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png diff --git a/res/drawable-mdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png similarity index 100% rename from res/drawable-mdpi/search_shadow.9.png rename to java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png diff --git a/res/drawable-mdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png similarity index 100% rename from res/drawable-mdpi/shadow_contact_photo.png rename to java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png diff --git a/res/drawable-xhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png similarity index 100% rename from res/drawable-xhdpi/empty_call_log.png rename to java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png diff --git a/res/drawable-xhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png similarity index 100% rename from res/drawable-xhdpi/empty_contacts.png rename to java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png diff --git a/res/drawable-xhdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png similarity index 100% rename from res/drawable-xhdpi/empty_speed_dial.png rename to java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png diff --git a/res/drawable-xhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png similarity index 100% rename from res/drawable-xhdpi/fab_ic_dial.png rename to java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png diff --git a/res/drawable-xhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_archive_white_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png diff --git a/res/drawable-xhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png similarity index 100% rename from res/drawable-xhdpi/ic_call_arrow.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png diff --git a/res/drawable-xhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_content_copy_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png diff --git a/res/drawable-xhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_delete_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png diff --git a/res/drawable-xhdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png similarity index 100% rename from res/drawable-xhdpi/ic_dialer_fork_add_call.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png diff --git a/res/drawable-xhdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png similarity index 100% rename from res/drawable-xhdpi/ic_dialer_fork_current_call.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png diff --git a/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png similarity index 100% rename from res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png diff --git a/res/drawable-xhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_grade_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png diff --git a/res/drawable-xhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png similarity index 100% rename from res/drawable-xhdpi/ic_handle.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png diff --git a/res/drawable-xhdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png similarity index 100% rename from res/drawable-xhdpi/ic_menu_history_lt.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png diff --git a/res/drawable-xhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png similarity index 100% rename from res/drawable-xhdpi/ic_mic_grey600.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png diff --git a/res/drawable-xhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_more_vert_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png diff --git a/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_not_interested_googblue_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png new file mode 100644 index 0000000000000000000000000000000000000000..138f27cdbc7791d33a3dcd8e1fee39099dc6c593 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png differ diff --git a/res/drawable-xhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_pause_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png diff --git a/res/drawable-xhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_people_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png diff --git a/res/drawable-xhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_phone_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png diff --git a/res/drawable-xhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_play_arrow_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png diff --git a/res/drawable-xhdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png similarity index 100% rename from res/drawable-xhdpi/ic_remove.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png diff --git a/res/drawable-xhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png similarity index 100% rename from res/drawable-xhdpi/ic_results_phone.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png diff --git a/res/drawable-xhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_schedule_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png diff --git a/res/drawable-xhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_share_white_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png diff --git a/res/drawable-xhdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png similarity index 100% rename from res/drawable-xhdpi/ic_star.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png diff --git a/res/drawable-xhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png similarity index 100% rename from res/drawable-xhdpi/ic_unblock.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png diff --git a/res/drawable-xhdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png similarity index 100% rename from res/drawable-xhdpi/ic_vm_sound_off_dis.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png diff --git a/res/drawable-xhdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png similarity index 100% rename from res/drawable-xhdpi/ic_vm_sound_off_dk.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png diff --git a/res/drawable-xhdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png similarity index 100% rename from res/drawable-xhdpi/ic_vm_sound_on_dis.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png diff --git a/res/drawable-xhdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png similarity index 100% rename from res/drawable-xhdpi/ic_vm_sound_on_dk.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png diff --git a/res/drawable-xhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_voicemail_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png diff --git a/res/drawable-xhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_volume_down_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png diff --git a/res/drawable-xhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png similarity index 100% rename from res/drawable-xhdpi/ic_volume_up_24dp.png rename to java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png diff --git a/res/drawable-xhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png similarity index 100% rename from res/drawable-xhdpi/search_shadow.9.png rename to java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png diff --git a/res/drawable-xhdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png similarity index 100% rename from res/drawable-xhdpi/shadow_contact_photo.png rename to java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png diff --git a/res/drawable-xxhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png similarity index 100% rename from res/drawable-xxhdpi/empty_call_log.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png diff --git a/res/drawable-xxhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png similarity index 100% rename from res/drawable-xxhdpi/empty_contacts.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png diff --git a/res/drawable-xxhdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png similarity index 100% rename from res/drawable-xxhdpi/empty_speed_dial.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png diff --git a/res/drawable-xxhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png similarity index 100% rename from res/drawable-xxhdpi/fab_ic_dial.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png diff --git a/res/drawable-xxhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_archive_white_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png diff --git a/res/drawable-xxhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png similarity index 100% rename from res/drawable-xxhdpi/ic_call_arrow.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png diff --git a/res/drawable-xxhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_content_copy_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png diff --git a/res/drawable-xxhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_delete_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png diff --git a/res/drawable-xxhdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png similarity index 100% rename from res/drawable-xxhdpi/ic_dialer_fork_add_call.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png diff --git a/res/drawable-xxhdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png similarity index 100% rename from res/drawable-xxhdpi/ic_dialer_fork_current_call.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png diff --git a/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png similarity index 100% rename from res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png diff --git a/res/drawable-xxhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_grade_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png diff --git a/res/drawable-xxhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png similarity index 100% rename from res/drawable-xxhdpi/ic_handle.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png diff --git a/res/drawable-xxhdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png similarity index 100% rename from res/drawable-xxhdpi/ic_menu_history_lt.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png diff --git a/res/drawable-xxhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png similarity index 100% rename from res/drawable-xxhdpi/ic_mic_grey600.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png diff --git a/res/drawable-xxhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_more_vert_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png diff --git a/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png new file mode 100644 index 0000000000000000000000000000000000000000..f699959cb3fa31196c394e9b99a63f8ae477d50b Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7192ad487eacb4f8f530ebe2878760e2528fbc5f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6c68435fbc02c7ab472eca96805a807560fabcd3 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_phone_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png diff --git a/res/drawable-xxhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_play_arrow_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png diff --git a/res/drawable-xxhdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png similarity index 100% rename from res/drawable-xxhdpi/ic_remove.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png diff --git a/res/drawable-xxhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png similarity index 100% rename from res/drawable-xxhdpi/ic_results_phone.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png diff --git a/res/drawable-xxhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_schedule_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png diff --git a/res/drawable-xxhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_share_white_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png diff --git a/res/drawable-xxhdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png similarity index 100% rename from res/drawable-xxhdpi/ic_star.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png diff --git a/res/drawable-xxhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png similarity index 100% rename from res/drawable-xxhdpi/ic_unblock.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png diff --git a/res/drawable-xxhdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png similarity index 100% rename from res/drawable-xxhdpi/ic_vm_sound_off_dis.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png diff --git a/res/drawable-xxhdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png similarity index 100% rename from res/drawable-xxhdpi/ic_vm_sound_off_dk.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png diff --git a/res/drawable-xxhdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png similarity index 100% rename from res/drawable-xxhdpi/ic_vm_sound_on_dis.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png diff --git a/res/drawable-xxhdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png similarity index 100% rename from res/drawable-xxhdpi/ic_vm_sound_on_dk.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..28b8e936a05146771d8171de680a7cdefe0c9b2a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png similarity index 100% rename from res/drawable-xxhdpi/ic_volume_down_24dp.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2e751a40f53b82208e70aaa474d4395a81910a12 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png differ diff --git a/res/drawable-xxhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png similarity index 100% rename from res/drawable-xxhdpi/search_shadow.9.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png diff --git a/res/drawable-xxhdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png similarity index 100% rename from res/drawable-xxhdpi/shadow_contact_photo.png rename to java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png diff --git a/res/drawable-xxxhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png similarity index 100% rename from res/drawable-xxxhdpi/empty_call_log.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png diff --git a/res/drawable-xxxhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png similarity index 100% rename from res/drawable-xxxhdpi/empty_contacts.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png diff --git a/res/drawable-xxxhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png similarity index 100% rename from res/drawable-xxxhdpi/fab_ic_dial.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png diff --git a/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_archive_white_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png diff --git a/res/drawable-xxxhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png similarity index 100% rename from res/drawable-xxxhdpi/ic_call_arrow.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png diff --git a/res/drawable-xxxhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_content_copy_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png diff --git a/res/drawable-xxxhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_delete_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png diff --git a/res/drawable-xxxhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_grade_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png diff --git a/res/drawable-xxxhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png similarity index 100% rename from res/drawable-xxxhdpi/ic_handle.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png diff --git a/res/drawable-xxxhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png similarity index 100% rename from res/drawable-xxxhdpi/ic_mic_grey600.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png diff --git a/res/drawable-xxxhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_more_vert_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png diff --git a/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png new file mode 100644 index 0000000000000000000000000000000000000000..2a18de24e837e1eb1e2355617afa492f46d49145 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png differ diff --git a/res/drawable-xxxhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_pause_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png diff --git a/res/drawable-xxxhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_people_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png diff --git a/res/drawable-xxxhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_phone_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png diff --git a/res/drawable-xxxhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_play_arrow_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png diff --git a/res/drawable-xxxhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png similarity index 100% rename from res/drawable-xxxhdpi/ic_results_phone.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png diff --git a/res/drawable-xxxhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_schedule_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png diff --git a/res/drawable-xxxhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_share_white_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png diff --git a/res/drawable-xxxhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png similarity index 100% rename from res/drawable-xxxhdpi/ic_unblock.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png diff --git a/res/drawable-xxxhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_voicemail_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png diff --git a/res/drawable-xxxhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_volume_down_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png diff --git a/res/drawable-xxxhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png similarity index 100% rename from res/drawable-xxxhdpi/ic_volume_up_24dp.png rename to java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png diff --git a/res/drawable/background_dial_holo_dark.xml b/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml similarity index 84% rename from res/drawable/background_dial_holo_dark.xml rename to java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml index e06507f27302a6172fd0e335ae4a8dcaa35c162e..35afbe0250d05b4dac455c987cce467bb67937cf 100644 --- a/res/drawable/background_dial_holo_dark.xml +++ b/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml @@ -15,8 +15,8 @@ --> - + diff --git a/res/drawable/floating_action_button.xml b/java/com/android/dialer/app/res/drawable/floating_action_button.xml similarity index 76% rename from res/drawable/floating_action_button.xml rename to java/com/android/dialer/app/res/drawable/floating_action_button.xml index d550190a81d9d6a73dbf31bfdb3ea712f7a8266b..0b9af5229e4fccb9eae434cad55023f43d0f48ff 100644 --- a/res/drawable/floating_action_button.xml +++ b/java/com/android/dialer/app/res/drawable/floating_action_button.xml @@ -16,10 +16,10 @@ --> - - - - - + android:color="@color/floating_action_button_touch_tint"> + + + + + \ No newline at end of file diff --git a/res/drawable/ic_call_detail_content_copy.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml similarity index 87% rename from res/drawable/ic_call_detail_content_copy.xml rename to java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml index dd604dff72eb32bdffcfaea23f2da56bc3016ed9..87e0fbc6f21f682e3db096db5faef7132f752715 100644 --- a/res/drawable/ic_call_detail_content_copy.xml +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml @@ -16,5 +16,5 @@ --> + android:src="@drawable/ic_content_copy_24dp" + android:tint="@color/call_detail_footer_icon_tint"/> diff --git a/res/drawable/ic_call_detail_edit.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml similarity index 88% rename from res/drawable/ic_call_detail_edit.xml rename to java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml index e5ad3e59e3a2f70391fed820fadb34506866a4fd..e6d5c47767ec75be733aad4490c13acb6637f5c1 100644 --- a/res/drawable/ic_call_detail_edit.xml +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml @@ -16,5 +16,5 @@ --> + android:src="@drawable/ic_create_24dp" + android:tint="@color/call_detail_footer_icon_tint"/> diff --git a/res/drawable/ic_call_detail_report.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml similarity index 88% rename from res/drawable/ic_call_detail_report.xml rename to java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml index 201ac4cb6385388258d2c6fbaf8e844405f57867..e90e83e8b3dabae01346948a6536d6bc16ada748 100644 --- a/res/drawable/ic_call_detail_report.xml +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml @@ -16,5 +16,5 @@ --> + android:src="@drawable/ic_report_24dp" + android:tint="@color/call_detail_footer_icon_tint"/> diff --git a/res/drawable/ic_call_detail_unblock.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml similarity index 88% rename from res/drawable/ic_call_detail_unblock.xml rename to java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml index ba5378b10a62965db86fd7b6ecf21f694ebccc00..3b614cf0d31ab6046ac7dfc3cb9d18417ffcb3a8 100644 --- a/res/drawable/ic_call_detail_unblock.xml +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml @@ -16,5 +16,5 @@ --> + android:src="@drawable/ic_unblock" + android:tint="@color/call_detail_footer_icon_tint"/> diff --git a/java/com/android/dialer/app/res/drawable/ic_pause.xml b/java/com/android/dialer/app/res/drawable/ic_pause.xml new file mode 100644 index 0000000000000000000000000000000000000000..5bea581929326a0d5e3fbf6501a1d44747b2a392 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_pause.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml b/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml new file mode 100644 index 0000000000000000000000000000000000000000..d7d93501699208b9f49e399c5d0b06ab38c64068 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/res/drawable/ic_search_phone.xml b/java/com/android/dialer/app/res/drawable/ic_search_phone.xml similarity index 88% rename from res/drawable/ic_search_phone.xml rename to java/com/android/dialer/app/res/drawable/ic_search_phone.xml index ac9053273789e16fe4ccffb38e70afe2c597999a..5d449ee5665174ed4ede13207984bd46bfec7ce7 100644 --- a/res/drawable/ic_search_phone.xml +++ b/java/com/android/dialer/app/res/drawable/ic_search_phone.xml @@ -16,5 +16,5 @@ --> + android:src="@drawable/ic_results_phone" + android:tint="@color/search_shortcut_icon_color"/> diff --git a/res/drawable/ic_speakerphone_off.xml b/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml similarity index 83% rename from res/drawable/ic_speakerphone_off.xml rename to java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml index 3dfedeafbd68791a1f3b6889ba29447cacb17e59..f07d0a88996fab7b50966013dcf26dc111b78347 100644 --- a/res/drawable/ic_speakerphone_off.xml +++ b/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml @@ -15,6 +15,6 @@ --> - - + + diff --git a/res/drawable/ic_speakerphone_on.xml b/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml similarity index 83% rename from res/drawable/ic_speakerphone_on.xml rename to java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml index ae7bb32df3f66aa07b12cb46d416bd6f764e19af..456a0483ed718b00fdfb0da68a74719b6eb5bdb7 100644 --- a/res/drawable/ic_speakerphone_on.xml +++ b/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml @@ -15,6 +15,6 @@ --> - - + + diff --git a/res/drawable/ic_voicemail_seek_handle.xml b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml similarity index 88% rename from res/drawable/ic_voicemail_seek_handle.xml rename to java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml index d3fc95a65ac6915f5bb0477207339d0ffa1001f2..84cda03103c809b23e56f8d64c47f9661be7d6bb 100644 --- a/res/drawable/ic_voicemail_seek_handle.xml +++ b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml @@ -15,6 +15,6 @@ ~ limitations under the License --> + android:src="@drawable/ic_handle" + android:tint="@color/actionbar_background_color"> \ No newline at end of file diff --git a/res/drawable/ic_voicemail_seek_handle_disabled.xml b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml similarity index 87% rename from res/drawable/ic_voicemail_seek_handle_disabled.xml rename to java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml index 2be52ade6f611baba6d2a76c7693d06010723322..5e974c45a8a071e5c955853d51ed11dbd8d68a24 100644 --- a/res/drawable/ic_voicemail_seek_handle_disabled.xml +++ b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml @@ -15,6 +15,6 @@ ~ limitations under the License --> + android:src="@drawable/ic_handle" + android:tint="@color/voicemail_icon_disabled_tint"> \ No newline at end of file diff --git a/res/drawable/oval_ripple.xml b/java/com/android/dialer/app/res/drawable/oval_ripple.xml similarity index 80% rename from res/drawable/oval_ripple.xml rename to java/com/android/dialer/app/res/drawable/oval_ripple.xml index 0022d26719c1502a22043f81e10cb961d6f0c576..abb002588378cb77148163e7bee9310b4ca09844 100644 --- a/res/drawable/oval_ripple.xml +++ b/java/com/android/dialer/app/res/drawable/oval_ripple.xml @@ -17,10 +17,10 @@ --> - - - - - + android:color="?android:attr/colorControlHighlight"> + + + + + diff --git a/res/drawable/overflow_menu.xml b/java/com/android/dialer/app/res/drawable/overflow_menu.xml similarity index 84% rename from res/drawable/overflow_menu.xml rename to java/com/android/dialer/app/res/drawable/overflow_menu.xml index 0467d6bf1efcf39cbea99bb8dc37d1b06fb9de6c..81be5dcd53dbb199678db4cf4e3abd2e4c36bd61 100644 --- a/res/drawable/overflow_menu.xml +++ b/java/com/android/dialer/app/res/drawable/overflow_menu.xml @@ -15,6 +15,6 @@ ~ limitations under the License --> + android:autoMirrored="true" + android:src="@drawable/ic_overflow_menu" + android:tint="@color/actionbar_icon_color"/> diff --git a/res/drawable/rounded_corner.xml b/java/com/android/dialer/app/res/drawable/rounded_corner.xml similarity index 84% rename from res/drawable/rounded_corner.xml rename to java/com/android/dialer/app/res/drawable/rounded_corner.xml index fb8f4f56db4829d22369d12af4d575a3bf3eab3e..97b58b6b1c7a639bdc96a478727d6c43e85f9307 100644 --- a/res/drawable/rounded_corner.xml +++ b/java/com/android/dialer/app/res/drawable/rounded_corner.xml @@ -16,7 +16,7 @@ ~ limitations under the License --> - - + android:shape="rectangle"> + + diff --git a/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml b/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml new file mode 100644 index 0000000000000000000000000000000000000000..e47a6406c69ef7f8dd921e621a212a47b039c69b --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/selectable_primary_flat_button.xml b/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml similarity index 78% rename from res/drawable/selectable_primary_flat_button.xml rename to java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml index c6eb7a26a221aea205320ad3b1432ceae1f44ef5..47d1152dbb56ba6ca69b481ff4b8cdfd5b6c4b09 100644 --- a/res/drawable/selectable_primary_flat_button.xml +++ b/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml @@ -18,10 +18,14 @@ --> - - - + + + + + - + + + \ No newline at end of file diff --git a/res/drawable/shadow_fade_left.xml b/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml similarity index 80% rename from res/drawable/shadow_fade_left.xml rename to java/com/android/dialer/app/res/drawable/shadow_fade_left.xml index cb87cf536d2bf62ad15e107080cff51f46b85bee..6271a8f863028349c96efd5500f01c93e7addaa6 100644 --- a/res/drawable/shadow_fade_left.xml +++ b/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml @@ -15,10 +15,10 @@ --> - + android:shape="rectangle"> + diff --git a/res/drawable/shadow_fade_up.xml b/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml similarity index 80% rename from res/drawable/shadow_fade_up.xml rename to java/com/android/dialer/app/res/drawable/shadow_fade_up.xml index e961c860a122231de0b8660151e3a0b2a9f02f36..86d37a9bc4d37214bf76e443fda00c871cba60f2 100644 --- a/res/drawable/shadow_fade_up.xml +++ b/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml @@ -15,10 +15,10 @@ --> - + android:shape="rectangle"> + \ No newline at end of file diff --git a/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml b/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..8d8236a43aa2232982cb38fa7b44836f6dfff07c --- /dev/null +++ b/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml b/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f8068067a08a53e39bd8743e7d5593a13bdd82b --- /dev/null +++ b/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml b/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml new file mode 100644 index 0000000000000000000000000000000000000000..c6e186257661da05cdc08160d3e9db517d7a89c2 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/res/layout/all_contacts_activity.xml b/java/com/android/dialer/app/res/layout/all_contacts_activity.xml similarity index 75% rename from res/layout/all_contacts_activity.xml rename to java/com/android/dialer/app/res/layout/all_contacts_activity.xml index 50cba1eca84540012575e43884e91d1ac95b88ea..72f0a147f7d2b4a2f529ec899d19564f0c1a7147 100644 --- a/res/layout/all_contacts_activity.xml +++ b/java/com/android/dialer/app/res/layout/all_contacts_activity.xml @@ -15,11 +15,12 @@ --> + - + android:name="com.android.dialer.app.list.AllContactsFragment"/> diff --git a/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml b/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..f59847825ec1958f236aa90af2f32b8c4da42ee8 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/blocked_number_footer.xml b/java/com/android/dialer/app/res/layout/blocked_number_footer.xml new file mode 100644 index 0000000000000000000000000000000000000000..9e05cfbf409cec11c3a5d6ff2e39e5c731278157 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/blocked_number_footer.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml b/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..745b913ccde556df0a8d0860ca950884d98c4974 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/blocked_number_header.xml b/java/com/android/dialer/app/res/layout/blocked_number_header.xml new file mode 100644 index 0000000000000000000000000000000000000000..e34510b73df49f7994ec7c5ede6ef609be88fb9f --- /dev/null +++ b/java/com/android/dialer/app/res/layout/blocked_number_header.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + +