summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/voicemail
diff options
context:
space:
mode:
authorChiao Cheng <chiaocheng@google.com>2012-08-17 16:59:12 -0700
committerChiao Cheng <chiaocheng@google.com>2012-08-21 13:31:19 -0700
commit94b10b530c0fc297e2974e57e094c500d3ee6003 (patch)
treeb74d663c2663b5db2f6da888081648ce054480f5 /src/com/android/dialer/voicemail
parentdab5cd8890c0d0ca9001a13c2197114a4002338a (diff)
Initial move of dialer features from contacts app.
Bug: 6993891 Change-Id: I758ce359ca7e87a1d184303822979318be171921
Diffstat (limited to 'src/com/android/dialer/voicemail')
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java474
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java630
-rw-r--r--src/com/android/dialer/voicemail/VoicemailStatusHelper.java86
-rw-r--r--src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java272
4 files changed, 1462 insertions, 0 deletions
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
new file mode 100644
index 000000000..473d40bc6
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
@@ -0,0 +1,474 @@
+/*
+ * 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.dialer.voicemail;
+
+import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK;
+import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_URI;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.provider.VoicemailContract;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.android.common.io.MoreCloseables;
+import com.android.contacts.ProximitySensorAware;
+import com.android.contacts.R;
+import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.VariableSpeed;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+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 String TAG = "VoicemailPlayback";
+ private static final int NUMBER_OF_THREADS_IN_POOL = 2;
+ private static final String[] HAS_CONTENT_PROJECTION = new String[] {
+ VoicemailContract.Voicemails.HAS_CONTENT,
+ };
+
+ private VoicemailPlaybackPresenter mPresenter;
+ private ScheduledExecutorService mScheduledExecutorService;
+ private View mPlaybackLayout;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null);
+ return mPlaybackLayout;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mScheduledExecutorService = createScheduledExecutorService();
+ Bundle arguments = getArguments();
+ Preconditions.checkNotNull(arguments, "fragment must be started with arguments");
+ Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI);
+ Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI");
+ boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
+ PowerManager powerManager =
+ (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock wakeLock =
+ powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName());
+ mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
+ createMediaPlayer(mScheduledExecutorService), voicemailUri,
+ mScheduledExecutorService, startPlayback,
+ AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock);
+ 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();
+ }
+
+ @Override
+ public void onPause() {
+ mPresenter.onPause();
+ super.onPause();
+ }
+
+ private PlaybackViewImpl createPlaybackViewImpl() {
+ return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
+ mPlaybackLayout);
+ }
+
+ 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 static 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);
+ }
+
+ /**
+ * An object that can provide us with an Activity.
+ * <p>
+ * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This
+ * can happen if the Fragment is detached, for example. In that situation a call to
+ * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling
+ * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly
+ * calling a method on the result of getActivity() is dangerous too.
+ * <p>
+ * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does
+ * not have access to any Fragment methods directly. Instead it uses an application Context for
+ * things like accessing strings, accessing system services. It only uses the Activity when it
+ * absolutely needs it - and does so through this class. This makes it easy to see where we have
+ * to check for null properly.
+ */
+ private final class ActivityReference {
+ /** Gets this Fragment's Activity: <b>may be null</b>. */
+ public final Activity get() {
+ return getActivity();
+ }
+ }
+
+ /** Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */
+ private static final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView {
+ private final ActivityReference mActivityReference;
+ private final Context mApplicationContext;
+ private final SeekBar mPlaybackSeek;
+ private final ImageButton mStartStopButton;
+ private final ImageButton mPlaybackSpeakerphone;
+ private final ImageButton mRateDecreaseButton;
+ private final ImageButton mRateIncreaseButton;
+ private final TextViewWithMessagesController mTextController;
+
+ public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext,
+ View playbackLayout) {
+ Preconditions.checkNotNull(activityReference);
+ Preconditions.checkNotNull(applicationContext);
+ Preconditions.checkNotNull(playbackLayout);
+ mActivityReference = activityReference;
+ mApplicationContext = applicationContext;
+ mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek);
+ mStartStopButton = (ImageButton) playbackLayout.findViewById(
+ R.id.playback_start_stop);
+ mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById(
+ R.id.playback_speakerphone);
+ mRateDecreaseButton = (ImageButton) playbackLayout.findViewById(
+ R.id.rate_decrease_button);
+ mRateIncreaseButton = (ImageButton) playbackLayout.findViewById(
+ R.id.rate_increase_button);
+ mTextController = new TextViewWithMessagesController(
+ (TextView) playbackLayout.findViewById(R.id.playback_position_text),
+ (TextView) playbackLayout.findViewById(R.id.playback_speed_text));
+ }
+
+ @Override
+ public void finish() {
+ Activity activity = mActivityReference.get();
+ if (activity != null) {
+ activity.finish();
+ }
+ }
+
+ @Override
+ public void runOnUiThread(Runnable runnable) {
+ Activity activity = mActivityReference.get();
+ if (activity != null) {
+ activity.runOnUiThread(runnable);
+ }
+ }
+
+ @Override
+ public Context getDataSourceContext() {
+ return mApplicationContext;
+ }
+
+ @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, int stringResourceId) {
+ mTextController.setTemporaryText(
+ mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) {
+ mPlaybackSeek.setOnSeekBarChangeListener(listener);
+ }
+
+ @Override
+ public void playbackStarted() {
+ mStartStopButton.setImageResource(R.drawable.ic_hold_pause_holo_dark);
+ }
+
+ @Override
+ public void playbackStopped() {
+ mStartStopButton.setImageResource(R.drawable.ic_play);
+ }
+
+ @Override
+ public void enableProximitySensor() {
+ // Only change the state if the activity is still around.
+ Activity activity = mActivityReference.get();
+ if (activity != null && activity instanceof ProximitySensorAware) {
+ ((ProximitySensorAware) activity).enableProximitySensor();
+ }
+ }
+
+ @Override
+ public void disableProximitySensor() {
+ // Only change the state if the activity is still around.
+ Activity activity = mActivityReference.get();
+ if (activity != null && activity instanceof ProximitySensorAware) {
+ ((ProximitySensorAware) activity).disableProximitySensor(true);
+ }
+ }
+
+ @Override
+ public void registerContentObserver(Uri uri, ContentObserver observer) {
+ mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer);
+ }
+
+ @Override
+ public void unregisterContentObserver(ContentObserver observer) {
+ mApplicationContext.getContentResolver().unregisterContentObserver(observer);
+ }
+
+ @Override
+ public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) {
+ int seekBarPosition = Math.max(0, clipPositionInMillis);
+ int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis);
+ if (mPlaybackSeek.getMax() != seekBarMax) {
+ mPlaybackSeek.setMax(seekBarMax);
+ }
+ mPlaybackSeek.setProgress(seekBarPosition);
+ mTextController.setPermanentText(
+ formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
+ }
+
+ private String getString(int resId) {
+ return mApplicationContext.getString(resId);
+ }
+
+ @Override
+ public void setIsBuffering() {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_buffering));
+ }
+
+ @Override
+ public void setIsFetchingContent() {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_fetching_content));
+ }
+
+ @Override
+ public void setFetchContentTimeout() {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_fetching_timout));
+ }
+
+ @Override
+ public int getDesiredClipPosition() {
+ return mPlaybackSeek.getProgress();
+ }
+
+ @Override
+ public void disableUiElements() {
+ mRateIncreaseButton.setEnabled(false);
+ mRateDecreaseButton.setEnabled(false);
+ mStartStopButton.setEnabled(false);
+ mPlaybackSpeakerphone.setEnabled(false);
+ mPlaybackSeek.setProgress(0);
+ mPlaybackSeek.setEnabled(false);
+ }
+
+ @Override
+ public void playbackError(Exception e) {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_playback_error));
+ Log.e(TAG, "Could not play voicemail", e);
+ }
+
+ @Override
+ public void enableUiElements() {
+ mRateIncreaseButton.setEnabled(true);
+ mRateDecreaseButton.setEnabled(true);
+ mStartStopButton.setEnabled(true);
+ mPlaybackSpeakerphone.setEnabled(true);
+ mPlaybackSeek.setEnabled(true);
+ }
+
+ @Override
+ public void sendFetchVoicemailRequest(Uri voicemailUri) {
+ Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri);
+ mApplicationContext.sendBroadcast(intent);
+ }
+
+ @Override
+ public boolean queryHasContent(Uri voicemailUri) {
+ ContentResolver contentResolver = mApplicationContext.getContentResolver();
+ Cursor cursor = contentResolver.query(
+ voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
+ try {
+ if (cursor != null && cursor.moveToNext()) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(
+ VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
+ }
+ } finally {
+ MoreCloseables.closeQuietly(cursor);
+ }
+ return false;
+ }
+
+ private AudioManager getAudioManager() {
+ return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ @Override
+ public boolean isSpeakerPhoneOn() {
+ return getAudioManager().isSpeakerphoneOn();
+ }
+
+ @Override
+ public void setSpeakerPhoneOn(boolean on) {
+ getAudioManager().setSpeakerphoneOn(on);
+ if (on) {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on);
+ } else {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off);
+ }
+ }
+
+ @Override
+ public void setVolumeControlStream(int streamType) {
+ Activity activity = mActivityReference.get();
+ if (activity != null) {
+ activity.setVolumeControlStream(streamType);
+ }
+ }
+ }
+
+ /**
+ * Controls a TextView with dynamically changing text.
+ * <p>
+ * There are two methods here of interest,
+ * {@link TextViewWithMessagesController#setPermanentText(String)} and
+ * {@link TextViewWithMessagesController#setTemporaryText(String, long, TimeUnit)}. The
+ * former is used to set the text on the text view immediately, and is used in our case for
+ * the countdown of duration remaining during voicemail playback. The second is used to
+ * temporarily replace this countdown with a message, in our case faster voicemail speed or
+ * slower voicemail speed, before returning to the countdown display.
+ * <p>
+ * All the methods on this class must be called from the ui thread.
+ */
+ private static final class TextViewWithMessagesController {
+ private static final float VISIBLE = 1;
+ private static final float INVISIBLE = 0;
+ private static final long SHORT_ANIMATION_MS = 200;
+ private static final long LONG_ANIMATION_MS = 400;
+ private final Object mLock = new Object();
+ private final TextView mPermanentTextView;
+ private final TextView mTemporaryTextView;
+ @GuardedBy("mLock") private Runnable mRunnable;
+
+ public TextViewWithMessagesController(TextView permanentTextView,
+ TextView temporaryTextView) {
+ mPermanentTextView = permanentTextView;
+ mTemporaryTextView = temporaryTextView;
+ }
+
+ public void setPermanentText(String text) {
+ mPermanentTextView.setText(text);
+ }
+
+ public void setTemporaryText(String text, long duration, TimeUnit units) {
+ synchronized (mLock) {
+ mTemporaryTextView.setText(text);
+ mTemporaryTextView.animate().alpha(VISIBLE).setDuration(SHORT_ANIMATION_MS);
+ mPermanentTextView.animate().alpha(INVISIBLE).setDuration(SHORT_ANIMATION_MS);
+ mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ // We check for (mRunnable == this) becuase if not true, then another
+ // setTemporaryText call has taken place in the meantime, and this
+ // one is now defunct and needs to take no action.
+ if (mRunnable == this) {
+ mRunnable = null;
+ mTemporaryTextView.animate()
+ .alpha(INVISIBLE).setDuration(LONG_ANIMATION_MS);
+ mPermanentTextView.animate()
+ .alpha(VISIBLE).setDuration(LONG_ANIMATION_MS);
+ }
+ }
+ }
+ };
+ mTemporaryTextView.postDelayed(mRunnable, units.toMillis(duration));
+ }
+ }
+ }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
new file mode 100644
index 000000000..93b60de1d
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -0,0 +1,630 @@
+/*
+ * 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.dialer.voicemail;
+
+import static android.util.MathUtils.constrain;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.view.View;
+import android.widget.SeekBar;
+
+import com.android.contacts.R;
+import com.android.contacts.util.AsyncTaskExecutor;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+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.dialer.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
+@VisibleForTesting
+public class VoicemailPlaybackPresenter {
+ /** The stream used to playback voicemail. */
+ private static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
+
+ /** 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 setIsBuffering();
+ void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
+ int getDesiredClipPosition();
+ void playbackStarted();
+ void playbackStopped();
+ void playbackError(Exception e);
+ boolean isSpeakerPhoneOn();
+ void setSpeakerPhoneOn(boolean on);
+ void finish();
+ void setRateDisplay(float rate, int stringResourceId);
+ void setRateIncreaseButtonListener(View.OnClickListener listener);
+ void setRateDecreaseButtonListener(View.OnClickListener listener);
+ void setIsFetchingContent();
+ void disableUiElements();
+ void enableUiElements();
+ void sendFetchVoicemailRequest(Uri voicemailUri);
+ boolean queryHasContent(Uri voicemailUri);
+ void setFetchContentTimeout();
+ void registerContentObserver(Uri uri, ContentObserver observer);
+ void unregisterContentObserver(ContentObserver observer);
+ void enableProximitySensor();
+ void disableProximitySensor();
+ void setVolumeControlStream(int streamType);
+ }
+
+ /** The enumeration of {@link AsyncTask} objects we use in this class. */
+ public enum Tasks {
+ CHECK_FOR_CONTENT,
+ CHECK_CONTENT_AFTER_CHANGE,
+ PREPARE_MEDIA_PLAYER,
+ RESET_PREPARE_START_MEDIA_PLAYER,
+ }
+
+ /** Update rate for the slider, 30fps. */
+ private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
+ /** Time our ui will wait for content to be fetched before reporting not available. */
+ private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
+ /**
+ * 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
+ };
+ /** The string resource ids corresponding to the names given to the above preset rates. */
+ private static final int[] PRESET_NAMES = new int[] {
+ R.string.voicemail_speed_slowest,
+ R.string.voicemail_speed_slower,
+ R.string.voicemail_speed_normal,
+ R.string.voicemail_speed_faster,
+ R.string.voicemail_speed_fastest,
+ };
+
+ /**
+ * Pointer into the {@link VoicemailPlaybackPresenter#PRESET_RATES} array.
+ * <p>
+ * This doesn't need to be synchronized, it's used only by the {@link RateChangeListener}
+ * which in turn is only executed on the ui thread. This can't be encapsulated inside the
+ * rate change listener since multiple rate change listeners must share the same value.
+ */
+ private int mRateIndex = 2;
+
+ /**
+ * The most recently calculated duration.
+ * <p>
+ * We cache this in a field since we don't want to keep requesting it from the player, as
+ * this can easily lead to throwing {@link IllegalStateException} (any time the player is
+ * released, it's illegal to ask for the duration).
+ */
+ private final AtomicInteger mDuration = new AtomicInteger(0);
+
+ private final PlaybackView mView;
+ private final MediaPlayerProxy mPlayer;
+ private final PositionUpdater mPositionUpdater;
+
+ /** Voicemail uri to play. */
+ private final Uri mVoicemailUri;
+ /** Start playing in onCreate iff this is true. */
+ private final boolean mStartPlayingImmediately;
+ /** Used to run async tasks that need to interact with the ui. */
+ private final AsyncTaskExecutor mAsyncTaskExecutor;
+
+ /**
+ * Used to handle the result of a successful or time-out fetch result.
+ * <p>
+ * This variable is thread-contained, accessed only on the ui thread.
+ */
+ private FetchResultHandler mFetchResultHandler;
+ private PowerManager.WakeLock mWakeLock;
+ private AsyncTask<Void, ?, ?> mPrepareTask;
+
+ public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player,
+ Uri voicemailUri, ScheduledExecutorService executorService,
+ boolean startPlayingImmediately, AsyncTaskExecutor asyncTaskExecutor,
+ PowerManager.WakeLock wakeLock) {
+ mView = view;
+ mPlayer = player;
+ mVoicemailUri = voicemailUri;
+ mStartPlayingImmediately = startPlayingImmediately;
+ mAsyncTaskExecutor = asyncTaskExecutor;
+ mPositionUpdater = new PositionUpdater(executorService, SLIDER_UPDATE_PERIOD_MILLIS);
+ mWakeLock = wakeLock;
+ }
+
+ public void onCreate(Bundle bundle) {
+ mView.setVolumeControlStream(PLAYBACK_STREAM);
+ checkThatWeHaveContent();
+ }
+
+ /**
+ * Checks to see if we have content available for this voicemail.
+ * <p>
+ * This method will be called once, after the fragment has been created, before we know if the
+ * voicemail we've been asked to play has any content available.
+ * <p>
+ * This method will notify the user through the ui that we are fetching the content, then check
+ * to see if the content field in the db is set. If set, we proceed to
+ * {@link #postSuccessfullyFetchedContent()} method. If not set, we will make a request to fetch
+ * the content asynchronously via {@link #makeRequestForContent()}.
+ */
+ private void checkThatWeHaveContent() {
+ mView.setIsFetchingContent();
+ mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Void... params) {
+ return mView.queryHasContent(mVoicemailUri);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ if (hasContent) {
+ postSuccessfullyFetchedContent();
+ } else {
+ makeRequestForContent();
+ }
+ }
+ });
+ }
+
+ /**
+ * Makes a broadcast request to ask that a voicemail source fetch this content.
+ * <p>
+ * This method <b>must be called on the ui thread</b>.
+ * <p>
+ * This method will be called when we realise that we don't have content for this voicemail. It
+ * will trigger a broadcast to request that the content be downloaded. It will add a listener to
+ * the content resolver so that it will be notified when the has_content field changes. It will
+ * also set a timer. If the has_content field changes to true within the allowed time, we will
+ * proceed to {@link #postSuccessfullyFetchedContent()}. If the has_content field does not
+ * become true within the allowed time, we will update the ui to reflect the fact that content
+ * was not available.
+ */
+ private void makeRequestForContent() {
+ Handler handler = new Handler();
+ Preconditions.checkState(mFetchResultHandler == null, "mFetchResultHandler should be null");
+ mFetchResultHandler = new FetchResultHandler(handler);
+ mView.registerContentObserver(mVoicemailUri, mFetchResultHandler);
+ handler.postDelayed(mFetchResultHandler.getTimeoutRunnable(), FETCH_CONTENT_TIMEOUT_MS);
+ mView.sendFetchVoicemailRequest(mVoicemailUri);
+ }
+
+ @ThreadSafe
+ private class FetchResultHandler extends ContentObserver implements Runnable {
+ private AtomicBoolean mResultStillPending = new AtomicBoolean(true);
+ private final Handler mHandler;
+
+ public FetchResultHandler(Handler handler) {
+ super(handler);
+ mHandler = handler;
+ }
+
+ public Runnable getTimeoutRunnable() {
+ return this;
+ }
+
+ @Override
+ public void run() {
+ if (mResultStillPending.getAndSet(false)) {
+ mView.unregisterContentObserver(FetchResultHandler.this);
+ mView.setFetchContentTimeout();
+ }
+ }
+
+ public void destroy() {
+ if (mResultStillPending.getAndSet(false)) {
+ mView.unregisterContentObserver(FetchResultHandler.this);
+ mHandler.removeCallbacks(this);
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Void... params) {
+ return mView.queryHasContent(mVoicemailUri);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ if (hasContent) {
+ if (mResultStillPending.getAndSet(false)) {
+ mView.unregisterContentObserver(FetchResultHandler.this);
+ postSuccessfullyFetchedContent();
+ }
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Prepares the voicemail content for playback.
+ * <p>
+ * This method will be called once we know that our voicemail has content (according to the
+ * content provider). This method will try to prepare the data source through the media player.
+ * If preparing the media player works, we will call through to
+ * {@link #postSuccessfulPrepareActions()}. If preparing the media player fails (perhaps the
+ * file the content provider points to is actually missing, perhaps it is of an unknown file
+ * format that we can't play, who knows) then we will show an error on the ui.
+ */
+ private void postSuccessfullyFetchedContent() {
+ mView.setIsBuffering();
+ mAsyncTaskExecutor.submit(Tasks.PREPARE_MEDIA_PLAYER,
+ new AsyncTask<Void, Void, Exception>() {
+ @Override
+ public Exception doInBackground(Void... params) {
+ try {
+ mPlayer.reset();
+ mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
+ mPlayer.setAudioStreamType(PLAYBACK_STREAM);
+ mPlayer.prepare();
+ return null;
+ } catch (Exception e) {
+ return e;
+ }
+ }
+
+ @Override
+ public void onPostExecute(Exception exception) {
+ if (exception == null) {
+ postSuccessfulPrepareActions();
+ } else {
+ mView.playbackError(exception);
+ }
+ }
+ });
+ }
+
+ /**
+ * Enables the ui, and optionally starts playback immediately.
+ * <p>
+ * This will be called once we have successfully prepared the media player, and will optionally
+ * playback immediately.
+ */
+ private void postSuccessfulPrepareActions() {
+ mView.enableUiElements();
+ mView.setPositionSeekListener(new PlaybackPositionListener());
+ mView.setStartStopListener(new StartStopButtonListener());
+ mView.setSpeakerphoneListener(new SpeakerphoneListener());
+ mPlayer.setOnErrorListener(new MediaPlayerErrorListener());
+ mPlayer.setOnCompletionListener(new MediaPlayerCompletionListener());
+ mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn());
+ mView.setRateDecreaseButtonListener(createRateDecreaseListener());
+ mView.setRateIncreaseButtonListener(createRateIncreaseListener());
+ mView.setClipPosition(0, mPlayer.getDuration());
+ mView.playbackStopped();
+ // Always disable on stop.
+ mView.disableProximitySensor();
+ if (mStartPlayingImmediately) {
+ resetPrepareStartPlaying(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();
+ if (mFetchResultHandler != null) {
+ mFetchResultHandler.destroy();
+ mFetchResultHandler = null;
+ }
+ mPositionUpdater.stopUpdating();
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+
+ 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);
+ }
+
+ /**
+ * Listens to clicks on the rate increase and decrease buttons.
+ * <p>
+ * This class is not thread-safe, but all interactions with it will happen on the ui thread.
+ */
+ private class RateChangeListener implements View.OnClickListener {
+ private final boolean mIncrease;
+
+ public RateChangeListener(boolean increase) {
+ mIncrease = increase;
+ }
+
+ @Override
+ public void onClick(View v) {
+ // Adjust the current rate, then clamp it to the allowed values.
+ mRateIndex = constrain(mRateIndex + (mIncrease ? 1 : -1), 0, PRESET_RATES.length - 1);
+ // Whether or not we have actually changed the index, call changeRate().
+ // This will ensure that we show the "fastest" or "slowest" text on the ui to indicate
+ // to the user that it doesn't get any faster or slower.
+ changeRate(PRESET_RATES[mRateIndex], PRESET_NAMES[mRateIndex]);
+ }
+ }
+
+ private void resetPrepareStartPlaying(final int clipPositionInMillis) {
+ if (mPrepareTask != null) {
+ mPrepareTask.cancel(false);
+ }
+ mPrepareTask = mAsyncTaskExecutor.submit(Tasks.RESET_PREPARE_START_MEDIA_PLAYER,
+ new AsyncTask<Void, Void, Exception>() {
+ @Override
+ public Exception doInBackground(Void... params) {
+ try {
+ mPlayer.reset();
+ mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
+ mPlayer.setAudioStreamType(PLAYBACK_STREAM);
+ mPlayer.prepare();
+ return null;
+ } catch (Exception e) {
+ return e;
+ }
+ }
+
+ @Override
+ public void onPostExecute(Exception exception) {
+ mPrepareTask = null;
+ if (exception == null) {
+ mDuration.set(mPlayer.getDuration());
+ int startPosition =
+ constrain(clipPositionInMillis, 0, mDuration.get());
+ mView.setClipPosition(startPosition, mDuration.get());
+ mPlayer.seekTo(startPosition);
+ mPlayer.start();
+ mView.playbackStarted();
+ if (!mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ }
+ // Only enable if we are not currently using the speaker phone.
+ if (!mView.isSpeakerPhoneOn()) {
+ mView.enableProximitySensor();
+ }
+ mPositionUpdater.startUpdating(startPosition, mDuration.get());
+ } else {
+ handleError(exception);
+ }
+ }
+ });
+ }
+
+ private void handleError(Exception e) {
+ mView.playbackError(e);
+ mPositionUpdater.stopUpdating();
+ mPlayer.release();
+ }
+
+ public void handleCompletion(MediaPlayer mediaPlayer) {
+ stopPlaybackAtPosition(0, mDuration.get());
+ }
+
+ private void stopPlaybackAtPosition(int clipPosition, int duration) {
+ mPositionUpdater.stopUpdating();
+ mView.playbackStopped();
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ // Always disable on stop.
+ mView.disableProximitySensor();
+ mView.setClipPosition(clipPosition, duration);
+ 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(), mDuration.get());
+ } else {
+ mShouldResumePlaybackAfterSeeking = false;
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar arg0) {
+ if (mPlayer.isPlaying()) {
+ stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+ }
+ if (mShouldResumePlaybackAfterSeeking) {
+ resetPrepareStartPlaying(mView.getDesiredClipPosition());
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ mView.setClipPosition(seekBar.getProgress(), seekBar.getMax());
+ }
+ }
+
+ private void changeRate(float rate, int stringResourceId) {
+ ((SingleThreadedMediaPlayerProxy) mPlayer).setVariableSpeed(rate);
+ mView.setRateDisplay(rate, stringResourceId);
+ }
+
+ private class SpeakerphoneListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ boolean previousState = mView.isSpeakerPhoneOn();
+ mView.setSpeakerPhoneOn(!previousState);
+ if (mPlayer.isPlaying() && previousState) {
+ // If we are currently playing and we are disabling the speaker phone, enable the
+ // sensor.
+ mView.enableProximitySensor();
+ } else {
+ // If we are not currently playing, disable the sensor.
+ mView.disableProximitySensor();
+ }
+ }
+ }
+
+ private class StartStopButtonListener implements View.OnClickListener {
+ @Override
+ public void onClick(View arg0) {
+ if (mPlayer.isPlaying()) {
+ stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+ } 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;
+ private final Runnable mSetClipPostitionRunnable = new Runnable() {
+ @Override
+ public void run() {
+ int currentPosition = 0;
+ synchronized (mLock) {
+ if (mScheduledFuture == null) {
+ // This task has been canceled. Just stop now.
+ return;
+ }
+ currentPosition = mPlayer.getCurrentPosition();
+ }
+ mView.setClipPosition(currentPosition, mDuration.get());
+ }
+ };
+
+ public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) {
+ mExecutorService = executorService;
+ mPeriodMillis = periodMillis;
+ }
+
+ @Override
+ public void run() {
+ mView.runOnUiThread(mSetClipPostitionRunnable);
+ }
+
+ 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;
+ }
+ }
+ }
+ }
+
+ public void onPause() {
+ if (mPlayer.isPlaying()) {
+ stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+ }
+ if (mPrepareTask != null) {
+ mPrepareTask.cancel(false);
+ }
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailStatusHelper.java b/src/com/android/dialer/voicemail/VoicemailStatusHelper.java
new file mode 100644
index 000000000..545691ec9
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailStatusHelper.java
@@ -0,0 +1,86 @@
+/*
+ * 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.dialer.voicemail;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+
+import java.util.List;
+
+/**
+ * Interface used by the call log UI to determine what user message, if any, related to voicemail
+ * source status needs to be shown. The messages are returned in the order of importance.
+ * <p>
+ * The implementation of this interface interacts with the voicemail content provider to fetch
+ * statuses of all the registered voicemail sources and determines if any status message needs to
+ * be shown. The user of this interface must observe/listen to provider changes and invoke
+ * this class to check if any message needs to be shown.
+ */
+public interface VoicemailStatusHelper {
+ public class StatusMessage {
+ /** Package of the source on behalf of which this message has to be shown.*/
+ public final String sourcePackage;
+ /**
+ * The string resource id of the status message that should be shown in the call log
+ * page. Set to -1, if this message is not to be shown in call log.
+ */
+ public final int callLogMessageId;
+ /**
+ * The string resource id of the status message that should be shown in the call details
+ * page. Set to -1, if this message is not to be shown in call details page.
+ */
+ public final int callDetailsMessageId;
+ /** The string resource id of the action message that should be shown. */
+ public final int actionMessageId;
+ /** URI for the corrective action, where applicable. Null if no action URI is available. */
+ public final Uri actionUri;
+ public StatusMessage(String sourcePackage, int callLogMessageId, int callDetailsMessageId,
+ int actionMessageId, Uri actionUri) {
+ this.sourcePackage = sourcePackage;
+ this.callLogMessageId = callLogMessageId;
+ this.callDetailsMessageId = callDetailsMessageId;
+ this.actionMessageId = actionMessageId;
+ this.actionUri = actionUri;
+ }
+
+ /** Whether this message should be shown in the call log page. */
+ public boolean showInCallLog() {
+ return callLogMessageId != -1;
+ }
+
+ /** Whether this message should be shown in the call details page. */
+ public boolean showInCallDetails() {
+ return callDetailsMessageId != -1;
+ }
+ }
+
+ /**
+ * Returns a list of messages, in the order or priority that should be shown to the user. An
+ * empty list is returned if no message needs to be shown.
+ * @param cursor The cursor pointing to the query on {@link Status#CONTENT_URI}. The projection
+ * to be used is defined by the implementation class of this interface.
+ */
+ public List<StatusMessage> getStatusMessages(Cursor cursor);
+
+ /**
+ * Returns the number of active voicemail sources installed.
+ * <p>
+ * The number of sources is counted by querying the voicemail status table.
+ */
+ public int getNumberActivityVoicemailSources(Cursor cursor);
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java b/src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java
new file mode 100644
index 000000000..3a08e2bff
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java
@@ -0,0 +1,272 @@
+/*
+ * 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.dialer.voicemail;
+
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_CAN_BE_CONFIGURED;
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_OK;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_OK;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_OK;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+
+import com.android.contacts.R;
+import com.android.contacts.util.UriUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/** Implementation of {@link VoicemailStatusHelper}. */
+public class VoicemailStatusHelperImpl implements VoicemailStatusHelper {
+ private static final int SOURCE_PACKAGE_INDEX = 0;
+ private static final int CONFIGURATION_STATE_INDEX = 1;
+ private static final int DATA_CHANNEL_STATE_INDEX = 2;
+ private static final int NOTIFICATION_CHANNEL_STATE_INDEX = 3;
+ private static final int SETTINGS_URI_INDEX = 4;
+ private static final int VOICEMAIL_ACCESS_URI_INDEX = 5;
+ private static final int NUM_COLUMNS = 6;
+ /** Projection on the voicemail_status table used by this class. */
+ public static final String[] PROJECTION = new String[NUM_COLUMNS];
+ static {
+ PROJECTION[SOURCE_PACKAGE_INDEX] = Status.SOURCE_PACKAGE;
+ PROJECTION[CONFIGURATION_STATE_INDEX] = Status.CONFIGURATION_STATE;
+ PROJECTION[DATA_CHANNEL_STATE_INDEX] = Status.DATA_CHANNEL_STATE;
+ PROJECTION[NOTIFICATION_CHANNEL_STATE_INDEX] = Status.NOTIFICATION_CHANNEL_STATE;
+ PROJECTION[SETTINGS_URI_INDEX] = Status.SETTINGS_URI;
+ PROJECTION[VOICEMAIL_ACCESS_URI_INDEX] = Status.VOICEMAIL_ACCESS_URI;
+ }
+
+ /** Possible user actions. */
+ public static enum Action {
+ NONE(-1),
+ CALL_VOICEMAIL(R.string.voicemail_status_action_call_server),
+ CONFIGURE_VOICEMAIL(R.string.voicemail_status_action_configure);
+
+ private final int mMessageId;
+ private Action(int messageId) {
+ mMessageId = messageId;
+ }
+
+ public int getMessageId() {
+ return mMessageId;
+ }
+ }
+
+ /**
+ * Overall state of the source status. Each state is associated with the corresponding display
+ * string and the corrective action. The states are also assigned a relative priority which is
+ * used to order the messages from different sources.
+ */
+ private static enum OverallState {
+ // TODO: Add separate string for call details and call log pages for the states that needs
+ // to be shown in both.
+ /** Both notification and data channel are not working. */
+ NO_CONNECTION(0, Action.CALL_VOICEMAIL, R.string.voicemail_status_voicemail_not_available,
+ R.string.voicemail_status_audio_not_available),
+ /** Notifications working, but data channel is not working. Audio cannot be downloaded. */
+ NO_DATA(1, Action.CALL_VOICEMAIL, R.string.voicemail_status_voicemail_not_available,
+ R.string.voicemail_status_audio_not_available),
+ /** Messages are known to be waiting but data channel is not working. */
+ MESSAGE_WAITING(2, Action.CALL_VOICEMAIL, R.string.voicemail_status_messages_waiting,
+ R.string.voicemail_status_audio_not_available),
+ /** Notification channel not working, but data channel is. */
+ NO_NOTIFICATIONS(3, Action.CALL_VOICEMAIL,
+ R.string.voicemail_status_voicemail_not_available),
+ /** Invite user to set up voicemail. */
+ INVITE_FOR_CONFIGURATION(4, Action.CONFIGURE_VOICEMAIL,
+ R.string.voicemail_status_configure_voicemail),
+ /**
+ * No detailed notifications, but data channel is working.
+ * This is normal mode of operation for certain sources. No action needed.
+ */
+ NO_DETAILED_NOTIFICATION(5, Action.NONE, -1),
+ /** Visual voicemail not yet set up. No local action needed. */
+ NOT_CONFIGURED(6, Action.NONE, -1),
+ /** Everything is OK. */
+ OK(7, Action.NONE, -1),
+ /** If one or more state value set by the source is not valid. */
+ INVALID(8, Action.NONE, -1);
+
+ private final int mPriority;
+ private final Action mAction;
+ private final int mCallLogMessageId;
+ private final int mCallDetailsMessageId;
+
+ private OverallState(int priority, Action action, int callLogMessageId) {
+ this(priority, action, callLogMessageId, -1);
+ }
+
+ private OverallState(int priority, Action action, int callLogMessageId,
+ int callDetailsMessageId) {
+ mPriority = priority;
+ mAction = action;
+ mCallLogMessageId = callLogMessageId;
+ mCallDetailsMessageId = callDetailsMessageId;
+ }
+
+ public Action getAction() {
+ return mAction;
+ }
+
+ public int getPriority() {
+ return mPriority;
+ }
+
+ public int getCallLogMessageId() {
+ return mCallLogMessageId;
+ }
+
+ public int getCallDetailsMessageId() {
+ return mCallDetailsMessageId;
+ }
+ }
+
+ /** A wrapper on {@link StatusMessage} which additionally stores the priority of the message. */
+ private static class MessageStatusWithPriority {
+ private final StatusMessage mMessage;
+ private final int mPriority;
+
+ public MessageStatusWithPriority(StatusMessage message, int priority) {
+ mMessage = message;
+ mPriority = priority;
+ }
+ }
+
+ @Override
+ public List<StatusMessage> getStatusMessages(Cursor cursor) {
+ List<MessageStatusWithPriority> messages =
+ new ArrayList<VoicemailStatusHelperImpl.MessageStatusWithPriority>();
+ cursor.moveToPosition(-1);
+ while(cursor.moveToNext()) {
+ MessageStatusWithPriority message = getMessageForStatusEntry(cursor);
+ if (message != null) {
+ messages.add(message);
+ }
+ }
+ // Finally reorder the messages by their priority.
+ return reorderMessages(messages);
+ }
+
+ @Override
+ public int getNumberActivityVoicemailSources(Cursor cursor) {
+ int count = 0;
+ cursor.moveToPosition(-1);
+ while(cursor.moveToNext()) {
+ if (isVoicemailSourceActive(cursor)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ /** Returns whether the source status in the cursor corresponds to an active source. */
+ private boolean isVoicemailSourceActive(Cursor cursor) {
+ return cursor.getString(SOURCE_PACKAGE_INDEX) != null
+ && cursor.getInt(CONFIGURATION_STATE_INDEX) == Status.CONFIGURATION_STATE_OK;
+ }
+
+ private List<StatusMessage> reorderMessages(List<MessageStatusWithPriority> messageWrappers) {
+ Collections.sort(messageWrappers, new Comparator<MessageStatusWithPriority>() {
+ @Override
+ public int compare(MessageStatusWithPriority msg1, MessageStatusWithPriority msg2) {
+ return msg1.mPriority - msg2.mPriority;
+ }
+ });
+ List<StatusMessage> reorderMessages = new ArrayList<VoicemailStatusHelper.StatusMessage>();
+ // Copy the ordered message objects into the final list.
+ for (MessageStatusWithPriority messageWrapper : messageWrappers) {
+ reorderMessages.add(messageWrapper.mMessage);
+ }
+ return reorderMessages;
+ }
+
+ /**
+ * Returns the message for the status entry pointed to by the cursor.
+ */
+ private MessageStatusWithPriority getMessageForStatusEntry(Cursor cursor) {
+ final String sourcePackage = cursor.getString(SOURCE_PACKAGE_INDEX);
+ if (sourcePackage == null) {
+ return null;
+ }
+ final OverallState overallState = getOverallState(cursor.getInt(CONFIGURATION_STATE_INDEX),
+ cursor.getInt(DATA_CHANNEL_STATE_INDEX),
+ cursor.getInt(NOTIFICATION_CHANNEL_STATE_INDEX));
+ final Action action = overallState.getAction();
+
+ // No source package or no action, means no message shown.
+ if (action == Action.NONE) {
+ return null;
+ }
+
+ Uri actionUri = null;
+ if (action == Action.CALL_VOICEMAIL) {
+ actionUri = UriUtils.parseUriOrNull(cursor.getString(VOICEMAIL_ACCESS_URI_INDEX));
+ // Even if actionUri is null, it is still be useful to show the notification.
+ } else if (action == Action.CONFIGURE_VOICEMAIL) {
+ actionUri = UriUtils.parseUriOrNull(cursor.getString(SETTINGS_URI_INDEX));
+ // If there is no settings URI, there is no point in showing the notification.
+ if (actionUri == null) {
+ return null;
+ }
+ }
+ return new MessageStatusWithPriority(
+ new StatusMessage(sourcePackage, overallState.getCallLogMessageId(),
+ overallState.getCallDetailsMessageId(), action.getMessageId(),
+ actionUri),
+ overallState.getPriority());
+ }
+
+ private OverallState getOverallState(int configurationState, int dataChannelState,
+ int notificationChannelState) {
+ if (configurationState == CONFIGURATION_STATE_OK) {
+ // Voicemail is configured. Let's see how is the data channel.
+ if (dataChannelState == DATA_CHANNEL_STATE_OK) {
+ // Data channel is fine. What about notification channel?
+ if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+ return OverallState.OK;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+ return OverallState.NO_DETAILED_NOTIFICATION;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+ return OverallState.NO_NOTIFICATIONS;
+ }
+ } else if (dataChannelState == DATA_CHANNEL_STATE_NO_CONNECTION) {
+ // Data channel is not working. What about notification channel?
+ if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+ return OverallState.NO_DATA;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+ return OverallState.MESSAGE_WAITING;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+ return OverallState.NO_CONNECTION;
+ }
+ }
+ } else if (configurationState == CONFIGURATION_STATE_CAN_BE_CONFIGURED) {
+ // Voicemail not configured. data/notification channel states are irrelevant.
+ return OverallState.INVITE_FOR_CONFIGURATION;
+ } else if (configurationState == Status.CONFIGURATION_STATE_NOT_CONFIGURED) {
+ // Voicemail not configured. data/notification channel states are irrelevant.
+ return OverallState.NOT_CONFIGURED;
+ }
+ // Will reach here only if the source has set an invalid value.
+ return OverallState.INVALID;
+ }
+}