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.
Change-Id: I83fe6beae9fd4c3fe710426b7dd0863e094cbc89
diff --git a/samples/training/ContactsList/AndroidManifest.xml b/samples/training/ContactsList/AndroidManifest.xml
new file mode 100644
index 0000000..025e9cf
--- /dev/null
+++ b/samples/training/ContactsList/AndroidManifest.xml
@@ -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>
diff --git a/samples/training/ContactsList/libs/android-support-v4.jar b/samples/training/ContactsList/libs/android-support-v4.jar
new file mode 100644
index 0000000..65ebaf8
--- /dev/null
+++ b/samples/training/ContactsList/libs/android-support-v4.jar
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_add.png b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_add.png
new file mode 100644
index 0000000..0e4f334
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_add.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_edit.png b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_edit.png
new file mode 100644
index 0000000..9d4c934
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_edit.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_search.png b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_search.png
new file mode 100644
index 0000000..6b7ce8d
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_search.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/ic_action_add.png b/samples/training/ContactsList/res/drawable-hdpi/ic_action_add.png
new file mode 100644
index 0000000..644d1c1
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/ic_action_add.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/ic_action_edit.png b/samples/training/ContactsList/res/drawable-hdpi/ic_action_edit.png
new file mode 100644
index 0000000..d423b9e
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/ic_action_edit.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/ic_action_map.png b/samples/training/ContactsList/res/drawable-hdpi/ic_action_map.png
new file mode 100644
index 0000000..c233914
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/ic_action_map.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/ic_action_search.png b/samples/training/ContactsList/res/drawable-hdpi/ic_action_search.png
new file mode 100644
index 0000000..1ef4a82
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/ic_action_search.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_180_holo_light.png b/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_180_holo_light.png
new file mode 100644
index 0000000..38e4c30
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_180_holo_light.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_holo_light.png b/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_holo_light.png
new file mode 100644
index 0000000..4c0e35e
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_holo_light.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/ic_launcher.png b/samples/training/ContactsList/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..2b3a35a
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_pressed.9.png b/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_pressed.9.png
new file mode 100644
index 0000000..ee030fb
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_pressed.9.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_unpressed.9.png b/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_unpressed.9.png
new file mode 100644
index 0000000..7140957
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_unpressed.9.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_add.png b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_add.png
new file mode 100644
index 0000000..86097d8
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_add.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_edit.png b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_edit.png
new file mode 100644
index 0000000..71fb427
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_edit.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_search.png b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_search.png
new file mode 100644
index 0000000..aad37b5
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_search.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/ic_action_add.png b/samples/training/ContactsList/res/drawable-mdpi/ic_action_add.png
new file mode 100644
index 0000000..d7ba6fe
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/ic_action_add.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/ic_action_edit.png b/samples/training/ContactsList/res/drawable-mdpi/ic_action_edit.png
new file mode 100644
index 0000000..6a3afe2
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/ic_action_edit.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/ic_action_map.png b/samples/training/ContactsList/res/drawable-mdpi/ic_action_map.png
new file mode 100644
index 0000000..68499c5
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/ic_action_map.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/ic_action_search.png b/samples/training/ContactsList/res/drawable-mdpi/ic_action_search.png
new file mode 100644
index 0000000..6b3d131
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/ic_action_search.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_180_holo_light.png b/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_180_holo_light.png
new file mode 100644
index 0000000..0b52683
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_180_holo_light.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_holo_light.png b/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_holo_light.png
new file mode 100644
index 0000000..ead9718
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_holo_light.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/ic_launcher.png b/samples/training/ContactsList/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..1baf723
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_pressed.9.png b/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_pressed.9.png
new file mode 100644
index 0000000..b23e921
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_pressed.9.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_unpressed.9.png b/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_unpressed.9.png
new file mode 100644
index 0000000..38f14f7
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_unpressed.9.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_add.png b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_add.png
new file mode 100644
index 0000000..1ebdb43
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_add.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_edit.png b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_edit.png
new file mode 100644
index 0000000..6f7e335
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_edit.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_search.png b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_search.png
new file mode 100644
index 0000000..340031b
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_search.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi/ic_action_add.png b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_add.png
new file mode 100644
index 0000000..b064476
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_add.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi/ic_action_edit.png b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_edit.png
new file mode 100644
index 0000000..6f2eb59
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_edit.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi/ic_action_map.png b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_map.png
new file mode 100644
index 0000000..4aed873
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_map.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi/ic_action_search.png b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_search.png
new file mode 100644
index 0000000..c2b58df
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_search.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_180_holo_light.png b/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_180_holo_light.png
new file mode 100644
index 0000000..f6fd172
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_180_holo_light.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_holo_light.png b/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_holo_light.png
new file mode 100644
index 0000000..05a65f6
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_holo_light.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xhdpi/ic_launcher.png b/samples/training/ContactsList/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e0b49df
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable-xxhdpi/ic_launcher.png b/samples/training/ContactsList/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..e9e1527
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/ContactsList/res/drawable/quickcontact_badge_small.xml b/samples/training/ContactsList/res/drawable/quickcontact_badge_small.xml
new file mode 100644
index 0000000..9e68152
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable/quickcontact_badge_small.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/layout-land/contact_detail_fragment.xml b/samples/training/ContactsList/res/layout-land/contact_detail_fragment.xml
new file mode 100644
index 0000000..a6dd381
--- /dev/null
+++ b/samples/training/ContactsList/res/layout-land/contact_detail_fragment.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/layout/activity_main.xml b/samples/training/ContactsList/res/layout/activity_main.xml
new file mode 100644
index 0000000..20fe26b
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/activity_main.xml
@@ -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"/>
diff --git a/samples/training/ContactsList/res/layout/activity_main_twopanes.xml b/samples/training/ContactsList/res/layout/activity_main_twopanes.xml
new file mode 100644
index 0000000..d67f548
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/activity_main_twopanes.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/layout/contact_detail_fragment.xml b/samples/training/ContactsList/res/layout/contact_detail_fragment.xml
new file mode 100644
index 0000000..1676e2e
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_detail_fragment.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/layout/contact_detail_item.xml b/samples/training/ContactsList/res/layout/contact_detail_item.xml
new file mode 100644
index 0000000..b1e832a
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_detail_item.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/layout/contact_list_fragment.xml b/samples/training/ContactsList/res/layout/contact_list_fragment.xml
new file mode 100644
index 0000000..3fc2ae4
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_list_fragment.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/layout/contact_list_item.xml b/samples/training/ContactsList/res/layout/contact_list_item.xml
new file mode 100644
index 0000000..1ca24ad
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_list_item.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/menu/contact_detail_menu.xml b/samples/training/ContactsList/res/menu/contact_detail_menu.xml
new file mode 100644
index 0000000..f2c17df
--- /dev/null
+++ b/samples/training/ContactsList/res/menu/contact_detail_menu.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/menu/contact_list_menu.xml b/samples/training/ContactsList/res/menu/contact_list_menu.xml
new file mode 100644
index 0000000..e784054
--- /dev/null
+++ b/samples/training/ContactsList/res/menu/contact_list_menu.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-sw360dp/styles.xml b/samples/training/ContactsList/res/values-sw360dp/styles.xml
new file mode 100644
index 0000000..eb25832
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw360dp/styles.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-sw600dp-port/integers.xml b/samples/training/ContactsList/res/values-sw600dp-port/integers.xml
new file mode 100644
index 0000000..cb09a2b
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp-port/integers.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-sw600dp/bools.xml b/samples/training/ContactsList/res/values-sw600dp/bools.xml
new file mode 100644
index 0000000..69ce942
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/bools.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-sw600dp/integers.xml b/samples/training/ContactsList/res/values-sw600dp/integers.xml
new file mode 100644
index 0000000..eef8547
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/integers.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-sw600dp/layout.xml b/samples/training/ContactsList/res/values-sw600dp/layout.xml
new file mode 100644
index 0000000..c9f8848
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/layout.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-sw600dp/styles.xml b/samples/training/ContactsList/res/values-sw600dp/styles.xml
new file mode 100644
index 0000000..980fd97
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/styles.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-sw720dp/dimens.xml b/samples/training/ContactsList/res/values-sw720dp/dimens.xml
new file mode 100644
index 0000000..2184cd0
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw720dp/dimens.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values-v11/styles.xml b/samples/training/ContactsList/res/values-v11/styles.xml
new file mode 100644
index 0000000..d22ffc9
--- /dev/null
+++ b/samples/training/ContactsList/res/values-v11/styles.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values/bools.xml b/samples/training/ContactsList/res/values/bools.xml
new file mode 100644
index 0000000..1197eaa
--- /dev/null
+++ b/samples/training/ContactsList/res/values/bools.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values/colors.xml b/samples/training/ContactsList/res/values/colors.xml
new file mode 100644
index 0000000..e078dcf
--- /dev/null
+++ b/samples/training/ContactsList/res/values/colors.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values/dimens.xml b/samples/training/ContactsList/res/values/dimens.xml
new file mode 100644
index 0000000..25db89d
--- /dev/null
+++ b/samples/training/ContactsList/res/values/dimens.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values/integers.xml b/samples/training/ContactsList/res/values/integers.xml
new file mode 100644
index 0000000..10596a6
--- /dev/null
+++ b/samples/training/ContactsList/res/values/integers.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values/strings.xml b/samples/training/ContactsList/res/values/strings.xml
new file mode 100644
index 0000000..3253e22
--- /dev/null
+++ b/samples/training/ContactsList/res/values/strings.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/values/styles.xml b/samples/training/ContactsList/res/values/styles.xml
new file mode 100644
index 0000000..18d85f5
--- /dev/null
+++ b/samples/training/ContactsList/res/values/styles.xml
@@ -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>
diff --git a/samples/training/ContactsList/res/xml/searchable_contacts.xml b/samples/training/ContactsList/res/xml/searchable_contacts.xml
new file mode 100644
index 0000000..ce52eec
--- /dev/null
+++ b/samples/training/ContactsList/res/xml/searchable_contacts.xml
@@ -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"/>
diff --git a/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactDetailActivity.java b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactDetailActivity.java
new file mode 100644
index 0000000..d53017f
--- /dev/null
+++ b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactDetailActivity.java
@@ -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);
+ }
+}
diff --git a/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactDetailFragment.java b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactDetailFragment.java
new file mode 100644
index 0000000..94d477c
--- /dev/null
+++ b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactDetailFragment.java
@@ -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;
+ }
+}
diff --git a/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactsListActivity.java b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactsListActivity.java
new file mode 100644
index 0000000..3a6cab2
--- /dev/null
+++ b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactsListActivity.java
@@ -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();
+ }
+}
diff --git a/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactsListFragment.java b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactsListFragment.java
new file mode 100644
index 0000000..c3a8a66
--- /dev/null
+++ b/samples/training/ContactsList/src/com/example/android/contactslist/ui/ContactsListFragment.java
@@ -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;
+ }
+}
diff --git a/samples/training/ContactsList/src/com/example/android/contactslist/util/ImageCache.java b/samples/training/ContactsList/src/com/example/android/contactslist/util/ImageCache.java
new file mode 100644
index 0000000..7e86018
--- /dev/null
+++ b/samples/training/ContactsList/src/com/example/android/contactslist/util/ImageCache.java
@@ -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;
+ }
+ }
+
+}
diff --git a/samples/training/ContactsList/src/com/example/android/contactslist/util/ImageLoader.java b/samples/training/ContactsList/src/com/example/android/contactslist/util/ImageLoader.java
new file mode 100644
index 0000000..7d915bb
--- /dev/null
+++ b/samples/training/ContactsList/src/com/example/android/contactslist/util/ImageLoader.java
@@ -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;
+ }
+}
diff --git a/samples/training/ContactsList/src/com/example/android/contactslist/util/Utils.java b/samples/training/ContactsList/src/com/example/android/contactslist/util/Utils.java
new file mode 100644
index 0000000..9c0cc44
--- /dev/null
+++ b/samples/training/ContactsList/src/com/example/android/contactslist/util/Utils.java
@@ -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;
+ }
+}