Merge "Fix for loading the same contact photo twice."
diff --git a/Android.mk b/Android.mk
index c6bea9d..e9e7515 100644
--- a/Android.mk
+++ b/Android.mk
@@ -11,7 +11,10 @@
android-common \
guava \
android-support-v13 \
- android-support-v4
+ android-support-v4 \
+ android-ex-variablespeed \
+
+LOCAL_REQUIRED_MODULES := libvariablespeed
LOCAL_PACKAGE_NAME := Contacts
LOCAL_CERTIFICATE := shared
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7fb7f37..d2cc492 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -30,6 +30,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.mail" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
diff --git a/res/drawable/attachment_button.png b/res/drawable/attachment_button.png
new file mode 100644
index 0000000..3020e6e
--- /dev/null
+++ b/res/drawable/attachment_button.png
Binary files differ
diff --git a/res/drawable/pause_button.png b/res/drawable/pause_button.png
new file mode 100644
index 0000000..4b2f0e7
--- /dev/null
+++ b/res/drawable/pause_button.png
Binary files differ
diff --git a/res/drawable/play_button.png b/res/drawable/play_button.png
new file mode 100644
index 0000000..ef34449
--- /dev/null
+++ b/res/drawable/play_button.png
Binary files differ
diff --git a/res/drawable/seek_bar_thumb.png b/res/drawable/seek_bar_thumb.png
new file mode 100644
index 0000000..a512ef4
--- /dev/null
+++ b/res/drawable/seek_bar_thumb.png
Binary files differ
diff --git a/res/drawable/seekbar_drawable.xml b/res/drawable/seekbar_drawable.xml
new file mode 100644
index 0000000..2533b7f
--- /dev/null
+++ b/res/drawable/seekbar_drawable.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/background">
+ <shape android:shape="line">
+ <stroke
+ android:width="2dip"
+ android:color="@color/voicemail_playback_seek_bar_yet_to_play"
+ />
+ </shape>
+ </item>
+ <!-- I am not defining a secondary progress colour - we don't use it. -->
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape android:shape="line">
+ <stroke
+ android:width="2dip"
+ android:color="@color/voicemail_playback_seek_bar_already_played"
+ />
+ </shape>
+ </clip>
+ </item>
+</layer-list>
diff --git a/res/drawable/speakerphone_off_button.png b/res/drawable/speakerphone_off_button.png
new file mode 100644
index 0000000..ad6820b
--- /dev/null
+++ b/res/drawable/speakerphone_off_button.png
Binary files differ
diff --git a/res/drawable/speakerphone_on_button.png b/res/drawable/speakerphone_on_button.png
new file mode 100644
index 0000000..e6deda3
--- /dev/null
+++ b/res/drawable/speakerphone_on_button.png
Binary files differ
diff --git a/res/drawable/trash_button.png b/res/drawable/trash_button.png
new file mode 100644
index 0000000..2fbb1dd
--- /dev/null
+++ b/res/drawable/trash_button.png
Binary files differ
diff --git a/res/layout-sw580dp/group_detail_fragment.xml b/res/layout-sw580dp/group_detail_fragment.xml
index a824319..7c65036 100644
--- a/res/layout-sw580dp/group_detail_fragment.xml
+++ b/res/layout-sw580dp/group_detail_fragment.xml
@@ -37,8 +37,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/group_detail_border_padding"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:attr/textColorSecondary" />
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorTertiary" />
<View
android:layout_width="match_parent"
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
index e797f0d..1e40964 100644
--- a/res/layout/call_detail.xml
+++ b/res/layout/call_detail.xml
@@ -44,10 +44,22 @@
android:layout_below="@id/action_bar"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
-
android:background="@drawable/ic_contact_picture"
/>
<LinearLayout
+ android:id="@+id/voicemail_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/contact_background"
+ >
+ <fragment
+ class="com.android.contacts.voicemail.VoicemailPlaybackFragment"
+ android:id="@+id/voicemail_playback_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ />
+ </LinearLayout>
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="?attr/call_detail_contact_background_overlay_height"
android:background="#3F000000"
@@ -85,7 +97,7 @@
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@id/contact_background"
+ android:layout_below="@id/voicemail_container"
android:background="?attr/call_log_primary_background_color"
/>
<ListView
diff --git a/res/layout/playback_layout.xml b/res/layout/playback_layout.xml
new file mode 100644
index 0000000..5fee6fc
--- /dev/null
+++ b/res/layout/playback_layout.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+>
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dip"
+ android:orientation="vertical"
+ android:background="@color/voicemail_playback_ui_background"
+ >
+ <!-- Mute, playback, trash buttons. -->
+ <LinearLayout
+ android:id="@+id/buttons_linear_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_alignParentTop="true"
+ >
+ <ImageButton
+ android:id="@+id/playback_speakerphone"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="5px"
+ android:background="@color/voicemail_playback_ui_background"
+ android:src="@drawable/speakerphone_on_button"
+ android:layout_weight="1"
+ />
+ <ImageButton
+ android:id="@+id/playback_start_stop"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="5px"
+ android:background="@color/voicemail_playback_ui_background"
+ android:src="@drawable/pause_button"
+ android:layout_weight="1"
+ />
+ <ImageButton
+ android:id="@+id/playback_trash"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="5px"
+ android:background="@color/voicemail_playback_ui_background"
+ android:src="@drawable/trash_button"
+ android:layout_weight="1"
+ />
+ </LinearLayout>
+ <SeekBar
+ android:id="@+id/playback_seek"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:progressDrawable="@drawable/seekbar_drawable"
+ android:thumb="@drawable/seek_bar_thumb"
+ android:thumbOffset="0dip"
+ android:paddingLeft="30dip"
+ android:paddingRight="30dip"
+ android:paddingTop="10dip"
+ android:paddingBottom="25dip"
+ android:progress="0"
+ android:max="50"
+ android:layout_alignParentBottom="true"
+ />
+ <TextView
+ android:id="@+id/playback_position_text"
+ android:text="@string/voicemail_initial_time"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingBottom="5dip"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ />
+ <Button
+ android:id="@+id/rate_decrease_button"
+ android:layout_width="30dip"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:textColor="@color/voicemail_playback_ui_text"
+ android:textSize="20dip"
+ android:textStyle="bold"
+ android:paddingTop="10dip"
+ android:paddingBottom="15dip"
+ android:text="@string/voicemail_decrease_button"
+ android:layout_alignLeft="@id/playback_seek"
+ android:layout_alignBottom="@id/playback_seek"
+ />
+ <Button
+ android:id="@+id/rate_increase_button"
+ android:layout_width="30dip"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:textColor="@color/voicemail_playback_ui_text"
+ android:textSize="20dip"
+ android:textStyle="bold"
+ android:paddingTop="10dip"
+ android:paddingBottom="15dip"
+ android:text="@string/voicemail_increase_button"
+ android:layout_alignRight="@id/playback_seek"
+ android:layout_alignBottom="@id/playback_seek"
+ />
+ </RelativeLayout>
+</LinearLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index b92b82c..35727e0 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -61,4 +61,17 @@
<!-- Color of the text describing an unconsumed voicemail. -->
<color name="call_log_voicemail_highlight_color">#0000FF</color>
+
+ <!-- Palette section:
+ If you need a new color then add a new one and delete the ones that are no longer used. -->
+ <color name="lighter_grey">#cccccc</color>
+ <color name="seek_bar_blue">#32bdf1</color>
+ <color name="semi_transparent_grey">#cc696969</color>
+
+ <!-- This section defines the color used by different UI components and maps them to one of the
+ colors defined in the palette section above. -->
+ <color name="voicemail_playback_ui_background">@color/@android:color/white</color>
+ <color name="voicemail_playback_seek_bar_yet_to_play">@color/lighter_grey</color>
+ <color name="voicemail_playback_seek_bar_already_played">@color/seek_bar_blue</color>
+ <color name="voicemail_playback_ui_text">@color/semi_transparent_grey</color>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 52f8394..54bb5ed 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1569,6 +1569,12 @@
(Contacts themselves will not be deleted.)
</string>
+ <!-- Subtitle of the group detail page that describes how many people are in the current group [CHAR LIMIT=30] -->
+ <plurals name="num_contacts_in_group">
+ <item quantity="one"><xliff:g id="count">%1$d</xliff:g> person from <xliff:g id="account_type">%2$s</xliff:g></item>
+ <item quantity="other"><xliff:g id="count">%1$d</xliff:g> people from <xliff:g id="account_type">%2$s</xliff:g></item>
+ </plurals>
+
<!-- Toast displayed when the user creates a new contact and attempts to join it
with another before entering any data [CHAR LIMIT=256] -->
<string name="toast_join_with_empty_contact">Please enter contact name before joining
@@ -1611,6 +1617,13 @@
<!-- Title of the notification of new voicemail. -->
<string name="notification_voicemail_title">New voicemail</string>
+ <!-- Initial display for position of current playback, do not translate. -->
+ <string name="voicemail_initial_time">00:05</string>
+ <!-- This string is temporary whilst I wait for an art asset to use on the button. -->
+ <string name="voicemail_decrease_button">-</string>
+ <!-- This string is temporary whilst I wait for an art asset to use on the button. -->
+ <string name="voicemail_increase_button">+</string>
+
<!-- The separator between the call type text and the date in the call log [CHAR LIMIT=3] -->
<string name="call_log_type_date_separator">/</string>
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index cce48dd..9fd1b03 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -19,7 +19,9 @@
import com.android.contacts.calllog.CallDetailHistoryAdapter;
import com.android.contacts.calllog.CallTypeHelper;
import com.android.contacts.calllog.PhoneNumberHelper;
+import com.android.contacts.voicemail.VoicemailPlaybackFragment;
+import android.app.FragmentManager;
import android.app.ListActivity;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -63,7 +65,11 @@
private static final String TAG = "CallDetail";
/** A long array extra containing ids of call log entries to display. */
- public static final String EXTRA_CALL_LOG_IDS = "com.android.contacts.CALL_LOG_IDS";
+ public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
+ /** If we are started with a voicemail, we'll find the uri to play with this extra. */
+ public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
+ /** If we should immediately start playback of the voicemail, this extra will be set to true. */
+ public static final String EXTRA_VOICEMAIL_START_PLAYBACK = "EXTRA_VOICEMAIL_START_PLAYBACK";
/** The views representing the details of a phone call. */
private PhoneCallDetailsViews mPhoneCallDetailsViews;
@@ -103,7 +109,7 @@
PhoneLookup.LABEL,
PhoneLookup.NUMBER,
PhoneLookup.NORMALIZED_NUMBER,
- PhoneLookup.PHOTO_ID,
+ PhoneLookup.PHOTO_URI,
};
static final int COLUMN_INDEX_ID = 0;
static final int COLUMN_INDEX_NAME = 1;
@@ -111,7 +117,7 @@
static final int COLUMN_INDEX_LABEL = 3;
static final int COLUMN_INDEX_NUMBER = 4;
static final int COLUMN_INDEX_NORMALIZED_NUMBER = 5;
- static final int COLUMN_INDEX_PHOTO_ID = 6;
+ static final int COLUMN_INDEX_PHOTO_URI = 6;
@Override
protected void onCreate(Bundle icicle) {
@@ -151,6 +157,29 @@
public void onResume() {
super.onResume();
updateData(getCallLogEntryUris());
+ optionallyHandleVoicemail();
+ }
+
+ /**
+ * Handle voicemail playback or hide voicemail ui.
+ * <p>
+ * If the Intent used to start this Activity contains the suitable extras, then start voicemail
+ * playback. If it doesn't, then hide the voicemail ui.
+ */
+ private void optionallyHandleVoicemail() {
+ FragmentManager manager = getFragmentManager();
+ VoicemailPlaybackFragment fragment = (VoicemailPlaybackFragment) manager.findFragmentById(
+ R.id.voicemail_playback_fragment);
+ Uri voicemailUri = getIntent().getExtras().getParcelable(EXTRA_VOICEMAIL_URI);
+ if (voicemailUri == null) {
+ // No voicemail uri: hide the voicemail fragment.
+ manager.beginTransaction().hide(fragment).commit();
+ } else {
+ // A voicemail: extra tells us if we should start playback or not.
+ boolean startPlayback = getIntent().getExtras().getBoolean(
+ EXTRA_VOICEMAIL_START_PLAYBACK, false);
+ fragment.setVoicemailUri(voicemailUri, startPlayback);
+ }
}
/**
@@ -220,7 +249,7 @@
// We know that all calls are from the same number and the same contact, so pick the first.
mNumber = details[0].number.toString();
final long personId = details[0].personId;
- final long photoId = details[0].photoId;
+ final Uri photoUri = details[0].photoUri;
// Set the details header, based on the first phone call.
mPhoneCallDetailsHelper.setPhoneCallDetails(mPhoneCallDetailsViews,
@@ -327,7 +356,7 @@
ListView historyList = (ListView) findViewById(R.id.history);
historyList.setAdapter(
new CallDetailHistoryAdapter(this, mInflater, mCallTypeHelper, details));
- loadContactPhotos(photoId);
+ loadContactPhotos(photoUri);
}
/** Return the phone call details for a given call log URI. */
@@ -356,7 +385,7 @@
int numberType = 0;
CharSequence numberLabel = "";
long personId = -1L;
- long photoId = 0L;
+ Uri photoUri = null;
// If this is not a regular number, there is no point in looking it up in the contacts.
if (!mPhoneNumberHelper.canPlaceCallsTo(number)) {
numberText = mPhoneNumberHelper.getDisplayNumber(number, null);
@@ -370,7 +399,8 @@
if (phonesCursor != null && phonesCursor.moveToFirst()) {
personId = phonesCursor.getLong(COLUMN_INDEX_ID);
nameText = phonesCursor.getString(COLUMN_INDEX_NAME);
- photoId = phonesCursor.getLong(COLUMN_INDEX_PHOTO_ID);
+ String photoUriString = phonesCursor.getString(COLUMN_INDEX_PHOTO_URI);
+ photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
candidateNumberText = PhoneNumberUtils.formatNumber(
phonesCursor.getString(COLUMN_INDEX_NUMBER),
phonesCursor.getString(COLUMN_INDEX_NORMALIZED_NUMBER),
@@ -390,7 +420,7 @@
}
}
return new PhoneCallDetails(number, numberText, new int[]{ callType }, date, duration,
- nameText, numberType, numberLabel, personId, photoId);
+ nameText, numberType, numberLabel, personId, photoUri);
} finally {
if (callCursor != null) {
callCursor.close();
@@ -399,8 +429,8 @@
}
/** Load the contact photos and places them in the corresponding views. */
- private void loadContactPhotos(final long photoId) {
- mContactPhotoManager.loadPhoto(mContactBackgroundView, photoId);
+ private void loadContactPhotos(Uri photoUri) {
+ mContactPhotoManager.loadPhoto(mContactBackgroundView, photoUri);
}
private String getVoicemailNumber() {
diff --git a/src/com/android/contacts/PhoneCallDetails.java b/src/com/android/contacts/PhoneCallDetails.java
index 6ab47aa..d4786d9 100644
--- a/src/com/android/contacts/PhoneCallDetails.java
+++ b/src/com/android/contacts/PhoneCallDetails.java
@@ -16,6 +16,7 @@
package com.android.contacts;
+import android.net.Uri;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -45,19 +46,22 @@
public final CharSequence numberLabel;
/** The id of the contact associated with this phone call. */
public final long personId;
- /** The photo id of the contact associated with this phone call. */
- public final long photoId;
+ /**
+ * The photo uri of the picture of the contact that is associated with this phone call or
+ * null if there is none.
+ */
+ public final Uri photoUri;
/** Create the details for a call with a number not associated with a contact. */
public PhoneCallDetails(CharSequence number, CharSequence formattedNumber, int[] callTypes,
long date, long duration) {
- this(number, formattedNumber, callTypes, date, duration, "", 0, "", -1L, 0L);
+ this(number, formattedNumber, callTypes, date, duration, "", 0, "", -1L, null);
}
/** Create the details for a call with a number associated with a contact. */
public PhoneCallDetails(CharSequence number, CharSequence formattedNumber, int[] callTypes,
long date, long duration, CharSequence name, int numberType, CharSequence numberLabel,
- long personId, long photoId) {
+ long personId, Uri photoUri) {
this.number = number;
this.formattedNumber = formattedNumber;
this.callTypes = callTypes;
@@ -67,6 +71,6 @@
this.numberType = numberType;
this.numberLabel = numberLabel;
this.personId = personId;
- this.photoId = photoId;
+ this.photoUri = photoUri;
}
}
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index 0637fd5..de262b2 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -172,9 +172,8 @@
}
@Override
- public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
- boolean globalSearch) {
- // Ignore search key press
+ public boolean onSearchRequested() {
+ return true; // Don't respond to the search key.
}
@Override
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
index 1ee7f1d..6655c81 100644
--- a/src/com/android/contacts/activities/ContactEditorActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -120,9 +120,8 @@
}
@Override
- public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
- boolean globalSearch) {
- // Ignore search key press
+ public boolean onSearchRequested() {
+ return true; // Don't respond to the search key.
}
@Override
diff --git a/src/com/android/contacts/activities/GroupDetailActivity.java b/src/com/android/contacts/activities/GroupDetailActivity.java
index 635c130..dcd35fa 100644
--- a/src/com/android/contacts/activities/GroupDetailActivity.java
+++ b/src/com/android/contacts/activities/GroupDetailActivity.java
@@ -92,8 +92,7 @@
}
@Override
- public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
- boolean globalSearch) {
- // Ignore search key press
+ public boolean onSearchRequested() {
+ return true; // Don't respond to the search key.
}
}
diff --git a/src/com/android/contacts/activities/GroupEditorActivity.java b/src/com/android/contacts/activities/GroupEditorActivity.java
index ecadcec..b2553a4 100644
--- a/src/com/android/contacts/activities/GroupEditorActivity.java
+++ b/src/com/android/contacts/activities/GroupEditorActivity.java
@@ -83,9 +83,8 @@
}
@Override
- public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
- boolean globalSearch) {
- // Ignore search key press
+ public boolean onSearchRequested() {
+ return true; // Don't respond to the search key.
}
@Override
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 9dfa972..ee36d8f 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -120,8 +120,6 @@
private ActionBarAdapter mActionBarAdapter;
- private boolean mSearchMode;
-
private ContactDetailFragment mContactDetailFragment;
private ContactDetailUpdatesFragment mContactDetailUpdatesFragment;
private final ContactDetailFragmentListener mContactDetailFragmentListener =
@@ -138,8 +136,6 @@
private StrequentContactListFragment.Listener mFavoritesFragmentListener =
new StrequentContactListFragmentListener();
- private boolean mSearchInitiated;
-
private ContactListFilterController mContactListFilterController;
private ContactsUnavailableFragment mContactsUnavailableFragment;
@@ -470,6 +466,7 @@
if (fromRequest) {
ContactListFilter filter = null;
int actionCode = mRequest.getActionCode();
+ boolean searchMode = mRequest.isSearchMode();
switch (actionCode) {
case ContactsRequest.ACTION_ALL_CONTACTS:
filter = ContactListFilter.createFilterWithType(
@@ -494,25 +491,17 @@
}
}
- mSearchMode = mRequest.isSearchMode();
if (filter != null) {
mContactListFilterController.setContactListFilter(filter, false);
- mSearchMode = false;
- } else if (mRequest.getActionCode() == ContactsRequest.ACTION_ALL_CONTACTS) {
- mContactListFilterController.setContactListFilter(
- ContactListFilter.createFilterWithType(
- ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), false);
+ searchMode = false;
}
if (mRequest.getContactUri() != null) {
- mSearchMode = false;
+ searchMode = false;
}
- mAllFragment.setContactsRequest(mRequest);
+ mActionBarAdapter.setSearchMode(searchMode);
configureContactListFragmentForRequest();
-
- } else {
- mSearchMode = mActionBarAdapter.isSearchMode();
}
configureContactListFragment();
@@ -805,13 +794,15 @@
}
private void configureContactListFragmentForRequest() {
+ mAllFragment.setContactsRequest(mRequest);
+
Uri contactUri = mRequest.getContactUri();
if (contactUri != null) {
mAllFragment.setSelectedContactUri(contactUri);
}
- mAllFragment.setSearchMode(mRequest.isSearchMode());
- mAllFragment.setQueryString(mRequest.getQueryString(), false);
+ mAllFragment.setSearchMode(mActionBarAdapter.isSearchMode());
+ mAllFragment.setQueryString(mActionBarAdapter.getQueryString(), false);
if (mRequest.isDirectorySearchEnabled()) {
mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DEFAULT);
@@ -825,10 +816,11 @@
}
private void configureContactListFragment() {
- mAllFragment.setSearchMode(mSearchMode);
+ final boolean searchMode = mActionBarAdapter.isSearchMode();
+ mAllFragment.setSearchMode(searchMode);
final boolean useTwoPane = PhoneCapabilityTester.isUsingTwoPanes(this);
- mAllFragment.setVisibleScrollbarEnabled(!mSearchMode);
+ mAllFragment.setVisibleScrollbarEnabled(!searchMode);
mAllFragment.setVerticalScrollbarPosition(
useTwoPane
? View.SCROLLBAR_POSITION_LEFT
@@ -1340,13 +1332,9 @@
}
@Override
- public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
- boolean globalSearch) {
- if (mAllFragment != null && mAllFragment.isAdded() && !globalSearch) {
- mAllFragment.startSearch(initialQuery);
- } else {
- super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
- }
+ public boolean onSearchRequested() { // Search key pressed.
+ mActionBarAdapter.setSearchMode(true);
+ return true;
}
@Override
@@ -1437,12 +1425,6 @@
mActionBarAdapter.setQueryString(query);
mActionBarAdapter.setSearchMode(true);
return true;
- } else if (!mRequest.isSearchMode()) {
- if (!mSearchInitiated) {
- mSearchInitiated = true;
- startSearch(query, false, null, false);
- return true;
- }
}
}
}
@@ -1453,7 +1435,7 @@
@Override
public void onBackPressed() {
- if (mSearchMode && mActionBarAdapter != null) {
+ if (mActionBarAdapter.isSearchMode()) {
mActionBarAdapter.setSearchMode(false);
} else {
super.onBackPressed();
@@ -1478,7 +1460,6 @@
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
mActionBarAdapter.onSaveInstanceState(outState);
if (mContactDetailLayoutController != null) {
mContactDetailLayoutController.onSaveInstanceState(outState);
@@ -1493,7 +1474,6 @@
@Override
protected void onRestoreInstanceState(Bundle inState) {
super.onRestoreInstanceState(inState);
- mSearchMode = inState.getBoolean(KEY_SEARCH_MODE);
if (mContactDetailLayoutController != null) {
mContactDetailLayoutController.onRestoreInstanceState(inState);
}
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index 1fb0a03..7db5281 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -37,11 +37,15 @@
import android.content.res.Resources;
import android.database.CharArrayBuffer;
import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteFullException;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
+import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.Contacts;
@@ -75,8 +79,10 @@
/** The size of the cache of contact info. */
private static final int CONTACT_INFO_CACHE_SIZE = 100;
- /** The query for the call log table */
+ /** The query for the call log table. */
public static final class CallLogQuery {
+ // If you alter this, you must also alter the method that inserts a fake row to the headers
+ // in the CallLogQueryHandler class called createHeaderCursorFor().
public static final String[] _PROJECTION = new String[] {
Calls._ID,
Calls.NUMBER,
@@ -84,13 +90,16 @@
Calls.DURATION,
Calls.TYPE,
Calls.COUNTRY_ISO,
+ Calls.VOICEMAIL_URI,
};
+
public static final int ID = 0;
public static final int NUMBER = 1;
public static final int DATE = 2;
public static final int DURATION = 3;
public static final int CALL_TYPE = 4;
public static final int COUNTRY_ISO = 5;
+ public static final int VOICEMAIL_URI = 6;
/**
* The name of the synthetic "section" column.
@@ -100,7 +109,7 @@
*/
public static final String SECTION_NAME = "section";
/** The index of the "section" column in the projection. */
- public static final int SECTION = 6;
+ public static final int SECTION = 7;
/** The value of the "section" column for the header of the new section. */
public static final int SECTION_NEW_HEADER = 0;
/** The value of the "section" column for the items of the new section. */
@@ -120,7 +129,7 @@
PhoneLookup.LABEL,
PhoneLookup.NUMBER,
PhoneLookup.NORMALIZED_NUMBER,
- PhoneLookup.PHOTO_ID,
+ PhoneLookup.PHOTO_THUMBNAIL_URI,
PhoneLookup.LOOKUP_KEY};
public static final int PERSON_ID = 0;
@@ -129,7 +138,7 @@
public static final int LABEL = 3;
public static final int MATCHED_NUMBER = 4;
public static final int NORMALIZED_NUMBER = 5;
- public static final int PHOTO_ID = 6;
+ public static final int THUMBNAIL_URI = 6;
public static final int LOOKUP_KEY = 7;
}
@@ -158,7 +167,7 @@
public String number;
public String formattedNumber;
public String normalizedNumber;
- public long photoId;
+ public Uri thumbnailUri;
public String lookupKey;
public static ContactInfo EMPTY = new ContactInfo();
@@ -170,10 +179,53 @@
public String name;
public int numberType;
public String numberLabel;
- public long photoId;
+ public Uri thumbnailUri;
public String lookupKey;
}
+ /** Encapsulates the information needed to call a number from the call log. */
+ private static final class NumberAndType {
+ private final String mNumber;
+ private final long mRowId;
+ private final int mCallType;
+ private final String mVoicemailUri;
+
+ public NumberAndType(String number, long rowId, int callType, String voicemailUri) {
+ mNumber = number;
+ mRowId = rowId;
+ mCallType = callType;
+ mVoicemailUri = voicemailUri;
+ }
+
+ public Intent getIntent(Context context) {
+ switch (mCallType) {
+ case CallLog.Calls.VOICEMAIL_TYPE:
+ Intent intent = new Intent(context, CallDetailActivity.class);
+ intent.setData(ContentUris.withAppendedId(
+ Calls.CONTENT_URI_WITH_VOICEMAIL, mRowId));
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+ Uri.parse(mVoicemailUri));
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
+ return intent;
+ case CallLog.Calls.INCOMING_TYPE:
+ case CallLog.Calls.OUTGOING_TYPE:
+ case CallLog.Calls.MISSED_TYPE:
+ default: {
+ // Here, "number" can either be a PSTN phone number or a
+ // SIP address. So turn it into either a tel: URI or a
+ // sip: URI, as appropriate.
+ Uri uri;
+ if (PhoneNumberUtils.isUriNumber(mNumber)) {
+ uri = Uri.fromParts("sip", mNumber, null);
+ } else {
+ uri = Uri.fromParts("tel", mNumber, null);
+ }
+ return new Intent(Intent.ACTION_CALL_PRIVILEGED, uri);
+ }
+ }
+ }
+ }
+
/** Adapter class to fill in data for the Call Log */
public final class CallLogAdapter extends GroupingListAdapter
implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener {
@@ -220,18 +272,9 @@
@Override
public void onClick(View view) {
- String number = (String) view.getTag();
- if (!TextUtils.isEmpty(number)) {
- // Here, "number" can either be a PSTN phone number or a
- // SIP address. So turn it into either a tel: URI or a
- // sip: URI, as appropriate.
- Uri callUri;
- if (PhoneNumberUtils.isUriNumber(number)) {
- callUri = Uri.fromParts("sip", number, null);
- } else {
- callUri = Uri.fromParts("tel", number, null);
- }
- startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri));
+ NumberAndType numberAndType = (NumberAndType) view.getTag();
+ if (numberAndType != null) {
+ startActivity(numberAndType.getIntent(CallLogFragment.this.getActivity()));
}
}
@@ -341,15 +384,15 @@
mContactInfoCache.expireAll();
}
- private void enqueueRequest(String number, boolean immediate, int position,
- String name, int numberType, String numberLabel, long photoId, String lookupKey) {
+ private void enqueueRequest(String number, boolean immediate, int position, String name,
+ int numberType, String numberLabel, Uri thumbnailUri, String lookupKey) {
CallerInfoQuery ciq = new CallerInfoQuery();
ciq.number = number;
ciq.position = position;
ciq.name = name;
ciq.numberType = numberType;
ciq.numberLabel = numberLabel;
- ciq.photoId = photoId;
+ ciq.thumbnailUri = thumbnailUri;
ciq.lookupKey = lookupKey;
synchronized (mRequests) {
mRequests.add(ciq);
@@ -430,8 +473,11 @@
info.number = dataTableCursor.getString(
dataTableCursor.getColumnIndex(Data.DATA1));
info.normalizedNumber = null; // meaningless for SIP addresses
- info.photoId = dataTableCursor.getLong(
- dataTableCursor.getColumnIndex(Data.PHOTO_ID));
+ final String thumbnailUriString = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
+ info.thumbnailUri = thumbnailUriString == null
+ ? null
+ : Uri.parse(thumbnailUriString);
info.lookupKey = dataTableCursor.getString(
dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
@@ -458,7 +504,11 @@
.getString(PhoneQuery.MATCHED_NUMBER);
info.normalizedNumber = phonesCursor
.getString(PhoneQuery.NORMALIZED_NUMBER);
- info.photoId = phonesCursor.getLong(PhoneQuery.PHOTO_ID);
+ final String thumbnailUriString = phonesCursor.getString(
+ PhoneQuery.THUMBNAIL_URI);
+ info.thumbnailUri = thumbnailUriString == null
+ ? null
+ : Uri.parse(thumbnailUriString);
info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
infoUpdated = true;
@@ -679,8 +729,11 @@
final String formattedNumber;
final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
// Store away the number so we can call it directly if you click on the call icon
- if (views.callView != null) {
- views.callView.setTag(number);
+ if (views.callView != null && !TextUtils.isEmpty(number)) {
+ int callType = c.getInt(CallLogQuery.CALL_TYPE);
+ String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
+ long rowId = c.getLong(CallLogQuery.ID);
+ views.callView.setTag(new NumberAndType(number, rowId, callType, voicemailUri));
}
// Lookup contacts with this number
@@ -696,7 +749,7 @@
mContactInfoCache.put(number, info);
Log.d(TAG, "Contact info missing: " + number);
// Request the contact details immediately since they are currently missing.
- enqueueRequest(number, true, c.getPosition(), "", 0, "", 0L, "");
+ enqueueRequest(number, true, c.getPosition(), "", 0, "", null, "");
} else if (info != ContactInfo.EMPTY) { // Has been queried
if (cachedInfo.isExpired()) {
Log.d(TAG, "Contact info expired: " + number);
@@ -706,7 +759,7 @@
// The contact info is no longer up to date, we should request it. However, we
// do not need to request them immediately.
enqueueRequest(number, false, c.getPosition(), info.name, info.type, info.label,
- info.photoId, info.lookupKey);
+ info.thumbnailUri, info.lookupKey);
}
// Format and cache phone number for found contact
@@ -724,7 +777,7 @@
final String name = info.name;
final int ntype = info.type;
final String label = info.label;
- final long photoId = info.photoId;
+ final Uri thumbnailUri = info.thumbnailUri;
final String lookupKey = info.lookupKey;
// Assumes the call back feature is on most of the
// time. For private and unknown numbers: hide it.
@@ -738,7 +791,7 @@
details = new PhoneCallDetails(number, formattedNumber, callTypes, date, duration);
} else {
details = new PhoneCallDetails(number, formattedNumber, callTypes, date, duration,
- name, ntype, label, personId, photoId);
+ name, ntype, label, personId, thumbnailUri);
}
final boolean isNew = isNewSection(c);
@@ -748,7 +801,7 @@
final boolean isHighlighted = isNew;
mCallLogViewsHelper.setPhoneCallDetails(views, details, useIcons, isHighlighted);
if (views.photoView != null) {
- bindQuickContact(views.photoView, photoId, personId, lookupKey);
+ bindQuickContact(views.photoView, thumbnailUri, personId, lookupKey);
}
@@ -778,10 +831,10 @@
return callTypes;
}
- private void bindQuickContact(QuickContactBadge view, long photoId, long contactId,
+ private void bindQuickContact(QuickContactBadge view, Uri thumbnailUri, long contactId,
String lookupKey) {
view.assignContactUri(getContactUri(contactId, lookupKey));
- mContactPhotoManager.loadPhoto(view, photoId);
+ mContactPhotoManager.loadPhoto(view, thumbnailUri);
}
private Uri getContactUri(long contactId, String lookupKey) {
@@ -1064,22 +1117,25 @@
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Intent intent = new Intent(getActivity(), CallDetailActivity.class);
+ Cursor cursor = (Cursor) mAdapter.getItem(position);
if (mAdapter.isGroupHeader(position)) {
+ // We want to restore the position in the cursor at the end.
+ int currentPosition = cursor.getPosition();
int groupSize = mAdapter.getGroupSize(position);
long[] ids = new long[groupSize];
// Copy the ids of the rows in the group.
- Cursor cursor = (Cursor) mAdapter.getItem(position);
- // Restore the position in the cursor at the end.
- int currentPosition = cursor.getPosition();
for (int index = 0; index < groupSize; ++index) {
ids[index] = cursor.getLong(CallLogQuery.ID);
cursor.moveToNext();
}
- cursor.moveToPosition(currentPosition);
intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, ids);
+ cursor.moveToPosition(currentPosition);
} else {
// If there is a single item, use the direct URI for it.
intent.setData(ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, id));
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+ Uri.parse(cursor.getString(CallLogQuery.VOICEMAIL_URI)));
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, false);
}
startActivity(intent);
}
diff --git a/src/com/android/contacts/calllog/CallLogQueryHandler.java b/src/com/android/contacts/calllog/CallLogQueryHandler.java
index 6359479..977c84a 100644
--- a/src/com/android/contacts/calllog/CallLogQueryHandler.java
+++ b/src/com/android/contacts/calllog/CallLogQueryHandler.java
@@ -103,7 +103,9 @@
/** Creates a cursor that contains a single row and maps the section to the given value. */
private Cursor createHeaderCursorFor(int section) {
MatrixCursor matrixCursor = new MatrixCursor(getHeaderColumns());
- matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", section });
+ // The values in this row correspond to default values for _PROJECTION from CallLogQuery
+ // plus the section value.
+ matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", "", section });
return matrixCursor;
}
@@ -251,4 +253,4 @@
fragment.onCallsFetched(combinedCursor);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/contacts/group/GroupDetailFragment.java b/src/com/android/contacts/group/GroupDetailFragment.java
index c5f6ef4..7f0536f 100644
--- a/src/com/android/contacts/group/GroupDetailFragment.java
+++ b/src/com/android/contacts/group/GroupDetailFragment.java
@@ -23,6 +23,8 @@
import com.android.contacts.interactions.GroupDeletionDialogFragment;
import com.android.contacts.list.ContactTileAdapter;
import com.android.contacts.list.ContactTileAdapter.DisplayType;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountTypeManager;
import android.app.Activity;
import android.app.Fragment;
@@ -90,10 +92,12 @@
private ContactTileAdapter mAdapter;
private ContactPhotoManager mPhotoManager;
+ private AccountTypeManager mAccountTypeManager;
private Uri mGroupUri;
private long mGroupId;
private String mGroupName;
+ private String mAccountTypeString;
private boolean mOptionsMenuEditable;
private boolean mCloseActivityAfterDelete;
@@ -105,6 +109,7 @@
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity;
+ mAccountTypeManager = AccountTypeManager.getInstance(mContext);
Resources res = getResources();
int columnCount = res.getInteger(R.integer.contact_tile_column_count);
@@ -214,7 +219,7 @@
return;
}
}
- updateSize(null);
+ updateSize(-1);
updateTitle(null);
}
@@ -235,7 +240,7 @@
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- updateSize(Integer.toString(data.getCount()));
+ updateSize(data.getCount());
mAdapter.loadFromCursor(data);
}
@@ -246,6 +251,7 @@
private void bindGroupMetaData(Cursor cursor) {
cursor.moveToPosition(-1);
if (cursor.moveToNext()) {
+ mAccountTypeString = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
mGroupId = cursor.getLong(GroupMetaDataLoader.GROUP_ID);
mGroupName = cursor.getString(GroupMetaDataLoader.TITLE);
updateTitle(mGroupName);
@@ -265,11 +271,26 @@
}
}
- private void updateSize(String size) {
- if (mGroupSize != null) {
- mGroupSize.setText(size);
+ /**
+ * Display the count of the number of group members.
+ * @param size of the group (can be -1 if no size could be determined)
+ */
+ private void updateSize(int size) {
+ String groupSizeString;
+ if (size == -1) {
+ groupSizeString = null;
} else {
- mListener.onGroupSizeUpdated(size);
+ String groupSizeTemplateString = getResources().getQuantityString(
+ R.plurals.num_contacts_in_group, size);
+ AccountType accountType = mAccountTypeManager.getAccountType(mAccountTypeString);
+ groupSizeString = String.format(groupSizeTemplateString, size,
+ accountType.getDisplayLabel(mContext));
+ }
+
+ if (mGroupSize != null) {
+ mGroupSize.setText(groupSizeString);
+ } else {
+ mListener.onGroupSizeUpdated(groupSizeString);
}
}
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
new file mode 100644
index 0000000..227b1fc
--- /dev/null
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2011 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.android.contacts.voicemail;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.android.contacts.CallDetailActivity;
+import com.android.contacts.R;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.VariableSpeed;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * Displays and plays back a single voicemail.
+ * <p>
+ * When the Activity containing this Fragment is created, voicemail playback
+ * will begin immediately. The Activity is expected to be started via an intent
+ * containing a suitable voicemail uri to playback.
+ * <p>
+ * This class is not thread-safe, it is thread-confined. All calls to all public
+ * methods on this class are expected to come from the main ui thread.
+ */
+@NotThreadSafe
+public class VoicemailPlaybackFragment extends Fragment {
+ private static final int NUMBER_OF_THREADS_IN_POOL = 2;
+
+ private VoicemailPlaybackPresenter mPresenter;
+ private ScheduledExecutorService mScheduledExecutorService;
+ private SeekBar mPlaybackSeek;
+ private ImageButton mStartStopButton;
+ private ImageButton mPlaybackSpeakerphone;
+ private ImageButton mPlaybackTrashButton;
+ private TextView mPlaybackPositionText;
+ private Button mRateDecreaseButton;
+ private Button mRateIncreaseButton;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.playback_layout, container);
+ mPlaybackSeek = (SeekBar) view.findViewById(R.id.playback_seek);
+ mPlaybackSeek = (SeekBar) view.findViewById(R.id.playback_seek);
+ mStartStopButton = (ImageButton) view.findViewById(R.id.playback_start_stop);
+ mPlaybackSpeakerphone = (ImageButton) view.findViewById(R.id.playback_speakerphone);
+ mPlaybackTrashButton = (ImageButton) view.findViewById(R.id.playback_trash);
+ mPlaybackPositionText = (TextView) view.findViewById(R.id.playback_position_text);
+ mRateDecreaseButton = (Button) view.findViewById(R.id.rate_decrease_button);
+ mRateIncreaseButton = (Button) view.findViewById(R.id.rate_increase_button);
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mScheduledExecutorService = createScheduledExecutorService();
+ mPresenter = new VoicemailPlaybackPresenter(new PlaybackViewImpl(),
+ createMediaPlayer(mScheduledExecutorService), mScheduledExecutorService);
+ mPresenter.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ mPresenter.onSaveInstanceState(outState);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroy() {
+ mPresenter.onDestroy();
+ mScheduledExecutorService.shutdown();
+ super.onDestroy();
+ }
+
+ /** Call this from the Activity containing this fragment to set the voicemail to play. */
+ public void setVoicemailUri(Uri voicemailUri, boolean startPlaying) {
+ mPresenter.setVoicemailUri(voicemailUri, startPlaying);
+ }
+
+ private MediaPlayerProxy createMediaPlayer(ExecutorService executorService) {
+ return VariableSpeed.createVariableSpeed(executorService);
+ }
+
+ private ScheduledExecutorService createScheduledExecutorService() {
+ return Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
+ }
+
+ /**
+ * Formats a number of milliseconds as something that looks like {@code 00:05}.
+ * <p>
+ * We always use four digits, two for minutes two for seconds. In the very unlikely event
+ * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
+ */
+ private String formatAsMinutesAndSeconds(int millis) {
+ int seconds = millis / 1000;
+ int minutes = seconds / 60;
+ seconds -= minutes * 60;
+ if (minutes > 99) {
+ minutes = 99;
+ }
+ return String.format("%02d:%02d", minutes, seconds);
+ }
+
+ private AudioManager getAudioManager() {
+ return (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ /** Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */
+ private class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView {
+ @Override
+ public void finish() {
+ getActivity().finish();
+ }
+
+ @Override
+ public void runOnUiThread(Runnable runnable) {
+ getActivity().runOnUiThread(runnable);
+ }
+
+ @Override
+ public Context getDataSourceContext() {
+ return getActivity();
+ }
+
+ @Override
+ public void setRateDecreaseButtonListener(View.OnClickListener listener) {
+ mRateDecreaseButton.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setRateIncreaseButtonListener(View.OnClickListener listener) {
+ mRateIncreaseButton.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setStartStopListener(View.OnClickListener listener) {
+ mStartStopButton.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setSpeakerphoneListener(View.OnClickListener listener) {
+ mPlaybackSpeakerphone.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setRateDisplay(float rate) {
+ // TODO: This isn't being done yet. Old rate display code has been removed.
+ // Instead we're going to temporarily fade out the track position when you change
+ // rate, and display one of the words "slowest", "slower", "normal", "faster",
+ // "fastest" briefly when you change speed, before fading back in the time.
+ // At least, that's the current thinking.
+ }
+
+ @Override
+ public void setDeleteButtonListener(View.OnClickListener listener) {
+ mPlaybackTrashButton.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) {
+ mPlaybackSeek.setOnSeekBarChangeListener(listener);
+ }
+
+ @Override
+ public void playbackStarted() {
+ mStartStopButton.setImageResource(R.drawable.pause_button);
+ }
+
+ @Override
+ public void playbackStopped() {
+ mStartStopButton.setImageResource(R.drawable.play_button);
+ }
+
+ @Override
+ public void setClipLength(int clipLengthInMillis) {
+ mPlaybackSeek.setMax(clipLengthInMillis);
+ // TODO: The old code used to set the static lenght-of-clip text field, but now
+ // the thinking is that we will only show this text whilst the recording is stopped.
+ }
+
+ @Override
+ public void setClipPosition(int clipPositionInMillis) {
+ mPlaybackSeek.setProgress(clipPositionInMillis);
+ mPlaybackPositionText.setText(formatAsMinutesAndSeconds(clipPositionInMillis));
+ }
+
+ @Override
+ public int getDesiredClipPosition() {
+ return mPlaybackSeek.getProgress();
+ }
+
+ @Override
+ public void playbackError() {
+ mStartStopButton.setEnabled(false);
+ mPlaybackSeek.setProgress(0);
+ mPlaybackSeek.setEnabled(false);
+ }
+
+ @Override
+ public boolean isSpeakerPhoneOn() {
+ return getAudioManager().isSpeakerphoneOn();
+ }
+
+ @Override
+ public void setSpeakerPhoneOn(boolean on) {
+ getAudioManager().setMode(AudioManager.MODE_IN_CALL);
+ getAudioManager().setSpeakerphoneOn(on);
+ if (on) {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.speakerphone_on_button);
+ } else {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.speakerphone_off_button);
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
new file mode 100644
index 0000000..f727080
--- /dev/null
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2011 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.android.contacts.voicemail;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.SeekBar;
+
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Contains the controlling logic for a voicemail playback ui.
+ * <p>
+ * Specifically right now this class is used to control the
+ * {@link com.android.contacts.voicemail.VoicemailPlaybackFragment}.
+ * <p>
+ * This class is not thread safe. The thread policy for this class is
+ * thread-confinement, all calls into this class from outside must be done from
+ * the main ui thread.
+ */
+@NotThreadSafe
+/*package*/ class VoicemailPlaybackPresenter {
+ /** Contract describing the behaviour we need from the ui we are controlling. */
+ public interface PlaybackView {
+ Context getDataSourceContext();
+ void runOnUiThread(Runnable runnable);
+ void setStartStopListener(View.OnClickListener listener);
+ void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener);
+ void setSpeakerphoneListener(View.OnClickListener listener);
+ void setDeleteButtonListener(View.OnClickListener listener);
+ void setClipLength(int clipLengthInMillis);
+ void setClipPosition(int clipPositionInMillis);
+ int getDesiredClipPosition();
+ void playbackStarted();
+ void playbackStopped();
+ void playbackError();
+ boolean isSpeakerPhoneOn();
+ void setSpeakerPhoneOn(boolean on);
+ void finish();
+ void setRateDisplay(float rate);
+ void setRateIncreaseButtonListener(View.OnClickListener listener);
+ void setRateDecreaseButtonListener(View.OnClickListener listener);
+ }
+
+ /** Update rate for the slider, 30fps. */
+ private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
+ /**
+ * If present in the saved instance bundle, we should not resume playback on
+ * create.
+ */
+ private static final String PAUSED_STATE_KEY = VoicemailPlaybackPresenter.class.getName()
+ + ".PAUSED_STATE_KEY";
+ /**
+ * If present in the saved instance bundle, indicates where to set the
+ * playback slider.
+ */
+ private static final String CLIP_POSITION_KEY = VoicemailPlaybackPresenter.class.getName()
+ + ".CLIP_POSITION_KEY";
+
+ /** The preset variable-speed rates. Each is greater than the previous by 25%. */
+ private static final float[] PRESET_RATES = new float[] {
+ 0.64f, 0.8f, 1.0f, 1.25f, 1.5625f
+ };
+
+ /** Index into {@link #PRESET_RATES} indicating the current playback speed. */
+ private final AtomicInteger mCurrentPlaybackRate = new AtomicInteger(2);
+
+ private final PlaybackView mView;
+ private final MediaPlayerProxy mPlayer;
+ private final PositionUpdater mPositionUpdater;
+
+ /** Voicemail uri to play, will be set with a call to {@link #setVoicemailUri(Uri, boolean)}. */
+ private Uri mVoicemailUri;
+
+ public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player,
+ ScheduledExecutorService executorService) {
+ mView = view;
+ mPlayer = player;
+ mPositionUpdater = new PositionUpdater(executorService, SLIDER_UPDATE_PERIOD_MILLIS);
+ }
+
+ public void onCreate(Bundle bundle) {
+ mView.setPositionSeekListener(new PlaybackPositionListener());
+ mView.setStartStopListener(new StartStopButtonListener());
+ mView.setSpeakerphoneListener(new SpeakerphoneListener());
+ mView.setDeleteButtonListener(new DeleteButtonListener());
+ mPlayer.setOnErrorListener(new MediaPlayerErrorListener());
+ mPlayer.setOnCompletionListener(new MediaPlayerCompletionListener());
+ mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn());
+ mView.setRateDecreaseButtonListener(createRateDecreaseListener());
+ mView.setRateIncreaseButtonListener(createRateIncreaseListener());
+ mView.setClipPosition(0);
+ // TODO: Now I'm ignoring the bundle, when previously I was checking for contains against
+ // the PAUSED_STATE_KEY, and CLIP_POSITION_KEY.
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
+ if (!mPlayer.isPlaying()) {
+ outState.putBoolean(PAUSED_STATE_KEY, true);
+ }
+ }
+
+ public void onDestroy() {
+ mPlayer.release();
+ }
+
+ public void setVoicemailUri(Uri voicemailUri, boolean startPlaying) {
+ mVoicemailUri = voicemailUri;
+ if (startPlaying) {
+ resetPrepareStartPlaying(0);
+ }
+ }
+
+ private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener {
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ mView.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleError(new IllegalStateException("MediaPlayer error listener invoked"));
+ }
+ });
+ return true;
+ }
+ }
+
+ private class MediaPlayerCompletionListener implements MediaPlayer.OnCompletionListener {
+ @Override
+ public void onCompletion(final MediaPlayer mp) {
+ mView.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleCompletion(mp);
+ }
+ });
+ }
+ }
+
+ public View.OnClickListener createRateDecreaseListener() {
+ return new RateChangeListener(false);
+ }
+
+ public View.OnClickListener createRateIncreaseListener() {
+ return new RateChangeListener(true);
+ }
+
+ private class RateChangeListener implements View.OnClickListener {
+ private final boolean mIncrease;
+
+ public RateChangeListener(boolean increase) {
+ mIncrease = increase;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int adjustment = (mIncrease ? 1 : -1);
+ int andGet = mCurrentPlaybackRate.addAndGet(adjustment);
+ if (andGet < 0) {
+ // TODO: discussions with interaction design have suggested that we might make
+ // an audible tone play here to indicate that you've hit the end of the range?
+ // Let's firm up this decision.
+ mCurrentPlaybackRate.set(0);
+ } else if (andGet >= PRESET_RATES.length) {
+ mCurrentPlaybackRate.set(PRESET_RATES.length - 1);
+ } else {
+ changeRate(PRESET_RATES[andGet]);
+ }
+ }
+ }
+
+ private void resetPrepareStartPlaying(int clipPositionInMillis) {
+ try {
+ mPlayer.reset();
+ mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
+ mPlayer.prepare();
+ int clipLengthInMillis = mPlayer.getDuration();
+ mView.setClipLength(clipLengthInMillis);
+ int startPosition = Math.min(Math.max(clipPositionInMillis, 0), clipLengthInMillis);
+ mPlayer.seekTo(startPosition);
+ mPlayer.start();
+ mView.playbackStarted();
+ mPositionUpdater.startUpdating(startPosition, clipLengthInMillis);
+ } catch (IOException e) {
+ handleError(e);
+ }
+ }
+
+ private void handleError(Exception e) {
+ mView.playbackError();
+ mPlayer.release();
+ mPositionUpdater.stopUpdating();
+ }
+
+ public void handleCompletion(MediaPlayer mediaPlayer) {
+ stopPlaybackAtPosition(0);
+ }
+
+ private void stopPlaybackAtPosition(int clipPosition) {
+ mView.playbackStopped();
+ mPositionUpdater.stopUpdating();
+ mView.setClipPosition(clipPosition);
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ }
+ }
+
+ private class PlaybackPositionListener implements SeekBar.OnSeekBarChangeListener {
+ private boolean mShouldResumePlaybackAfterSeeking;
+
+ @Override
+ public void onStartTrackingTouch(SeekBar arg0) {
+ if (mPlayer.isPlaying()) {
+ mShouldResumePlaybackAfterSeeking = true;
+ stopPlaybackAtPosition(mPlayer.getCurrentPosition());
+ } else {
+ mShouldResumePlaybackAfterSeeking = false;
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar arg0) {
+ if (mPlayer.isPlaying()) {
+ stopPlaybackAtPosition(mPlayer.getCurrentPosition());
+ }
+ if (mShouldResumePlaybackAfterSeeking) {
+ resetPrepareStartPlaying(mView.getDesiredClipPosition());
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ mView.setClipPosition(seekBar.getProgress());
+ }
+ }
+
+ private void changeRate(float rate) {
+ ((SingleThreadedMediaPlayerProxy) mPlayer).setVariableSpeed(rate);
+ mView.setRateDisplay(rate);
+ }
+
+ private class SpeakerphoneListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ mView.setSpeakerPhoneOn(!mView.isSpeakerPhoneOn());
+ }
+ }
+
+ private class DeleteButtonListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ // TODO: Temporarily removed this whilst the team discuss the merits of porting
+ // the VoicemailHelper class across vs just hard-coding the delete via cursor.
+ mView.finish();
+ }
+ }
+
+ private class StartStopButtonListener implements View.OnClickListener {
+ @Override
+ public void onClick(View arg0) {
+ if (mPlayer.isPlaying()) {
+ stopPlaybackAtPosition(mPlayer.getCurrentPosition());
+ } else {
+ resetPrepareStartPlaying(mView.getDesiredClipPosition());
+ }
+ }
+ }
+
+ /**
+ * Controls the animation of the playback slider.
+ */
+ @ThreadSafe
+ private final class PositionUpdater implements Runnable {
+ private final ScheduledExecutorService mExecutorService;
+ private final int mPeriodMillis;
+ private final Object mLock = new Object();
+ @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture;
+
+ public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) {
+ mExecutorService = executorService;
+ mPeriodMillis = periodMillis;
+ }
+
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ if (mScheduledFuture != null) {
+ mView.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mView.setClipPosition(mPlayer.getCurrentPosition());
+ }
+ });
+ }
+ }
+ }
+
+ public void startUpdating(int beginPosition, int endPosition) {
+ synchronized (mLock) {
+ if (mScheduledFuture != null) {
+ mScheduledFuture.cancel(false);
+ }
+ mScheduledFuture = mExecutorService.scheduleAtFixedRate(this, 0, mPeriodMillis,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public void stopUpdating() {
+ synchronized (mLock) {
+ if (mScheduledFuture != null) {
+ mScheduledFuture.cancel(false);
+ mScheduledFuture = null;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/contacts/activities/CallLogActivityTests.java b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
index ee7f608..8900c5e 100644
--- a/tests/src/com/android/contacts/activities/CallLogActivityTests.java
+++ b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
@@ -26,6 +26,7 @@
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.telephony.PhoneNumberUtils;
@@ -69,8 +70,8 @@
/** A test value for the person id of a contact. */
private static final long TEST_PERSON_ID = 1;
- /** A test value for the photo id of a contact. */
- private static final long TEST_PHOTO_ID = 2;
+ /** A test value for the photo uri of a contact. */
+ private static final Uri TEST_THUMBNAIL_URI = Uri.parse("something://picture/2");
/** A test value for the lookup key for contacts. */
private static final String TEST_LOOKUP_KEY = "contact_id";
/** A test value for the country ISO of the phone number in the call log. */
@@ -398,7 +399,7 @@
}
contactInfo.formattedNumber = formattedNumber;
contactInfo.normalizedNumber = number;
- contactInfo.photoId = TEST_PHOTO_ID;
+ contactInfo.thumbnailUri = TEST_THUMBNAIL_URI;
contactInfo.lookupKey = TEST_LOOKUP_KEY;
mAdapter.injectContactInfoForTest(number, contactInfo);
}