diff --git a/samples/training/ContactsList/AndroidManifest.xml b/samples/training/ContactsList/AndroidManifest.xml
new file mode 100644
index 000000000..025e9cf3f
--- /dev/null
+++ b/samples/training/ContactsList/AndroidManifest.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 000000000..65ebaf8dc
Binary files /dev/null and b/samples/training/ContactsList/libs/android-support-v4.jar 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 000000000..0e4f33474
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_add.png 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 000000000..9d4c9343b
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_edit.png 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 000000000..6b7ce8d9e
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi-v11/ic_action_search.png 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 000000000..644d1c107
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/ic_action_add.png 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 000000000..d423b9e9b
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/ic_action_edit.png 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 000000000..c23391432
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/ic_action_map.png 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 000000000..1ef4a82d1
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/ic_action_search.png 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 000000000..38e4c30f0
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_180_holo_light.png 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 000000000..4c0e35e32
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/ic_contact_picture_holo_light.png 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 000000000..2b3a35a64
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/ic_launcher.png 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 000000000..ee030fbe7
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_pressed.9.png 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 000000000..71409578b
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-hdpi/quickcontact_badge_small_unpressed.9.png 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 000000000..86097d840
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_add.png 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 000000000..71fb427e6
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_edit.png 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 000000000..aad37b58e
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi-v11/ic_action_search.png 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 000000000..d7ba6fe04
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/ic_action_add.png 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 000000000..6a3afe2ba
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/ic_action_edit.png 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 000000000..68499c56b
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/ic_action_map.png 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 000000000..6b3d131cd
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/ic_action_search.png 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 000000000..0b5268339
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_180_holo_light.png 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 000000000..ead9718b9
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/ic_contact_picture_holo_light.png 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 000000000..1baf72309
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/ic_launcher.png 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 000000000..b23e92146
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_pressed.9.png 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 000000000..38f14f75b
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-mdpi/quickcontact_badge_small_unpressed.9.png 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 000000000..1ebdb432b
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_add.png 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 000000000..6f7e335b9
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_edit.png 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 000000000..340031b80
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi-v11/ic_action_search.png 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 000000000..b06447660
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_add.png 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 000000000..6f2eb5985
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_edit.png 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 000000000..4aed873a6
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_map.png 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 000000000..c2b58df9e
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi/ic_action_search.png 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 000000000..f6fd17269
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_180_holo_light.png 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 000000000..05a65f609
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi/ic_contact_picture_holo_light.png 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 000000000..e0b49dfe0
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xhdpi/ic_launcher.png 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 000000000..e9e152707
Binary files /dev/null and b/samples/training/ContactsList/res/drawable-xxhdpi/ic_launcher.png 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 000000000..9e68152f7
--- /dev/null
+++ b/samples/training/ContactsList/res/drawable/quickcontact_badge_small.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
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 000000000..a6dd3815c
--- /dev/null
+++ b/samples/training/ContactsList/res/layout-land/contact_detail_fragment.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 000000000..20fe26bce
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/activity_main.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
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 000000000..d67f54811
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/activity_main_twopanes.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
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 000000000..1676e2ec4
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_detail_fragment.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 000000000..b1e832aad
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_detail_item.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 000000000..3fc2ae41b
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_list_fragment.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
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 000000000..1ca24ad72
--- /dev/null
+++ b/samples/training/ContactsList/res/layout/contact_list_item.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 000000000..f2c17df54
--- /dev/null
+++ b/samples/training/ContactsList/res/menu/contact_detail_menu.xml
@@ -0,0 +1,25 @@
+
+
+
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 000000000..e7840547a
--- /dev/null
+++ b/samples/training/ContactsList/res/menu/contact_list_menu.xml
@@ -0,0 +1,35 @@
+
+
+
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 000000000..eb2583244
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw360dp/styles.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
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 000000000..cb09a2b27
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp-port/integers.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ 50
+ 50
+
+
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 000000000..69ce94264
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/bools.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ true
+
+
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 000000000..eef85472f
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/integers.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ 35
+ 65
+
+
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 000000000..c9f88482b
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/layout.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ @layout/activity_main_twopanes
+
+
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 000000000..980fd977b
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw600dp/styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
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 000000000..2184cd05c
--- /dev/null
+++ b/samples/training/ContactsList/res/values-sw720dp/dimens.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ 32dp
+
+
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 000000000..d22ffc989
--- /dev/null
+++ b/samples/training/ContactsList/res/values-v11/styles.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/training/ContactsList/res/values/bools.xml b/samples/training/ContactsList/res/values/bools.xml
new file mode 100644
index 000000000..1197eaa65
--- /dev/null
+++ b/samples/training/ContactsList/res/values/bools.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ false
+
+
diff --git a/samples/training/ContactsList/res/values/colors.xml b/samples/training/ContactsList/res/values/colors.xml
new file mode 100644
index 000000000..e078dcfcc
--- /dev/null
+++ b/samples/training/ContactsList/res/values/colors.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ #FF33B5E5
+
+
diff --git a/samples/training/ContactsList/res/values/dimens.xml b/samples/training/ContactsList/res/values/dimens.xml
new file mode 100644
index 000000000..25db89d66
--- /dev/null
+++ b/samples/training/ContactsList/res/values/dimens.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ 16dp
+ 16dp
+
+
diff --git a/samples/training/ContactsList/res/values/integers.xml b/samples/training/ContactsList/res/values/integers.xml
new file mode 100644
index 000000000..10596a60d
--- /dev/null
+++ b/samples/training/ContactsList/res/values/integers.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ 45
+ 55
+
+
diff --git a/samples/training/ContactsList/res/values/strings.xml b/samples/training/ContactsList/res/values/strings.xml
new file mode 100644
index 000000000..3253e22c6
--- /dev/null
+++ b/samples/training/ContactsList/res/values/strings.xml
@@ -0,0 +1,48 @@
+
+
+
+
+ Contacts List
+ Contacts List
+ Contact Detail
+ Contacts List Search for \"%s\"
+ This is a sample app, demonstrating use of the Android system Contacts Provider.
+ Contact Thumbnail
+ View Address
+ Search
+ Add Contact
+ Edit Contact
+ No Contacts Found
+ No Contact Selected
+ Find contacts
+
+
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ
+
+
+ Matches Other Field
+
+ No addresses found
+ No application found to handle this action
+
+
diff --git a/samples/training/ContactsList/res/values/styles.xml b/samples/training/ContactsList/res/values/styles.xml
new file mode 100644
index 000000000..18d85f53d
--- /dev/null
+++ b/samples/training/ContactsList/res/values/styles.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 000000000..ce52eece3
--- /dev/null
+++ b/samples/training/ContactsList/res/xml/searchable_contacts.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
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 000000000..d53017f4f
--- /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 000000000..94d477c93
--- /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 {
+
+ 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 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 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 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 000000000..3a6cab25c
--- /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 000000000..c3a8a66b1
--- /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 {
+
+ // 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 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 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 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- directories, where 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 000000000..7e86018ef
--- /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 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(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 000000000..7d915bbff
--- /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