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