am 303c2c87: am 0a605d37: am 90808a36: am bc1a645f: Contacts Provider Training Class - Sample App Initial Commit This is the sample app for the Contacts Provider Android training class. It\'s a basic master/detail view with a list of contacts in the master and contact
* commit '303c2c87f893d144e6982f1b2ad097141fe25ea7': Contacts Provider Training Class - Sample App Initial Commit This is the sample app for the Contacts Provider Android training class. It's a basic master/detail view with a list of contacts in the master and contact name, photo and mailing addresses in the detail. This sample app is backward compatible to API level 7 and also optimized for all screen sizes.
72
samples/training/ContactsList/AndroidManifest.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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.example.android.contactslist"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="5"
|
||||
android:targetSdkVersion="17" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
|
||||
<application
|
||||
android:description="@string/app_description"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:allowBackup="true">
|
||||
|
||||
<!-- When the soft keyboard is showing the views of this activity should be resized in the
|
||||
remaining space so that inline searching can take place without having to dismiss the
|
||||
keyboard to see all the content. Therefore windowSoftInputMode is set to
|
||||
adjustResize. -->
|
||||
<activity
|
||||
android:name=".ui.ContactsListActivity"
|
||||
android:label="@string/activity_contacts_list"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- Add intent-filter for search intent action and specify searchable configuration
|
||||
via meta-data tag. This allows this activity to receive search intents via the
|
||||
system hooks. In this sample this is only used on older OS versions (pre-Honeycomb)
|
||||
via the activity search dialog. See the Search API guide for more information:
|
||||
http://developer.android.com/guide/topics/search/search-dialog.html -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable_contacts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.ContactDetailActivity"
|
||||
android:label="@string/activity_contact_detail"
|
||||
android:parentActivityName=".ui.ContactsListActivity">
|
||||
<!-- Define hierarchical parent of this activity, both via the system
|
||||
parentActivityName attribute (added in API Level 16) and via meta-data annotation.
|
||||
This allows use of the support library NavUtils class in a way that works over
|
||||
all Android versions. See the "Tasks and Back Stack" guide for more information:
|
||||
http://developer.android.com/guide/components/tasks-and-back-stack.html
|
||||
-->
|
||||
<meta-data android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.ContactsListActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
samples/training/ContactsList/libs/android-support-v4.jar
Normal file
|
After Width: | Height: | Size: 308 B |
|
After Width: | Height: | Size: 945 B |
|
After Width: | Height: | Size: 819 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 944 B |
BIN
samples/training/ContactsList/res/drawable-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 633 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 183 B |
|
After Width: | Height: | Size: 603 B |
|
After Width: | Height: | Size: 515 B |
|
After Width: | Height: | Size: 693 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 902 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 686 B |
BIN
samples/training/ContactsList/res/drawable-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 605 B |
|
After Width: | Height: | Size: 609 B |
|
After Width: | Height: | Size: 349 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
BIN
samples/training/ContactsList/res/drawable-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,29 @@
|
||||
<?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.
|
||||
-->
|
||||
<!-- Refer to documentation for the <selector> element. -->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:state_focused="false"
|
||||
android:state_selected="false"
|
||||
android:state_pressed="false"
|
||||
android:drawable="@drawable/quickcontact_badge_small_unpressed"/>
|
||||
|
||||
<item
|
||||
android:state_pressed="true"
|
||||
android:drawable="@drawable/quickcontact_badge_small_pressed"/>
|
||||
|
||||
</selector>
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="100">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/contact_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="@integer/contact_detail_photo_percent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_contact_picture_180_holo_light"
|
||||
android:contentDescription="@string/imageview_description"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="@integer/contact_detail_info_percent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:id="@+id/contact_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/padding"
|
||||
android:paddingRight="@dimen/padding"
|
||||
android:paddingTop="@dimen/padding"
|
||||
android:visibility="gone"
|
||||
android:textAppearance="@style/contactNameTitle"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contact_details_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/padding"
|
||||
android:orientation="vertical">
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- This view will be displayed when the views above are hidden. That happens when in two-pane
|
||||
layout mode and no contact is currently selected and therefore the this fragment will
|
||||
simply show a text message instead of contact details. -->
|
||||
<TextView android:id="@id/android:empty"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/no_contact_selected"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:visibility="gone"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
|
||||
</FrameLayout>
|
||||
26
samples/training/ContactsList/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- Main Activity single pane layout. This layout contains a single ContactsListFragment that
|
||||
displays a list of contacts. Tapping on a contact will start a new activity to display the
|
||||
contact details. -->
|
||||
|
||||
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="com.example.android.contactslist.ui.ContactsListFragment"
|
||||
android:id="@+id/contact_list"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"/>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- Main Activity two-pane layout. This layout has two panes, a ContactsListFragment on the left
|
||||
and a ContactDetailFragment on the right. Tapping on a contact in the list loads the details
|
||||
of that contact on the right. -->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:showDividers="middle"
|
||||
android:divider="?android:attr/listDivider"
|
||||
android:weightSum="100"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<fragment class="com.example.android.contactslist.ui.ContactsListFragment"
|
||||
android:id="@+id/contact_list"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="@integer/contact_list_percent"/>
|
||||
|
||||
<fragment class="com.example.android.contactslist.ui.ContactDetailFragment"
|
||||
android:id="@+id/contact_detail"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="@integer/contact_detail_percent"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,86 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- This layout is used by ContactDetailFragment to show contact details: contact photo, contact
|
||||
display name and a dynamic number of addresses (if the contact has any) inside a ScrollView.
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:weightSum="100">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/contact_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="@integer/contact_detail_photo_percent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_contact_picture_180_holo_light"
|
||||
android:contentDescription="@string/imageview_description"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="@integer/contact_detail_info_percent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:id="@+id/contact_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/padding"
|
||||
android:paddingRight="@dimen/padding"
|
||||
android:paddingTop="@dimen/padding"
|
||||
android:visibility="gone"
|
||||
style="@style/contactNameTitle"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contact_details_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/padding"
|
||||
android:orientation="vertical">
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- This view will be displayed when the views above are hidden. That happens when in two-pane
|
||||
layout mode and no contact is currently selected and therefore the this fragment will
|
||||
simply show a text message instead of contact details. -->
|
||||
<TextView android:id="@id/android:empty"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/no_contact_selected"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:visibility="gone"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- This layout is used to display a single mailing address for a contact. In the case of multiple
|
||||
mailing addresses it could be inflated multiple times and displayed in a ScrollView container
|
||||
to let the user more easily scroll over all addresses. -->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="@dimen/padding"
|
||||
android:paddingLeft="@dimen/padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_detail_header"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
style="@style/addressHeader"/>
|
||||
|
||||
<LinearLayout android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:showDividers="middle"
|
||||
android:dividerPadding="12dp"
|
||||
android:minHeight="48dp"
|
||||
android:divider="?android:attr/listDivider">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_detail_item"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingRight="@dimen/padding"
|
||||
android:layout_gravity="center"
|
||||
style="@style/addressDetail"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_view_address"
|
||||
android:src="@drawable/ic_action_map"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/address_button_description"
|
||||
style="@style/addressButton"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- Use standard android.R class list id instead of app specific id. This is just useful for
|
||||
consistency. -->
|
||||
<ListView android:id="@id/android:list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
style="@style/ContactListView"/>
|
||||
|
||||
<!-- Use standard android.R class empty id instead of app specific id. This is just useful for
|
||||
consistency. -->
|
||||
<TextView android:id="@id/android:empty"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/no_contacts"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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.
|
||||
-->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
style="@style/listViewActivatedStyle">
|
||||
|
||||
<!-- Use standard android.R class icon id instead of app specific id. This is just useful for
|
||||
consistency. Use scaleType=centerCrop to give a nice full cropped image in the assigned
|
||||
space -->
|
||||
<QuickContactBadge android:id="@android:id/icon"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:layout_width="?android:attr/listPreferredItemHeight"
|
||||
android:scaleType="centerCrop"
|
||||
style="@style/quickContactBadgeStyle"
|
||||
android:src="@drawable/ic_contact_picture_holo_light"/>
|
||||
|
||||
<!-- Use standard android.R class text2 id instead of app specific id. This is just useful for
|
||||
consistency. This is secondary text and not always visible so by default is has its
|
||||
visibility set to gone -->
|
||||
<TextView android:id="@android:id/text2"
|
||||
android:paddingLeft="@dimen/listview_item_padding"
|
||||
android:paddingRight="@dimen/listview_item_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="26dp"
|
||||
android:layout_toRightOf="@android:id/icon"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:fontFamily="sans-serif"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:visibility="gone"
|
||||
android:text="@string/search_match_other"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"/>
|
||||
|
||||
<!-- Use standard android.R class text1 id instead of app specific id. This is just useful for
|
||||
consistency. This view also sets layout_alignWithParentIfMissing=true which lets the view
|
||||
align with the parent view if the text2 view is not part of the view hierarchy (which is
|
||||
its initial state). -->
|
||||
<TextView android:id="@android:id/text1"
|
||||
android:paddingLeft="@dimen/listview_item_padding"
|
||||
android:paddingRight="@dimen/listview_item_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@android:id/text2"
|
||||
android:layout_toRightOf="@android:id/icon"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_edit_contact"
|
||||
android:title="@string/menu_edit_contact"
|
||||
android:icon="@drawable/ic_action_edit"
|
||||
android:showAsAction="ifRoom"/>
|
||||
|
||||
</menu>
|
||||
35
samples/training/ContactsList/res/menu/contact_list_menu.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- The search menu item. Honeycomb and above uses an ActionView or specifically a SearchView
|
||||
which expands within the Action Bar directly. Note the initial collapsed state set using
|
||||
collapseActionView in the showAsAction attribute. -->
|
||||
<item
|
||||
android:id="@+id/menu_search"
|
||||
android:title="@string/menu_search"
|
||||
android:icon="@drawable/ic_action_search"
|
||||
android:showAsAction="ifRoom|collapseActionView"
|
||||
android:actionViewClass="android.widget.SearchView"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_add_contact"
|
||||
android:title="@string/menu_add_contact"
|
||||
android:icon="@drawable/ic_action_add"
|
||||
android:showAsAction="ifRoom"/>
|
||||
|
||||
</menu>
|
||||
26
samples/training/ContactsList/res/values-sw360dp/styles.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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>
|
||||
|
||||
<!-- This style bumps up the address details font size to large on devices that have a smallest
|
||||
width of 360dp (larger phones). -->
|
||||
<style name="addressDetail" parent="@android:style/TextAppearance.Large">
|
||||
<item name="android:fontFamily">sans-serif-light</item>
|
||||
<item name="android:textIsSelectable">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
|
||||
<!-- On devices with a smallest width of 600dp or more in portrait orientation, the two-pane
|
||||
layout should allocate equal space to each fragment. -->
|
||||
<integer name="contact_list_percent">50</integer>
|
||||
<integer name="contact_detail_percent">50</integer>
|
||||
|
||||
</resources>
|
||||
22
samples/training/ContactsList/res/values-sw600dp/bools.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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>
|
||||
|
||||
<!-- On devices with a smallest width of 600dp or more, switch to a two-pane layout.-->
|
||||
<bool name="has_two_panes">true</bool>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
|
||||
<!-- On devices with a smallest width of 600dp or more, the two-pane layout should allocate
|
||||
a larger portion of the screen to the detail fragment. -->
|
||||
<integer name="contact_list_percent">35</integer>
|
||||
<integer name="contact_detail_percent">65</integer>
|
||||
|
||||
</resources>
|
||||
23
samples/training/ContactsList/res/values-sw600dp/layout.xml
Normal file
@@ -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>
|
||||
|
||||
<!-- Create a layout alias so that devices with a minimum width of 600dp or more will use the
|
||||
two-pane layout when referring to the activity_main layout identifier. -->
|
||||
<item name="activity_main" type="layout">@layout/activity_main_twopanes</item>
|
||||
|
||||
</resources>
|
||||
25
samples/training/ContactsList/res/values-sw600dp/styles.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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>
|
||||
|
||||
<style name="ContactListView">
|
||||
<item name="android:verticalScrollbarPosition">left</item>
|
||||
<item name="android:fastScrollAlwaysVisible">true</item>
|
||||
<item name="android:scrollbarStyle">outsideInset</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
24
samples/training/ContactsList/res/values-sw720dp/dimens.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
|
||||
<!-- On devices with much larger screen sizes, such as a 10" tablet like Nexus 10, bump up the
|
||||
common padding value to add some extra white space which makes the layouts feel more
|
||||
suitable for the larger screen. -->
|
||||
<dimen name="padding">32dp</dimen>
|
||||
|
||||
</resources>
|
||||
40
samples/training/ContactsList/res/values-v11/styles.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- API Level 11 and above specific resource files. Some of these styles allow us to use new
|
||||
system styles or attributes introduced in API Level 11 and others allow overriding already
|
||||
defined style that are only suitable for older OS versions (such as quickContactBadgeStyle).-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="@android:style/Theme.Holo.Light"/>
|
||||
|
||||
<style name="listViewActivatedStyle">
|
||||
<item name="android:background">?android:attr/activatedBackgroundIndicator</item>
|
||||
</style>
|
||||
|
||||
<style name="quickContactBadgeStyle"/>
|
||||
|
||||
<style name="addressHeader" parent="@android:style/TextAppearance.Small">
|
||||
<item name="android:textAllCaps">true</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textColor">@color/holo_blue</item>
|
||||
</style>
|
||||
|
||||
<style name="addressButton" parent="@android:style/Widget.Holo.Button.Borderless"/>
|
||||
|
||||
</resources>
|
||||
22
samples/training/ContactsList/res/values/bools.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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>
|
||||
|
||||
<!-- Default is to use a single pane layout -->
|
||||
<bool name="has_two_panes">false</bool>
|
||||
|
||||
</resources>
|
||||
24
samples/training/ContactsList/res/values/colors.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
|
||||
<!-- Define a standard holo blue color. Useful as we can refer to it from various other
|
||||
resource files or even code and it only needs to be updated in one place if we wanted
|
||||
to change it. -->
|
||||
<color name="holo_blue">#FF33B5E5</color>
|
||||
|
||||
</resources>
|
||||
26
samples/training/ContactsList/res/values/dimens.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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>
|
||||
|
||||
<!-- Define some key view padding values. This is useful because the values can be used in
|
||||
multiple views and changed from one central location. It also gives us the ability to
|
||||
provide alternate values for different device configurations using resource directory
|
||||
qualifiers. -->
|
||||
<dimen name="padding">16dp</dimen>
|
||||
<dimen name="listview_item_padding">16dp</dimen>
|
||||
|
||||
</resources>
|
||||
24
samples/training/ContactsList/res/values/integers.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
|
||||
<!-- These are the default percent values that the contact photo and information should take up
|
||||
in the ContactDetailFragment. -->
|
||||
<integer name="contact_detail_photo_percent">45</integer>
|
||||
<integer name="contact_detail_info_percent">55</integer>
|
||||
|
||||
</resources>
|
||||
48
samples/training/ContactsList/res/values/strings.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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>
|
||||
|
||||
<string name="app_name">Contacts List</string>
|
||||
<string name="activity_contacts_list">Contacts List</string>
|
||||
<string name="activity_contact_detail">Contact Detail</string>
|
||||
<string name="contacts_list_search_results_title">Contacts List Search for \"%s\"</string>
|
||||
<string name="app_description">This is a sample app, demonstrating use of the Android system Contacts Provider.</string>
|
||||
<string name="imageview_description">Contact Thumbnail</string>
|
||||
<string name="address_button_description">View Address</string>
|
||||
<string name="menu_search">Search</string>
|
||||
<string name="menu_add_contact">Add Contact</string>
|
||||
<string name="menu_edit_contact">Edit Contact</string>
|
||||
<string name="no_contacts">No Contacts Found</string>
|
||||
<string name="no_contact_selected">No Contact Selected</string>
|
||||
<string name="search_hint">Find contacts</string>
|
||||
|
||||
<!-- Used for the AlphabetIndexer in ContactsListFragment to provide quick navigation by
|
||||
alphabet using ListView fast scroll. -->
|
||||
<string name="alphabet">ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
|
||||
|
||||
<!-- When using ContactsContract.Contacts#CONTENT_FILTER_URI to search contacts, a match occurs
|
||||
when using a number of different fields, such as name, e-mail address, address, phone
|
||||
number, etc. When a match occurs that is not the name, there is currently no way to tell
|
||||
which other field was matched. This string is displayed in the secondary display text in
|
||||
ContactsListFragment when a search query match occurs that is not the display name.
|
||||
-->
|
||||
<string name="search_match_other">Matches Other Field</string>
|
||||
|
||||
<string name="no_address">No addresses found</string>
|
||||
<string name="no_intent_found">No application found to handle this action</string>
|
||||
|
||||
</resources>
|
||||
61
samples/training/ContactsList/res/values/styles.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- This file defines various styles for the application. As this file is located in the /values
|
||||
subdirectory the styles defined here will be used by default unless the styles are redefined
|
||||
inside a more specific resource directory such as /values-sw600dp. -->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="@android:style/Theme"/>
|
||||
|
||||
<style name="ContactListView">
|
||||
<item name="android:verticalScrollbarPosition">right</item>
|
||||
<item name="android:fastScrollAlwaysVisible">true</item>
|
||||
<item name="android:scrollbarStyle">outsideInset</item>
|
||||
</style>
|
||||
|
||||
<style name="listViewActivatedStyle"/>
|
||||
|
||||
<style name="quickContactBadgeStyle">
|
||||
<item name="android:background">@drawable/quickcontact_badge_small</item>
|
||||
</style>
|
||||
|
||||
<style name="searchTextHiglight">
|
||||
<item name="android:textColor">@color/holo_blue</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="addressHeader" parent="@android:style/TextAppearance.Small">
|
||||
<item name="android:textAllCaps">true</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="addressDetail" parent="@android:style/TextAppearance.Medium">
|
||||
<item name="android:fontFamily">sans-serif-light</item>
|
||||
<item name="android:textIsSelectable">true</item>
|
||||
</style>
|
||||
|
||||
<style name="contactNameTitle" parent="@android:style/TextAppearance.Large">
|
||||
<item name="android:textSize">38sp</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:textIsSelectable">true</item>
|
||||
</style>
|
||||
|
||||
<style name="addressButton"/>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- Define a searchable configuration. See the docs for more information:
|
||||
http://developer.android.com/guide/topics/search/searchable-config.html -->
|
||||
|
||||
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:label="@string/app_name"
|
||||
android:inputType="textPersonName"
|
||||
android:hint="@string/search_hint"/>
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.example.android.contactslist.ui;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.example.android.contactslist.BuildConfig;
|
||||
import com.example.android.contactslist.util.Utils;
|
||||
|
||||
/**
|
||||
* This class defines a simple FragmentActivity as the parent of {@link ContactDetailFragment}.
|
||||
*/
|
||||
public class ContactDetailActivity extends FragmentActivity {
|
||||
// Defines a tag for identifying the single fragment that this activity holds
|
||||
private static final String TAG = "ContactDetailActivity";
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
// Enable strict mode checks when in debug modes
|
||||
Utils.enableStrictMode();
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// This activity expects to receive an intent that contains the uri of a contact
|
||||
if (getIntent() != null) {
|
||||
|
||||
// For OS versions honeycomb and higher use action bar
|
||||
if (Utils.hasHoneycomb()) {
|
||||
// Enables action bar "up" navigation
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
// Fetch the data Uri from the intent provided to this activity
|
||||
final Uri uri = getIntent().getData();
|
||||
|
||||
// Checks to see if fragment has already been added, otherwise adds a new
|
||||
// ContactDetailFragment with the Uri provided in the intent
|
||||
if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {
|
||||
final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
|
||||
|
||||
// Adds a newly created ContactDetailFragment that is instantiated with the
|
||||
// data Uri
|
||||
ft.add(android.R.id.content, ContactDetailFragment.newInstance(uri), TAG);
|
||||
ft.commit();
|
||||
}
|
||||
} else {
|
||||
// No intent provided, nothing to do so finish()
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// Tapping on top left ActionBar icon navigates "up" to hierarchical parent screen.
|
||||
// The parent is defined in the AndroidManifest entry for this activity via the
|
||||
// parentActivityName attribute (and via meta-data tag for OS versions before API
|
||||
// Level 16). See the "Tasks and Back Stack" guide for more information:
|
||||
// http://developer.android.com/guide/components/tasks-and-back-stack.html
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
// Otherwise, pass the item to the super implementation for handling, as described in the
|
||||
// documentation.
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
/*
|
||||
* 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.example.android.contactslist.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
|
||||
import android.provider.ContactsContract.Contacts;
|
||||
import android.provider.ContactsContract.Contacts.Photo;
|
||||
import android.provider.ContactsContract.Data;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.android.contactslist.BuildConfig;
|
||||
import com.example.android.contactslist.R;
|
||||
import com.example.android.contactslist.util.ImageLoader;
|
||||
import com.example.android.contactslist.util.Utils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This fragment displays details of a specific contact from the contacts provider. It shows the
|
||||
* contact's display photo, name and all its mailing addresses. You can also modify this fragment
|
||||
* to show other information, such as phone numbers, email addresses and so forth.
|
||||
*
|
||||
* This fragment appears full-screen in an activity on devices with small screen sizes, and as
|
||||
* part of a two-pane layout on devices with larger screens, alongside the
|
||||
* {@link ContactsListFragment}.
|
||||
*
|
||||
* To create an instance of this fragment, use the factory method
|
||||
* {@link ContactDetailFragment#newInstance(android.net.Uri)}, passing as an argument the contact
|
||||
* Uri for the contact you want to display.
|
||||
*/
|
||||
public class ContactDetailFragment extends Fragment implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
public static final String EXTRA_CONTACT_URI =
|
||||
"com.example.android.contactslist.ui.EXTRA_CONTACT_URI";
|
||||
|
||||
// Defines a tag for identifying log entries
|
||||
private static final String TAG = "ContactDetailFragment";
|
||||
|
||||
// The geo Uri scheme prefix, used with Intent.ACTION_VIEW to form a geographical address
|
||||
// intent that will trigger available apps to handle viewing a location (such as Maps)
|
||||
private static final String GEO_URI_SCHEME_PREFIX = "geo:0,0?q=";
|
||||
|
||||
// Whether or not this fragment is showing in a two pane layout
|
||||
private boolean mIsTwoPaneLayout;
|
||||
|
||||
private Uri mContactUri; // Stores the contact Uri for this fragment instance
|
||||
private ImageLoader mImageLoader; // Handles loading the contact image in a background thread
|
||||
|
||||
// Used to store references to key views, layouts and menu items as these need to be updated
|
||||
// in multiple methods throughout this class.
|
||||
private ImageView mImageView;
|
||||
private LinearLayout mDetailsLayout;
|
||||
private TextView mEmptyView;
|
||||
private TextView mContactName;
|
||||
private MenuItem mEditContactMenuItem;
|
||||
|
||||
/**
|
||||
* Factory method to generate a new instance of the fragment given a contact Uri. A factory
|
||||
* method is preferable to simply using the constructor as it handles creating the bundle and
|
||||
* setting the bundle as an argument.
|
||||
*
|
||||
* @param contactUri The contact Uri to load
|
||||
* @return A new instance of {@link ContactDetailFragment}
|
||||
*/
|
||||
public static ContactDetailFragment newInstance(Uri contactUri) {
|
||||
// Create new instance of this fragment
|
||||
final ContactDetailFragment fragment = new ContactDetailFragment();
|
||||
|
||||
// Create and populate the args bundle
|
||||
final Bundle args = new Bundle();
|
||||
args.putParcelable(EXTRA_CONTACT_URI, contactUri);
|
||||
|
||||
// Assign the args bundle to the new fragment
|
||||
fragment.setArguments(args);
|
||||
|
||||
// Return fragment
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragments require an empty constructor.
|
||||
*/
|
||||
public ContactDetailFragment() {}
|
||||
|
||||
/**
|
||||
* Sets the contact that this Fragment displays, or clears the display if the contact argument
|
||||
* is null. This will re-initialize all the views and start the queries to the system contacts
|
||||
* provider to populate the contact information.
|
||||
*
|
||||
* @param contactLookupUri The contact lookup Uri to load and display in this fragment. Passing
|
||||
* null is valid and the fragment will display a message that no
|
||||
* contact is currently selected instead.
|
||||
*/
|
||||
public void setContact(Uri contactLookupUri) {
|
||||
|
||||
// In version 3.0 and later, stores the provided contact lookup Uri in a class field. This
|
||||
// Uri is then used at various points in this class to map to the provided contact.
|
||||
if (Utils.hasHoneycomb()) {
|
||||
mContactUri = contactLookupUri;
|
||||
} else {
|
||||
// For versions earlier than Android 3.0, stores a contact Uri that's constructed from
|
||||
// contactLookupUri. Later on, the resulting Uri is combined with
|
||||
// Contacts.Data.CONTENT_DIRECTORY to map to the provided contact. It's done
|
||||
// differently for these earlier versions because Contacts.Data.CONTENT_DIRECTORY works
|
||||
// differently for Android versions before 3.0.
|
||||
mContactUri = Contacts.lookupContact(getActivity().getContentResolver(),
|
||||
contactLookupUri);
|
||||
}
|
||||
|
||||
// If the Uri contains data, load the contact's image and load contact details.
|
||||
if (contactLookupUri != null) {
|
||||
// Asynchronously loads the contact image
|
||||
mImageLoader.loadImage(mContactUri, mImageView);
|
||||
|
||||
// Shows the contact photo ImageView and hides the empty view
|
||||
mImageView.setVisibility(View.VISIBLE);
|
||||
mEmptyView.setVisibility(View.GONE);
|
||||
|
||||
// Shows the edit contact action/menu item
|
||||
if (mEditContactMenuItem != null) {
|
||||
mEditContactMenuItem.setVisible(true);
|
||||
}
|
||||
|
||||
// Starts two queries to to retrieve contact information from the Contacts Provider.
|
||||
// restartLoader() is used instead of initLoader() as this method may be called
|
||||
// multiple times.
|
||||
getLoaderManager().restartLoader(ContactDetailQuery.QUERY_ID, null, this);
|
||||
getLoaderManager().restartLoader(ContactAddressQuery.QUERY_ID, null, this);
|
||||
} else {
|
||||
// If contactLookupUri is null, then the method was called when no contact was selected
|
||||
// in the contacts list. This should only happen in a two-pane layout when the user
|
||||
// hasn't yet selected a contact. Don't display an image for the contact, and don't
|
||||
// account for the view's space in the layout. Turn on the TextView that appears when
|
||||
// the layout is empty, and set the contact name to the empty string. Turn off any menu
|
||||
// items that are visible.
|
||||
mImageView.setVisibility(View.GONE);
|
||||
mEmptyView.setVisibility(View.VISIBLE);
|
||||
mDetailsLayout.removeAllViews();
|
||||
if (mContactName != null) {
|
||||
mContactName.setText("");
|
||||
}
|
||||
if (mEditContactMenuItem != null) {
|
||||
mEditContactMenuItem.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the Fragment is first created, this callback is invoked. It initializes some key
|
||||
* class fields.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Check if this fragment is part of a two pane set up or a single pane
|
||||
mIsTwoPaneLayout = getResources().getBoolean(R.bool.has_two_panes);
|
||||
|
||||
// Let this fragment contribute menu items
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
/*
|
||||
* The ImageLoader takes care of loading and resizing images asynchronously into the
|
||||
* ImageView. More thorough sample code demonstrating background image loading as well as
|
||||
* details on how it works can be found in the following Android Training class:
|
||||
* http://developer.android.com/training/displaying-bitmaps/
|
||||
*/
|
||||
mImageLoader = new ImageLoader(getActivity(), getLargestScreenDimension()) {
|
||||
@Override
|
||||
protected Bitmap processBitmap(Object data) {
|
||||
// This gets called in a background thread and passed the data from
|
||||
// ImageLoader.loadImage().
|
||||
return loadContactPhoto((Uri) data, getImageSize());
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Set a placeholder loading image for the image loader
|
||||
mImageLoader.setLoadingImage(R.drawable.ic_contact_picture_180_holo_light);
|
||||
|
||||
// Tell the image loader to set the image directly when it's finished loading
|
||||
// rather than fading in
|
||||
mImageLoader.setImageFadeIn(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
// Inflates the main layout to be used by this fragment
|
||||
final View detailView =
|
||||
inflater.inflate(R.layout.contact_detail_fragment, container, false);
|
||||
|
||||
// Gets handles to view objects in the layout
|
||||
mImageView = (ImageView) detailView.findViewById(R.id.contact_image);
|
||||
mDetailsLayout = (LinearLayout) detailView.findViewById(R.id.contact_details_layout);
|
||||
mEmptyView = (TextView) detailView.findViewById(android.R.id.empty);
|
||||
|
||||
if (mIsTwoPaneLayout) {
|
||||
// If this is a two pane view, the following code changes the visibility of the contact
|
||||
// name in details. For a one-pane view, the contact name is displayed as a title.
|
||||
mContactName = (TextView) detailView.findViewById(R.id.contact_name);
|
||||
mContactName.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
return detailView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
// If not being created from a previous state
|
||||
if (savedInstanceState == null) {
|
||||
// Sets the argument extra as the currently displayed contact
|
||||
setContact(getArguments() != null ?
|
||||
(Uri) getArguments().getParcelable(EXTRA_CONTACT_URI) : null);
|
||||
} else {
|
||||
// If being recreated from a saved state, sets the contact from the incoming
|
||||
// savedInstanceState Bundle
|
||||
setContact((Uri) savedInstanceState.getParcelable(EXTRA_CONTACT_URI));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the Fragment is being saved in order to change activity state, save the
|
||||
* currently-selected contact.
|
||||
*/
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
// Saves the contact Uri
|
||||
outState.putParcelable(EXTRA_CONTACT_URI, mContactUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
// When "edit" menu option selected
|
||||
case R.id.menu_edit_contact:
|
||||
// Standard system edit contact intent
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT, mContactUri);
|
||||
|
||||
// Because of an issue in Android 4.0 (API level 14), clicking Done or Back in the
|
||||
// People app doesn't return the user to your app; instead, it displays the People
|
||||
// app's contact list. A workaround, introduced in Android 4.0.3 (API level 15) is
|
||||
// to set a special flag in the extended data for the Intent you send to the People
|
||||
// app. The issue is does not appear in versions prior to Android 4.0. You can use
|
||||
// the flag with any version of the People app; if the workaround isn't needed,
|
||||
// the flag is ignored.
|
||||
intent.putExtra("finishActivityOnSaveCompleted", true);
|
||||
|
||||
// Start the edit activity
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
// Inflates the options menu for this fragment
|
||||
inflater.inflate(R.menu.contact_detail_menu, menu);
|
||||
|
||||
// Gets a handle to the "find" menu item
|
||||
mEditContactMenuItem = menu.findItem(R.id.menu_edit_contact);
|
||||
|
||||
// If contactUri is null the edit menu item should be hidden, otherwise
|
||||
// it is visible.
|
||||
mEditContactMenuItem.setVisible(mContactUri != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
switch (id) {
|
||||
// Two main queries to load the required information
|
||||
case ContactDetailQuery.QUERY_ID:
|
||||
// This query loads main contact details, see
|
||||
// ContactDetailQuery for more information.
|
||||
return new CursorLoader(getActivity(), mContactUri,
|
||||
ContactDetailQuery.PROJECTION,
|
||||
null, null, null);
|
||||
case ContactAddressQuery.QUERY_ID:
|
||||
// This query loads contact address details, see
|
||||
// ContactAddressQuery for more information.
|
||||
final Uri uri = Uri.withAppendedPath(mContactUri, Contacts.Data.CONTENT_DIRECTORY);
|
||||
return new CursorLoader(getActivity(), uri,
|
||||
ContactAddressQuery.PROJECTION,
|
||||
ContactAddressQuery.SELECTION,
|
||||
null, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
|
||||
// If this fragment was cleared while the query was running
|
||||
// eg. from from a call like setContact(uri) then don't do
|
||||
// anything.
|
||||
if (mContactUri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (loader.getId()) {
|
||||
case ContactDetailQuery.QUERY_ID:
|
||||
// Moves to the first row in the Cursor
|
||||
if (data.moveToFirst()) {
|
||||
// For the contact details query, fetches the contact display name.
|
||||
// ContactDetailQuery.DISPLAY_NAME maps to the appropriate display
|
||||
// name field based on OS version.
|
||||
final String contactName = data.getString(ContactDetailQuery.DISPLAY_NAME);
|
||||
if (mIsTwoPaneLayout && mContactName != null) {
|
||||
// In the two pane layout, there is a dedicated TextView
|
||||
// that holds the contact name.
|
||||
mContactName.setText(contactName);
|
||||
} else {
|
||||
// In the single pane layout, sets the activity title
|
||||
// to the contact name. On HC+ this will be set as
|
||||
// the ActionBar title text.
|
||||
getActivity().setTitle(contactName);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ContactAddressQuery.QUERY_ID:
|
||||
// This query loads the contact address details. More than
|
||||
// one contact address is possible, so move each one to a
|
||||
// LinearLayout in a Scrollview so multiple addresses can
|
||||
// be scrolled by the user.
|
||||
|
||||
// Each LinearLayout has the same LayoutParams so this can
|
||||
// be created once and used for each address.
|
||||
final LinearLayout.LayoutParams layoutParams =
|
||||
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
// Clears out the details layout first in case the details
|
||||
// layout has addresses from a previous data load still
|
||||
// added as children.
|
||||
mDetailsLayout.removeAllViews();
|
||||
|
||||
// Loops through all the rows in the Cursor
|
||||
if (data.moveToFirst()) {
|
||||
do {
|
||||
// Builds the address layout
|
||||
final LinearLayout layout = buildAddressLayout(
|
||||
data.getInt(ContactAddressQuery.TYPE),
|
||||
data.getString(ContactAddressQuery.LABEL),
|
||||
data.getString(ContactAddressQuery.ADDRESS));
|
||||
// Adds the new address layout to the details layout
|
||||
mDetailsLayout.addView(layout, layoutParams);
|
||||
} while (data.moveToNext());
|
||||
} else {
|
||||
// If nothing found, adds an empty address layout
|
||||
mDetailsLayout.addView(buildEmptyAddressLayout(), layoutParams);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
// Nothing to do here. The Cursor does not need to be released as it was never directly
|
||||
// bound to anything (like an adapter).
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an empty address layout that just shows that no addresses
|
||||
* were found for this contact.
|
||||
*
|
||||
* @return A LinearLayout to add to the contact details layout
|
||||
*/
|
||||
private LinearLayout buildEmptyAddressLayout() {
|
||||
return buildAddressLayout(0, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an address LinearLayout based on address information from the Contacts Provider.
|
||||
* Each address for the contact gets its own LinearLayout object; for example, if the contact
|
||||
* has three postal addresses, then 3 LinearLayouts are generated.
|
||||
*
|
||||
* @param addressType From
|
||||
* {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#TYPE}
|
||||
* @param addressTypeLabel From
|
||||
* {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#LABEL}
|
||||
* @param address From
|
||||
* {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#FORMATTED_ADDRESS}
|
||||
* @return A LinearLayout to add to the contact details layout,
|
||||
* populated with the provided address details.
|
||||
*/
|
||||
private LinearLayout buildAddressLayout(int addressType, String addressTypeLabel,
|
||||
final String address) {
|
||||
|
||||
// Inflates the address layout
|
||||
final LinearLayout addressLayout =
|
||||
(LinearLayout) LayoutInflater.from(getActivity()).inflate(
|
||||
R.layout.contact_detail_item, mDetailsLayout, false);
|
||||
|
||||
// Gets handles to the view objects in the layout
|
||||
final TextView headerTextView =
|
||||
(TextView) addressLayout.findViewById(R.id.contact_detail_header);
|
||||
final TextView addressTextView =
|
||||
(TextView) addressLayout.findViewById(R.id.contact_detail_item);
|
||||
final ImageButton viewAddressButton =
|
||||
(ImageButton) addressLayout.findViewById(R.id.button_view_address);
|
||||
|
||||
// If there's no addresses for the contact, shows the empty view and message, and hides the
|
||||
// header and button.
|
||||
if (addressTypeLabel == null && addressType == 0) {
|
||||
headerTextView.setVisibility(View.GONE);
|
||||
viewAddressButton.setVisibility(View.GONE);
|
||||
addressTextView.setText(R.string.no_address);
|
||||
} else {
|
||||
// Gets postal address label type
|
||||
CharSequence label =
|
||||
StructuredPostal.getTypeLabel(getResources(), addressType, addressTypeLabel);
|
||||
|
||||
// Sets TextView objects in the layout
|
||||
headerTextView.setText(label);
|
||||
addressTextView.setText(address);
|
||||
|
||||
// Defines an onClickListener object for the address button
|
||||
viewAddressButton.setOnClickListener(new View.OnClickListener() {
|
||||
// Defines what to do when users click the address button
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
|
||||
final Intent viewIntent =
|
||||
new Intent(Intent.ACTION_VIEW, constructGeoUri(address));
|
||||
|
||||
// A PackageManager instance is needed to verify that there's a default app
|
||||
// that handles ACTION_VIEW and a geo Uri.
|
||||
final PackageManager packageManager = getActivity().getPackageManager();
|
||||
|
||||
// Checks for an activity that can handle this intent. Preferred in this
|
||||
// case over Intent.createChooser() as it will still let the user choose
|
||||
// a default (or use a previously set default) for geo Uris.
|
||||
if (packageManager.resolveActivity(
|
||||
viewIntent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
||||
startActivity(viewIntent);
|
||||
} else {
|
||||
// If no default is found, displays a message that no activity can handle
|
||||
// the view button.
|
||||
Toast.makeText(getActivity(),
|
||||
R.string.no_intent_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
return addressLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a geo scheme Uri from a postal address.
|
||||
*
|
||||
* @param postalAddress A postal address.
|
||||
* @return the geo:// Uri for the postal address.
|
||||
*/
|
||||
private Uri constructGeoUri(String postalAddress) {
|
||||
// Concatenates the geo:// prefix to the postal address. The postal address must be
|
||||
// converted to Uri format and encoded for special characters.
|
||||
return Uri.parse(GEO_URI_SCHEME_PREFIX + Uri.encode(postalAddress));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the width or height of the screen in pixels, whichever is larger. This is used to
|
||||
* set a maximum size limit on the contact photo that is retrieved from the Contacts Provider.
|
||||
* This limit prevents the app from trying to decode and load an image that is much larger than
|
||||
* the available screen area.
|
||||
*
|
||||
* @return The largest screen dimension in pixels.
|
||||
*/
|
||||
private int getLargestScreenDimension() {
|
||||
// Gets a DisplayMetrics object, which is used to retrieve the display's pixel height and
|
||||
// width
|
||||
final DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
|
||||
// Retrieves a displayMetrics object for the device's default display
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
final int height = displayMetrics.heightPixels;
|
||||
final int width = displayMetrics.widthPixels;
|
||||
|
||||
// Returns the larger of the two values
|
||||
return height > width ? height : width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes and returns the contact's thumbnail image.
|
||||
* @param contactUri The Uri of the contact containing the image.
|
||||
* @param imageSize The desired target width and height of the output image in pixels.
|
||||
* @return If a thumbnail image exists for the contact, a Bitmap image, otherwise null.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
private Bitmap loadContactPhoto(Uri contactUri, int imageSize) {
|
||||
|
||||
// Ensures the Fragment is still added to an activity. As this method is called in a
|
||||
// background thread, there's the possibility the Fragment is no longer attached and
|
||||
// added to an activity. If so, no need to spend resources loading the contact photo.
|
||||
if (!isAdded() || getActivity() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Instantiates a ContentResolver for retrieving the Uri of the image
|
||||
final ContentResolver contentResolver = getActivity().getContentResolver();
|
||||
|
||||
// Instantiates an AssetFileDescriptor. Given a content Uri pointing to an image file, the
|
||||
// ContentResolver can return an AssetFileDescriptor for the file.
|
||||
AssetFileDescriptor afd = null;
|
||||
|
||||
if (Utils.hasICS()) {
|
||||
// On platforms running Android 4.0 (API version 14) and later, a high resolution image
|
||||
// is available from Photo.DISPLAY_PHOTO.
|
||||
try {
|
||||
// Constructs the content Uri for the image
|
||||
Uri displayImageUri = Uri.withAppendedPath(contactUri, Photo.DISPLAY_PHOTO);
|
||||
|
||||
// Retrieves an AssetFileDescriptor from the Contacts Provider, using the
|
||||
// constructed Uri
|
||||
afd = contentResolver.openAssetFileDescriptor(displayImageUri, "r");
|
||||
// If the file exists
|
||||
if (afd != null) {
|
||||
// Reads and decodes the file to a Bitmap and scales it to the desired size
|
||||
return ImageLoader.decodeSampledBitmapFromDescriptor(
|
||||
afd.getFileDescriptor(), imageSize, imageSize);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// Catches file not found exceptions
|
||||
if (BuildConfig.DEBUG) {
|
||||
// Log debug message, this is not an error message as this exception is thrown
|
||||
// when a contact is legitimately missing a contact photo (which will be quite
|
||||
// frequently in a long contacts list).
|
||||
Log.d(TAG, "Contact photo not found for contact " + contactUri.toString()
|
||||
+ ": " + e.toString());
|
||||
}
|
||||
} finally {
|
||||
// Once the decode is complete, this closes the file. You must do this each time
|
||||
// you access an AssetFileDescriptor; otherwise, every image load you do will open
|
||||
// a new descriptor.
|
||||
if (afd != null) {
|
||||
try {
|
||||
afd.close();
|
||||
} catch (IOException e) {
|
||||
// Closing a file descriptor might cause an IOException if the file is
|
||||
// already closed. Nothing extra is needed to handle this.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the platform version is less than Android 4.0 (API Level 14), use the only available
|
||||
// image URI, which points to a normal-sized image.
|
||||
try {
|
||||
// Constructs the image Uri from the contact Uri and the directory twig from the
|
||||
// Contacts.Photo table
|
||||
Uri imageUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
|
||||
|
||||
// Retrieves an AssetFileDescriptor from the Contacts Provider, using the constructed
|
||||
// Uri
|
||||
afd = getActivity().getContentResolver().openAssetFileDescriptor(imageUri, "r");
|
||||
|
||||
// If the file exists
|
||||
if (afd != null) {
|
||||
// Reads the image from the file, decodes it, and scales it to the available screen
|
||||
// area
|
||||
return ImageLoader.decodeSampledBitmapFromDescriptor(
|
||||
afd.getFileDescriptor(), imageSize, imageSize);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// Catches file not found exceptions
|
||||
if (BuildConfig.DEBUG) {
|
||||
// Log debug message, this is not an error message as this exception is thrown
|
||||
// when a contact is legitimately missing a contact photo (which will be quite
|
||||
// frequently in a long contacts list).
|
||||
Log.d(TAG, "Contact photo not found for contact " + contactUri.toString()
|
||||
+ ": " + e.toString());
|
||||
}
|
||||
} finally {
|
||||
// Once the decode is complete, this closes the file. You must do this each time you
|
||||
// access an AssetFileDescriptor; otherwise, every image load you do will open a new
|
||||
// descriptor.
|
||||
if (afd != null) {
|
||||
try {
|
||||
afd.close();
|
||||
} catch (IOException e) {
|
||||
// Closing a file descriptor might cause an IOException if the file is
|
||||
// already closed. Ignore this.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the case selectors match, returns null.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface defines constants used by contact retrieval queries.
|
||||
*/
|
||||
public interface ContactDetailQuery {
|
||||
// A unique query ID to distinguish queries being run by the
|
||||
// LoaderManager.
|
||||
final static int QUERY_ID = 1;
|
||||
|
||||
// The query projection (columns to fetch from the provider)
|
||||
@SuppressLint("InlinedApi")
|
||||
final static String[] PROJECTION = {
|
||||
Contacts._ID,
|
||||
Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME,
|
||||
};
|
||||
|
||||
// The query column numbers which map to each value in the projection
|
||||
final static int ID = 0;
|
||||
final static int DISPLAY_NAME = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface defines constants used by address retrieval queries.
|
||||
*/
|
||||
public interface ContactAddressQuery {
|
||||
// A unique query ID to distinguish queries being run by the
|
||||
// LoaderManager.
|
||||
final static int QUERY_ID = 2;
|
||||
|
||||
// The query projection (columns to fetch from the provider)
|
||||
final static String[] PROJECTION = {
|
||||
StructuredPostal._ID,
|
||||
StructuredPostal.FORMATTED_ADDRESS,
|
||||
StructuredPostal.TYPE,
|
||||
StructuredPostal.LABEL,
|
||||
};
|
||||
|
||||
// The query selection criteria. In this case matching against the
|
||||
// StructuredPostal content mime type.
|
||||
final static String SELECTION =
|
||||
Data.MIMETYPE + "='" + StructuredPostal.CONTENT_ITEM_TYPE + "'";
|
||||
|
||||
// The query column numbers which map to each value in the projection
|
||||
final static int ID = 0;
|
||||
final static int ADDRESS = 1;
|
||||
final static int TYPE = 2;
|
||||
final static int LABEL = 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.example.android.contactslist.ui;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import com.example.android.contactslist.BuildConfig;
|
||||
import com.example.android.contactslist.R;
|
||||
import com.example.android.contactslist.util.Utils;
|
||||
|
||||
/**
|
||||
* FragmentActivity to hold the main {@link ContactsListFragment}. On larger screen devices which
|
||||
* can fit two panes also load {@link ContactDetailFragment}.
|
||||
*/
|
||||
public class ContactsListActivity extends FragmentActivity implements
|
||||
ContactsListFragment.OnContactsInteractionListener {
|
||||
|
||||
// Defines a tag for identifying log entries
|
||||
private static final String TAG = "ContactsListActivity";
|
||||
|
||||
private ContactDetailFragment mContactDetailFragment;
|
||||
|
||||
// If true, this is a larger screen device which fits two panes
|
||||
private boolean isTwoPaneLayout;
|
||||
|
||||
// True if this activity instance is a search result view (used on pre-HC devices that load
|
||||
// search results in a separate instance of the activity rather than loading results in-line
|
||||
// as the query is typed.
|
||||
private boolean isSearchResultView = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Utils.enableStrictMode();
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Set main content view. On smaller screen devices this is a single pane view with one
|
||||
// fragment. One larger screen devices this is a two pane view with two fragments.
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// Check if two pane bool is set based on resource directories
|
||||
isTwoPaneLayout = getResources().getBoolean(R.bool.has_two_panes);
|
||||
|
||||
// Check if this activity instance has been triggered as a result of a search query. This
|
||||
// will only happen on pre-HC OS versions as from HC onward search is carried out using
|
||||
// an ActionBar SearchView which carries out the search in-line without loading a new
|
||||
// Activity.
|
||||
if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
|
||||
|
||||
// Fetch query from intent and notify the fragment that it should display search
|
||||
// results instead of all contacts.
|
||||
String searchQuery = getIntent().getStringExtra(SearchManager.QUERY);
|
||||
ContactsListFragment mContactsListFragment = (ContactsListFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.contact_list);
|
||||
|
||||
// This flag notes that the Activity is doing a search, and so the result will be
|
||||
// search results rather than all contacts. This prevents the Activity and Fragment
|
||||
// from trying to a search on search results.
|
||||
isSearchResultView = true;
|
||||
mContactsListFragment.setSearchQuery(searchQuery);
|
||||
|
||||
// Set special title for search results
|
||||
String title = getString(R.string.contacts_list_search_results_title, searchQuery);
|
||||
setTitle(title);
|
||||
}
|
||||
|
||||
if (isTwoPaneLayout) {
|
||||
// If two pane layout, locate the contact detail fragment
|
||||
mContactDetailFragment = (ContactDetailFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.contact_detail);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface callback lets the main contacts list fragment notify
|
||||
* this activity that a contact has been selected.
|
||||
*
|
||||
* @param contactUri The contact Uri to the selected contact.
|
||||
*/
|
||||
@Override
|
||||
public void onContactSelected(Uri contactUri) {
|
||||
if (isTwoPaneLayout && mContactDetailFragment != null) {
|
||||
// If two pane layout then update the detail fragment to show the selected contact
|
||||
mContactDetailFragment.setContact(contactUri);
|
||||
} else {
|
||||
// Otherwise single pane layout, start a new ContactDetailActivity with
|
||||
// the contact Uri
|
||||
Intent intent = new Intent(this, ContactDetailActivity.class);
|
||||
intent.setData(contactUri);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface callback lets the main contacts list fragment notify
|
||||
* this activity that a contact is no longer selected.
|
||||
*/
|
||||
@Override
|
||||
public void onSelectionCleared() {
|
||||
if (isTwoPaneLayout && mContactDetailFragment != null) {
|
||||
mContactDetailFragment.setContact(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
// Don't allow another search if this activity instance is already showing
|
||||
// search results. Only used pre-HC.
|
||||
return !isSearchResultView && super.onSearchRequested();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,935 @@
|
||||
/*
|
||||
* 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.example.android.contactslist.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract.Contacts;
|
||||
import android.provider.ContactsContract.Contacts.Photo;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.TextAppearanceSpan;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AlphabetIndexer;
|
||||
import android.widget.ListView;
|
||||
import android.widget.QuickContactBadge;
|
||||
import android.widget.SearchView;
|
||||
import android.widget.SectionIndexer;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.example.android.contactslist.BuildConfig;
|
||||
import com.example.android.contactslist.R;
|
||||
import com.example.android.contactslist.util.ImageLoader;
|
||||
import com.example.android.contactslist.util.Utils;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This fragment displays a list of contacts stored in the Contacts Provider. Each item in the list
|
||||
* shows the contact's thumbnail photo and display name. On devices with large screens, this
|
||||
* fragment's UI appears as part of a two-pane layout, along with the UI of
|
||||
* {@link ContactDetailFragment}. On smaller screens, this fragment's UI appears as a single pane.
|
||||
*
|
||||
* This Fragment retrieves contacts based on a search string. If the user doesn't enter a search
|
||||
* string, then the list contains all the contacts in the Contacts Provider. If the user enters a
|
||||
* search string, then the list contains only those contacts whose data matches the string. The
|
||||
* Contacts Provider itself controls the matching algorithm, which is a "substring" search: if the
|
||||
* search string is a substring of any of the contacts data, then there is a match.
|
||||
*
|
||||
* On newer API platforms, the search is implemented in a SearchView in the ActionBar; as the user
|
||||
* types the search string, the list automatically refreshes to display results ("type to filter").
|
||||
* On older platforms, the user must enter the full string and trigger the search. In response, the
|
||||
* trigger starts a new Activity which loads a fresh instance of this fragment. The resulting UI
|
||||
* displays the filtered list and disables the search feature to prevent furthering searching.
|
||||
*/
|
||||
public class ContactsListFragment extends ListFragment implements
|
||||
AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
// Defines a tag for identifying log entries
|
||||
private static final String TAG = "ContactsListFragment";
|
||||
|
||||
// Bundle key for saving previously selected search result item
|
||||
private static final String STATE_PREVIOUSLY_SELECTED_KEY =
|
||||
"com.example.android.contactslist.ui.SELECTED_ITEM";
|
||||
|
||||
private ContactsAdapter mAdapter; // The main query adapter
|
||||
private ImageLoader mImageLoader; // Handles loading the contact image in a background thread
|
||||
private String mSearchTerm; // Stores the current search query term
|
||||
|
||||
// Contact selected listener that allows the activity holding this fragment to be notified of
|
||||
// a contact being selected
|
||||
private OnContactsInteractionListener mOnContactSelectedListener;
|
||||
|
||||
// Stores the previously selected search item so that on a configuration change the same item
|
||||
// can be reselected again
|
||||
private int mPreviouslySelectedSearchItem = 0;
|
||||
|
||||
// Whether or not the search query has changed since the last time the loader was refreshed
|
||||
private boolean mSearchQueryChanged;
|
||||
|
||||
// Whether or not this fragment is showing in a two-pane layout
|
||||
private boolean mIsTwoPaneLayout;
|
||||
|
||||
// Whether or not this is a search result view of this fragment, only used on pre-honeycomb
|
||||
// OS versions as search results are shown in-line via Action Bar search from honeycomb onward
|
||||
private boolean mIsSearchResultView = false;
|
||||
|
||||
/**
|
||||
* Fragments require an empty constructor.
|
||||
*/
|
||||
public ContactsListFragment() {}
|
||||
|
||||
/**
|
||||
* In platform versions prior to Android 3.0, the ActionBar and SearchView are not supported,
|
||||
* and the UI gets the search string from an EditText. However, the fragment doesn't allow
|
||||
* another search when search results are already showing. This would confuse the user, because
|
||||
* the resulting search would re-query the Contacts Provider instead of searching the listed
|
||||
* results. This method sets the search query and also a boolean that tracks if this Fragment
|
||||
* should be displayed as a search result view or not.
|
||||
*
|
||||
* @param query The contacts search query.
|
||||
*/
|
||||
public void setSearchQuery(String query) {
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
mIsSearchResultView = false;
|
||||
} else {
|
||||
mSearchTerm = query;
|
||||
mIsSearchResultView = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Check if this fragment is part of a two-pane set up or a single pane by reading a
|
||||
// boolean from the application resource directories. This lets allows us to easily specify
|
||||
// which screen sizes should use a two-pane layout by setting this boolean in the
|
||||
// corresponding resource size-qualified directory.
|
||||
mIsTwoPaneLayout = getResources().getBoolean(R.bool.has_two_panes);
|
||||
|
||||
// Let this fragment contribute menu items
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
// Create the main contacts adapter
|
||||
mAdapter = new ContactsAdapter(getActivity());
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
// If we're restoring state after this fragment was recreated then
|
||||
// retrieve previous search term and previously selected search
|
||||
// result.
|
||||
mSearchTerm = savedInstanceState.getString(SearchManager.QUERY);
|
||||
mPreviouslySelectedSearchItem =
|
||||
savedInstanceState.getInt(STATE_PREVIOUSLY_SELECTED_KEY, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* An ImageLoader object loads and resizes an image in the background and binds it to the
|
||||
* QuickContactBadge in each item layout of the ListView. ImageLoader implements memory
|
||||
* caching for each image, which substantially improves refreshes of the ListView as the
|
||||
* user scrolls through it.
|
||||
*
|
||||
* To learn more about downloading images asynchronously and caching the results, read the
|
||||
* Android training class Displaying Bitmaps Efficiently.
|
||||
*
|
||||
* http://developer.android.com/training/displaying-bitmaps/
|
||||
*/
|
||||
mImageLoader = new ImageLoader(getActivity(), getListPreferredItemHeight()) {
|
||||
@Override
|
||||
protected Bitmap processBitmap(Object data) {
|
||||
// This gets called in a background thread and passed the data from
|
||||
// ImageLoader.loadImage().
|
||||
return loadContactPhotoThumbnail((String) data, getImageSize());
|
||||
}
|
||||
};
|
||||
|
||||
// Set a placeholder loading image for the image loader
|
||||
mImageLoader.setLoadingImage(R.drawable.ic_contact_picture_holo_light);
|
||||
|
||||
// Add a cache to the image loader
|
||||
mImageLoader.addImageCache(getActivity().getSupportFragmentManager(), 0.1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the list fragment layout
|
||||
return inflater.inflate(R.layout.contact_list_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
// Set up ListView, assign adapter and set some listeners. The adapter was previously
|
||||
// created in onCreate().
|
||||
setListAdapter(mAdapter);
|
||||
getListView().setOnItemClickListener(this);
|
||||
getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
|
||||
// Pause image loader to ensure smoother scrolling when flinging
|
||||
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
|
||||
mImageLoader.setPauseWork(true);
|
||||
} else {
|
||||
mImageLoader.setPauseWork(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScroll(AbsListView absListView, int i, int i1, int i2) {}
|
||||
});
|
||||
|
||||
if (mIsTwoPaneLayout) {
|
||||
// In a two-pane layout, set choice mode to single as there will be two panes
|
||||
// when an item in the ListView is selected it should remain highlighted while
|
||||
// the content shows in the second pane.
|
||||
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
|
||||
}
|
||||
|
||||
// If there's a previously selected search item from a saved state then don't bother
|
||||
// initializing the loader as it will be restarted later when the query is populated into
|
||||
// the action bar search view (see onQueryTextChange() in onCreateOptionsMenu()).
|
||||
if (mPreviouslySelectedSearchItem == 0) {
|
||||
// Initialize the loader, and create a loader identified by ContactsQuery.QUERY_ID
|
||||
getLoaderManager().initLoader(ContactsQuery.QUERY_ID, null, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
try {
|
||||
// Assign callback listener which the holding activity must implement. This is used
|
||||
// so that when a contact item is interacted with (selected by the user) the holding
|
||||
// activity will be notified and can take further action such as populating the contact
|
||||
// detail pane (if in multi-pane layout) or starting a new activity with the contact
|
||||
// details (single pane layout).
|
||||
mOnContactSelectedListener = (OnContactsInteractionListener) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(activity.toString()
|
||||
+ " must implement OnContactsInteractionListener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// In the case onPause() is called during a fling the image loader is
|
||||
// un-paused to let any remaining background work complete.
|
||||
mImageLoader.setPauseWork(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
||||
// Gets the Cursor object currently bound to the ListView
|
||||
final Cursor cursor = mAdapter.getCursor();
|
||||
|
||||
// Moves to the Cursor row corresponding to the ListView item that was clicked
|
||||
cursor.moveToPosition(position);
|
||||
|
||||
// Creates a contact lookup Uri from contact ID and lookup_key
|
||||
final Uri uri = Contacts.getLookupUri(
|
||||
cursor.getLong(ContactsQuery.ID),
|
||||
cursor.getString(ContactsQuery.LOOKUP_KEY));
|
||||
|
||||
// Notifies the parent activity that the user selected a contact. In a two-pane layout, the
|
||||
// parent activity loads a ContactDetailFragment that displays the details for the selected
|
||||
// contact. In a single-pane layout, the parent activity starts a new activity that
|
||||
// displays contact details in its own Fragment.
|
||||
mOnContactSelectedListener.onContactSelected(uri);
|
||||
|
||||
// If two-pane layout sets the selected item to checked so it remains highlighted. In a
|
||||
// single-pane layout a new activity is started so this is not needed.
|
||||
if (mIsTwoPaneLayout) {
|
||||
getListView().setItemChecked(position, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ListView selection is cleared, for example
|
||||
* when search mode is finished and the currently selected
|
||||
* contact should no longer be selected.
|
||||
*/
|
||||
private void onSelectionCleared() {
|
||||
// Uses callback to notify activity this contains this fragment
|
||||
mOnContactSelectedListener.onSelectionCleared();
|
||||
|
||||
// Clears currently checked item
|
||||
getListView().clearChoices();
|
||||
}
|
||||
|
||||
// This method uses APIs from newer OS versions than the minimum that this app supports. This
|
||||
// annotation tells Android lint that they are properly guarded so they won't run on older OS
|
||||
// versions and can be ignored by lint.
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
|
||||
// Inflate the menu items
|
||||
inflater.inflate(R.menu.contact_list_menu, menu);
|
||||
// Locate the search item
|
||||
MenuItem searchItem = menu.findItem(R.id.menu_search);
|
||||
|
||||
// In versions prior to Android 3.0, hides the search item to prevent additional
|
||||
// searches. In Android 3.0 and later, searching is done via a SearchView in the ActionBar.
|
||||
// Since the search doesn't create a new Activity to do the searching, the menu item
|
||||
// doesn't need to be turned off.
|
||||
if (mIsSearchResultView) {
|
||||
searchItem.setVisible(false);
|
||||
}
|
||||
|
||||
// In version 3.0 and later, sets up and configures the ActionBar SearchView
|
||||
if (Utils.hasHoneycomb()) {
|
||||
|
||||
// Retrieves the system search manager service
|
||||
final SearchManager searchManager =
|
||||
(SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE);
|
||||
|
||||
// Retrieves the SearchView from the search menu item
|
||||
final SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
|
||||
// Assign searchable info to SearchView
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(getActivity().getComponentName()));
|
||||
|
||||
// Set listeners for SearchView
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String queryText) {
|
||||
// Nothing needs to happen when the user submits the search string
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
// Called when the action bar search text has changed. Updates
|
||||
// the search filter, and restarts the loader to do a new query
|
||||
// using the new search string.
|
||||
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
|
||||
|
||||
// Don't do anything if the filter is empty
|
||||
if (mSearchTerm == null && newFilter == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't do anything if the new filter is the same as the current filter
|
||||
if (mSearchTerm != null && mSearchTerm.equals(newFilter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Updates current filter to new filter
|
||||
mSearchTerm = newFilter;
|
||||
|
||||
// Restarts the loader. This triggers onCreateLoader(), which builds the
|
||||
// necessary content Uri from mSearchTerm.
|
||||
mSearchQueryChanged = true;
|
||||
getLoaderManager().restartLoader(
|
||||
ContactsQuery.QUERY_ID, null, ContactsListFragment.this);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (Utils.hasICS()) {
|
||||
// This listener added in ICS
|
||||
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(MenuItem menuItem) {
|
||||
// Nothing to do when the action item is expanded
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(MenuItem menuItem) {
|
||||
// When the user collapses the SearchView the current search string is
|
||||
// cleared and the loader restarted.
|
||||
if (!TextUtils.isEmpty(mSearchTerm)) {
|
||||
onSelectionCleared();
|
||||
}
|
||||
mSearchTerm = null;
|
||||
getLoaderManager().restartLoader(
|
||||
ContactsQuery.QUERY_ID, null, ContactsListFragment.this);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mSearchTerm != null) {
|
||||
// If search term is already set here then this fragment is
|
||||
// being restored from a saved state and the search menu item
|
||||
// needs to be expanded and populated again.
|
||||
|
||||
// Stores the search term (as it will be wiped out by
|
||||
// onQueryTextChange() when the menu item is expanded).
|
||||
final String savedSearchTerm = mSearchTerm;
|
||||
|
||||
// Expands the search menu item
|
||||
if (Utils.hasICS()) {
|
||||
searchItem.expandActionView();
|
||||
}
|
||||
|
||||
// Sets the SearchView to the previous search string
|
||||
searchView.setQuery(savedSearchTerm, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (!TextUtils.isEmpty(mSearchTerm)) {
|
||||
// Saves the current search string
|
||||
outState.putString(SearchManager.QUERY, mSearchTerm);
|
||||
|
||||
// Saves the currently selected contact
|
||||
outState.putInt(STATE_PREVIOUSLY_SELECTED_KEY, getListView().getCheckedItemPosition());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
// Sends a request to the People app to display the create contact screen
|
||||
case R.id.menu_add_contact:
|
||||
final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
|
||||
startActivity(intent);
|
||||
break;
|
||||
// For platforms earlier than Android 3.0, triggers the search activity
|
||||
case R.id.menu_search:
|
||||
if (!Utils.hasHoneycomb()) {
|
||||
getActivity().onSearchRequested();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
|
||||
// If this is the loader for finding contacts in the Contacts Provider
|
||||
// (the only one supported)
|
||||
if (id == ContactsQuery.QUERY_ID) {
|
||||
Uri contentUri;
|
||||
|
||||
// There are two types of searches, one which displays all contacts and
|
||||
// one which filters contacts by a search query. If mSearchTerm is set
|
||||
// then a search query has been entered and the latter should be used.
|
||||
|
||||
if (mSearchTerm == null) {
|
||||
// Since there's no search string, use the content URI that searches the entire
|
||||
// Contacts table
|
||||
contentUri = ContactsQuery.CONTENT_URI;
|
||||
} else {
|
||||
// Since there's a search string, use the special content Uri that searches the
|
||||
// Contacts table. The URI consists of a base Uri and the search string.
|
||||
contentUri =
|
||||
Uri.withAppendedPath(ContactsQuery.FILTER_URI, Uri.encode(mSearchTerm));
|
||||
}
|
||||
|
||||
// Returns a new CursorLoader for querying the Contacts table. No arguments are used
|
||||
// for the selection clause. The search string is either encoded onto the content URI,
|
||||
// or no contacts search string is used. The other search criteria are constants. See
|
||||
// the ContactsQuery interface.
|
||||
return new CursorLoader(getActivity(),
|
||||
contentUri,
|
||||
ContactsQuery.PROJECTION,
|
||||
ContactsQuery.SELECTION,
|
||||
null,
|
||||
ContactsQuery.SORT_ORDER);
|
||||
}
|
||||
|
||||
Log.e(TAG, "onCreateLoader - incorrect ID provided (" + id + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
// This swaps the new cursor into the adapter.
|
||||
if (loader.getId() == ContactsQuery.QUERY_ID) {
|
||||
mAdapter.swapCursor(data);
|
||||
|
||||
// If this is a two-pane layout and there is a search query then
|
||||
// there is some additional work to do around default selected
|
||||
// search item.
|
||||
if (mIsTwoPaneLayout && !TextUtils.isEmpty(mSearchTerm) && mSearchQueryChanged) {
|
||||
// Selects the first item in results, unless this fragment has
|
||||
// been restored from a saved state (like orientation change)
|
||||
// in which case it selects the previously selected search item.
|
||||
if (data != null && data.moveToPosition(mPreviouslySelectedSearchItem)) {
|
||||
// Creates the content Uri for the previously selected contact by appending the
|
||||
// contact's ID to the Contacts table content Uri
|
||||
final Uri uri = Uri.withAppendedPath(
|
||||
Contacts.CONTENT_URI, String.valueOf(data.getLong(ContactsQuery.ID)));
|
||||
mOnContactSelectedListener.onContactSelected(uri);
|
||||
getListView().setItemChecked(mPreviouslySelectedSearchItem, true);
|
||||
} else {
|
||||
// No results, clear selection.
|
||||
onSelectionCleared();
|
||||
}
|
||||
// Only restore from saved state one time. Next time fall back
|
||||
// to selecting first item. If the fragment state is saved again
|
||||
// then the currently selected item will once again be saved.
|
||||
mPreviouslySelectedSearchItem = 0;
|
||||
mSearchQueryChanged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
if (loader.getId() == ContactsQuery.QUERY_ID) {
|
||||
// When the loader is being reset, clear the cursor from the adapter. This allows the
|
||||
// cursor resources to be freed.
|
||||
mAdapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the preferred height for each item in the ListView, in pixels, after accounting for
|
||||
* screen density. ImageLoader uses this value to resize thumbnail images to match the ListView
|
||||
* item height.
|
||||
*
|
||||
* @return The preferred height in pixels, based on the current theme.
|
||||
*/
|
||||
private int getListPreferredItemHeight() {
|
||||
final TypedValue typedValue = new TypedValue();
|
||||
|
||||
// Resolve list item preferred height theme attribute into typedValue
|
||||
getActivity().getTheme().resolveAttribute(
|
||||
android.R.attr.listPreferredItemHeight, typedValue, true);
|
||||
|
||||
// Create a new DisplayMetrics object
|
||||
final DisplayMetrics metrics = new android.util.DisplayMetrics();
|
||||
|
||||
// Populate the DisplayMetrics
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
||||
|
||||
// Return theme value based on DisplayMetrics
|
||||
return (int) typedValue.getDimension(metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes and scales a contact's image from a file pointed to by a Uri in the contact's data,
|
||||
* and returns the result as a Bitmap. The column that contains the Uri varies according to the
|
||||
* platform version.
|
||||
*
|
||||
* @param photoData For platforms prior to Android 3.0, provide the Contact._ID column value.
|
||||
* For Android 3.0 and later, provide the Contact.PHOTO_THUMBNAIL_URI value.
|
||||
* @param imageSize The desired target width and height of the output image in pixels.
|
||||
* @return A Bitmap containing the contact's image, resized to fit the provided image size. If
|
||||
* no thumbnail exists, returns null.
|
||||
*/
|
||||
private Bitmap loadContactPhotoThumbnail(String photoData, int imageSize) {
|
||||
|
||||
// Ensures the Fragment is still added to an activity. As this method is called in a
|
||||
// background thread, there's the possibility the Fragment is no longer attached and
|
||||
// added to an activity. If so, no need to spend resources loading the contact photo.
|
||||
if (!isAdded() || getActivity() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Instantiates an AssetFileDescriptor. Given a content Uri pointing to an image file, the
|
||||
// ContentResolver can return an AssetFileDescriptor for the file.
|
||||
AssetFileDescriptor afd = null;
|
||||
|
||||
// This "try" block catches an Exception if the file descriptor returned from the Contacts
|
||||
// Provider doesn't point to an existing file.
|
||||
try {
|
||||
Uri thumbUri;
|
||||
// If Android 3.0 or later, converts the Uri passed as a string to a Uri object.
|
||||
if (Utils.hasHoneycomb()) {
|
||||
thumbUri = Uri.parse(photoData);
|
||||
} else {
|
||||
// For versions prior to Android 3.0, appends the string argument to the content
|
||||
// Uri for the Contacts table.
|
||||
final Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_URI, photoData);
|
||||
|
||||
// Appends the content Uri for the Contacts.Photo table to the previously
|
||||
// constructed contact Uri to yield a content URI for the thumbnail image
|
||||
thumbUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
|
||||
}
|
||||
// Retrieves a file descriptor from the Contacts Provider. To learn more about this
|
||||
// feature, read the reference documentation for
|
||||
// ContentResolver#openAssetFileDescriptor.
|
||||
afd = getActivity().getContentResolver().openAssetFileDescriptor(thumbUri, "r");
|
||||
|
||||
// Gets a FileDescriptor from the AssetFileDescriptor. A BitmapFactory object can
|
||||
// decode the contents of a file pointed to by a FileDescriptor into a Bitmap.
|
||||
FileDescriptor fileDescriptor = afd.getFileDescriptor();
|
||||
|
||||
if (fileDescriptor != null) {
|
||||
// Decodes a Bitmap from the image pointed to by the FileDescriptor, and scales it
|
||||
// to the specified width and height
|
||||
return ImageLoader.decodeSampledBitmapFromDescriptor(
|
||||
fileDescriptor, imageSize, imageSize);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// If the file pointed to by the thumbnail URI doesn't exist, or the file can't be
|
||||
// opened in "read" mode, ContentResolver.openAssetFileDescriptor throws a
|
||||
// FileNotFoundException.
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Contact photo thumbnail not found for contact " + photoData
|
||||
+ ": " + e.toString());
|
||||
}
|
||||
} finally {
|
||||
// If an AssetFileDescriptor was returned, try to close it
|
||||
if (afd != null) {
|
||||
try {
|
||||
afd.close();
|
||||
} catch (IOException e) {
|
||||
// Closing a file descriptor might cause an IOException if the file is
|
||||
// already closed. Nothing extra is needed to handle this.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the decoding failed, returns null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a subclass of CursorAdapter that supports binding Cursor columns to a view layout.
|
||||
* If those items are part of search results, the search string is marked by highlighting the
|
||||
* query text. An {@link AlphabetIndexer} is used to allow quicker navigation up and down the
|
||||
* ListView.
|
||||
*/
|
||||
private class ContactsAdapter extends CursorAdapter implements SectionIndexer {
|
||||
private LayoutInflater mInflater; // Stores the layout inflater
|
||||
private AlphabetIndexer mAlphabetIndexer; // Stores the AlphabetIndexer instance
|
||||
private TextAppearanceSpan highlightTextSpan; // Stores the highlight text appearance style
|
||||
|
||||
/**
|
||||
* Instantiates a new Contacts Adapter.
|
||||
* @param context A context that has access to the app's layout.
|
||||
*/
|
||||
public ContactsAdapter(Context context) {
|
||||
super(context, null, 0);
|
||||
|
||||
// Stores inflater for use later
|
||||
mInflater = LayoutInflater.from(context);
|
||||
|
||||
// Loads a string containing the English alphabet. To fully localize the app, provide a
|
||||
// strings.xml file in res/values-<x> directories, where <x> is a locale. In the file,
|
||||
// define a string with android:name="alphabet" and contents set to all of the
|
||||
// alphabetic characters in the language in their proper sort order, in upper case if
|
||||
// applicable.
|
||||
final String alphabet = context.getString(R.string.alphabet);
|
||||
|
||||
// Instantiates a new AlphabetIndexer bound to the column used to sort contact names.
|
||||
// The cursor is left null, because it has not yet been retrieved.
|
||||
mAlphabetIndexer = new AlphabetIndexer(null, ContactsQuery.SORT_KEY, alphabet);
|
||||
|
||||
// Defines a span for highlighting the part of a display name that matches the search
|
||||
// string
|
||||
highlightTextSpan = new TextAppearanceSpan(getActivity(), R.style.searchTextHiglight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the start of the search string in the display name column of a Cursor row.
|
||||
* E.g. If displayName was "Adam" and search query (mSearchTerm) was "da" this would
|
||||
* return 1.
|
||||
*
|
||||
* @param displayName The contact display name.
|
||||
* @return The starting position of the search string in the display name, 0-based. The
|
||||
* method returns -1 if the string is not found in the display name, or if the search
|
||||
* string is empty or null.
|
||||
*/
|
||||
private int indexOfSearchQuery(String displayName) {
|
||||
if (!TextUtils.isEmpty(mSearchTerm)) {
|
||||
return displayName.toLowerCase(Locale.getDefault()).indexOf(
|
||||
mSearchTerm.toLowerCase(Locale.getDefault()));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides newView() to inflate the list item views.
|
||||
*/
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
|
||||
// Inflates the list item layout.
|
||||
final View itemLayout =
|
||||
mInflater.inflate(R.layout.contact_list_item, viewGroup, false);
|
||||
|
||||
// Creates a new ViewHolder in which to store handles to each view resource. This
|
||||
// allows bindView() to retrieve stored references instead of calling findViewById for
|
||||
// each instance of the layout.
|
||||
final ViewHolder holder = new ViewHolder();
|
||||
holder.text1 = (TextView) itemLayout.findViewById(android.R.id.text1);
|
||||
holder.text2 = (TextView) itemLayout.findViewById(android.R.id.text2);
|
||||
holder.icon = (QuickContactBadge) itemLayout.findViewById(android.R.id.icon);
|
||||
|
||||
// Stores the resourceHolder instance in itemLayout. This makes resourceHolder
|
||||
// available to bindView and other methods that receive a handle to the item view.
|
||||
itemLayout.setTag(holder);
|
||||
|
||||
// Returns the item layout view
|
||||
return itemLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds data from the Cursor to the provided view.
|
||||
*/
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
// Gets handles to individual view resources
|
||||
final ViewHolder holder = (ViewHolder) view.getTag();
|
||||
|
||||
// For Android 3.0 and later, gets the thumbnail image Uri from the current Cursor row.
|
||||
// For platforms earlier than 3.0, this isn't necessary, because the thumbnail is
|
||||
// generated from the other fields in the row.
|
||||
final String photoUri = cursor.getString(ContactsQuery.PHOTO_THUMBNAIL_DATA);
|
||||
|
||||
final String displayName = cursor.getString(ContactsQuery.DISPLAY_NAME);
|
||||
|
||||
final int startIndex = indexOfSearchQuery(displayName);
|
||||
|
||||
if (startIndex == -1) {
|
||||
// If the user didn't do a search, or the search string didn't match a display
|
||||
// name, show the display name without highlighting
|
||||
holder.text1.setText(displayName);
|
||||
|
||||
if (TextUtils.isEmpty(mSearchTerm)) {
|
||||
// If the search search is empty, hide the second line of text
|
||||
holder.text2.setVisibility(View.GONE);
|
||||
} else {
|
||||
// Shows a second line of text that indicates the search string matched
|
||||
// something other than the display name
|
||||
holder.text2.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
// If the search string matched the display name, applies a SpannableString to
|
||||
// highlight the search string with the displayed display name
|
||||
|
||||
// Wraps the display name in the SpannableString
|
||||
final SpannableString highlightedName = new SpannableString(displayName);
|
||||
|
||||
// Sets the span to start at the starting point of the match and end at "length"
|
||||
// characters beyond the starting point
|
||||
highlightedName.setSpan(highlightTextSpan, startIndex,
|
||||
startIndex + mSearchTerm.length(), 0);
|
||||
|
||||
// Binds the SpannableString to the display name View object
|
||||
holder.text1.setText(highlightedName);
|
||||
|
||||
// Since the search string matched the name, this hides the secondary message
|
||||
holder.text2.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Processes the QuickContactBadge. A QuickContactBadge first appears as a contact's
|
||||
// thumbnail image with styling that indicates it can be touched for additional
|
||||
// information. When the user clicks the image, the badge expands into a dialog box
|
||||
// containing the contact's details and icons for the built-in apps that can handle
|
||||
// each detail type.
|
||||
|
||||
// Generates the contact lookup Uri
|
||||
final Uri contactUri = Contacts.getLookupUri(
|
||||
cursor.getLong(ContactsQuery.ID),
|
||||
cursor.getString(ContactsQuery.LOOKUP_KEY));
|
||||
|
||||
// Binds the contact's lookup Uri to the QuickContactBadge
|
||||
holder.icon.assignContactUri(contactUri);
|
||||
|
||||
// Loads the thumbnail image pointed to by photoUri into the QuickContactBadge in a
|
||||
// background worker thread
|
||||
mImageLoader.loadImage(photoUri, holder.icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides swapCursor to move the new Cursor into the AlphabetIndex as well as the
|
||||
* CursorAdapter.
|
||||
*/
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
// Update the AlphabetIndexer with new cursor as well
|
||||
mAlphabetIndexer.setCursor(newCursor);
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* An override of getCount that simplifies accessing the Cursor. If the Cursor is null,
|
||||
* getCount returns zero. As a result, no test for Cursor == null is needed.
|
||||
*/
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (getCursor() == null) {
|
||||
return 0;
|
||||
}
|
||||
return super.getCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the SectionIndexer.getSections() interface.
|
||||
*/
|
||||
@Override
|
||||
public Object[] getSections() {
|
||||
return mAlphabetIndexer.getSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the SectionIndexer.getPositionForSection() interface.
|
||||
*/
|
||||
@Override
|
||||
public int getPositionForSection(int i) {
|
||||
if (getCursor() == null) {
|
||||
return 0;
|
||||
}
|
||||
return mAlphabetIndexer.getPositionForSection(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the SectionIndexer.getSectionForPosition() interface.
|
||||
*/
|
||||
@Override
|
||||
public int getSectionForPosition(int i) {
|
||||
if (getCursor() == null) {
|
||||
return 0;
|
||||
}
|
||||
return mAlphabetIndexer.getSectionForPosition(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that defines fields for each resource ID in the list item layout. This allows
|
||||
* ContactsAdapter.newView() to store the IDs once, when it inflates the layout, instead of
|
||||
* calling findViewById in each iteration of bindView.
|
||||
*/
|
||||
private class ViewHolder {
|
||||
TextView text1;
|
||||
TextView text2;
|
||||
QuickContactBadge icon;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface must be implemented by any activity that loads this fragment. When an
|
||||
* interaction occurs, such as touching an item from the ListView, these callbacks will
|
||||
* be invoked to communicate the event back to the activity.
|
||||
*/
|
||||
public interface OnContactsInteractionListener {
|
||||
/**
|
||||
* Called when a contact is selected from the ListView.
|
||||
* @param contactUri The contact Uri.
|
||||
*/
|
||||
public void onContactSelected(Uri contactUri);
|
||||
|
||||
/**
|
||||
* Called when the ListView selection is cleared like when
|
||||
* a contact search is taking place or is finishing.
|
||||
*/
|
||||
public void onSelectionCleared();
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface defines constants for the Cursor and CursorLoader, based on constants defined
|
||||
* in the {@link android.provider.ContactsContract.Contacts} class.
|
||||
*/
|
||||
public interface ContactsQuery {
|
||||
|
||||
// An identifier for the loader
|
||||
final static int QUERY_ID = 1;
|
||||
|
||||
// A content URI for the Contacts table
|
||||
final static Uri CONTENT_URI = Contacts.CONTENT_URI;
|
||||
|
||||
// The search/filter query Uri
|
||||
final static Uri FILTER_URI = Contacts.CONTENT_FILTER_URI;
|
||||
|
||||
// The selection clause for the CursorLoader query. The search criteria defined here
|
||||
// restrict results to contacts that have a display name and are linked to visible groups.
|
||||
// Notice that the search on the string provided by the user is implemented by appending
|
||||
// the search string to CONTENT_FILTER_URI.
|
||||
@SuppressLint("InlinedApi")
|
||||
final static String SELECTION =
|
||||
(Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME) +
|
||||
"<>''" + " AND " + Contacts.IN_VISIBLE_GROUP + "=1";
|
||||
|
||||
// The desired sort order for the returned Cursor. In Android 3.0 and later, the primary
|
||||
// sort key allows for localization. In earlier versions. use the display name as the sort
|
||||
// key.
|
||||
@SuppressLint("InlinedApi")
|
||||
final static String SORT_ORDER =
|
||||
Utils.hasHoneycomb() ? Contacts.SORT_KEY_PRIMARY : Contacts.DISPLAY_NAME;
|
||||
|
||||
// The projection for the CursorLoader query. This is a list of columns that the Contacts
|
||||
// Provider should return in the Cursor.
|
||||
@SuppressLint("InlinedApi")
|
||||
final static String[] PROJECTION = {
|
||||
|
||||
// The contact's row id
|
||||
Contacts._ID,
|
||||
|
||||
// A pointer to the contact that is guaranteed to be more permanent than _ID. Given
|
||||
// a contact's current _ID value and LOOKUP_KEY, the Contacts Provider can generate
|
||||
// a "permanent" contact URI.
|
||||
Contacts.LOOKUP_KEY,
|
||||
|
||||
// In platform version 3.0 and later, the Contacts table contains
|
||||
// DISPLAY_NAME_PRIMARY, which either contains the contact's displayable name or
|
||||
// some other useful identifier such as an email address. This column isn't
|
||||
// available in earlier versions of Android, so you must use Contacts.DISPLAY_NAME
|
||||
// instead.
|
||||
Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME,
|
||||
|
||||
// In Android 3.0 and later, the thumbnail image is pointed to by
|
||||
// PHOTO_THUMBNAIL_URI. In earlier versions, there is no direct pointer; instead,
|
||||
// you generate the pointer from the contact's ID value and constants defined in
|
||||
// android.provider.ContactsContract.Contacts.
|
||||
Utils.hasHoneycomb() ? Contacts.PHOTO_THUMBNAIL_URI : Contacts._ID,
|
||||
|
||||
// The sort order column for the returned Cursor, used by the AlphabetIndexer
|
||||
SORT_ORDER,
|
||||
};
|
||||
|
||||
// The query column numbers which map to each value in the projection
|
||||
final static int ID = 0;
|
||||
final static int LOOKUP_KEY = 1;
|
||||
final static int DISPLAY_NAME = 2;
|
||||
final static int PHOTO_THUMBNAIL_DATA = 3;
|
||||
final static int SORT_KEY = 4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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.example.android.contactslist.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.util.LruCache;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.android.contactslist.BuildConfig;
|
||||
|
||||
/**
|
||||
* This class holds our bitmap caches (memory and disk).
|
||||
*/
|
||||
public class ImageCache {
|
||||
private static final String TAG = "ImageCache";
|
||||
private LruCache<String, Bitmap> mMemoryCache;
|
||||
|
||||
/**
|
||||
* Creating a new ImageCache object using the specified parameters.
|
||||
*
|
||||
* @param memCacheSizePercent The cache size as a percent of available app memory.
|
||||
*/
|
||||
private ImageCache(float memCacheSizePercent) {
|
||||
init(memCacheSizePercent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
|
||||
* one is created using the supplied params and saved to a {@link RetainFragment}.
|
||||
*
|
||||
* @param fragmentManager The fragment manager to use when dealing with the retained fragment.
|
||||
* @param memCacheSizePercent The cache size as a percent of available app memory.
|
||||
* @return An existing retained ImageCache object or a new one if one did not exist
|
||||
*/
|
||||
public static ImageCache getInstance(
|
||||
FragmentManager fragmentManager, float memCacheSizePercent) {
|
||||
|
||||
// Search for, or create an instance of the non-UI RetainFragment
|
||||
final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
|
||||
|
||||
// See if we already have an ImageCache stored in RetainFragment
|
||||
ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
|
||||
|
||||
// No existing ImageCache, create one and store it in RetainFragment
|
||||
if (imageCache == null) {
|
||||
imageCache = new ImageCache(memCacheSizePercent);
|
||||
mRetainFragment.setObject(imageCache);
|
||||
}
|
||||
|
||||
return imageCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the cache.
|
||||
*
|
||||
* @param memCacheSizePercent The cache size as a percent of available app memory.
|
||||
*/
|
||||
private void init(float memCacheSizePercent) {
|
||||
int memCacheSize = calculateMemCacheSize(memCacheSizePercent);
|
||||
|
||||
// Set up memory cache
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Memory cache created (size = " + memCacheSize + ")");
|
||||
}
|
||||
mMemoryCache = new LruCache<String, Bitmap>(memCacheSize) {
|
||||
/**
|
||||
* Measure item size in kilobytes rather than units which is more practical
|
||||
* for a bitmap cache
|
||||
*/
|
||||
@Override
|
||||
protected int sizeOf(String key, Bitmap bitmap) {
|
||||
final int bitmapSize = getBitmapSize(bitmap) / 1024;
|
||||
return bitmapSize == 0 ? 1 : bitmapSize;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a bitmap to both memory and disk cache.
|
||||
* @param data Unique identifier for the bitmap to store
|
||||
* @param bitmap The bitmap to store
|
||||
*/
|
||||
public void addBitmapToCache(String data, Bitmap bitmap) {
|
||||
if (data == null || bitmap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to memory cache
|
||||
if (mMemoryCache != null && mMemoryCache.get(data) == null) {
|
||||
mMemoryCache.put(data, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get from memory cache.
|
||||
*
|
||||
* @param data Unique identifier for which item to get
|
||||
* @return The bitmap if found in cache, null otherwise
|
||||
*/
|
||||
public Bitmap getBitmapFromMemCache(String data) {
|
||||
if (mMemoryCache != null) {
|
||||
final Bitmap memBitmap = mMemoryCache.get(data);
|
||||
if (memBitmap != null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Memory cache hit");
|
||||
}
|
||||
return memBitmap;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size in bytes of a bitmap.
|
||||
*
|
||||
* @param bitmap The bitmap to calculate the size of.
|
||||
* @return size of bitmap in bytes.
|
||||
*/
|
||||
@TargetApi(12)
|
||||
public static int getBitmapSize(Bitmap bitmap) {
|
||||
if (Utils.hasHoneycombMR1()) {
|
||||
return bitmap.getByteCount();
|
||||
}
|
||||
// Pre HC-MR1
|
||||
return bitmap.getRowBytes() * bitmap.getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the memory cache size based on a percentage of the max available VM memory.
|
||||
* Eg. setting percent to 0.2 would set the memory cache to one fifth of the available
|
||||
* memory. Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8.
|
||||
* memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed
|
||||
* to construct a LruCache which takes an int in its constructor.
|
||||
*
|
||||
* This value should be chosen carefully based on a number of factors
|
||||
* Refer to the corresponding Android Training class for more discussion:
|
||||
* http://developer.android.com/training/displaying-bitmaps/
|
||||
*
|
||||
* @param percent Percent of available app memory to use to size memory cache.
|
||||
*/
|
||||
public static int calculateMemCacheSize(float percent) {
|
||||
if (percent < 0.05f || percent > 0.8f) {
|
||||
throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
|
||||
+ "between 0.05 and 0.8 (inclusive)");
|
||||
}
|
||||
return Math.round(percent * Runtime.getRuntime().maxMemory() / 1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate an existing instance of this Fragment or if not found, create and
|
||||
* add it using FragmentManager.
|
||||
*
|
||||
* @param fm The FragmentManager manager to use.
|
||||
* @return The existing instance of the Fragment or the new instance if just
|
||||
* created.
|
||||
*/
|
||||
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
|
||||
// Check to see if we have retained the worker fragment.
|
||||
RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
|
||||
|
||||
// If not retained (or first time running), we need to create and add it.
|
||||
if (mRetainFragment == null) {
|
||||
mRetainFragment = new RetainFragment();
|
||||
fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
return mRetainFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple non-UI Fragment that stores a single Object and is retained over configuration
|
||||
* changes. It will be used to retain the ImageCache object.
|
||||
*/
|
||||
public static class RetainFragment extends Fragment {
|
||||
private Object mObject;
|
||||
|
||||
/**
|
||||
* Empty constructor as per the Fragment documentation
|
||||
*/
|
||||
public RetainFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Make sure this Fragment is retained over a configuration change
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a single object in this Fragment.
|
||||
*
|
||||
* @param object The object to store
|
||||
*/
|
||||
public void setObject(Object object) {
|
||||
mObject = object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored object.
|
||||
*
|
||||
* @return The stored object
|
||||
*/
|
||||
public Object getObject() {
|
||||
return mObject;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* 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.example.android.contactslist.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.TransitionDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.example.android.contactslist.BuildConfig;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* This class wraps up completing some arbitrary long running work when loading a bitmap to an
|
||||
* ImageView. It handles things like using a memory and disk cache, running the work in a background
|
||||
* thread and setting a placeholder image.
|
||||
*/
|
||||
public abstract class ImageLoader {
|
||||
private static final String TAG = "ImageLoader";
|
||||
private static final int FADE_IN_TIME = 200;
|
||||
|
||||
private ImageCache mImageCache;
|
||||
private Bitmap mLoadingBitmap;
|
||||
private boolean mFadeInBitmap = true;
|
||||
private boolean mPauseWork = false;
|
||||
private final Object mPauseWorkLock = new Object();
|
||||
private int mImageSize;
|
||||
private Resources mResources;
|
||||
|
||||
protected ImageLoader(Context context, int imageSize) {
|
||||
mResources = context.getResources();
|
||||
mImageSize = imageSize;
|
||||
}
|
||||
|
||||
public int getImageSize() {
|
||||
return mImageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an image specified by the data parameter into an ImageView (override
|
||||
* {@link ImageLoader#processBitmap(Object)} to define the processing logic). If the image is
|
||||
* found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} will be
|
||||
* created to asynchronously load the bitmap.
|
||||
*
|
||||
* @param data The URL of the image to download.
|
||||
* @param imageView The ImageView to bind the downloaded image to.
|
||||
*/
|
||||
public void loadImage(Object data, ImageView imageView) {
|
||||
if (data == null) {
|
||||
imageView.setImageBitmap(mLoadingBitmap);
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap bitmap = null;
|
||||
|
||||
if (mImageCache != null) {
|
||||
bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data));
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
// Bitmap found in memory cache
|
||||
imageView.setImageBitmap(bitmap);
|
||||
} else if (cancelPotentialWork(data, imageView)) {
|
||||
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
|
||||
final AsyncDrawable asyncDrawable =
|
||||
new AsyncDrawable(mResources, mLoadingBitmap, task);
|
||||
imageView.setImageDrawable(asyncDrawable);
|
||||
task.execute(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set placeholder bitmap that shows when the the background thread is running.
|
||||
*
|
||||
* @param resId Resource ID of loading image.
|
||||
*/
|
||||
public void setLoadingImage(int resId) {
|
||||
mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link ImageCache} to this image loader.
|
||||
*
|
||||
* @param fragmentManager A FragmentManager to use to retain the cache over configuration
|
||||
* changes such as an orientation change.
|
||||
* @param memCacheSizePercent The cache size as a percent of available app memory.
|
||||
*/
|
||||
public void addImageCache(FragmentManager fragmentManager, float memCacheSizePercent) {
|
||||
mImageCache = ImageCache.getInstance(fragmentManager, memCacheSizePercent);
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to true, the image will fade-in once it has been loaded by the background thread.
|
||||
*/
|
||||
public void setImageFadeIn(boolean fadeIn) {
|
||||
mFadeInBitmap = fadeIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses should override this to define any processing or work that must happen to produce
|
||||
* the final bitmap. This will be executed in a background thread and be long running. For
|
||||
* example, you could resize a large bitmap here, or pull down an image from the network.
|
||||
*
|
||||
* @param data The data to identify which image to process, as provided by
|
||||
* {@link ImageLoader#loadImage(Object, ImageView)}
|
||||
* @return The processed bitmap
|
||||
*/
|
||||
protected abstract Bitmap processBitmap(Object data);
|
||||
|
||||
/**
|
||||
* Cancels any pending work attached to the provided ImageView.
|
||||
*/
|
||||
public static void cancelWork(ImageView imageView) {
|
||||
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
|
||||
if (bitmapWorkerTask != null) {
|
||||
bitmapWorkerTask.cancel(true);
|
||||
if (BuildConfig.DEBUG) {
|
||||
final Object bitmapData = bitmapWorkerTask.data;
|
||||
Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current work has been canceled or if there was no work in
|
||||
* progress on this image view.
|
||||
* Returns false if the work in progress deals with the same data. The work is not
|
||||
* stopped in that case.
|
||||
*/
|
||||
public static boolean cancelPotentialWork(Object data, ImageView imageView) {
|
||||
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
|
||||
|
||||
if (bitmapWorkerTask != null) {
|
||||
final Object bitmapData = bitmapWorkerTask.data;
|
||||
if (bitmapData == null || !bitmapData.equals(data)) {
|
||||
bitmapWorkerTask.cancel(true);
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
|
||||
}
|
||||
} else {
|
||||
// The same work is already in progress.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param imageView Any imageView
|
||||
* @return Retrieve the currently active work task (if any) associated with this imageView.
|
||||
* null if there is no such task.
|
||||
*/
|
||||
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
|
||||
if (imageView != null) {
|
||||
final Drawable drawable = imageView.getDrawable();
|
||||
if (drawable instanceof AsyncDrawable) {
|
||||
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
||||
return asyncDrawable.getBitmapWorkerTask();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual AsyncTask that will asynchronously process the image.
|
||||
*/
|
||||
private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> {
|
||||
private Object data;
|
||||
private final WeakReference<ImageView> imageViewReference;
|
||||
|
||||
public BitmapWorkerTask(ImageView imageView) {
|
||||
imageViewReference = new WeakReference<ImageView>(imageView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Background processing.
|
||||
*/
|
||||
@Override
|
||||
protected Bitmap doInBackground(Object... params) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "doInBackground - starting work");
|
||||
}
|
||||
|
||||
data = params[0];
|
||||
final String dataString = String.valueOf(data);
|
||||
Bitmap bitmap = null;
|
||||
|
||||
// Wait here if work is paused and the task is not cancelled
|
||||
synchronized (mPauseWorkLock) {
|
||||
while (mPauseWork && !isCancelled()) {
|
||||
try {
|
||||
mPauseWorkLock.wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// If the task has not been cancelled by another thread and the ImageView that was
|
||||
// originally bound to this task is still bound back to this task and our "exit early"
|
||||
// flag is not set, then call the main process method (as implemented by a subclass)
|
||||
if (!isCancelled() && getAttachedImageView() != null) {
|
||||
bitmap = processBitmap(params[0]);
|
||||
}
|
||||
|
||||
// If the bitmap was processed and the image cache is available, then add the processed
|
||||
// bitmap to the cache for future use. Note we don't check if the task was cancelled
|
||||
// here, if it was, and the thread is still running, we may as well add the processed
|
||||
// bitmap to our cache as it might be used again in the future
|
||||
if (bitmap != null && mImageCache != null) {
|
||||
mImageCache.addBitmapToCache(dataString, bitmap);
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "doInBackground - finished work");
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the image is processed, associates it to the imageView
|
||||
*/
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
// if cancel was called on this task or the "exit early" flag is set then we're done
|
||||
if (isCancelled()) {
|
||||
bitmap = null;
|
||||
}
|
||||
|
||||
final ImageView imageView = getAttachedImageView();
|
||||
if (bitmap != null && imageView != null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "onPostExecute - setting bitmap");
|
||||
}
|
||||
setImageBitmap(imageView, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(Bitmap bitmap) {
|
||||
super.onCancelled(bitmap);
|
||||
synchronized (mPauseWorkLock) {
|
||||
mPauseWorkLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ImageView associated with this task as long as the ImageView's task still
|
||||
* points to this task as well. Returns null otherwise.
|
||||
*/
|
||||
private ImageView getAttachedImageView() {
|
||||
final ImageView imageView = imageViewReference.get();
|
||||
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
|
||||
|
||||
if (this == bitmapWorkerTask) {
|
||||
return imageView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom Drawable that will be attached to the imageView while the work is in progress.
|
||||
* Contains a reference to the actual worker task, so that it can be stopped if a new binding is
|
||||
* required, and makes sure that only the last started worker process can bind its result,
|
||||
* independently of the finish order.
|
||||
*/
|
||||
private static class AsyncDrawable extends BitmapDrawable {
|
||||
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
|
||||
|
||||
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
|
||||
super(res, bitmap);
|
||||
bitmapWorkerTaskReference =
|
||||
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
|
||||
}
|
||||
|
||||
public BitmapWorkerTask getBitmapWorkerTask() {
|
||||
return bitmapWorkerTaskReference.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the processing is complete and the final bitmap should be set on the ImageView.
|
||||
*
|
||||
* @param imageView The ImageView to set the bitmap to.
|
||||
* @param bitmap The new bitmap to set.
|
||||
*/
|
||||
private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
|
||||
if (mFadeInBitmap) {
|
||||
// Transition drawable to fade from loading bitmap to final bitmap
|
||||
final TransitionDrawable td =
|
||||
new TransitionDrawable(new Drawable[] {
|
||||
new ColorDrawable(android.R.color.transparent),
|
||||
new BitmapDrawable(mResources, bitmap)
|
||||
});
|
||||
imageView.setBackgroundDrawable(imageView.getDrawable());
|
||||
imageView.setImageDrawable(td);
|
||||
td.startTransition(FADE_IN_TIME);
|
||||
} else {
|
||||
imageView.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause any ongoing background work. This can be used as a temporary
|
||||
* measure to improve performance. For example background work could
|
||||
* be paused when a ListView or GridView is being scrolled using a
|
||||
* {@link android.widget.AbsListView.OnScrollListener} to keep
|
||||
* scrolling smooth.
|
||||
* <p>
|
||||
* If work is paused, be sure setPauseWork(false) is called again
|
||||
* before your fragment or activity is destroyed (for example during
|
||||
* {@link android.app.Activity#onPause()}), or there is a risk the
|
||||
* background thread will never finish.
|
||||
*/
|
||||
public void setPauseWork(boolean pauseWork) {
|
||||
synchronized (mPauseWorkLock) {
|
||||
mPauseWork = pauseWork;
|
||||
if (!mPauseWork) {
|
||||
mPauseWorkLock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and sample down a bitmap from a file input stream to the requested width and height.
|
||||
*
|
||||
* @param fileDescriptor The file descriptor to read from
|
||||
* @param reqWidth The requested width of the resulting bitmap
|
||||
* @param reqHeight The requested height of the resulting bitmap
|
||||
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
|
||||
* that are equal to or greater than the requested width and height
|
||||
*/
|
||||
public static Bitmap decodeSampledBitmapFromDescriptor(
|
||||
FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
|
||||
|
||||
// First decode with inJustDecodeBounds=true to check dimensions
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
|
||||
|
||||
// Calculate inSampleSize
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
||||
|
||||
// Decode bitmap with inSampleSize set
|
||||
options.inJustDecodeBounds = false;
|
||||
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
|
||||
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
|
||||
* the closest inSampleSize that will result in the final decoded bitmap having a width and
|
||||
* height equal to or larger than the requested width and height. This implementation does not
|
||||
* ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
|
||||
* results in a larger bitmap which isn't as useful for caching purposes.
|
||||
*
|
||||
* @param options An options object with out* params already populated (run through a decode*
|
||||
* method with inJustDecodeBounds==true
|
||||
* @param reqWidth The requested width of the resulting bitmap
|
||||
* @param reqHeight The requested height of the resulting bitmap
|
||||
* @return The value to be used for inSampleSize
|
||||
*/
|
||||
public static int calculateInSampleSize(BitmapFactory.Options options,
|
||||
int reqWidth, int reqHeight) {
|
||||
// Raw height and width of image
|
||||
final int height = options.outHeight;
|
||||
final int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
|
||||
// Calculate ratios of height and width to requested height and width
|
||||
final int heightRatio = Math.round((float) height / (float) reqHeight);
|
||||
final int widthRatio = Math.round((float) width / (float) reqWidth);
|
||||
|
||||
// Choose the smallest ratio as inSampleSize value, this will guarantee a final image
|
||||
// with both dimensions larger than or equal to the requested height and width.
|
||||
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
|
||||
|
||||
// This offers some additional logic in case the image has a strange
|
||||
// aspect ratio. For example, a panorama may have a much larger
|
||||
// width than height. In these cases the total pixels might still
|
||||
// end up being too large to fit comfortably in memory, so we should
|
||||
// be more aggressive with sample down the image (=larger inSampleSize).
|
||||
|
||||
final float totalPixels = width * height;
|
||||
|
||||
// Anything more than 2x the requested pixels we'll sample down further
|
||||
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
|
||||
|
||||
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
|
||||
inSampleSize++;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.example.android.contactslist.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.os.StrictMode;
|
||||
|
||||
import com.example.android.contactslist.ui.ContactDetailActivity;
|
||||
import com.example.android.contactslist.ui.ContactsListActivity;
|
||||
|
||||
/**
|
||||
* This class contains static utility methods.
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
// Prevents instantiation.
|
||||
private Utils() {}
|
||||
|
||||
/**
|
||||
* Enables strict mode. This should only be called when debugging the application and is useful
|
||||
* for finding some potential bugs or best practice violations.
|
||||
*/
|
||||
@TargetApi(11)
|
||||
public static void enableStrictMode() {
|
||||
// Strict mode is only available on gingerbread or later
|
||||
if (Utils.hasGingerbread()) {
|
||||
|
||||
// Enable all thread strict mode policies
|
||||
StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
|
||||
new StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog();
|
||||
|
||||
// Enable all VM strict mode policies
|
||||
StrictMode.VmPolicy.Builder vmPolicyBuilder =
|
||||
new StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog();
|
||||
|
||||
// Honeycomb introduced some additional strict mode features
|
||||
if (Utils.hasHoneycomb()) {
|
||||
// Flash screen when thread policy is violated
|
||||
threadPolicyBuilder.penaltyFlashScreen();
|
||||
// For each activity class, set an instance limit of 1. Any more instances and
|
||||
// there could be a memory leak.
|
||||
vmPolicyBuilder
|
||||
.setClassInstanceLimit(ContactsListActivity.class, 1)
|
||||
.setClassInstanceLimit(ContactDetailActivity.class, 1);
|
||||
}
|
||||
|
||||
// Use builders to enable strict mode policies
|
||||
StrictMode.setThreadPolicy(threadPolicyBuilder.build());
|
||||
StrictMode.setVmPolicy(vmPolicyBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses static final constants to detect if the device's platform version is Gingerbread or
|
||||
* later.
|
||||
*/
|
||||
public static boolean hasGingerbread() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses static final constants to detect if the device's platform version is Honeycomb or
|
||||
* later.
|
||||
*/
|
||||
public static boolean hasHoneycomb() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses static final constants to detect if the device's platform version is Honeycomb MR1 or
|
||||
* later.
|
||||
*/
|
||||
public static boolean hasHoneycombMR1() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses static final constants to detect if the device's platform version is ICS or
|
||||
* later.
|
||||
*/
|
||||
public static boolean hasICS() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
|
||||
}
|
||||
}
|
||||