From 94b10b530c0fc297e2974e57e094c500d3ee6003 Mon Sep 17 00:00:00 2001 From: Chiao Cheng Date: Fri, 17 Aug 2012 16:59:12 -0700 Subject: Initial move of dialer features from contacts app. Bug: 6993891 Change-Id: I758ce359ca7e87a1d184303822979318be171921 --- .../voicemail/VoicemailPlaybackFragment.java | 474 ++++++++++++++++ .../voicemail/VoicemailPlaybackPresenter.java | 630 +++++++++++++++++++++ .../dialer/voicemail/VoicemailStatusHelper.java | 86 +++ .../voicemail/VoicemailStatusHelperImpl.java | 272 +++++++++ 4 files changed, 1462 insertions(+) create mode 100644 src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java create mode 100644 src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java create mode 100644 src/com/android/dialer/voicemail/VoicemailStatusHelper.java create mode 100644 src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java (limited to 'src/com/android/dialer/voicemail') 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. + *

+ * 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. + *

+ * 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}. + *

+ * 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. + *

+ * 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. + *

+ * 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: may be null. */ + 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. + *

+ * 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. + *

+ * 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. + *

+ * Specifically right now this class is used to control the + * {@link com.android.dialer.voicemail.VoicemailPlaybackFragment}. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * This variable is thread-contained, accessed only on the ui thread. + */ + private FetchResultHandler mFetchResultHandler; + private PowerManager.WakeLock mWakeLock; + private AsyncTask 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. + *

+ * 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. + *

+ * 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() { + @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. + *

+ * This method must be called on the ui thread. + *

+ * 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() { + @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. + *

+ * 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() { + @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. + *

+ * 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. + *

+ * 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() { + @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. + *

+ * 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 getStatusMessages(Cursor cursor); + + /** + * Returns the number of active voicemail sources installed. + *

+ * 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 getStatusMessages(Cursor cursor) { + List messages = + new ArrayList(); + 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 reorderMessages(List messageWrappers) { + Collections.sort(messageWrappers, new Comparator() { + @Override + public int compare(MessageStatusWithPriority msg1, MessageStatusWithPriority msg2) { + return msg1.mPriority - msg2.mPriority; + } + }); + List reorderMessages = new ArrayList(); + // 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; + } +} -- cgit v1.2.3