Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6faccb8a authored by Hugo Hudson's avatar Hugo Hudson
Browse files

Fetching content via intent, timeout on ui.

- Adding a new state, fetching the content.
- Set ui in fetching content state whilst we check has_content field.
- If content not available, move to make request for content state.
- If fetching content fails within a timeout, show this on the ui.
- If content is available, move to buffering state as before.
- Disable ui during buffering and fetching content states.
- Re-enable ui elements once successfully prepared.
- Add speakerphone to list of ui elements to be disabled on error.

Other:
- Makes inner fragment class static, to prevent possible NPE when
  accessing the Activity via getActivity().
- Makes use of mApplicationContext where it makes sense, rather than
  using the Activity directly.

Bug: 5059965
Bug: 5114261
Change-Id: Id2fee5e279fb02688198a1d6b602555f7a450450
parent fcd462b5
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -1635,6 +1635,12 @@
    <!-- Message to display before we have prepared the media player, i.e. before we know duration. [CHAR LIMIT=40] -->
    <string name="voicemail_buffering">buffering...</string>

    <!-- Message to display whilst we are waiting for the content to be fetched. [CHAR LIMIT=40] -->
    <string name="voicemail_fetching_content">fetching voicemail...</string>

    <!-- Message to display if we fail to get content within a suitable time period. [CHAR LIMIT=40] -->
    <string name="voicemail_fetching_timout">failed to fetch voicemail</string>

    <!-- The header in the call log used to identify missed calls and voicemail that have not yet been consumed [CHAR LIMIT=10] -->
    <string name="call_log_new_header">New</string>

+151 −28
Original line number Diff line number Diff line
@@ -19,17 +19,24 @@ package com.android.contacts.voicemail;
import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK;
import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_URI;

import com.android.common.io.MoreCloseables;
import com.android.contacts.R;
import com.android.contacts.util.BackgroundTaskService;
import com.android.ex.variablespeed.MediaPlayerProxy;
import com.android.ex.variablespeed.VariableSpeed;
import com.google.common.base.Preconditions;

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.provider.VoicemailContract;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -60,30 +67,19 @@ import javax.annotation.concurrent.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 SeekBar mPlaybackSeek;
    private ImageButton mStartStopButton;
    private ImageButton mPlaybackSpeakerphone;
    private ImageButton mRateDecreaseButton;
    private ImageButton mRateIncreaseButton;
    private TextViewWithMessagesController mTextController;
    private View mPlaybackLayout;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.playback_layout, null);
        mPlaybackSeek = (SeekBar) view.findViewById(R.id.playback_seek);
        mPlaybackSeek = (SeekBar) view.findViewById(R.id.playback_seek);
        mStartStopButton = (ImageButton) view.findViewById(R.id.playback_start_stop);
        mPlaybackSpeakerphone = (ImageButton) view.findViewById(R.id.playback_speakerphone);
        mRateDecreaseButton = (ImageButton) view.findViewById(R.id.rate_decrease_button);
        mRateIncreaseButton = (ImageButton) view.findViewById(R.id.rate_increase_button);
        mTextController = new TextViewWithMessagesController(
                (TextView) view.findViewById(R.id.playback_position_text),
                (TextView) view.findViewById(R.id.playback_speed_text));
        return view;
        mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null);
        return mPlaybackLayout;
    }

    @Override
@@ -95,7 +91,7 @@ public class VoicemailPlaybackFragment extends Fragment {
        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);
        mPresenter = new VoicemailPlaybackPresenter(new PlaybackViewImpl(),
        mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
                createMediaPlayer(mScheduledExecutorService), voicemailUri,
                mScheduledExecutorService, startPlayback, getBackgroundTaskService());
        mPresenter.onCreate(savedInstanceState);
@@ -119,6 +115,11 @@ public class VoicemailPlaybackFragment extends Fragment {
        super.onDestroy();
    }

    private PlaybackViewImpl createPlaybackViewImpl() {
        return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
                mPlaybackLayout);
    }

    private MediaPlayerProxy createMediaPlayer(ExecutorService executorService) {
        return VariableSpeed.createVariableSpeed(executorService);
    }
@@ -133,7 +134,7 @@ public class VoicemailPlaybackFragment extends Fragment {
     * We always use four digits, two for minutes two for seconds.  In the very unlikely event
     * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
     */
    private String formatAsMinutesAndSeconds(int millis) {
    private static String formatAsMinutesAndSeconds(int millis) {
        int seconds = millis / 1000;
        int minutes = seconds / 60;
        seconds -= minutes * 60;
@@ -143,25 +144,79 @@ public class VoicemailPlaybackFragment extends Fragment {
        return String.format("%02d:%02d", minutes, seconds);
    }

    private AudioManager getAudioManager() {
        return (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE);
    /**
     * 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 class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView {
    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() {
            getActivity().finish();
            Activity activity = mActivityReference.get();
            if (activity != null) {
                activity.finish();
            }
        }

        @Override
        public void runOnUiThread(Runnable runnable) {
            getActivity().runOnUiThread(runnable);
            Activity activity = mActivityReference.get();
            if (activity != null) {
                activity.runOnUiThread(runnable);
            }
        }

        @Override
        public Context getDataSourceContext() {
            return getActivity();
            return mApplicationContext;
        }

        @Override
@@ -187,7 +242,7 @@ public class VoicemailPlaybackFragment extends Fragment {
        @Override
        public void setRateDisplay(float rate, int stringResourceId) {
            mTextController.setTemporaryText(
                    getActivity().getString(stringResourceId), 1, TimeUnit.SECONDS);
                    mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS);
        }

        @Override
@@ -205,6 +260,16 @@ public class VoicemailPlaybackFragment extends Fragment {
            mStartStopButton.setImageResource(R.drawable.ic_play_holo_dark);
        }

        @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);
@@ -217,27 +282,85 @@ public class VoicemailPlaybackFragment extends Fragment {
                    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 playbackError(Exception e) {
        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();
+156 −0
Original line number Diff line number Diff line
@@ -23,12 +23,15 @@ import com.android.contacts.util.BackgroundTask;
import com.android.contacts.util.BackgroundTaskService;
import com.android.ex.variablespeed.MediaPlayerProxy;
import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
import com.google.common.base.Preconditions;

import android.content.Context;
import android.database.ContentObserver;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.SeekBar;

@@ -36,6 +39,7 @@ import java.io.IOException;
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;
@@ -73,10 +77,20 @@ import javax.annotation.concurrent.ThreadSafe;
        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);
    }

    /** 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.
@@ -102,6 +116,7 @@ import javax.annotation.concurrent.ThreadSafe;
        R.string.voicemail_speed_faster,
        R.string.voicemail_speed_fastest,
    };

    /**
     * Pointer into the {@link VoicemailPlaybackPresenter#PRESET_RATES} array.
     * <p>
@@ -131,6 +146,13 @@ import javax.annotation.concurrent.ThreadSafe;
    /** Used to run background tasks that need to interact with the ui. */
    private final BackgroundTaskService mBackgroundTaskService;

    /**
     * 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;

    public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player,
            Uri voicemailUri, ScheduledExecutorService executorService,
            boolean startPlayingImmediately, BackgroundTaskService backgroundTaskService) {
@@ -143,6 +165,126 @@ import javax.annotation.concurrent.ThreadSafe;
    }

    public void onCreate(Bundle bundle) {
        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();
        mBackgroundTaskService.submit(new BackgroundTask() {
            private boolean mHasContent = false;

            @Override
            public void doInBackground() {
                mHasContent = mView.queryHasContent(mVoicemailUri);
            }

            @Override
            public void onPostExecute() {
                if (mHasContent) {
                    postSuccessfullyFetchedContent();
                } else {
                    makeRequestForContent();
                }
            }
        }, AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * 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) {
            mBackgroundTaskService.submit(new BackgroundTask() {
                private boolean mHasContent = false;

                @Override
                public void doInBackground() {
                    mHasContent = mView.queryHasContent(mVoicemailUri);
                }

                @Override
                public void onPostExecute() {
                    if (mHasContent) {
                        if (mResultStillPending.getAndSet(false)) {
                            mView.unregisterContentObserver(FetchResultHandler.this);
                            postSuccessfullyFetchedContent();
                        }
                    }
                }
            }, AsyncTask.THREAD_POOL_EXECUTOR);
        }
    }

    /**
     * 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();
        mBackgroundTaskService.submit(new BackgroundTask() {
            private Exception mException;
@@ -169,7 +311,14 @@ import javax.annotation.concurrent.ThreadSafe;
        }, AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * 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());
@@ -194,7 +343,14 @@ import javax.annotation.concurrent.ThreadSafe;
        }
    }

    /**
     * This method should be called <b>only on the ui thread</b>.
     */
    public void onDestroy() {
        if (mFetchResultHandler != null) {
            mFetchResultHandler.destroy();
            mFetchResultHandler = null;
        }
        mPositionUpdater.stopUpdating();
        mPlayer.release();
    }