Messaging: Vendor chips, colorpicker, photoviewer, and vcard in-tree

Clone no longer depends on LineageOS gitlinks; the libraries used by the app live in this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-29 06:19:53 +09:00
co-authored by Cursor
parent a6b23f0f3c
commit 49789fc4a5
239 changed files with 35551 additions and 4 deletions
+1
View File
@@ -15,6 +15,7 @@
app/build/
modules/*/build/
modules/*/.git/
modules/*/sample/
*.apk
*.ap_
*.dex
Submodule modules/chips deleted from 7ccb8ba59b
+33
View File
@@ -0,0 +1,33 @@
# 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.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libchips
LOCAL_STATIC_ANDROID_LIBRARIES := \
android-support-compat \
android-support-annotations
LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := \
$(call all-java-files-under, src) \
$(call all-logtags-files-under, src)
LOCAL_USE_AAPT2 := true
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
include $(BUILD_STATIC_JAVA_LIBRARY)
##################################################
# Build all sub-directories
include $(call all-makefiles-under,$(LOCAL_PATH))
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ex.chips"
android:versionCode="1">
<application />
</manifest>
+31
View File
@@ -0,0 +1,31 @@
plugins {
id 'com.android.library'
}
android {
namespace 'com.android.ex.chips'
compileSdk 35
defaultConfig {
minSdk 33
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
res.srcDirs = ['res']
}
}
}
dependencies {
implementation 'androidx.annotation:annotation:1.8.2'
implementation 'androidx.core:core:1.15.0'
implementation 'androidx.collection:collection:1.4.4'
}
+15
View File
@@ -0,0 +1,15 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-19
android.library=true
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:color="@color/chips_dropdown_text_activated"/>
<item android:color="@color/chips_dropdown_text_default"/>
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="90"
android:startColor="@android:color/transparent"
android:endColor="@color/autocomplete_bottom_shadow_start" />
</shape>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/chips_dropdown_background_activated"
android:state_activated="true"/>
<item android:drawable="@color/chips_dropdown_background_pressed" android:state_pressed="true"/>
<item android:drawable="@android:color/white"/>
</selector>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:color="@android:color/white" />
<item android:state_checked="true" android:color="@android:color/white" />
<item android:state_selected="true" android:color="@android:color/white" />
<item android:color="#333333"/>
</selector>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:color="@android:color/white" />
<item android:state_checked="true" android:color="@android:color/white" />
<item android:state_selected="true" android:color="@android:color/white" />
<item android:color="#777777"/>
</selector>
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical">
<View
android:id="@+id/chip_autocomplete_top_divider"
style="@style/ChipAutocompleteDividerStyle" />
<LinearLayout
android:id="@+id/chip_person_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/ChipAutocompleteWrapperStyle">
<LinearLayout style="@style/ChipTextViewLayoutStyle">
<TextView style="@style/ChipTitleStyle" />
<TextView style="@style/ChipSubtitleStyle" />
</LinearLayout>
<com.android.ex.chips.CircularImageView style="@style/ChipEndIconStyle" />
</LinearLayout>
<LinearLayout
android:id="@+id/chip_permission_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/ChipAutocompleteWrapperStyle">
<TextView style="@style/ChipPermissionStyle" />
<ImageView style="@style/ChipPermissionDismissIconStyle"/>
</LinearLayout>
<View
android:id="@+id/chip_autocomplete_bottom_divider"
style="@style/ChipAutocompleteDividerStyle" />
<View
android:id="@+id/chip_permission_bottom_divider"
android:visibility="gone"
style="@style/ChipAutocompleteDividerStyle.PermissionItem" />
</LinearLayout>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/chips_dropdown_background"
android:minHeight="@dimen/chip_dropdown_height"
android:padding="12dp">
<com.android.ex.chips.CircularImageView style="@style/ChipStartIconStyle" />
<LinearLayout style="@style/ChipTextViewLayoutStyle">
<TextView style="@style/ChipTitleStyle" />
<TextView style="@style/ChipSubtitleStyle" />
<TextView
android:id="@+id/chip_indicator_text"
style="@style/ChipIndicatorTextStyle" />
</LinearLayout>
<ImageView style="@style/ChipDeleteIconStyle" />
</LinearLayout>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text1"
android:textColor="#aaaaaa"
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/more_string"
android:paddingLeft="8dip"
android:paddingRight="8dip"
/>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<dimen name="chip_height">26dip</dimen>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:tools="http://schemas.android.com/tools"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<style name="RecipientEditTextView" parent="@style/RecipientEditTextViewBase">
<item name="android:imeOptions">actionNext</item>
</style>
</resources>
+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<style name="ChipAutocompleteWrapperStyle">
<item name="android:paddingStart">@dimen/chip_wrapper_start_padding</item>
<item name="android:paddingTop">@dimen/chip_wrapper_top_padding</item>
<item name="android:paddingEnd">@dimen/chip_wrapper_end_padding</item>
<item name="android:paddingBottom">@dimen/chip_wrapper_bottom_padding</item>
</style>
<style name="ChipAutocompleteDividerStyle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1px</item>
<item name="android:background">@color/autocomplete_divider_color</item>
<item name="android:layout_marginStart">@dimen/chip_wrapper_start_padding</item>
</style>
<style name="ChipAutocompleteDividerStyle.PermissionItem">
<item name="android:background">@drawable/chip_autocomplete_divider_background</item>
<item name="android:layout_height">3dp</item>
<item name="android:layout_marginStart">0dp</item>
</style>
<style name="ChipStartIconStyle" parent="@style/ChipIconBaseStyle">
<item name="android:layout_marginEnd">@dimen/chip_icon_margin_end</item>
</style>
<style name="ChipEndIconStyle" parent="@style/ChipIconBaseStyle">
<item name="android:layout_marginStart">@dimen/chip_icon_margin_end</item>
</style>
<style name="ChipDeleteIconStyle" parent="@style/ChipDeleteIconBaseStyle">
<item name="android:paddingStart">@dimen/chip_icon_padding</item>
</style>
</resources>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<declare-styleable name="RecipientEditTextView">
<attr name="avatarPosition">
<enum name="end" value="0" />
<enum name="start" value="1" />
</attr>
<attr name="chipBackground" format="reference" />
<attr name="chipDelete" format="reference" />
<attr name="chipFontSize" format="reference" />
<attr name="chipHeight" format="reference" />
<attr name="chipPadding" format="reference" />
<attr name="disableDelete" format="boolean" />
<attr name="invalidChipBackground" format="reference" />
<attr name="imageSpanAlignment">
<enum name="bottom" value = "0"/>
<enum name="baseline" value = "1"/>
</attr>
<attr name="unselectedChipBackgroundColor" format="color" />
<attr name="unselectedChipTextColor" format="color" />
</declare-styleable>
</resources>
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<color name="chips_dropdown_background_activated">#4285f4</color>
<color name="chips_dropdown_background_pressed">#ededed</color>
<color name="autocomplete_divider_color">#cccccc</color>
<color name="autocomplete_bottom_shadow_start">#40000000</color>
<color name="chip_background">#f6f6f6</color>
<color name="chip_background_invalid">#db4437</color>
<color name="chips_dropdown_text_default">#212121</color>
<color name="chips_dropdown_text_activated">@android:color/white</color>
<color name="chips_dropdown_text_highlighted">#757575</color>
<color name="chips_dropdown_permission_text">#4285f4</color>
</resources>
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<!-- override default padding values, -1dp if it's not set to override -->
<dimen name="chip_padding_start">-1dp</dimen>
<dimen name="chip_padding_end">12dp</dimen>
<!-- chip -->
<dimen name="chip_padding">8dp</dimen>
<dimen name="chip_height">32dp</dimen>
<dimen name="chip_text_size">14sp</dimen>
<dimen name="line_spacing_extra">3dp</dimen>
<integer name="chips_max_lines">-1</integer>
<!-- dropdown -->
<dimen name="chip_icon_margin_end">12dp</dimen>
<dimen name="chip_dropdown_height">72dp</dimen>
<dimen name="chip_custom_divider_height">0.5dp</dimen>
<dimen name="chip_icon_padding">8dp</dimen>
<!-- autocomplete dropdown -->
<dimen name="chip_wrapper_start_padding">16dp</dimen>
<dimen name="chip_wrapper_top_padding">16dp</dimen>
<dimen name="chip_wrapper_end_padding">16dp</dimen>
<dimen name="chip_wrapper_bottom_padding">16dp</dimen>
</resources>
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Text displayed when the recipientedittextview is not focused. Displays the total number of recipients since the field is shrunk to just display a portion -->
<string name="more_string">\u002B<xliff:g id="count">%1$d</xliff:g></string>
<!-- Text displayed when the user long presses on a chip to copy the recipients email address [CHAR LIMIT=200] -->
<string name="copy_email">Copy email address</string>
<!-- Text displayed when the user long presses on a chip to copy the recipient's phone number [CHAR LIMIT=200] -->
<string name="copy_number">Copy phone number</string>
<!-- Text displayed in the enter key slot when the recipientedittextview has focus [CHAR LIMIT=12] -->
<string name="action_label">Return</string>
<!-- Announce the number of possible recipient entries that the user can select from for accessibility purposes [CHAR LIMIT=200] -->
<string name="accessbility_suggestion_dropdown_opened">Contact suggestions opened</string>
<!-- Announce that there are no possible recipient entries that the user can select from for accessibility purposes [CHAR LIMIT=200] -->
<string name="accessbility_suggestion_dropdown_closed">No suggestions available</string>
<!-- Description for the delete button that removes the selected contact from the recipients [CHAR LIMIT=100] -->
<string name="dropdown_delete_button_desc">Remove <xliff:g id="contact">%s</xliff:g> from recipients</string>
<!-- Text displayed as the primary action for a dialog to copy text. [CHAR LIMIT=25] -->
<string name="chips_action_copy">Copy</string>
<!-- Text displayed as the secondary action for a dialog to cancel. [CHAR LIMIT=25] -->
<string name="chips_action_cancel">Cancel</string>
<!-- Text displayed on the drop down item that is shown when app doesn't have Contact permission. This item will open dialog to request permission. [CHAR LIMIT=50] -->
<string name="chips_permission_text">Allow contacts suggestions</string>
</resources>
+139
View File
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="RecipientEditTextViewBase">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@null</item>
<item name="android:inputType">textEmailAddress|textMultiLine</item>
<item name="android:minHeight">@dimen/chip_height</item>
<item name="android:popupBackground">@android:color/white</item>
<item name="android:textAlignment" tools:ignore="NewApi">viewStart</item>
<item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
<item name="android:textDirection" tools:ignore="NewApi">locale</item>
</style>
<style name="RecipientEditTextView" parent="@style/RecipientEditTextViewBase">
<item name="android:imeOptions">actionNext|flagNoFullscreen</item>
</style>
<style name="ChipAutocompleteWrapperStyle">
<item name="android:paddingLeft">@dimen/chip_wrapper_start_padding</item>
<item name="android:paddingTop">@dimen/chip_wrapper_top_padding</item>
<item name="android:paddingRight">@dimen/chip_wrapper_end_padding</item>
<item name="android:paddingBottom">@dimen/chip_wrapper_bottom_padding</item>
</style>
<style name="ChipAutocompleteDividerStyle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">@dimen/chip_custom_divider_height</item>
<item name="android:background">@color/autocomplete_divider_color</item>
<item name="android:layout_marginLeft">@dimen/chip_wrapper_start_padding</item>
</style>
<style name="ChipAutocompleteDividerStyle.PermissionItem">
<item name="android:background">@drawable/chip_autocomplete_divider_background</item>
<item name="android:layout_height">3dp</item>
<item name="android:layout_marginLeft">0dp</item>
</style>
<style name="ChipTextViewLayoutStyle">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:layout_weight">1</item>
<item name="android:orientation">vertical</item>
</style>
<style name="ChipTitleStyle">
<item name="android:id">@android:id/title</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:ellipsize">middle</item>
<item name="android:singleLine">true</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:textColor">@color/chips_dropdown_text</item>
<item name="android:textSize">16sp</item>
</style>
<style name="ChipSubtitleStyle">
<item name="android:id">@android:id/text1</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:ellipsize">middle</item>
<item name="android:singleLine">true</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:textColor">@color/chips_dropdown_text</item>
<item name="android:textSize">14sp</item>
</style>
<style name="ChipIndicatorTextStyle" parent="@style/ChipSubtitleStyle">
<item name="android:layout_marginTop">6dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:drawablePadding">4dp</item>
</style>
<style name="ChipPermissionStyle">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:layout_weight">1</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:textColor">@color/chips_dropdown_permission_text</item>
<item name="android:textSize">16sp</item>
<item name="android:text">@string/chips_permission_text</item>
</style>
<style name="ChipIconBaseStyle">
<item name="android:id">@android:id/icon</item>
<item name="android:layout_width">40dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:contentDescription">@null</item>
</style>
<style name="ChipStartIconStyle" parent="@style/ChipIconBaseStyle">
<item name="android:layout_marginRight">@dimen/chip_icon_margin_end</item>
</style>
<style name="ChipEndIconStyle" parent="@style/ChipIconBaseStyle">
<item name="android:layout_marginLeft">@dimen/chip_icon_margin_end</item>
</style>
<style name="ChipDeleteIconBaseStyle">
<item name="android:id">@android:id/icon1</item>
<item name="android:layout_width">36dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:paddingTop">10dp</item>
<item name="android:paddingBottom">10dp</item>
</style>
<style name="ChipDeleteIconStyle" parent="@style/ChipDeleteIconBaseStyle">
<item name="android:paddingLeft">@dimen/chip_icon_padding</item>
</style>
<style name="ChipPermissionDismissIconStyle">
<item name="android:id">@android:id/icon2</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:background">?android:attr/selectableItemBackground</item>
<item name="android:contentDescription">@android:string/cancel</item>
<item name="android:padding">@dimen/chip_icon_padding</item>
<item name="android:src">@drawable/chips_ic_close_24dp</item>
</style>
</resources>
@@ -0,0 +1,28 @@
/*
* 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.ex.chips;
import android.accounts.Account;
/**
* The AccountSpecificAdapter interface describes an Adapter
* that can take an account to retrieve information tied to
* a specific account.
*/
public interface AccountSpecifier {
public void setAccount(Account account);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,92 @@
/*
* 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.ex.chips;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Process;
import androidx.annotation.Nullable;
public class ChipsUtil {
/**
* Listener that gets notified when we check whether we have permission.
*/
public interface PermissionsCheckListener {
void onPermissionCheck(String permission, boolean granted);
}
/**
* Permissions required by Chips library.
*/
public static final String[] REQUIRED_PERMISSIONS =
new String[] { Manifest.permission.READ_CONTACTS };
/**
* Returns true when the caller can use Chips UI in its environment.
*/
public static boolean supportsChipsUi() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
}
/**
* Whether we are running on M or later version.
*
* <p>This is interesting for us because new permission model is introduced in M and we need to
* check if we have {@link #REQUIRED_PERMISSIONS}.
*/
public static boolean isRunningMOrLater() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
/**
* Returns {@link PackageManager#PERMISSION_GRANTED} if given permission is granted, or
* {@link PackageManager#PERMISSION_DENIED} if not.
*/
public static int checkPermission(Context context, String permission) {
if (isRunningMOrLater()) {
// TODO: Use "context.checkSelfPermission(permission)" once it's safe to move to M sdk
return context.checkPermission(permission, Process.myPid(), Process.myUid());
} else {
// Assume that we have permission before M.
return PackageManager.PERMISSION_GRANTED;
}
}
/**
* Returns true if all permissions in {@link #REQUIRED_PERMISSIONS} are granted.
*
* <p>If {@link PermissionsCheckListener} is specified it will be called for every
* {@link #checkPermission} call.
*/
public static boolean hasPermissions(Context context,
@Nullable PermissionsCheckListener permissionsCheckListener) {
for (String permission : REQUIRED_PERMISSIONS) {
final boolean granted =
checkPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
if (permissionsCheckListener != null) {
permissionsCheckListener.onPermissionCheck(permission, granted);
}
if (!granted) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,114 @@
package com.android.ex.chips;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* An ImageView class with a circle mask so that all images are drawn in a
* circle instead of a square.
*/
public class CircularImageView extends ImageView {
private static float circularImageBorder = 1f;
private final Matrix matrix;
private final RectF source;
private final RectF destination;
private final Paint bitmapPaint;
private final Paint borderPaint;
public CircularImageView(Context context) {
this(context, null, 0);
}
public CircularImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircularImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
matrix = new Matrix();
source = new RectF();
destination = new RectF();
bitmapPaint = new Paint();
bitmapPaint.setAntiAlias(true);
bitmapPaint.setFilterBitmap(true);
bitmapPaint.setDither(true);
borderPaint = new Paint();
borderPaint.setColor(Color.TRANSPARENT);
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(circularImageBorder);
borderPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
BitmapDrawable bitmapDrawable = null;
// support state list drawable by getting the current state
if (drawable instanceof StateListDrawable) {
if (((StateListDrawable) drawable).getCurrent() != null) {
bitmapDrawable = (BitmapDrawable) drawable.getCurrent();
}
} else {
bitmapDrawable = (BitmapDrawable) drawable;
}
if (bitmapDrawable == null) {
return;
}
Bitmap bitmap = bitmapDrawable.getBitmap();
if (bitmap == null) {
return;
}
source.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
destination.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom());
drawBitmapWithCircleOnCanvas(bitmap, canvas, source, destination);
}
/**
* Given the source bitmap and a canvas, draws the bitmap through a circular
* mask. Only draws a circle with diameter equal to the destination width.
*
* @param bitmap The source bitmap to draw.
* @param canvas The canvas to draw it on.
* @param source The source bound of the bitmap.
* @param dest The destination bound on the canvas.
*/
public void drawBitmapWithCircleOnCanvas(Bitmap bitmap, Canvas canvas,
RectF source, RectF dest) {
// Draw bitmap through shader first.
BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP);
matrix.reset();
// Fit bitmap to bounds.
matrix.setRectToRect(source, dest, Matrix.ScaleToFit.FILL);
shader.setLocalMatrix(matrix);
bitmapPaint.setShader(shader);
canvas.drawCircle(dest.centerX(), dest.centerY(), dest.width() / 2f,
bitmapPaint);
// Then draw the border.
canvas.drawCircle(dest.centerX(), dest.centerY(),
dest.width() / 2f - circularImageBorder / 2, borderPaint);
}
}
@@ -0,0 +1,51 @@
package com.android.ex.chips;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
/**
* Simple dialog fragment for copying the contents of a chip.
*/
public class CopyDialog extends DialogFragment implements DialogInterface.OnClickListener {
public static final String TAG = "chips-copy-dialog";
private static final String ARG_TEXT = "text";
private String mText;
public static CopyDialog newInstance(String text) {
final CopyDialog fragment = new CopyDialog();
final Bundle args = new Bundle(1);
args.putString(ARG_TEXT, text);
fragment.setArguments(args);
return fragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle args = getArguments();
mText = args.getString(ARG_TEXT);
return new AlertDialog.Builder(getActivity())
.setMessage(mText)
.setPositiveButton(R.string.chips_action_copy, this)
.setNegativeButton(R.string.chips_action_cancel, null)
.create();
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
final ClipboardManager clipboard = (ClipboardManager)
getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(null, mText));
}
}
}
@@ -0,0 +1,145 @@
/*
* 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.ex.chips;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import androidx.collection.LruCache;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Default implementation of {@link com.android.ex.chips.PhotoManager} that
* queries for photo bytes by using the {@link com.android.ex.chips.RecipientEntry}'s
* photoThumbnailUri.
*/
public class DefaultPhotoManager implements PhotoManager {
private static final String TAG = "DefaultPhotoManager";
private static final boolean DEBUG = false;
/**
* For reading photos for directory contacts, this is the chunk size for
* copying from the {@link InputStream} to the output stream.
*/
private static final int BUFFER_SIZE = 1024*16;
private static class PhotoQuery {
public static final String[] PROJECTION = {
ContactsContract.CommonDataKinds.Photo.PHOTO
};
public static final int PHOTO = 0;
}
private final ContentResolver mContentResolver;
private final LruCache<Uri, byte[]> mPhotoCacheMap;
public DefaultPhotoManager(ContentResolver contentResolver) {
mContentResolver = contentResolver;
mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE);
}
@Override
public void populatePhotoBytesAsync(RecipientEntry entry, PhotoManagerCallback callback) {
final Uri photoThumbnailUri = entry.getPhotoThumbnailUri();
if (photoThumbnailUri != null) {
final byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri);
if (photoBytes != null) {
entry.setPhotoBytes(photoBytes);
if (callback != null) {
callback.onPhotoBytesPopulated();
}
} else {
if (DEBUG) {
Log.d(TAG, "No photo cache for " + entry.getDisplayName()
+ ". Fetch one asynchronously");
}
fetchPhotoAsync(entry, photoThumbnailUri, callback);
}
} else if (callback != null) {
callback.onPhotoBytesAsyncLoadFailed();
}
}
private void fetchPhotoAsync(final RecipientEntry entry, final Uri photoThumbnailUri,
final PhotoManagerCallback callback) {
final AsyncTask<Void, Void, byte[]> photoLoadTask = new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(Void... params) {
// First try running a query. Images for local contacts are
// loaded by sending a query to the ContactsProvider.
final Cursor photoCursor = mContentResolver.query(
photoThumbnailUri, PhotoQuery.PROJECTION, null, null, null);
if (photoCursor != null) {
try {
if (photoCursor.moveToFirst()) {
return photoCursor.getBlob(PhotoQuery.PHOTO);
}
} finally {
photoCursor.close();
}
} else {
// If the query fails, try streaming the URI directly.
// For remote directory images, this URI resolves to the
// directory provider and the images are loaded by sending
// an openFile call to the provider.
try {
InputStream is = mContentResolver.openInputStream(
photoThumbnailUri);
if (is != null) {
byte[] buffer = new byte[BUFFER_SIZE];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
int size;
while ((size = is.read(buffer)) != -1) {
baos.write(buffer, 0, size);
}
} finally {
is.close();
}
return baos.toByteArray();
}
} catch (IOException ex) {
// ignore
}
}
return null;
}
@Override
protected void onPostExecute(final byte[] photoBytes) {
entry.setPhotoBytes(photoBytes);
if (photoBytes != null) {
mPhotoCacheMap.put(photoThumbnailUri, photoBytes);
if (callback != null) {
callback.onPhotoBytesAsynchronouslyPopulated();
}
} else if (callback != null) {
callback.onPhotoBytesAsyncLoadFailed();
}
}
};
photoLoadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
}
@@ -0,0 +1,569 @@
package com.android.ex.chips;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.net.Uri;
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.core.view.MarginLayoutParamsCompat;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.util.Rfc822Tokenizer;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.ex.chips.Queries.Query;
/**
* A class that inflates and binds the views in the dropdown list from
* RecipientEditTextView.
*/
public class DropdownChipLayouter {
/**
* The type of adapter that is requesting a chip layout.
*/
public enum AdapterType {
BASE_RECIPIENT,
RECIPIENT_ALTERNATES,
SINGLE_RECIPIENT
}
public interface ChipDeleteListener {
void onChipDelete();
}
/**
* Listener that handles the dismisses of the entries of the
* {@link RecipientEntry#ENTRY_TYPE_PERMISSION_REQUEST} type.
*/
public interface PermissionRequestDismissedListener {
/**
* Callback that occurs when user dismisses the item that asks user to grant permissions to
* the app.
*/
void onPermissionRequestDismissed();
}
private final LayoutInflater mInflater;
private final Context mContext;
private ChipDeleteListener mDeleteListener;
private PermissionRequestDismissedListener mPermissionRequestDismissedListener;
private Query mQuery;
private int mAutocompleteDividerMarginStart;
public DropdownChipLayouter(LayoutInflater inflater, Context context) {
mInflater = inflater;
mContext = context;
mAutocompleteDividerMarginStart =
context.getResources().getDimensionPixelOffset(R.dimen.chip_wrapper_start_padding);
}
public void setQuery(Query query) {
mQuery = query;
}
public void setDeleteListener(ChipDeleteListener listener) {
mDeleteListener = listener;
}
public void setPermissionRequestDismissedListener(PermissionRequestDismissedListener listener) {
mPermissionRequestDismissedListener = listener;
}
public void setAutocompleteDividerMarginStart(int autocompleteDividerMarginStart) {
mAutocompleteDividerMarginStart = autocompleteDividerMarginStart;
}
/**
* Layouts and binds recipient information to the view. If convertView is null, inflates a new
* view with getItemLaytout().
*
* @param convertView The view to bind information to.
* @param parent The parent to bind the view to if we inflate a new view.
* @param entry The recipient entry to get information from.
* @param position The position in the list.
* @param type The adapter type that is requesting the bind.
* @param constraint The constraint typed in the auto complete view.
*
* @return A view ready to be shown in the drop down list.
*/
public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
AdapterType type, String constraint) {
return bindView(convertView, parent, entry, position, type, constraint, null);
}
/**
* See {@link #bindView(View, ViewGroup, RecipientEntry, int, AdapterType, String)}
* @param deleteDrawable a {@link android.graphics.drawable.StateListDrawable} representing
* the delete icon. android.R.attr.state_activated should map to the delete icon, and the
* default state can map to a drawable of your choice (or null for no drawable).
*/
public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
AdapterType type, String constraint, StateListDrawable deleteDrawable) {
// Default to show all the information
CharSequence[] styledResults = getStyledResults(constraint, entry);
CharSequence displayName = styledResults[0];
CharSequence destination = styledResults[1];
boolean showImage = true;
CharSequence destinationType = getDestinationType(entry);
final View itemView = reuseOrInflateView(convertView, parent, type);
final ViewHolder viewHolder = new ViewHolder(itemView);
// Hide some information depending on the adapter type.
switch (type) {
case BASE_RECIPIENT:
if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) {
displayName = destination;
// We only show the destination for secondary entries, so clear it only for the
// first level.
if (entry.isFirstLevel()) {
destination = null;
}
}
if (!entry.isFirstLevel()) {
displayName = null;
showImage = false;
}
// For BASE_RECIPIENT set all top dividers except for the first one to be GONE.
if (viewHolder.topDivider != null) {
viewHolder.topDivider.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
MarginLayoutParamsCompat.setMarginStart(
(MarginLayoutParams) viewHolder.topDivider.getLayoutParams(),
mAutocompleteDividerMarginStart);
}
if (viewHolder.bottomDivider != null) {
MarginLayoutParamsCompat.setMarginStart(
(MarginLayoutParams) viewHolder.bottomDivider.getLayoutParams(),
mAutocompleteDividerMarginStart);
}
break;
case RECIPIENT_ALTERNATES:
if (position != 0) {
displayName = null;
showImage = false;
}
break;
case SINGLE_RECIPIENT:
if (!PhoneUtil.isPhoneNumber(entry.getDestination())) {
destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress();
}
destinationType = null;
}
// Bind the information to the view
bindTextToView(displayName, viewHolder.displayNameView);
bindTextToView(destination, viewHolder.destinationView);
bindTextToView(destinationType, viewHolder.destinationTypeView);
bindIconToView(showImage, entry, viewHolder.imageView, type);
bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView);
bindIndicatorToView(
entry.getIndicatorIconId(), entry.getIndicatorText(), viewHolder.indicatorView);
bindPermissionRequestDismissView(viewHolder.permissionRequestDismissView);
// Hide some view groups depending on the entry type
final int entryType = entry.getEntryType();
if (entryType == RecipientEntry.ENTRY_TYPE_PERSON) {
setViewVisibility(viewHolder.personViewGroup, View.VISIBLE);
setViewVisibility(viewHolder.permissionViewGroup, View.GONE);
setViewVisibility(viewHolder.permissionBottomDivider, View.GONE);
} else if (entryType == RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST) {
setViewVisibility(viewHolder.personViewGroup, View.GONE);
setViewVisibility(viewHolder.permissionViewGroup, View.VISIBLE);
setViewVisibility(viewHolder.permissionBottomDivider, View.VISIBLE);
}
return itemView;
}
/**
* Returns a new view with {@link #getItemLayoutResId(AdapterType)}.
*/
public View newView(AdapterType type) {
return mInflater.inflate(getItemLayoutResId(type), null);
}
/**
* Returns the same view, or inflates a new one if the given view was null.
*/
protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) {
int itemLayout = getItemLayoutResId(type);
switch (type) {
case BASE_RECIPIENT:
case RECIPIENT_ALTERNATES:
break;
case SINGLE_RECIPIENT:
itemLayout = getAlternateItemLayoutResId(type);
break;
}
return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false);
}
/**
* Binds the text to the given text view. If the text was null, hides the text view.
*/
protected void bindTextToView(CharSequence text, TextView view) {
if (view == null) {
return;
}
if (text != null) {
view.setText(text);
view.setVisibility(View.VISIBLE);
} else {
view.setVisibility(View.GONE);
}
}
/**
* Binds the avatar icon to the image view. If we don't want to show the image, hides the
* image view.
*/
protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view,
AdapterType type) {
if (view == null) {
return;
}
if (showImage) {
switch (type) {
case BASE_RECIPIENT:
byte[] photoBytes = entry.getPhotoBytes();
if (photoBytes != null && photoBytes.length > 0) {
final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
photoBytes.length);
view.setImageBitmap(photo);
} else {
view.setImageResource(getDefaultPhotoResId());
}
break;
case RECIPIENT_ALTERNATES:
Uri thumbnailUri = entry.getPhotoThumbnailUri();
if (thumbnailUri != null) {
// TODO: see if this needs to be done outside the main thread
// as it may be too slow to get immediately.
view.setImageURI(thumbnailUri);
} else {
view.setImageResource(getDefaultPhotoResId());
}
break;
case SINGLE_RECIPIENT:
default:
break;
}
view.setVisibility(View.VISIBLE);
} else {
view.setVisibility(View.GONE);
}
}
protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient,
ImageView view) {
if (view == null) {
return;
}
if (drawable == null) {
view.setVisibility(View.GONE);
} else {
final Resources res = mContext.getResources();
view.setImageDrawable(drawable);
view.setContentDescription(
res.getString(R.string.dropdown_delete_button_desc, recipient));
if (mDeleteListener != null) {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (drawable.getCurrent() != null) {
mDeleteListener.onChipDelete();
}
}
});
}
}
}
protected void bindIndicatorToView(
@DrawableRes int indicatorIconId, String indicatorText, TextView view) {
if (view != null) {
if (indicatorText != null || indicatorIconId != 0) {
view.setText(indicatorText);
view.setVisibility(View.VISIBLE);
final Drawable indicatorIcon;
if (indicatorIconId != 0) {
indicatorIcon = mContext.getDrawable(indicatorIconId).mutate();
indicatorIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
} else {
indicatorIcon = null;
}
view.setCompoundDrawablesRelativeWithIntrinsicBounds(
indicatorIcon, null, null, null);
} else {
view.setVisibility(View.GONE);
}
}
}
protected void bindPermissionRequestDismissView(ImageView view) {
if (view == null) {
return;
}
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mPermissionRequestDismissedListener != null) {
mPermissionRequestDismissedListener.onPermissionRequestDismissed();
}
}
});
}
protected void setViewVisibility(View view, int visibility) {
if (view != null) {
view.setVisibility(visibility);
}
}
protected CharSequence getDestinationType(RecipientEntry entry) {
return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(),
entry.getDestinationLabel()).toString().toUpperCase();
}
/**
* Returns a layout id for each item inside auto-complete list.
*
* Each View must contain two TextViews (for display name and destination) and one ImageView
* (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
* {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
*/
protected @LayoutRes int getItemLayoutResId(AdapterType type) {
switch (type) {
case BASE_RECIPIENT:
return R.layout.chips_autocomplete_recipient_dropdown_item;
case RECIPIENT_ALTERNATES:
return R.layout.chips_recipient_dropdown_item;
default:
return R.layout.chips_recipient_dropdown_item;
}
}
/**
* Returns a layout id for each item inside alternate auto-complete list.
*
* Each View must contain two TextViews (for display name and destination) and one ImageView
* (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
* {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
*/
protected @LayoutRes int getAlternateItemLayoutResId(AdapterType type) {
switch (type) {
case BASE_RECIPIENT:
return R.layout.chips_autocomplete_recipient_dropdown_item;
case RECIPIENT_ALTERNATES:
return R.layout.chips_recipient_dropdown_item;
default:
return R.layout.chips_recipient_dropdown_item;
}
}
/**
* Returns a resource ID representing an image which should be shown when ther's no relevant
* photo is available.
*/
protected @DrawableRes int getDefaultPhotoResId() {
return R.drawable.ic_contact_picture;
}
/**
* Returns an id for the ViewGroup in an item View that contains the person ui elements.
*/
protected @IdRes int getPersonGroupResId() {
return R.id.chip_person_wrapper;
}
/**
* Returns an id for TextView in an item View for showing a display name. By default
* {@link android.R.id#title} is returned.
*/
protected @IdRes int getDisplayNameResId() {
return android.R.id.title;
}
/**
* Returns an id for TextView in an item View for showing a destination
* (an email address or a phone number).
* By default {@link android.R.id#text1} is returned.
*/
protected @IdRes int getDestinationResId() {
return android.R.id.text1;
}
/**
* Returns an id for TextView in an item View for showing the type of the destination.
* By default {@link android.R.id#text2} is returned.
*/
protected @IdRes int getDestinationTypeResId() {
return android.R.id.text2;
}
/**
* Returns an id for ImageView in an item View for showing photo image for a person. In default
* {@link android.R.id#icon} is returned.
*/
protected @IdRes int getPhotoResId() {
return android.R.id.icon;
}
/**
* Returns an id for ImageView in an item View for showing the delete button. In default
* {@link android.R.id#icon1} is returned.
*/
protected @IdRes int getDeleteResId() { return android.R.id.icon1; }
/**
* Returns an id for the ViewGroup in an item View that contains the request permission ui
* elements.
*/
protected @IdRes int getPermissionGroupResId() {
return R.id.chip_permission_wrapper;
}
/**
* Returns an id for ImageView in an item View for dismissing the permission request. In default
* {@link android.R.id#icon2} is returned.
*/
protected @IdRes int getPermissionRequestDismissResId() {
return android.R.id.icon2;
}
/**
* Given a constraint and a recipient entry, tries to find the constraint in the name and
* destination in the recipient entry. A foreground font color style will be applied to the
* section that matches the constraint. As soon as a match has been found, no further matches
* are attempted.
*
* @param constraint A string that we will attempt to find within the results.
* @param entry The recipient entry to style results for.
*
* @return An array of CharSequences, the length determined by the length of results. Each
* CharSequence will either be a styled SpannableString or just the input String.
*/
protected CharSequence[] getStyledResults(@Nullable String constraint, RecipientEntry entry) {
return getStyledResults(constraint, entry.getDisplayName(), entry.getDestination());
}
/**
* Given a constraint and results, tries to find the constraint in those results, one at a time.
* A foreground font color style will be applied to the section that matches the constraint. As
* soon as a match has been found, no further matches are attempted.
*
* @param constraint A string that we will attempt to find within the results.
* @param results Strings that may contain the constraint. The order given is the order used to
* search for the constraint.
*
* @return An array of CharSequences, the length determined by the length of results. Each
* CharSequence will either be a styled SpannableString or just the input String.
*/
protected CharSequence[] getStyledResults(@Nullable String constraint, String... results) {
if (isAllWhitespace(constraint)) {
return results;
}
CharSequence[] styledResults = new CharSequence[results.length];
boolean foundMatch = false;
for (int i = 0; i < results.length; i++) {
String result = results[i];
if (result == null) {
continue;
}
if (!foundMatch) {
int index = result.toLowerCase().indexOf(constraint.toLowerCase());
if (index != -1) {
SpannableStringBuilder styled = SpannableStringBuilder.valueOf(result);
ForegroundColorSpan highlightSpan =
new ForegroundColorSpan(mContext.getResources().getColor(
R.color.chips_dropdown_text_highlighted));
styled.setSpan(highlightSpan,
index, index + constraint.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
styledResults[i] = styled;
foundMatch = true;
continue;
}
}
styledResults[i] = result;
}
return styledResults;
}
private static boolean isAllWhitespace(@Nullable String string) {
if (TextUtils.isEmpty(string)) {
return true;
}
for (int i = 0; i < string.length(); ++i) {
if (!Character.isWhitespace(string.charAt(i))) {
return false;
}
}
return true;
}
/**
* A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the
* corresponding views.
*/
protected class ViewHolder {
public final ViewGroup personViewGroup;
public final TextView displayNameView;
public final TextView destinationView;
public final TextView destinationTypeView;
public final TextView indicatorView;
public final ImageView imageView;
public final ImageView deleteView;
public final View topDivider;
public final View bottomDivider;
public final View permissionBottomDivider;
public final ViewGroup permissionViewGroup;
public final ImageView permissionRequestDismissView;
public ViewHolder(View view) {
personViewGroup = (ViewGroup) view.findViewById(getPersonGroupResId());
displayNameView = (TextView) view.findViewById(getDisplayNameResId());
destinationView = (TextView) view.findViewById(getDestinationResId());
destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId());
imageView = (ImageView) view.findViewById(getPhotoResId());
deleteView = (ImageView) view.findViewById(getDeleteResId());
topDivider = view.findViewById(R.id.chip_autocomplete_top_divider);
bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider);
permissionBottomDivider = view.findViewById(R.id.chip_permission_bottom_divider);
indicatorView = (TextView) view.findViewById(R.id.chip_indicator_text);
permissionViewGroup = (ViewGroup) view.findViewById(getPermissionGroupResId());
permissionRequestDismissView =
(ImageView) view.findViewById(getPermissionRequestDismissResId());
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.ex.chips;
import android.text.TextUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A utility class for chips using phone numbers.
*/
public class PhoneUtil {
// This pattern comes from android.util.Patterns. It has been tweaked to handle a "1" before
// parens, so numbers such as "1 (425) 222-2342" match.
private static final Pattern PHONE_PATTERN
= Pattern.compile( // sdd = space, dot, or dash
"(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>*
+ "(1?[ ]*\\([0-9]+\\)[\\- \\.]*)?" // 1(<digits>)<sdd>*
+ "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit>
/**
* Returns true if the string is a phone number.
*/
public static boolean isPhoneNumber(String number) {
// TODO: replace this function with libphonenumber's isPossibleNumber (see
// PhoneNumberUtil). One complication is that it requires the sender's region which
// comes from the CurrentCountryIso. For now, let's just do this simple match.
if (TextUtils.isEmpty(number)) {
return false;
}
Matcher match = PHONE_PATTERN.matcher(number);
return match.matches();
}
}
@@ -0,0 +1,46 @@
/*
* 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.ex.chips;
/**
* Used by the {@link com.android.ex.chips.BaseRecipientAdapter} to handle fetching
* photos from external sources and caching them for faster lookup later.
*/
public interface PhotoManager {
/** The number of photos cached in this Adapter. */
public static final int PHOTO_CACHE_SIZE = 20;
/**
* Sets the {@link com.android.ex.chips.RecipientEntry}'s photo bytes. If the photo bytes
* are cached, this action happens immediately. Otherwise, the work to fetch the photo
* bytes is performed asynchronously before setting the value on the UI thread.<p/>
*
* If the photo bytes were fetched asynchronously,
* {@link PhotoManagerCallback#onPhotoBytesAsynchronouslyPopulated()} is called. This
* method is not called if the photo bytes have been cached previously (because no
* asynchronous work was performed). In that case,
* {@link PhotoManagerCallback#onPhotoBytesPopulated()} is called.
*/
void populatePhotoBytesAsync(RecipientEntry entry, PhotoManagerCallback callback);
interface PhotoManagerCallback {
void onPhotoBytesPopulated();
void onPhotoBytesAsynchronouslyPopulated();
void onPhotoBytesAsyncLoadFailed();
}
}
@@ -0,0 +1,107 @@
/*
* 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.ex.chips;
import android.content.res.Resources;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
/**
* Phone and Email queries for supporting Chips UI.
*/
/* package */ class Queries {
public static final Query PHONE = new Query(new String[] {
Contacts.DISPLAY_NAME, // 0
Phone.NUMBER, // 1
Phone.TYPE, // 2
Phone.LABEL, // 3
Phone.CONTACT_ID, // 4
Phone._ID, // 5
Contacts.PHOTO_THUMBNAIL_URI, // 6
Contacts.DISPLAY_NAME_SOURCE, // 7
Contacts.LOOKUP_KEY, // 8
ContactsContract.CommonDataKinds.Email.MIMETYPE // 9
}, Phone.CONTENT_FILTER_URI, Phone.CONTENT_URI) {
@Override
public CharSequence getTypeLabel(Resources res, int type, CharSequence label) {
return Phone.getTypeLabel(res, type, label);
}
};
public static final Query EMAIL = new Query(new String[]{
Contacts.DISPLAY_NAME, // 0
Email.DATA, // 1
Email.TYPE, // 2
Email.LABEL, // 3
Email.CONTACT_ID, // 4
Email._ID, // 5
Contacts.PHOTO_THUMBNAIL_URI, // 6
Contacts.DISPLAY_NAME_SOURCE, // 7
Contacts.LOOKUP_KEY, // 8
ContactsContract.CommonDataKinds.Email.MIMETYPE // 9
}, Email.CONTENT_FILTER_URI, Email.CONTENT_URI) {
@Override
public CharSequence getTypeLabel(Resources res, int type, CharSequence label) {
return Email.getTypeLabel(res, type, label);
}
};
static abstract class Query {
private final String[] mProjection;
private final Uri mContentFilterUri;
private final Uri mContentUri;
public static final int NAME = 0; // String
public static final int DESTINATION = 1; // String
public static final int DESTINATION_TYPE = 2; // int
public static final int DESTINATION_LABEL = 3; // String
public static final int CONTACT_ID = 4; // long
public static final int DATA_ID = 5; // long
public static final int PHOTO_THUMBNAIL_URI = 6; // String
public static final int DISPLAY_NAME_SOURCE = 7; // int
public static final int LOOKUP_KEY = 8; // String
public static final int MIME_TYPE = 9; // String
public Query(String[] projection, Uri contentFilter, Uri content) {
mProjection = projection;
mContentFilterUri = contentFilter;
mContentUri = content;
}
public String[] getProjection() {
return mProjection;
}
public Uri getContentFilterUri() {
return mContentFilterUri;
}
public Uri getContentUri() {
return mContentUri;
}
public abstract CharSequence getTypeLabel(Resources res, int type, CharSequence label);
}
}
@@ -0,0 +1,621 @@
/*
* 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.ex.chips;
import android.accounts.Account;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.drawable.StateListDrawable;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import com.android.ex.chips.BaseRecipientAdapter.DirectoryListQuery;
import com.android.ex.chips.BaseRecipientAdapter.DirectorySearchParams;
import com.android.ex.chips.DropdownChipLayouter.AdapterType;
import com.android.ex.chips.Queries.Query;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* RecipientAlternatesAdapter backs the RecipientEditTextView for managing contacts
* queried by email or by phone number.
*/
public class RecipientAlternatesAdapter extends CursorAdapter {
public static final int MAX_LOOKUPS = 50;
private final long mCurrentId;
private int mCheckedItemPosition = -1;
private OnCheckedItemChangedListener mCheckedItemChangedListener;
private static final String TAG = "RecipAlternates";
public static final int QUERY_TYPE_EMAIL = 0;
public static final int QUERY_TYPE_PHONE = 1;
private final Long mDirectoryId;
private DropdownChipLayouter mDropdownChipLayouter;
private final StateListDrawable mDeleteDrawable;
private static final Map<String, String> sCorrectedPhotoUris = new HashMap<String, String>();
public interface RecipientMatchCallback {
public void matchesFound(Map<String, RecipientEntry> results);
/**
* Called with all addresses that could not be resolved to valid recipients.
*/
public void matchesNotFound(Set<String> unfoundAddresses);
}
public static void getMatchingRecipients(Context context, BaseRecipientAdapter adapter,
ArrayList<String> inAddresses, Account account, RecipientMatchCallback callback,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
getMatchingRecipients(context, adapter, inAddresses, QUERY_TYPE_EMAIL, account, callback,
permissionsCheckListener);
}
/**
* Get a HashMap of address to RecipientEntry that contains all contact
* information for a contact with the provided address, if one exists. This
* may block the UI, so run it in an async task.
*
* @param context Context.
* @param inAddresses Array of addresses on which to perform the lookup.
* @param callback RecipientMatchCallback called when a match or matches are found.
*/
public static void getMatchingRecipients(Context context, BaseRecipientAdapter adapter,
ArrayList<String> inAddresses, int addressType, Account account,
RecipientMatchCallback callback,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
Queries.Query query;
if (addressType == QUERY_TYPE_EMAIL) {
query = Queries.EMAIL;
} else {
query = Queries.PHONE;
}
int addressesSize = Math.min(MAX_LOOKUPS, inAddresses.size());
HashSet<String> addresses = new HashSet<String>();
StringBuilder bindString = new StringBuilder();
// Create the "?" string and set up arguments.
for (int i = 0; i < addressesSize; i++) {
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase());
addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
bindString.append("?");
if (i < addressesSize - 1) {
bindString.append(",");
}
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Doing reverse lookup for " + addresses.toString());
}
String[] addressArray = new String[addresses.size()];
addresses.toArray(addressArray);
HashMap<String, RecipientEntry> recipientEntries = null;
Cursor c = null;
try {
if (ChipsUtil.hasPermissions(context, permissionsCheckListener)) {
c = context.getContentResolver().query(
query.getContentUri(),
query.getProjection(),
query.getProjection()[Queries.Query.DESTINATION] + " IN ("
+ bindString.toString() + ")", addressArray, null);
}
recipientEntries = processContactEntries(c, null /* directoryId */);
callback.matchesFound(recipientEntries);
} finally {
if (c != null) {
c.close();
}
}
final Set<String> matchesNotFound = new HashSet<String>();
getMatchingRecipientsFromDirectoryQueries(context, recipientEntries,
addresses, account, matchesNotFound, query, callback, permissionsCheckListener);
getMatchingRecipientsFromExtensionMatcher(adapter, matchesNotFound, callback);
}
public static void getMatchingRecipientsFromDirectoryQueries(Context context,
Map<String, RecipientEntry> recipientEntries, Set<String> addresses,
Account account, Set<String> matchesNotFound,
RecipientMatchCallback callback,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
getMatchingRecipientsFromDirectoryQueries(
context, recipientEntries, addresses, account,
matchesNotFound, Queries.EMAIL, callback, permissionsCheckListener);
}
private static void getMatchingRecipientsFromDirectoryQueries(Context context,
Map<String, RecipientEntry> recipientEntries, Set<String> addresses,
Account account, Set<String> matchesNotFound, Queries.Query query,
RecipientMatchCallback callback,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
// See if any entries did not resolve; if so, we need to check other
// directories
if (recipientEntries.size() < addresses.size()) {
// Run a directory query for each unmatched recipient.
HashSet<String> unresolvedAddresses = new HashSet<String>();
for (String address : addresses) {
if (!recipientEntries.containsKey(address)) {
unresolvedAddresses.add(address);
}
}
matchesNotFound.addAll(unresolvedAddresses);
final List<DirectorySearchParams> paramsList;
Cursor directoryCursor = null;
try {
if (ChipsUtil.hasPermissions(context, permissionsCheckListener)) {
directoryCursor = context.getContentResolver().query(
DirectoryListQuery.URI, DirectoryListQuery.PROJECTION,
null, null, null);
}
if (directoryCursor == null) {
return;
}
paramsList = BaseRecipientAdapter.setupOtherDirectories(
context, directoryCursor, account);
} finally {
if (directoryCursor != null) {
directoryCursor.close();
}
}
if (paramsList != null) {
Cursor directoryContactsCursor = null;
for (String unresolvedAddress : unresolvedAddresses) {
for (int i = 0; i < paramsList.size(); i++) {
final long directoryId = paramsList.get(i).directoryId;
try {
directoryContactsCursor = doQuery(unresolvedAddress, 1 /* limit */,
directoryId, account, context, query, permissionsCheckListener);
if (directoryContactsCursor != null
&& directoryContactsCursor.getCount() != 0) {
// We found the directory with at least one contact
final Map<String, RecipientEntry> entries =
processContactEntries(directoryContactsCursor, directoryId);
for (final String address : entries.keySet()) {
matchesNotFound.remove(address);
}
callback.matchesFound(entries);
break;
}
} finally {
if (directoryContactsCursor != null) {
directoryContactsCursor.close();
directoryContactsCursor = null;
}
}
}
}
}
}
}
public static void getMatchingRecipientsFromExtensionMatcher(BaseRecipientAdapter adapter,
Set<String> matchesNotFound, RecipientMatchCallback callback) {
// If no matches found in contact provider or the directories, try the extension
// matcher.
// todo (aalbert): This whole method needs to be in the adapter?
if (adapter != null) {
final Map<String, RecipientEntry> entries =
adapter.getMatchingRecipients(matchesNotFound);
if (entries != null && entries.size() > 0) {
callback.matchesFound(entries);
for (final String address : entries.keySet()) {
matchesNotFound.remove(address);
}
}
}
callback.matchesNotFound(matchesNotFound);
}
private static HashMap<String, RecipientEntry> processContactEntries(Cursor c,
Long directoryId) {
HashMap<String, RecipientEntry> recipientEntries = new HashMap<String, RecipientEntry>();
if (c != null && c.moveToFirst()) {
do {
String address = c.getString(Queries.Query.DESTINATION);
final RecipientEntry newRecipientEntry = RecipientEntry.constructTopLevelEntry(
c.getString(Queries.Query.NAME),
c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),
c.getString(Queries.Query.DESTINATION),
c.getInt(Queries.Query.DESTINATION_TYPE),
c.getString(Queries.Query.DESTINATION_LABEL),
c.getLong(Queries.Query.CONTACT_ID),
directoryId,
c.getLong(Queries.Query.DATA_ID),
c.getString(Queries.Query.PHOTO_THUMBNAIL_URI),
true,
c.getString(Queries.Query.LOOKUP_KEY));
/*
* In certain situations, we may have two results for one address, where one of the
* results is just the email address, and the other has a name and photo, so we want
* to use the better one.
*/
final RecipientEntry recipientEntry =
getBetterRecipient(recipientEntries.get(address), newRecipientEntry);
recipientEntries.put(address, recipientEntry);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Received reverse look up information for " + address
+ " RESULTS: "
+ " NAME : " + c.getString(Queries.Query.NAME)
+ " CONTACT ID : " + c.getLong(Queries.Query.CONTACT_ID)
+ " ADDRESS :" + c.getString(Queries.Query.DESTINATION));
}
} while (c.moveToNext());
}
return recipientEntries;
}
/**
* Given two {@link RecipientEntry}s for the same email address, this will return the one that
* contains more complete information for display purposes. Defaults to <code>entry2</code> if
* no significant differences are found.
*/
static RecipientEntry getBetterRecipient(final RecipientEntry entry1,
final RecipientEntry entry2) {
// If only one has passed in, use it
if (entry2 == null) {
return entry1;
}
if (entry1 == null) {
return entry2;
}
// If only one has a display name, use it
if (!TextUtils.isEmpty(entry1.getDisplayName())
&& TextUtils.isEmpty(entry2.getDisplayName())) {
return entry1;
}
if (!TextUtils.isEmpty(entry2.getDisplayName())
&& TextUtils.isEmpty(entry1.getDisplayName())) {
return entry2;
}
// If only one has a display name that is not the same as the destination, use it
if (!TextUtils.equals(entry1.getDisplayName(), entry1.getDestination())
&& TextUtils.equals(entry2.getDisplayName(), entry2.getDestination())) {
return entry1;
}
if (!TextUtils.equals(entry2.getDisplayName(), entry2.getDestination())
&& TextUtils.equals(entry1.getDisplayName(), entry1.getDestination())) {
return entry2;
}
// If only one has a photo, use it
if ((entry1.getPhotoThumbnailUri() != null || entry1.getPhotoBytes() != null)
&& (entry2.getPhotoThumbnailUri() == null && entry2.getPhotoBytes() == null)) {
return entry1;
}
if ((entry2.getPhotoThumbnailUri() != null || entry2.getPhotoBytes() != null)
&& (entry1.getPhotoThumbnailUri() == null && entry1.getPhotoBytes() == null)) {
return entry2;
}
// Go with the second option as a default
return entry2;
}
private static Cursor doQuery(CharSequence constraint, int limit, Long directoryId,
Account account, Context context, Query query,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
if (!ChipsUtil.hasPermissions(context, permissionsCheckListener)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Not doing query because we don't have required permissions.");
}
return null;
}
final Uri.Builder builder = query
.getContentFilterUri()
.buildUpon()
.appendPath(constraint.toString())
.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
String.valueOf(limit + BaseRecipientAdapter.ALLOWANCE_FOR_DUPLICATES));
if (directoryId != null) {
builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
String.valueOf(directoryId));
}
if (account != null) {
builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_NAME, account.name);
builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_TYPE, account.type);
}
return context.getContentResolver()
.query(builder.build(), query.getProjection(), null, null, null);
}
public RecipientAlternatesAdapter(Context context, long contactId, Long directoryId,
String lookupKey, long currentId, int queryMode, OnCheckedItemChangedListener listener,
DropdownChipLayouter dropdownChipLayouter,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
this(context, contactId, directoryId, lookupKey, currentId, queryMode, listener,
dropdownChipLayouter, null, permissionsCheckListener);
}
public RecipientAlternatesAdapter(Context context, long contactId, Long directoryId,
String lookupKey, long currentId, int queryMode, OnCheckedItemChangedListener listener,
DropdownChipLayouter dropdownChipLayouter, StateListDrawable deleteDrawable,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
super(context,
getCursorForConstruction(context, contactId, directoryId, lookupKey, queryMode,
permissionsCheckListener),
0);
mCurrentId = currentId;
mDirectoryId = directoryId;
mCheckedItemChangedListener = listener;
mDropdownChipLayouter = dropdownChipLayouter;
mDeleteDrawable = deleteDrawable;
}
private static Cursor getCursorForConstruction(Context context, long contactId,
Long directoryId, String lookupKey, int queryType,
ChipsUtil.PermissionsCheckListener permissionsCheckListener) {
final Uri uri;
final String desiredMimeType;
final String[] projection;
if (queryType == QUERY_TYPE_EMAIL) {
projection = Queries.EMAIL.getProjection();
if (directoryId == null || lookupKey == null) {
uri = Queries.EMAIL.getContentUri();
desiredMimeType = null;
} else {
uri = Contacts.getLookupUri(contactId, lookupKey)
.buildUpon()
.appendPath(Contacts.Entity.CONTENT_DIRECTORY)
.appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
.build();
desiredMimeType = ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE;
}
} else {
projection = Queries.PHONE.getProjection();
if (lookupKey == null) {
uri = Queries.PHONE.getContentUri();
desiredMimeType = null;
} else {
uri = Contacts.getLookupUri(contactId, lookupKey)
.buildUpon()
.appendPath(Contacts.Entity.CONTENT_DIRECTORY)
.appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
.build();
desiredMimeType = ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE;
}
}
final String selection = new StringBuilder()
.append(projection[Queries.Query.CONTACT_ID])
.append(" = ?")
.toString();
final Cursor cursor;
if (ChipsUtil.hasPermissions(context, permissionsCheckListener)) {
cursor = context.getContentResolver().query(
uri, projection, selection, new String[] {String.valueOf(contactId)}, null);
} else {
cursor = new MatrixCursor(projection);
}
final Cursor resultCursor = removeUndesiredDestinations(cursor, desiredMimeType, lookupKey);
cursor.close();
return resultCursor;
}
/**
* @return a new cursor based on the given cursor with all duplicate destinations removed.
*
* It's only intended to use for the alternate list, so...
* - This method ignores all other fields and dedupe solely on the destination. Normally,
* if a cursor contains multiple contacts and they have the same destination, we'd still want
* to show both.
* - This method creates a MatrixCursor, so all data will be kept in memory. We wouldn't want
* to do this if the original cursor is large, but it's okay here because the alternate list
* won't be that big.
*
* @param desiredMimeType If this is non-<code>null</code>, only entries with this mime type
* will be added to the cursor
* @param lookupKey The lookup key used for this contact if there isn't one in the cursor. This
* should be the same one used in the query that returned the cursor
*/
// Visible for testing
static Cursor removeUndesiredDestinations(final Cursor original, final String desiredMimeType,
final String lookupKey) {
final MatrixCursor result = new MatrixCursor(
original.getColumnNames(), original.getCount());
final HashSet<String> destinationsSeen = new HashSet<String>();
String defaultDisplayName = null;
String defaultPhotoThumbnailUri = null;
int defaultDisplayNameSource = 0;
// Find some nice defaults in case we need them
original.moveToPosition(-1);
while (original.moveToNext()) {
final String mimeType = original.getString(Query.MIME_TYPE);
if (ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(
mimeType)) {
// Store this data
defaultDisplayName = original.getString(Query.NAME);
defaultPhotoThumbnailUri = original.getString(Query.PHOTO_THUMBNAIL_URI);
defaultDisplayNameSource = original.getInt(Query.DISPLAY_NAME_SOURCE);
break;
}
}
original.moveToPosition(-1);
while (original.moveToNext()) {
if (desiredMimeType != null) {
final String mimeType = original.getString(Query.MIME_TYPE);
if (!desiredMimeType.equals(mimeType)) {
continue;
}
}
final String destination = original.getString(Query.DESTINATION);
if (destinationsSeen.contains(destination)) {
continue;
}
destinationsSeen.add(destination);
final Object[] row = new Object[] {
original.getString(Query.NAME),
original.getString(Query.DESTINATION),
original.getInt(Query.DESTINATION_TYPE),
original.getString(Query.DESTINATION_LABEL),
original.getLong(Query.CONTACT_ID),
original.getLong(Query.DATA_ID),
original.getString(Query.PHOTO_THUMBNAIL_URI),
original.getInt(Query.DISPLAY_NAME_SOURCE),
original.getString(Query.LOOKUP_KEY),
original.getString(Query.MIME_TYPE)
};
if (row[Query.NAME] == null) {
row[Query.NAME] = defaultDisplayName;
}
if (row[Query.PHOTO_THUMBNAIL_URI] == null) {
row[Query.PHOTO_THUMBNAIL_URI] = defaultPhotoThumbnailUri;
}
if ((Integer) row[Query.DISPLAY_NAME_SOURCE] == 0) {
row[Query.DISPLAY_NAME_SOURCE] = defaultDisplayNameSource;
}
if (row[Query.LOOKUP_KEY] == null) {
row[Query.LOOKUP_KEY] = lookupKey;
}
// Ensure we don't have two '?' like content://.../...?account_name=...?sz=...
final String photoThumbnailUri = (String) row[Query.PHOTO_THUMBNAIL_URI];
if (photoThumbnailUri != null) {
if (sCorrectedPhotoUris.containsKey(photoThumbnailUri)) {
row[Query.PHOTO_THUMBNAIL_URI] = sCorrectedPhotoUris.get(photoThumbnailUri);
} else if (photoThumbnailUri.indexOf('?') != photoThumbnailUri.lastIndexOf('?')) {
final String[] parts = photoThumbnailUri.split("\\?");
final StringBuilder correctedUriBuilder = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (i == 1) {
correctedUriBuilder.append("?"); // We only want one of these
} else if (i > 1) {
correctedUriBuilder.append("&"); // And we want these elsewhere
}
correctedUriBuilder.append(parts[i]);
}
final String correctedUri = correctedUriBuilder.toString();
sCorrectedPhotoUris.put(photoThumbnailUri, correctedUri);
row[Query.PHOTO_THUMBNAIL_URI] = correctedUri;
}
}
result.addRow(row);
}
return result;
}
@Override
public long getItemId(int position) {
Cursor c = getCursor();
if (c.moveToPosition(position)) {
c.getLong(Queries.Query.DATA_ID);
}
return -1;
}
public RecipientEntry getRecipientEntry(int position) {
Cursor c = getCursor();
c.moveToPosition(position);
return RecipientEntry.constructTopLevelEntry(
c.getString(Queries.Query.NAME),
c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),
c.getString(Queries.Query.DESTINATION),
c.getInt(Queries.Query.DESTINATION_TYPE),
c.getString(Queries.Query.DESTINATION_LABEL),
c.getLong(Queries.Query.CONTACT_ID),
mDirectoryId,
c.getLong(Queries.Query.DATA_ID),
c.getString(Queries.Query.PHOTO_THUMBNAIL_URI),
true,
c.getString(Queries.Query.LOOKUP_KEY));
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Cursor cursor = getCursor();
cursor.moveToPosition(position);
if (convertView == null) {
convertView = mDropdownChipLayouter.newView(AdapterType.RECIPIENT_ALTERNATES);
}
if (cursor.getLong(Queries.Query.DATA_ID) == mCurrentId) {
mCheckedItemPosition = position;
if (mCheckedItemChangedListener != null) {
mCheckedItemChangedListener.onCheckedItemChanged(mCheckedItemPosition);
}
}
bindView(convertView, convertView.getContext(), cursor);
return convertView;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
int position = cursor.getPosition();
RecipientEntry entry = getRecipientEntry(position);
mDropdownChipLayouter.bindView(view, null, entry, position,
AdapterType.RECIPIENT_ALTERNATES, null, mDeleteDrawable);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mDropdownChipLayouter.newView(AdapterType.RECIPIENT_ALTERNATES);
}
/*package*/ static interface OnCheckedItemChangedListener {
public void onCheckedItemChanged(int position);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,377 @@
/*
* 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.ex.chips;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.DisplayNameSources;
import androidx.annotation.DrawableRes;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
/**
* Represents one entry inside recipient auto-complete list.
*/
public class RecipientEntry {
/* package */ static final int INVALID_CONTACT = -1;
/**
* A GENERATED_CONTACT is one that was created based entirely on
* information passed in to the RecipientEntry from an external source
* that is not a real contact.
*/
/* package */ static final int GENERATED_CONTACT = -2;
/** Used when {@link #mDestinationType} is invalid and thus shouldn't be used for display. */
public static final int INVALID_DESTINATION_TYPE = -1;
public static final int ENTRY_TYPE_PERSON = 0;
/**
* Entry of this type represents the item in auto-complete that asks user to grant permissions
* to the app. This permission model is introduced in M platform.
*
* <p>Entries of this type should have {@link #mPermissions} set as well.
*/
public static final int ENTRY_TYPE_PERMISSION_REQUEST = 1;
public static final int ENTRY_TYPE_SIZE = 2;
private final int mEntryType;
/**
* True when this entry is the first entry in a group, which should have a photo and display
* name, while the second or later entries won't.
*/
private boolean mIsFirstLevel;
private final String mDisplayName;
/** Destination for this contact entry. Would be an email address or a phone number. */
private final String mDestination;
/** Type of the destination like {@link Email#TYPE_HOME} */
private final int mDestinationType;
/**
* Label of the destination which will be used when type was {@link Email#TYPE_CUSTOM}.
* Can be null when {@link #mDestinationType} is {@link #INVALID_DESTINATION_TYPE}.
*/
private final String mDestinationLabel;
/** ID for the person */
private final long mContactId;
/** ID for the directory this contact came from, or <code>null</code> */
private final Long mDirectoryId;
/** ID for the destination */
private final long mDataId;
private final Uri mPhotoThumbnailUri;
/** Configures showing the icon in the chip */
private final boolean mShouldDisplayIcon;
private boolean mIsValid;
/**
* This can be updated after this object being constructed, when the photo is fetched
* from remote directories.
*/
private byte[] mPhotoBytes;
@DrawableRes private int mIndicatorIconId;
private String mIndicatorText;
/** See {@link android.provider.ContactsContract.ContactsColumns#LOOKUP_KEY} */
private final String mLookupKey;
/** Should be used when type is {@link #ENTRY_TYPE_PERMISSION_REQUEST}. */
private final String[] mPermissions;
/** Whether RecipientEntry is in a replaced chip or not. */
private boolean mInReplacedChip;
protected RecipientEntry(int entryType, String displayName, String destination,
int destinationType, String destinationLabel, long contactId, Long directoryId,
long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid,
String lookupKey, String[] permissions) {
this(entryType, displayName, destination, destinationType,
destinationLabel, contactId, directoryId, dataId, photoThumbnailUri,
true /* shouldDisplayIcon */, isFirstLevel, isValid, lookupKey, permissions);
}
protected RecipientEntry(int entryType, String displayName, String destination,
int destinationType, String destinationLabel, long contactId, Long directoryId,
long dataId, Uri photoThumbnailUri, boolean shouldDisplayIcon,
boolean isFirstLevel, boolean isValid, String lookupKey, String[] permissions) {
mEntryType = entryType;
mIsFirstLevel = isFirstLevel;
mDisplayName = displayName;
mDestination = destination;
mDestinationType = destinationType;
mDestinationLabel = destinationLabel;
mContactId = contactId;
mDirectoryId = directoryId;
mDataId = dataId;
mPhotoThumbnailUri = photoThumbnailUri;
mShouldDisplayIcon = shouldDisplayIcon;
mPhotoBytes = null;
mIsValid = isValid;
mLookupKey = lookupKey;
mIndicatorIconId = 0;
mIndicatorText = null;
mPermissions = permissions;
}
protected RecipientEntry(int entryType, String displayName, String destination,
int destinationType, String destinationLabel, long contactId, Long directoryId,
long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid,
String lookupKey) {
this(entryType, displayName, destination, destinationType, destinationLabel,
contactId, directoryId, dataId, photoThumbnailUri, isFirstLevel, isValid,
lookupKey, null);
}
public boolean isValid() {
return mIsValid;
}
/**
* Determine if this was a RecipientEntry created from recipient info or
* an entry from contacts.
*/
public static boolean isCreatedRecipient(long id) {
return id == RecipientEntry.INVALID_CONTACT || id == RecipientEntry.GENERATED_CONTACT;
}
/**
* Construct a RecipientEntry from just an address that has been entered.
* This address has not been resolved to a contact and therefore does not
* have a contact id or photo.
*/
public static RecipientEntry constructFakeEntry(final String address, final boolean isValid) {
final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
final String tokenizedAddress = tokens.length > 0 ? tokens[0].getAddress() : address;
return new RecipientEntry(ENTRY_TYPE_PERSON, tokenizedAddress, tokenizedAddress,
INVALID_DESTINATION_TYPE, null, INVALID_CONTACT, null /* directoryId */,
INVALID_CONTACT, null, true, isValid, null /* lookupKey */, null /* permissions */);
}
/**
* Construct a RecipientEntry from just a phone number.
*/
public static RecipientEntry constructFakePhoneEntry(final String phoneNumber,
final boolean isValid) {
return new RecipientEntry(ENTRY_TYPE_PERSON, phoneNumber, phoneNumber,
INVALID_DESTINATION_TYPE, null, INVALID_CONTACT, null /* directoryId */,
INVALID_CONTACT, null, true, isValid, null /* lookupKey */, null /* permissions */);
}
/**
* Construct a RecipientEntry from just an address that has been entered
* with both an associated display name. This address has not been resolved
* to a contact and therefore does not have a contact id or photo.
*/
public static RecipientEntry constructGeneratedEntry(String display, String address,
boolean isValid) {
return new RecipientEntry(ENTRY_TYPE_PERSON, display, address, INVALID_DESTINATION_TYPE,
null, GENERATED_CONTACT, null /* directoryId */, GENERATED_CONTACT, null, true,
isValid, null /* lookupKey */, null /* permissions */);
}
public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource,
String destination, int destinationType, String destinationLabel, long contactId,
Long directoryId, long dataId, Uri photoThumbnailUri, boolean isValid,
String lookupKey) {
return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
displayName, destination), destination, destinationType, destinationLabel,
contactId, directoryId, dataId, photoThumbnailUri, true, isValid, lookupKey,
null /* permissions */);
}
public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource,
String destination, int destinationType, String destinationLabel, long contactId,
Long directoryId, long dataId, String thumbnailUriAsString, boolean isValid,
String lookupKey) {
return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
displayName, destination), destination, destinationType, destinationLabel,
contactId, directoryId, dataId, (thumbnailUriAsString != null
? Uri.parse(thumbnailUriAsString) : null), true, isValid, lookupKey,
null /* permissions */);
}
public static RecipientEntry constructSecondLevelEntry(String displayName,
int displayNameSource, String destination, int destinationType,
String destinationLabel, long contactId, Long directoryId, long dataId,
String thumbnailUriAsString, boolean isValid, String lookupKey) {
return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
displayName, destination), destination, destinationType, destinationLabel,
contactId, directoryId, dataId, (thumbnailUriAsString != null
? Uri.parse(thumbnailUriAsString) : null), false, isValid, lookupKey,
null /* permissions */);
}
public static RecipientEntry constructPermissionEntry(String[] permissions) {
return new RecipientEntry(
ENTRY_TYPE_PERMISSION_REQUEST,
"" /* displayName */,
"" /* destination */,
Email.TYPE_CUSTOM,
"" /* destinationLabel */,
INVALID_CONTACT,
null /* directoryId */,
INVALID_CONTACT,
null /* photoThumbnailUri */,
true /* isFirstLevel*/,
false /* isValid */,
null /* lookupKey */,
permissions);
}
/**
* @return the display name for the entry. If the display name source is larger than
* {@link DisplayNameSources#PHONE} we use the contact's display name, but if not,
* i.e. the display name came from an email address or a phone number, we don't use it
* to avoid confusion and just use the destination instead.
*/
private static String pickDisplayName(int displayNameSource, String displayName,
String destination) {
return (displayNameSource > DisplayNameSources.PHONE) ? displayName : destination;
}
public int getEntryType() {
return mEntryType;
}
public String getDisplayName() {
return mDisplayName;
}
public String getDestination() {
return mDestination;
}
public int getDestinationType() {
return mDestinationType;
}
public String getDestinationLabel() {
return mDestinationLabel;
}
public long getContactId() {
return mContactId;
}
public Long getDirectoryId() {
return mDirectoryId;
}
public long getDataId() {
return mDataId;
}
public boolean isFirstLevel() {
return mIsFirstLevel;
}
public Uri getPhotoThumbnailUri() {
return mPhotoThumbnailUri;
}
/** Indicates whether the icon in the chip is displayed or not. */
public boolean shouldDisplayIcon() {
return mShouldDisplayIcon;
}
/** This can be called outside main Looper thread. */
public synchronized void setPhotoBytes(byte[] photoBytes) {
mPhotoBytes = photoBytes;
}
/** This can be called outside main Looper thread. */
public synchronized byte[] getPhotoBytes() {
return mPhotoBytes;
}
/**
* Used together with {@link #ENTRY_TYPE_PERMISSION_REQUEST} and indicates what permissions we
* need to ask user to grant.
*/
public String[] getPermissions() {
return mPermissions;
}
public String getLookupKey() {
return mLookupKey;
}
public boolean isSelectable() {
return mEntryType == ENTRY_TYPE_PERSON || mEntryType == ENTRY_TYPE_PERMISSION_REQUEST;
}
@Override
public String toString() {
return mDisplayName + " <" + mDestination + ">, isValid=" + mIsValid;
}
/**
* Returns if entry represents the same person as this instance. The default implementation
* checks whether the contact ids are the same, and subclasses may opt to override this.
*/
public boolean isSamePerson(final RecipientEntry entry) {
return entry != null && mContactId == entry.mContactId;
}
/**
* Returns the resource ID for the indicator icon, or 0 if no icon should be displayed.
*/
@DrawableRes
public int getIndicatorIconId() {
return mIndicatorIconId;
}
/**
* Sets the indicator icon to the given resource ID. Set to 0 to display no icon.
*/
public void setIndicatorIconId(@DrawableRes int indicatorIconId) {
mIndicatorIconId = indicatorIconId;
}
/**
* Get the indicator text, or null if no text should be displayed.
*/
public String getIndicatorText() {
return mIndicatorText;
}
/**
* Set the indicator text. Set to null for no text to be displayed.
*/
public void setIndicatorText(String indicatorText) {
mIndicatorText = indicatorText;
}
/**
* Get whether this RecipientEntry is in a replaced chip or not. Replaced chip only occurs
* if {@link RecipientEditTextView} uses a replacement chip for the entry.
*/
public boolean getInReplacedChip() {
return mInReplacedChip;
}
/**
* Sets {@link #mInReplacedChip} to {@param inReplacedChip}.
*/
public void setInReplacedChip(boolean inReplacedChip) {
mInReplacedChip = inReplacedChip;
}
}
@@ -0,0 +1,51 @@
/*
* 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.ex.chips;
import android.content.Context;
import android.graphics.drawable.StateListDrawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import com.android.ex.chips.DropdownChipLayouter.AdapterType;
class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry> {
private final DropdownChipLayouter mDropdownChipLayouter;
private final StateListDrawable mDeleteDrawable;
public SingleRecipientArrayAdapter(Context context, RecipientEntry entry,
DropdownChipLayouter dropdownChipLayouter) {
this(context, entry, dropdownChipLayouter, null);
}
public SingleRecipientArrayAdapter(Context context, RecipientEntry entry,
DropdownChipLayouter dropdownChipLayouter, StateListDrawable deleteDrawable) {
super(context,
dropdownChipLayouter.getAlternateItemLayoutResId(AdapterType.SINGLE_RECIPIENT),
new RecipientEntry[] { entry });
mDropdownChipLayouter = dropdownChipLayouter;
mDeleteDrawable = deleteDrawable;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return mDropdownChipLayouter.bindView(convertView, parent, getItem(position), position,
AdapterType.SINGLE_RECIPIENT, null, mDeleteDrawable);
}
}
@@ -0,0 +1,83 @@
/*
* 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.ex.chips.recipientchip;
import com.android.ex.chips.RecipientEntry;
/**
* BaseRecipientChip defines an object that contains information relevant to a
* particular recipient.
*/
interface BaseRecipientChip {
/**
* Set the selected state of the chip.
*/
void setSelected(boolean selected);
/**
* Return true if the chip is selected.
*/
boolean isSelected();
/**
* Get the text displayed in the chip.
*/
CharSequence getDisplay();
/**
* Get the text value this chip represents.
*/
CharSequence getValue();
/**
* Get the id of the contact associated with this chip.
*/
long getContactId();
/**
* Get the directory id of the contact associated with this chip.
*/
Long getDirectoryId();
/**
* Get the directory lookup key associated with this chip, or <code>null</code>.
*/
String getLookupKey();
/**
* Get the id of the data associated with this chip.
*/
long getDataId();
/**
* Get associated RecipientEntry.
*/
RecipientEntry getEntry();
/**
* Set the text in the edittextview originally associated with this chip
* before any reverse lookups.
*/
void setOriginalText(String text);
/**
* Set the text in the edittextview originally associated with this chip
* before any reverse lookups.
*/
CharSequence getOriginalText();
}
@@ -0,0 +1,42 @@
/*
* 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.ex.chips.recipientchip;
import android.graphics.Canvas;
import android.graphics.Rect;
/**
* RecipientChip defines a drawable object that contains information relevant to a
* particular recipient.
*/
public interface DrawableRecipientChip extends BaseRecipientChip {
/**
* Get the bounds of the chip; may be 0,0 if it is not visibly rendered.
*/
Rect getBounds();
/**
* Gets the bounds of the warning icon on the chip; may be 0,0 if not visibly rendered or if
* there is no warning icon.
*/
Rect getWarningIconBounds();
/**
* Draw the chip.
*/
void draw(Canvas canvas);
}
@@ -0,0 +1,121 @@
/*
* 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.ex.chips.recipientchip;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.style.ReplacementSpan;
import com.android.ex.chips.RecipientEntry;
/**
* RecipientChip defines a span that contains information relevant to a
* particular recipient.
*/
public class InvisibleRecipientChip extends ReplacementSpan implements DrawableRecipientChip {
private final SimpleRecipientChip mDelegate;
private static final Rect NULL_RECTANGLE = new Rect(0, 0, 0, 0);
public InvisibleRecipientChip(final RecipientEntry entry) {
super();
mDelegate = new SimpleRecipientChip(entry);
}
@Override
public void setSelected(final boolean selected) {
mDelegate.setSelected(selected);
}
@Override
public boolean isSelected() {
return mDelegate.isSelected();
}
@Override
public CharSequence getDisplay() {
return mDelegate.getDisplay();
}
@Override
public CharSequence getValue() {
return mDelegate.getValue();
}
@Override
public long getContactId() {
return mDelegate.getContactId();
}
@Override
public Long getDirectoryId() {
return mDelegate.getDirectoryId();
}
@Override
public String getLookupKey() {
return mDelegate.getLookupKey();
}
@Override
public long getDataId() {
return mDelegate.getDataId();
}
@Override
public RecipientEntry getEntry() {
return mDelegate.getEntry();
}
@Override
public void setOriginalText(final String text) {
mDelegate.setOriginalText(text);
}
@Override
public CharSequence getOriginalText() {
return mDelegate.getOriginalText();
}
@Override
public void draw(final Canvas canvas, final CharSequence text, final int start, final int end,
final float x, final int top, final int y, final int bottom, final Paint paint) {
// Do nothing.
}
@Override
public int getSize(final Paint paint, final CharSequence text, final int start, final int end,
final Paint.FontMetricsInt fm) {
return 0;
}
@Override
public Rect getBounds() {
return NULL_RECTANGLE;
}
@Override
public Rect getWarningIconBounds() {
return NULL_RECTANGLE;
}
@Override
public void draw(final Canvas canvas) {
// do nothing.
}
}
@@ -0,0 +1,63 @@
package com.android.ex.chips.recipientchip;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.style.ReplacementSpan;
/**
* ReplacementSpan that properly draws the drawable that is centered around the text
* without changing the default text size or layout.
*/
public class ReplacementDrawableSpan extends ReplacementSpan {
protected static final Paint sWorkPaint = new Paint();
protected Drawable mDrawable;
private float mExtraMargin;
public ReplacementDrawableSpan(Drawable drawable) {
super();
mDrawable = drawable;
}
public void setExtraMargin(float margin) {
mExtraMargin = margin;
}
private void setupFontMetrics(Paint.FontMetricsInt fm, Paint paint) {
sWorkPaint.set(paint);
if (fm != null) {
sWorkPaint.getFontMetricsInt(fm);
final Rect bounds = getBounds();
final int textHeight = fm.descent - fm.ascent;
final int halfMargin = (int) mExtraMargin / 2;
fm.ascent = Math.min(fm.top, fm.top + (textHeight - bounds.bottom) / 2) - halfMargin;
fm.descent = Math.max(fm.bottom, fm.bottom + (bounds.bottom - textHeight) / 2)
+ halfMargin;
fm.top = fm.ascent;
fm.bottom = fm.descent;
}
}
@Override
public int getSize(Paint paint, CharSequence text, int i, int i2, Paint.FontMetricsInt fm) {
setupFontMetrics(fm, paint);
return getBounds().right;
}
@Override
public void draw(Canvas canvas, CharSequence charSequence, int start, int end, float x, int top,
int y, int bottom, Paint paint) {
canvas.save();
int transY = (bottom - mDrawable.getBounds().bottom + top) / 2;
canvas.translate(x, transY);
mDrawable.draw(canvas);
canvas.restore();
}
protected Rect getBounds() {
return mDrawable.getBounds();
}
}
@@ -0,0 +1,115 @@
/*
* 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.ex.chips.recipientchip;
import com.android.ex.chips.RecipientEntry;
import android.text.TextUtils;
class SimpleRecipientChip implements BaseRecipientChip {
private final CharSequence mDisplay;
private final CharSequence mValue;
private final long mContactId;
private final Long mDirectoryId;
private final String mLookupKey;
private final long mDataId;
private final RecipientEntry mEntry;
private boolean mSelected = false;
private CharSequence mOriginalText;
public SimpleRecipientChip(final RecipientEntry entry) {
mDisplay = entry.getDisplayName();
mValue = entry.getDestination().trim();
mContactId = entry.getContactId();
mDirectoryId = entry.getDirectoryId();
mLookupKey = entry.getLookupKey();
mDataId = entry.getDataId();
mEntry = entry;
}
@Override
public void setSelected(final boolean selected) {
mSelected = selected;
}
@Override
public boolean isSelected() {
return mSelected;
}
@Override
public CharSequence getDisplay() {
return mDisplay;
}
@Override
public CharSequence getValue() {
return mValue;
}
@Override
public long getContactId() {
return mContactId;
}
@Override
public Long getDirectoryId() {
return mDirectoryId;
}
@Override
public String getLookupKey() {
return mLookupKey;
}
@Override
public long getDataId() {
return mDataId;
}
@Override
public RecipientEntry getEntry() {
return mEntry;
}
@Override
public void setOriginalText(final String text) {
if (TextUtils.isEmpty(text)) {
mOriginalText = text;
} else {
mOriginalText = text.trim();
}
}
@Override
public CharSequence getOriginalText() {
return !TextUtils.isEmpty(mOriginalText) ? mOriginalText : mEntry.getDestination();
}
@Override
public String toString() {
return mDisplay + " <" + mValue + ">";
}
}
@@ -0,0 +1,116 @@
/*
* 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.ex.chips.recipientchip;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import com.android.ex.chips.RecipientEntry;
/**
* VisibleRecipientChip defines a ReplacementSpan that contains information relevant to a
* particular recipient and renders a background asset to go with it.
*/
public class VisibleRecipientChip extends ReplacementDrawableSpan implements DrawableRecipientChip {
private final SimpleRecipientChip mDelegate;
private Rect mWarningIconBounds = new Rect(0, 0, 0, 0);
public VisibleRecipientChip(final Drawable drawable, final RecipientEntry entry) {
super(drawable);
mDelegate = new SimpleRecipientChip(entry);
}
@Override
public void setSelected(final boolean selected) {
mDelegate.setSelected(selected);
}
@Override
public boolean isSelected() {
return mDelegate.isSelected();
}
@Override
public CharSequence getDisplay() {
return mDelegate.getDisplay();
}
@Override
public CharSequence getValue() {
return mDelegate.getValue();
}
@Override
public long getContactId() {
return mDelegate.getContactId();
}
@Override
public Long getDirectoryId() {
return mDelegate.getDirectoryId();
}
@Override
public String getLookupKey() {
return mDelegate.getLookupKey();
}
@Override
public long getDataId() {
return mDelegate.getDataId();
}
@Override
public RecipientEntry getEntry() {
return mDelegate.getEntry();
}
@Override
public void setOriginalText(final String text) {
mDelegate.setOriginalText(text);
}
@Override
public CharSequence getOriginalText() {
return mDelegate.getOriginalText();
}
@Override
public Rect getBounds() {
return super.getBounds();
}
@Override
public void draw(final Canvas canvas) {
mDrawable.draw(canvas);
}
@Override
public String toString() {
return mDelegate.toString();
}
@Override
public Rect getWarningIconBounds() {
return mWarningIconBounds;
}
public void setWarningIconBounds(Rect warningIconBounds) {
mWarningIconBounds = warningIconBounds;
}
}
+29
View File
@@ -0,0 +1,29 @@
# 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.
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := libChipsTests
LOCAL_MODULE_TAGS := tests
LOCAL_SDK_VERSION := current
LOCAL_CERTIFICATE := platform
LOCAL_STATIC_JAVA_LIBRARIES := libchips legacy-android-test junit
LOCAL_RESOURCE_DIR := frameworks/opt/chips/res/
LOCAL_AAPT_FLAGS := --auto-add-overlay
LOCAL_AAPT_FLAGS += --extra-packages com.android.ex.chips
include $(BUILD_PACKAGE)
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ex.chips.tests"
android:sharedUserId="com.android.uid.test">
<application>
<uses-library android:name="android.test.runner" />
</application>
<!-- Run tests with "runtest android-common" -->
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.ex.chips.tests"
android:label="Chips Tests" />
</manifest>
File diff suppressed because it is too large Load Diff
@@ -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.ex.chips;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.provider.ContactsContract.DisplayNameSources;
import android.test.AndroidTestCase;
import com.android.ex.chips.RecipientAlternatesAdapter;
import com.android.ex.chips.RecipientEntry;
public class RecipientAlternatesAdapterTest extends AndroidTestCase {
public void testRemoveUndesiredDestinations() {
MatrixCursor c = new MatrixCursor(Queries.EMAIL.getProjection());
Cursor result;
// Test: Empty input
assertEquals(0, RecipientAlternatesAdapter.removeUndesiredDestinations(c,
null /* desiredMimeType */, null /* lookupKey */).getCount());
// Test: One row
addRow(c, "a", "1@android.com", 1, "home", 1000, 2000, "x", 0);
result = RecipientAlternatesAdapter.removeUndesiredDestinations(c,
null /* desiredMimeType */, null /* lookupKey */);
assertEquals(1, result.getCount());
assertRow(result, 0, "a", "1@android.com", 1, "home", 1000, 2000, "x", 0);
// Test: two unique rows, different destinations
addRow(c, "a", "2@android.com", 1, "home", 1000, 2000, "x", 0);
result = RecipientAlternatesAdapter.removeUndesiredDestinations(c,
null /* desiredMimeType */, null /* lookupKey */);
assertEquals(2, result.getCount());
assertRow(result, 0, "a", "1@android.com", 1, "home", 1000, 2000, "x", 0);
assertRow(result, 1, "a", "2@android.com", 1, "home", 1000, 2000, "x", 0);
// Test: add a third row with a non-unique destination.
addRow(c, "ax", "1@android.com", 11, "homex", 10001, 2000, "xx", 1);
// Third row should be removed.
result = RecipientAlternatesAdapter.removeUndesiredDestinations(c,
null /* desiredMimeType */, null /* lookupKey */);
assertEquals(2, result.getCount());
assertRow(result, 0, "a", "1@android.com", 1, "home", 1000, 2000, "x", 0);
assertRow(result, 1, "a", "2@android.com", 1, "home", 1000, 2000, "x", 0);
// Test: add a forth row with a non-unique destination again.
addRow(c, "ax", "2@android.com", 11, "homex", 10001, 2000, "xx", 1);
// Forth row should also be removed.
result = RecipientAlternatesAdapter.removeUndesiredDestinations(c,
null /* desiredMimeType */, null /* lookupKey */);
assertEquals(2, result.getCount());
assertRow(result, 0, "a", "1@android.com", 1, "home", 1000, 2000, "x", 0);
assertRow(result, 1, "a", "2@android.com", 1, "home", 1000, 2000, "x", 0);
}
private static MatrixCursor addRow(MatrixCursor c,
String displayName,
String destination,
int destinationType,
String destinationLabel,
long contactId,
long dataId,
String photoUri,
int displayNameSource
) {
c.addRow(new Object[] {displayName, destination, destinationType, destinationLabel,
contactId, dataId, photoUri, displayNameSource});
return c;
}
private static void assertRow(Cursor c, int position,
String displayName,
String destination,
int destinationType,
String destinationLabel,
long contactId,
long dataId,
String photoUri,
int displayNameSource
) {
assertTrue(c.moveToPosition(position));
assertEquals(displayName, c.getString(0));
assertEquals(destination, c.getString(1));
assertEquals(destinationType, c.getInt(2));
assertEquals(destinationLabel, c.getString(3));
assertEquals(contactId, c.getLong(4));
assertEquals(dataId, c.getLong(5));
assertEquals(photoUri, c.getString(6));
assertEquals(displayNameSource, c.getInt(7));
}
public void testGetBetterRecipient() {
// Ensure that if either (but not both) parameters are null, the other is returned
{
final RecipientEntry entry1 =
RecipientEntry.constructFakeEntry("1@android.com", true);
final RecipientEntry entry2 = null;
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry1, entry2), entry1);
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry2, entry1), entry1);
}
// Ensure that if only one has a display name, it is used
{
final RecipientEntry entry1 =
RecipientEntry.constructTopLevelEntry("Android", DisplayNameSources.NICKNAME,
"1@android.com", 0, null, 0, null /* directoryId */, 0, (Uri) null,
true, null /* lookupKey */);
final RecipientEntry entry2 = RecipientEntry.constructFakeEntry("1@android.com", true);
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry1, entry2), entry1);
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry2, entry1), entry1);
}
// Ensure that if one has a display name different from its destination, and the other's
// is equal to its destination, we use the unique one
{
final RecipientEntry entry1 =
RecipientEntry.constructTopLevelEntry("Android", DisplayNameSources.NICKNAME,
"1@android.com", 0, null, 0, null /* directoryId */, 0, (Uri) null,
true, null /* lookupKey */);
final RecipientEntry entry2 =
RecipientEntry.constructTopLevelEntry("2@android.com", DisplayNameSources.EMAIL,
"2@android.com", 0, null, 0, null /* directoryId */, 0, (Uri) null,
true, null /* lookupKey */);
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry1, entry2), entry1);
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry2, entry1), entry1);
}
// Ensure that if only one has a photo, it is used
{
final RecipientEntry entry1 =
RecipientEntry.constructTopLevelEntry("Android", DisplayNameSources.NICKNAME,
"1@android.com", 0, null, 0, null /* directoryId */, 0,
Uri.parse("http://www.android.com"), true, null /* lookupKey */);
final RecipientEntry entry2 =
RecipientEntry.constructTopLevelEntry("Android", DisplayNameSources.EMAIL,
"2@android.com", 0, null, 0, null /* directoryId */,
0, (Uri) null, true, null /* lookupKey */);
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry1, entry2), entry1);
assertEquals(RecipientAlternatesAdapter.getBetterRecipient(entry2, entry1), entry1);
}
}
}
Submodule modules/colorpicker deleted from 34d5137f88
+2
View File
@@ -0,0 +1,2 @@
bin/
gen/
+27
View File
@@ -0,0 +1,27 @@
// 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 {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_library {
name: "colorpicker",
sdk_version: "15",
srcs: [
"src/**/*.java",
"src/**/*.logtags",
],
resource_dirs: ["res"],
}
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.colorpicker"
android:versionCode="1"
android:versionName="1.0" >
</manifest>
+25
View File
@@ -0,0 +1,25 @@
plugins {
id 'com.android.library'
}
android {
namespace 'com.android.colorpicker'
compileSdk 35
defaultConfig {
minSdk 33
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
res.srcDirs = ['res']
}
}
}
+15
View File
@@ -0,0 +1,15 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-15
android.library=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" />
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center" >
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:padding="28dp" >
<ProgressBar
android:id="@android:id/progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center" />
<com.android.colorpicker.ColorPickerPalette
android:id="@+id/color_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="gone" />
</FrameLayout>
</ScrollView>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="@+id/color_picker_swatch"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/color_picker_checkmark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/ic_colorpicker_swatch_selected"
android:scaleType="fitXY"
android:visibility="gone" />
</merge>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<dimen name="color_swatch_large">64dip</dimen>
<dimen name="color_swatch_small">48dip</dimen>
<dimen name="color_swatch_margins_large">8dip</dimen>
<dimen name="color_swatch_margins_small">4dip</dimen>
</resources>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Default title for color picker dialog [CHAR LIMIT=30] -->
<string name="color_picker_default_title">Select a Color</string>
<!-- Content description for a color square. -->
<string name="color_swatch_description">Color <xliff:g id="color_index" example="14">%1$d</xliff:g></string>
<!-- Content description for a selected color square. -->
<string name="color_swatch_description_selected">Color <xliff:g id="color_index" example="14">%1$d</xliff:g> selected</string>
</resources>
@@ -0,0 +1,213 @@
/*
* 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.colorpicker;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
/**
* A dialog which takes in as input an array of colors and creates a palette allowing the user to
* select a specific color swatch, which invokes a listener.
*/
public class ColorPickerDialog extends DialogFragment implements OnColorSelectedListener {
public static final int SIZE_LARGE = 1;
public static final int SIZE_SMALL = 2;
protected AlertDialog mAlertDialog;
protected static final String KEY_TITLE_ID = "title_id";
protected static final String KEY_COLORS = "colors";
protected static final String KEY_COLOR_CONTENT_DESCRIPTIONS = "color_content_descriptions";
protected static final String KEY_SELECTED_COLOR = "selected_color";
protected static final String KEY_COLUMNS = "columns";
protected static final String KEY_SIZE = "size";
protected int mTitleResId = R.string.color_picker_default_title;
protected int[] mColors = null;
protected String[] mColorContentDescriptions = null;
protected int mSelectedColor;
protected int mColumns;
protected int mSize;
private ColorPickerPalette mPalette;
private ProgressBar mProgress;
protected OnColorSelectedListener mListener;
public ColorPickerDialog() {
// Empty constructor required for dialog fragments.
}
public static ColorPickerDialog newInstance(int titleResId, int[] colors, int selectedColor,
int columns, int size) {
ColorPickerDialog ret = new ColorPickerDialog();
ret.initialize(titleResId, colors, selectedColor, columns, size);
return ret;
}
public void initialize(int titleResId, int[] colors, int selectedColor, int columns, int size) {
setArguments(titleResId, columns, size);
setColors(colors, selectedColor);
}
public void setArguments(int titleResId, int columns, int size) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_TITLE_ID, titleResId);
bundle.putInt(KEY_COLUMNS, columns);
bundle.putInt(KEY_SIZE, size);
setArguments(bundle);
}
public void setOnColorSelectedListener(OnColorSelectedListener listener) {
mListener = listener;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mTitleResId = getArguments().getInt(KEY_TITLE_ID);
mColumns = getArguments().getInt(KEY_COLUMNS);
mSize = getArguments().getInt(KEY_SIZE);
}
if (savedInstanceState != null) {
mColors = savedInstanceState.getIntArray(KEY_COLORS);
mSelectedColor = (Integer) savedInstanceState.getSerializable(KEY_SELECTED_COLOR);
mColorContentDescriptions = savedInstanceState.getStringArray(
KEY_COLOR_CONTENT_DESCRIPTIONS);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity();
View view = LayoutInflater.from(getActivity()).inflate(R.layout.color_picker_dialog, null);
mProgress = (ProgressBar) view.findViewById(android.R.id.progress);
mPalette = (ColorPickerPalette) view.findViewById(R.id.color_picker);
mPalette.init(mSize, mColumns, this);
if (mColors != null) {
showPaletteView();
}
mAlertDialog = new AlertDialog.Builder(activity)
.setTitle(mTitleResId)
.setView(view)
.create();
return mAlertDialog;
}
@Override
public void onColorSelected(int color) {
if (mListener != null) {
mListener.onColorSelected(color);
}
if (getTargetFragment() instanceof OnColorSelectedListener) {
final OnColorSelectedListener listener =
(OnColorSelectedListener) getTargetFragment();
listener.onColorSelected(color);
}
if (color != mSelectedColor) {
mSelectedColor = color;
// Redraw palette to show checkmark on newly selected color before dismissing.
mPalette.drawPalette(mColors, mSelectedColor);
}
dismiss();
}
public void showPaletteView() {
if (mProgress != null && mPalette != null) {
mProgress.setVisibility(View.GONE);
refreshPalette();
mPalette.setVisibility(View.VISIBLE);
}
}
public void showProgressBarView() {
if (mProgress != null && mPalette != null) {
mProgress.setVisibility(View.VISIBLE);
mPalette.setVisibility(View.GONE);
}
}
public void setColors(int[] colors, int selectedColor) {
if (mColors != colors || mSelectedColor != selectedColor) {
mColors = colors;
mSelectedColor = selectedColor;
refreshPalette();
}
}
public void setColors(int[] colors) {
if (mColors != colors) {
mColors = colors;
refreshPalette();
}
}
public void setSelectedColor(int color) {
if (mSelectedColor != color) {
mSelectedColor = color;
refreshPalette();
}
}
public void setColorContentDescriptions(String[] colorContentDescriptions) {
if (mColorContentDescriptions != colorContentDescriptions) {
mColorContentDescriptions = colorContentDescriptions;
refreshPalette();
}
}
private void refreshPalette() {
if (mPalette != null && mColors != null) {
mPalette.drawPalette(mColors, mSelectedColor, mColorContentDescriptions);
}
}
public int[] getColors() {
return mColors;
}
public int getSelectedColor() {
return mSelectedColor;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntArray(KEY_COLORS, mColors);
outState.putSerializable(KEY_SELECTED_COLOR, mSelectedColor);
outState.putStringArray(KEY_COLOR_CONTENT_DESCRIPTIONS, mColorContentDescriptions);
}
}
@@ -0,0 +1,194 @@
/*
* 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.colorpicker;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TableLayout;
import android.widget.TableRow;
import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
/**
* A color picker custom view which creates an grid of color squares. The number of squares per
* row (and the padding between the squares) is determined by the user.
*/
public class ColorPickerPalette extends TableLayout {
public OnColorSelectedListener mOnColorSelectedListener;
private String mDescription;
private String mDescriptionSelected;
private int mSwatchLength;
private int mMarginSize;
private int mNumColumns;
public ColorPickerPalette(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ColorPickerPalette(Context context) {
super(context);
}
/**
* Initialize the size, columns, and listener. Size should be a pre-defined size (SIZE_LARGE
* or SIZE_SMALL) from ColorPickerDialogFragment.
*/
public void init(int size, int columns, OnColorSelectedListener listener) {
mNumColumns = columns;
Resources res = getResources();
if (size == ColorPickerDialog.SIZE_LARGE) {
mSwatchLength = res.getDimensionPixelSize(R.dimen.color_swatch_large);
mMarginSize = res.getDimensionPixelSize(R.dimen.color_swatch_margins_large);
} else {
mSwatchLength = res.getDimensionPixelSize(R.dimen.color_swatch_small);
mMarginSize = res.getDimensionPixelSize(R.dimen.color_swatch_margins_small);
}
mOnColorSelectedListener = listener;
mDescription = res.getString(R.string.color_swatch_description);
mDescriptionSelected = res.getString(R.string.color_swatch_description_selected);
}
private TableRow createTableRow() {
TableRow row = new TableRow(getContext());
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
row.setLayoutParams(params);
return row;
}
/**
* Adds swatches to table in a serpentine format.
*/
public void drawPalette(int[] colors, int selectedColor) {
drawPalette(colors, selectedColor, null);
}
/**
* Adds swatches to table in a serpentine format.
*/
public void drawPalette(int[] colors, int selectedColor, String[] colorContentDescriptions) {
if (colors == null) {
return;
}
this.removeAllViews();
int tableElements = 0;
int rowElements = 0;
int rowNumber = 0;
// Fills the table with swatches based on the array of colors.
TableRow row = createTableRow();
for (int color : colors) {
View colorSwatch = createColorSwatch(color, selectedColor);
setSwatchDescription(rowNumber, tableElements, rowElements, color == selectedColor,
colorSwatch, colorContentDescriptions);
addSwatchToRow(row, colorSwatch, rowNumber);
tableElements++;
rowElements++;
if (rowElements == mNumColumns) {
addView(row);
row = createTableRow();
rowElements = 0;
rowNumber++;
}
}
// Create blank views to fill the row if the last row has not been filled.
if (rowElements > 0) {
while (rowElements != mNumColumns) {
addSwatchToRow(row, createBlankSpace(), rowNumber);
rowElements++;
}
addView(row);
}
}
/**
* Appends a swatch to the end of the row for even-numbered rows (starting with row 0),
* to the beginning of a row for odd-numbered rows.
*/
private static void addSwatchToRow(TableRow row, View swatch, int rowNumber) {
if (rowNumber % 2 == 0) {
row.addView(swatch);
} else {
row.addView(swatch, 0);
}
}
/**
* Add a content description to the specified swatch view. Because the colors get added in a
* snaking form, every other row will need to compensate for the fact that the colors are added
* in an opposite direction from their left->right/top->bottom order, which is how the system
* will arrange them for accessibility purposes.
*/
private void setSwatchDescription(int rowNumber, int index, int rowElements, boolean selected,
View swatch, String[] contentDescriptions) {
String description;
if (contentDescriptions != null && contentDescriptions.length > index) {
description = contentDescriptions[index];
} else {
int accessibilityIndex;
if (rowNumber % 2 == 0) {
// We're in a regular-ordered row
accessibilityIndex = index + 1;
} else {
// We're in a backwards-ordered row.
int rowMax = ((rowNumber + 1) * mNumColumns);
accessibilityIndex = rowMax - rowElements;
}
if (selected) {
description = String.format(mDescriptionSelected, accessibilityIndex);
} else {
description = String.format(mDescription, accessibilityIndex);
}
}
swatch.setContentDescription(description);
}
/**
* Creates a blank space to fill the row.
*/
private ImageView createBlankSpace() {
ImageView view = new ImageView(getContext());
TableRow.LayoutParams params = new TableRow.LayoutParams(mSwatchLength, mSwatchLength);
params.setMargins(mMarginSize, mMarginSize, mMarginSize, mMarginSize);
view.setLayoutParams(params);
return view;
}
/**
* Creates a color swatch.
*/
private ColorPickerSwatch createColorSwatch(int color, int selectedColor) {
ColorPickerSwatch view = new ColorPickerSwatch(getContext(), color,
color == selectedColor, mOnColorSelectedListener);
TableRow.LayoutParams params = new TableRow.LayoutParams(mSwatchLength, mSwatchLength);
params.setMargins(mMarginSize, mMarginSize, mMarginSize, mMarginSize);
view.setLayoutParams(params);
return view;
}
}
@@ -0,0 +1,80 @@
/*
* 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.colorpicker;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
/**
* Creates a circular swatch of a specified color. Adds a checkmark if marked as checked.
*/
public class ColorPickerSwatch extends FrameLayout implements View.OnClickListener {
private int mColor;
private ImageView mSwatchImage;
private ImageView mCheckmarkImage;
private OnColorSelectedListener mOnColorSelectedListener;
/**
* Interface for a callback when a color square is selected.
*/
public interface OnColorSelectedListener {
/**
* Called when a specific color square has been selected.
*/
public void onColorSelected(int color);
}
public ColorPickerSwatch(Context context, int color, boolean checked,
OnColorSelectedListener listener) {
super(context);
mColor = color;
mOnColorSelectedListener = listener;
LayoutInflater.from(context).inflate(R.layout.color_picker_swatch, this);
mSwatchImage = (ImageView) findViewById(R.id.color_picker_swatch);
mCheckmarkImage = (ImageView) findViewById(R.id.color_picker_checkmark);
setColor(color);
setChecked(checked);
setOnClickListener(this);
}
protected void setColor(int color) {
Drawable[] colorDrawable = new Drawable[]
{getContext().getResources().getDrawable(R.drawable.color_picker_swatch)};
mSwatchImage.setImageDrawable(new ColorStateDrawable(colorDrawable, color));
}
private void setChecked(boolean checked) {
if (checked) {
mCheckmarkImage.setVisibility(View.VISIBLE);
} else {
mCheckmarkImage.setVisibility(View.GONE);
}
}
@Override
public void onClick(View v) {
if (mOnColorSelectedListener != null) {
mOnColorSelectedListener.onColorSelected(mColor);
}
}
}
@@ -0,0 +1,72 @@
/*
* 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.colorpicker;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
/**
* A drawable which sets its color filter to a color specified by the user, and changes to a
* slightly darker color when pressed or focused.
*/
public class ColorStateDrawable extends LayerDrawable {
private static final float PRESSED_STATE_MULTIPLIER = 0.70f;
private int mColor;
public ColorStateDrawable(Drawable[] layers, int color) {
super(layers);
mColor = color;
}
@Override
protected boolean onStateChange(int[] states) {
boolean pressedOrFocused = false;
for (int state : states) {
if (state == android.R.attr.state_pressed || state == android.R.attr.state_focused) {
pressedOrFocused = true;
break;
}
}
if (pressedOrFocused) {
super.setColorFilter(getPressedColor(mColor), PorterDuff.Mode.SRC_ATOP);
} else {
super.setColorFilter(mColor, PorterDuff.Mode.SRC_ATOP);
}
return super.onStateChange(states);
}
/**
* Given a particular color, adjusts its value by a multiplier.
*/
private static int getPressedColor(int color) {
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
hsv[2] = hsv[2] * PRESSED_STATE_MULTIPLIER;
return Color.HSVToColor(hsv);
}
@Override
public boolean isStateful() {
return true;
}
}
@@ -0,0 +1,61 @@
/*
* 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.colorpicker;
import android.graphics.Color;
import java.util.Comparator;
/**
* A color comparator which compares based on hue, saturation, and value.
*/
public class HsvColorComparator implements Comparator<Integer> {
@Override
public int compare(Integer lhs, Integer rhs) {
float[] hsv = new float[3];
Color.colorToHSV(lhs, hsv);
float hue1 = hsv[0];
float sat1 = hsv[1];
float val1 = hsv[2];
float[] hsv2 = new float[3];
Color.colorToHSV(rhs, hsv2);
float hue2 = hsv2[0];
float sat2 = hsv2[1];
float val2 = hsv2[2];
if (hue1 < hue2) {
return 1;
} else if (hue1 > hue2) {
return -1;
} else {
if (sat1 < sat2) {
return 1;
} else if (sat1 > sat2) {
return -1;
} else {
if (val1 < val2) {
return 1;
} else if (val1 > val2) {
return -1;
}
}
}
return 0;
}
}
Submodule modules/photoviewer deleted from fb5c100b4a
+7
View File
@@ -0,0 +1,7 @@
*~
*.bak
*.class
bin/
gen/
.classpath
.project
+74
View File
@@ -0,0 +1,74 @@
// 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.
//#################################################
// Build appcompat library
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_library {
name: "libphotoviewer_appcompat",
libs: ["androidx.annotation_annotation"],
static_libs: [
"androidx.core_core",
"androidx.legacy_legacy-support-core-ui",
"androidx.legacy_legacy-support-core-utils",
"androidx.fragment_fragment",
"androidx.appcompat_appcompat",
],
sdk_version: "current",
srcs: [
"src/**/*.java",
"appcompat/src/**/*.java",
"src/**/*.logtags",
],
resource_dirs: [
"appcompat/res",
"res",
],
}
//#################################################
// Build non-appcompat library
android_library {
name: "libphotoviewer",
libs: ["androidx.annotation_annotation"],
static_libs: [
"androidx.core_core",
"androidx.legacy_legacy-support-core-ui",
"androidx.legacy_legacy-support-core-utils",
"androidx.fragment_fragment",
],
sdk_version: "current",
srcs: [
"src/**/*.java",
"activity/src/**/*.java",
"src/**/*.logtags",
],
resource_dirs: [
"activity/res",
"res",
]
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ex.photo"
android:versionCode="1">
<application />
</manifest>
+1
View File
@@ -0,0 +1 @@
asapperstein@google.com
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<style name="PhotoViewTheme" parent="android:Theme.Holo">
<item name="android:colorPrimaryDark">@color/photo_status_bar_black</item>
<item name="android:navigationBarColor">@color/photo_status_bar_black</item>
<item name="android:windowNoTitle">false</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowBackground">@color/solid_black</item>
<item name="android:actionBarStyle">@style/Holo.ActionBar</item>
</style>
<style name="PhotoViewTheme.Translucent">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="PhotoViewTheme.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="Holo.ActionBar" parent="android:Widget.Holo.ActionBar">
<item name="android:background">@drawable/actionbar_translucent</item>
</style>
</resources>
@@ -0,0 +1,76 @@
package com.android.ex.photo;
import android.app.ActionBar;
import android.graphics.drawable.Drawable;
/**
* Wrapper around {@link ActionBar}.
*/
public class ActionBarWrapper implements ActionBarInterface {
private final ActionBar mActionBar;
private class MenuVisiblityListenerWrapper implements ActionBar.OnMenuVisibilityListener {
private final ActionBarInterface.OnMenuVisibilityListener mWrapped;
public MenuVisiblityListenerWrapper(ActionBarInterface.OnMenuVisibilityListener wrapped) {
mWrapped = wrapped;
}
@Override
public void onMenuVisibilityChanged(boolean isVisible) {
mWrapped.onMenuVisibilityChanged(isVisible);
}
}
public ActionBarWrapper(ActionBar actionBar) {
mActionBar = actionBar;
}
@Override
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
mActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
}
@Override
public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
mActionBar.addOnMenuVisibilityListener(new MenuVisiblityListenerWrapper(listener));
}
@Override
public void setDisplayOptionsShowTitle() {
mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
}
@Override
public CharSequence getTitle() {
return mActionBar.getTitle();
}
@Override
public void setTitle(CharSequence title) {
mActionBar.setTitle(title);
}
@Override
public void setSubtitle(CharSequence subtitle) {
mActionBar.setSubtitle(subtitle);
}
@Override
public void show() {
mActionBar.show();
}
@Override
public void hide() {
mActionBar.hide();
}
@Override
public void setLogo(Drawable logo) {
mActionBar.setLogo(logo);
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2011 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.ex.photo;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import android.view.Menu;
import android.view.MenuItem;
/**
* Activity to view the contents of an album.
*/
public class PhotoViewActivity extends FragmentActivity
implements PhotoViewController.ActivityInterface {
private PhotoViewController mController;
private ActionBarWrapper mActionBar;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mController = createController();
mController.onCreate(savedInstanceState);
}
public PhotoViewController createController() {
return new PhotoViewController(this);
}
@Override
public PhotoViewController getController() {
return mController;
}
@Override
protected void onStart() {
super.onStart();
mController.onStart();
}
@Override
protected void onResume() {
super.onResume();
mController.onResume();
}
@Override
protected void onPause() {
mController.onPause();
super.onPause();
}
@Override
protected void onStop() {
mController.onStop();
super.onStop();
}
@Override
protected void onDestroy() {
mController.onDestroy();
super.onDestroy();
}
@Override
public void onBackPressed() {
if (!mController.onBackPressed()) {
super.onBackPressed();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mController.onSaveInstanceState(outState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return mController.onCreateOptionsMenu(menu) || super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
return mController.onPrepareOptionsMenu(menu) || super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return mController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mController.onActivityResult(requestCode, resultCode, data);
}
@Override
public Context getContext() {
return this;
}
@Override
public ActionBarInterface getActionBarInterface() {
if (mActionBar == null) {
mActionBar = new ActionBarWrapper(getActionBar());
}
return mActionBar;
}
}
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<style name="PhotoViewTheme" parent="Theme.AppCompat">
<item name="actionBarStyle">@style/AppCompat.ActionBar</item>
<item name="colorPrimaryDark">@color/photo_status_bar_black</item>
<item name="android:navigationBarColor">@color/photo_status_bar_black</item>
<item name="windowActionBarOverlay">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@color/solid_black</item>
</style>
<style name="PhotoViewTheme.Translucent">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="PhotoViewTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppCompat.ActionBar" parent="Widget.AppCompat.ActionBar">
<item name="background">@drawable/actionbar_translucent</item>
</style>
</resources>
@@ -0,0 +1,77 @@
package com.android.ex.photo;
import android.graphics.drawable.Drawable;
import androidx.appcompat.app.ActionBar;
/**
* Wrapper around {@link ActionBar}.
*/
public class ActionBarWrapper implements ActionBarInterface {
private final ActionBar mActionBar;
private class MenuVisiblityListenerWrapper implements ActionBar.OnMenuVisibilityListener {
private final ActionBarInterface.OnMenuVisibilityListener mWrapped;
public MenuVisiblityListenerWrapper(ActionBarInterface.OnMenuVisibilityListener wrapped) {
mWrapped = wrapped;
}
@Override
public void onMenuVisibilityChanged(boolean isVisible) {
mWrapped.onMenuVisibilityChanged(isVisible);
}
}
public ActionBarWrapper(ActionBar actionBar) {
mActionBar = actionBar;
}
@Override
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
mActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
}
@Override
public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
mActionBar.addOnMenuVisibilityListener(new MenuVisiblityListenerWrapper(listener));
}
@Override
public void setDisplayOptionsShowTitle() {
mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
}
@Override
public CharSequence getTitle() {
return mActionBar.getTitle();
}
@Override
public void setTitle(CharSequence title) {
mActionBar.setTitle(title);
}
@Override
public void setSubtitle(CharSequence subtitle) {
mActionBar.setSubtitle(subtitle);
}
@Override
public void show() {
mActionBar.show();
}
@Override
public void hide() {
mActionBar.hide();
}
@Override
public void setLogo(Drawable logo) {
mActionBar.setLogo(logo);
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2011 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.ex.photo;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
/**
* Activity to view the contents of an album.
*/
public class PhotoViewActivity extends AppCompatActivity
implements PhotoViewController.ActivityInterface {
private PhotoViewController mController;
private ActionBarWrapper mActionBar;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mController = createController();
mController.onCreate(savedInstanceState);
}
protected PhotoViewController createController() {
return new PhotoViewController(this);
}
@Override
public PhotoViewController getController() {
return mController;
}
@Override
protected void onStart() {
super.onStart();
mController.onStart();
}
@Override
protected void onResume() {
super.onResume();
mController.onResume();
}
@Override
protected void onPause() {
mController.onPause();
super.onPause();
}
@Override
protected void onStop() {
mController.onStop();
super.onStop();
}
@Override
protected void onDestroy() {
mController.onDestroy();
super.onDestroy();
}
@Override
public void onBackPressed() {
if (!mController.onBackPressed()) {
super.onBackPressed();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mController.onSaveInstanceState(outState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return mController.onCreateOptionsMenu(menu) || super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
return mController.onPrepareOptionsMenu(menu) || super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return mController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mController.onActivityResult(requestCode, resultCode, data);
}
@Override
public Context getContext() {
return this;
}
@Override
public ActionBarInterface getActionBarInterface() {
if (mActionBar == null) {
mActionBar = new ActionBarWrapper(getSupportActionBar());
}
return mActionBar;
}
}
+33
View File
@@ -0,0 +1,33 @@
plugins {
id 'com.android.library'
}
android {
namespace 'com.android.ex.photo'
compileSdk 35
defaultConfig {
minSdk 33
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src', 'activity/src']
res.srcDirs = ['res', 'activity/res']
}
}
}
dependencies {
implementation 'androidx.annotation:annotation:1.8.2'
implementation 'androidx.core:core:1.15.0'
implementation 'androidx.fragment:fragment:1.8.5'
implementation 'androidx.loader:loader:1.1.0'
implementation 'androidx.viewpager:viewpager:1.0.0'
}
+15
View File
@@ -0,0 +1,15 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-19
android.library=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Some files were not shown because too many files have changed in this diff Show More