Contacts Provider Training Class - Sample App Initial Commit

This is the sample app for the Contacts Provider Android training
class. It's a basic master/detail view with a list of contacts in the
master and contact name, photo and mailing addresses in the detail.
This sample app is backward compatible to API level 7 and also
optimized for all screen sizes.

Change-Id: I83fe6beae9fd4c3fe710426b7dd0863e094cbc89
This commit is contained in:
Adam Koch
2013-03-13 16:34:44 -04:00
parent 4aed7b07cf
commit bc1a645f26
69 changed files with 3602 additions and 0 deletions

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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>

View File

@@ -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>

View 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"/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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.
-->
<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>

View 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>

View 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>

View 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 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>

View 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>

View 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 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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.
-->
<!-- 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"/>

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}