summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk5
-rw-r--r--AndroidManifest.xml1
-rw-r--r--res/drawable/attachment_button.pngbin0 -> 1117 bytes
-rw-r--r--res/drawable/pause_button.pngbin0 -> 406 bytes
-rw-r--r--res/drawable/play_button.pngbin0 -> 564 bytes
-rw-r--r--res/drawable/seek_bar_thumb.pngbin0 -> 440 bytes
-rw-r--r--res/drawable/seekbar_drawable.xml22
-rw-r--r--res/drawable/speakerphone_off_button.pngbin0 -> 1043 bytes
-rw-r--r--res/drawable/speakerphone_on_button.pngbin0 -> 1403 bytes
-rw-r--r--res/drawable/trash_button.pngbin0 -> 683 bytes
-rw-r--r--res/layout/call_detail.xml16
-rw-r--r--res/layout/playback_layout.xml102
-rw-r--r--res/values/colors.xml13
-rw-r--r--res/values/strings.xml7
-rw-r--r--src/com/android/contacts/CallDetailActivity.java31
-rw-r--r--src/com/android/contacts/calllog/CallLogFragment.java89
-rw-r--r--src/com/android/contacts/calllog/CallLogQueryHandler.java6
-rw-r--r--src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java246
-rw-r--r--src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java345
19 files changed, 857 insertions, 26 deletions
diff --git a/Android.mk b/Android.mk
index c6bea9d49..e9e75150b 100644
--- a/Android.mk
+++ b/Android.mk
@@ -11,7 +11,10 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
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 7fb7f37e3..d2cc49250 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 000000000..3020e6e28
--- /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 000000000..4b2f0e734
--- /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 000000000..ef344499e
--- /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 000000000..a512ef40e
--- /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 000000000..2533b7f93
--- /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 000000000..ad6820b4b
--- /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 000000000..e6deda311
--- /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 000000000..2fbb1ddc9
--- /dev/null
+++ b/res/drawable/trash_button.png
Binary files differ
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
index e797f0d19..1e40964dd 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 000000000..5fee6fc65
--- /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 b92b82cac..35727e078 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 52f8394cd..534fc4e60 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1611,6 +1611,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 cce48dd92..11888a551 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -19,7 +19,9 @@ package com.android.contacts;
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 @@ public class CallDetailActivity extends ListActivity implements
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;
@@ -151,6 +157,29 @@ public class CallDetailActivity extends ListActivity implements
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);
+ }
}
/**
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index cd95c88c7..783d06b23 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -36,11 +36,15 @@ import android.content.Intent;
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;
@@ -72,8 +76,10 @@ public class CallLogFragment extends ListFragment implements ViewPagerVisibility
/** 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,
@@ -81,13 +87,16 @@ public class CallLogFragment extends ListFragment implements ViewPagerVisibility
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.
@@ -97,7 +106,7 @@ public class CallLogFragment extends ListFragment implements ViewPagerVisibility
*/
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. */
@@ -166,6 +175,49 @@ public class CallLogFragment extends ListFragment implements ViewPagerVisibility
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 {
@@ -212,18 +264,9 @@ public class CallLogFragment extends ListFragment implements ViewPagerVisibility
@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()));
}
}
@@ -672,8 +715,11 @@ public class CallLogFragment extends ListFragment implements ViewPagerVisibility
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
@@ -1022,22 +1068,25 @@ public class CallLogFragment extends ListFragment implements ViewPagerVisibility
@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 63594792b..977c84a95 100644
--- a/src/com/android/contacts/calllog/CallLogQueryHandler.java
+++ b/src/com/android/contacts/calllog/CallLogQueryHandler.java
@@ -103,7 +103,9 @@ import javax.annotation.concurrent.GuardedBy;
/** 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 @@ import javax.annotation.concurrent.GuardedBy;
fragment.onCallsFetched(combinedCursor);
}
}
-} \ No newline at end of file
+}
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
new file mode 100644
index 000000000..227b1fc12
--- /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 000000000..f72708089
--- /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;
+ }
+ }
+ }
+ }
+}