diff options
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 Binary files differnew file mode 100644 index 000000000..3020e6e28 --- /dev/null +++ b/res/drawable/attachment_button.png diff --git a/res/drawable/pause_button.png b/res/drawable/pause_button.png Binary files differnew file mode 100644 index 000000000..4b2f0e734 --- /dev/null +++ b/res/drawable/pause_button.png diff --git a/res/drawable/play_button.png b/res/drawable/play_button.png Binary files differnew file mode 100644 index 000000000..ef344499e --- /dev/null +++ b/res/drawable/play_button.png diff --git a/res/drawable/seek_bar_thumb.png b/res/drawable/seek_bar_thumb.png Binary files differnew file mode 100644 index 000000000..a512ef40e --- /dev/null +++ b/res/drawable/seek_bar_thumb.png 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 Binary files differnew file mode 100644 index 000000000..ad6820b4b --- /dev/null +++ b/res/drawable/speakerphone_off_button.png diff --git a/res/drawable/speakerphone_on_button.png b/res/drawable/speakerphone_on_button.png Binary files differnew file mode 100644 index 000000000..e6deda311 --- /dev/null +++ b/res/drawable/speakerphone_on_button.png diff --git a/res/drawable/trash_button.png b/res/drawable/trash_button.png Binary files differnew file mode 100644 index 000000000..2fbb1ddc9 --- /dev/null +++ b/res/drawable/trash_button.png 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; + } + } + } + } +} |