• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dialer.voicemail;
18 
19 import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK;
20 import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_URI;
21 
22 import android.app.Activity;
23 import android.app.Fragment;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.database.ContentObserver;
28 import android.database.Cursor;
29 import android.media.AudioManager;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.os.PowerManager;
33 import android.provider.VoicemailContract;
34 import android.util.Log;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.ImageButton;
39 import android.widget.SeekBar;
40 import android.widget.TextView;
41 
42 import com.android.common.io.MoreCloseables;
43 import com.android.dialer.ProximitySensorAware;
44 import com.android.dialer.R;
45 import com.android.dialer.util.AsyncTaskExecutors;
46 import com.android.ex.variablespeed.MediaPlayerProxy;
47 import com.android.ex.variablespeed.VariableSpeed;
48 import com.google.common.base.Preconditions;
49 
50 import java.util.concurrent.ExecutorService;
51 import java.util.concurrent.Executors;
52 import java.util.concurrent.ScheduledExecutorService;
53 import java.util.concurrent.TimeUnit;
54 
55 import javax.annotation.concurrent.GuardedBy;
56 import javax.annotation.concurrent.NotThreadSafe;
57 
58 /**
59  * Displays and plays back a single voicemail.
60  * <p>
61  * When the Activity containing this Fragment is created, voicemail playback
62  * will begin immediately. The Activity is expected to be started via an intent
63  * containing a suitable voicemail uri to playback.
64  * <p>
65  * This class is not thread-safe, it is thread-confined. All calls to all public
66  * methods on this class are expected to come from the main ui thread.
67  */
68 @NotThreadSafe
69 public class VoicemailPlaybackFragment extends Fragment {
70     private static final String TAG = "VoicemailPlayback";
71     private static final int NUMBER_OF_THREADS_IN_POOL = 2;
72     private static final String[] HAS_CONTENT_PROJECTION = new String[] {
73         VoicemailContract.Voicemails.HAS_CONTENT,
74     };
75 
76     private VoicemailPlaybackPresenter mPresenter;
77     private ScheduledExecutorService mScheduledExecutorService;
78     private View mPlaybackLayout;
79 
80     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)81     public View onCreateView(LayoutInflater inflater, ViewGroup container,
82             Bundle savedInstanceState) {
83         mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null);
84         return mPlaybackLayout;
85     }
86 
87     @Override
onActivityCreated(Bundle savedInstanceState)88     public void onActivityCreated(Bundle savedInstanceState) {
89         super.onActivityCreated(savedInstanceState);
90         mScheduledExecutorService = createScheduledExecutorService();
91         Bundle arguments = getArguments();
92         Preconditions.checkNotNull(arguments, "fragment must be started with arguments");
93         Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI);
94         Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI");
95         boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
96         PowerManager powerManager =
97                 (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
98         PowerManager.WakeLock wakeLock =
99                 powerManager.newWakeLock(
100                         PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName());
101         mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
102                 createMediaPlayer(mScheduledExecutorService), voicemailUri,
103                 mScheduledExecutorService, startPlayback,
104                 AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock);
105         mPresenter.onCreate(savedInstanceState);
106     }
107 
108     @Override
onSaveInstanceState(Bundle outState)109     public void onSaveInstanceState(Bundle outState) {
110         mPresenter.onSaveInstanceState(outState);
111         super.onSaveInstanceState(outState);
112     }
113 
114     @Override
onDestroy()115     public void onDestroy() {
116         mPresenter.onDestroy();
117         mScheduledExecutorService.shutdown();
118         super.onDestroy();
119     }
120 
121     @Override
onPause()122     public void onPause() {
123         mPresenter.onPause();
124         super.onPause();
125     }
126 
createPlaybackViewImpl()127     private PlaybackViewImpl createPlaybackViewImpl() {
128         return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
129                 mPlaybackLayout);
130     }
131 
createMediaPlayer(ExecutorService executorService)132     private MediaPlayerProxy createMediaPlayer(ExecutorService executorService) {
133         return VariableSpeed.createVariableSpeed(executorService);
134     }
135 
createScheduledExecutorService()136     private ScheduledExecutorService createScheduledExecutorService() {
137         return Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
138     }
139 
140     /**
141      * Formats a number of milliseconds as something that looks like {@code 00:05}.
142      * <p>
143      * We always use four digits, two for minutes two for seconds.  In the very unlikely event
144      * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
145      */
formatAsMinutesAndSeconds(int millis)146     private static String formatAsMinutesAndSeconds(int millis) {
147         int seconds = millis / 1000;
148         int minutes = seconds / 60;
149         seconds -= minutes * 60;
150         if (minutes > 99) {
151             minutes = 99;
152         }
153         return String.format("%02d:%02d", minutes, seconds);
154     }
155 
156     /**
157      * An object that can provide us with an Activity.
158      * <p>
159      * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This
160      * can happen if the Fragment is detached, for example. In that situation a call to
161      * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling
162      * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly
163      * calling a method on the result of getActivity() is dangerous too.
164      * <p>
165      * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does
166      * not have access to any Fragment methods directly. Instead it uses an application Context for
167      * things like accessing strings, accessing system services. It only uses the Activity when it
168      * absolutely needs it - and does so through this class. This makes it easy to see where we have
169      * to check for null properly.
170      */
171     private final class ActivityReference {
172         /** Gets this Fragment's Activity: <b>may be null</b>. */
get()173         public final Activity get() {
174             return getActivity();
175         }
176     }
177 
178     /**  Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */
179     private static final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView {
180         private final ActivityReference mActivityReference;
181         private final Context mApplicationContext;
182         private final SeekBar mPlaybackSeek;
183         private final ImageButton mStartStopButton;
184         private final ImageButton mPlaybackSpeakerphone;
185         private final ImageButton mRateDecreaseButton;
186         private final ImageButton mRateIncreaseButton;
187         private final TextViewWithMessagesController mTextController;
188 
PlaybackViewImpl(ActivityReference activityReference, Context applicationContext, View playbackLayout)189         public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext,
190                 View playbackLayout) {
191             Preconditions.checkNotNull(activityReference);
192             Preconditions.checkNotNull(applicationContext);
193             Preconditions.checkNotNull(playbackLayout);
194             mActivityReference = activityReference;
195             mApplicationContext = applicationContext;
196             mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek);
197             mStartStopButton = (ImageButton) playbackLayout.findViewById(
198                     R.id.playback_start_stop);
199             mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById(
200                     R.id.playback_speakerphone);
201             mRateDecreaseButton = (ImageButton) playbackLayout.findViewById(
202                     R.id.rate_decrease_button);
203             mRateIncreaseButton = (ImageButton) playbackLayout.findViewById(
204                     R.id.rate_increase_button);
205             mTextController = new TextViewWithMessagesController(
206                     (TextView) playbackLayout.findViewById(R.id.playback_position_text),
207                     (TextView) playbackLayout.findViewById(R.id.playback_speed_text));
208         }
209 
210         @Override
finish()211         public void finish() {
212             Activity activity = mActivityReference.get();
213             if (activity != null) {
214                 activity.finish();
215             }
216         }
217 
218         @Override
runOnUiThread(Runnable runnable)219         public void runOnUiThread(Runnable runnable) {
220             Activity activity = mActivityReference.get();
221             if (activity != null) {
222                 activity.runOnUiThread(runnable);
223             }
224         }
225 
226         @Override
getDataSourceContext()227         public Context getDataSourceContext() {
228             return mApplicationContext;
229         }
230 
231         @Override
setRateDecreaseButtonListener(View.OnClickListener listener)232         public void setRateDecreaseButtonListener(View.OnClickListener listener) {
233             mRateDecreaseButton.setOnClickListener(listener);
234         }
235 
236         @Override
setRateIncreaseButtonListener(View.OnClickListener listener)237         public void setRateIncreaseButtonListener(View.OnClickListener listener) {
238             mRateIncreaseButton.setOnClickListener(listener);
239         }
240 
241         @Override
setStartStopListener(View.OnClickListener listener)242         public void setStartStopListener(View.OnClickListener listener) {
243             mStartStopButton.setOnClickListener(listener);
244         }
245 
246         @Override
setSpeakerphoneListener(View.OnClickListener listener)247         public void setSpeakerphoneListener(View.OnClickListener listener) {
248             mPlaybackSpeakerphone.setOnClickListener(listener);
249         }
250 
251         @Override
setRateDisplay(float rate, int stringResourceId)252         public void setRateDisplay(float rate, int stringResourceId) {
253             mTextController.setTemporaryText(
254                     mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS);
255         }
256 
257         @Override
setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener)258         public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) {
259             mPlaybackSeek.setOnSeekBarChangeListener(listener);
260         }
261 
262         @Override
playbackStarted()263         public void playbackStarted() {
264             mStartStopButton.setImageResource(R.drawable.ic_hold_pause_holo_dark);
265         }
266 
267         @Override
playbackStopped()268         public void playbackStopped() {
269             mStartStopButton.setImageResource(R.drawable.ic_play);
270         }
271 
272         @Override
enableProximitySensor()273         public void enableProximitySensor() {
274             // Only change the state if the activity is still around.
275             Activity activity = mActivityReference.get();
276             if (activity != null && activity instanceof ProximitySensorAware) {
277                 ((ProximitySensorAware) activity).enableProximitySensor();
278             }
279         }
280 
281         @Override
disableProximitySensor()282         public void disableProximitySensor() {
283             // Only change the state if the activity is still around.
284             Activity activity = mActivityReference.get();
285             if (activity != null && activity instanceof ProximitySensorAware) {
286                 ((ProximitySensorAware) activity).disableProximitySensor(true);
287             }
288         }
289 
290         @Override
registerContentObserver(Uri uri, ContentObserver observer)291         public void registerContentObserver(Uri uri, ContentObserver observer) {
292             mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer);
293         }
294 
295         @Override
unregisterContentObserver(ContentObserver observer)296         public void unregisterContentObserver(ContentObserver observer) {
297             mApplicationContext.getContentResolver().unregisterContentObserver(observer);
298         }
299 
300         @Override
setClipPosition(int clipPositionInMillis, int clipLengthInMillis)301         public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) {
302             int seekBarPosition = Math.max(0, clipPositionInMillis);
303             int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis);
304             if (mPlaybackSeek.getMax() != seekBarMax) {
305                 mPlaybackSeek.setMax(seekBarMax);
306             }
307             mPlaybackSeek.setProgress(seekBarPosition);
308             mTextController.setPermanentText(
309                     formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
310         }
311 
getString(int resId)312         private String getString(int resId) {
313             return mApplicationContext.getString(resId);
314         }
315 
316         @Override
setIsBuffering()317         public void setIsBuffering() {
318             disableUiElements();
319             mTextController.setPermanentText(getString(R.string.voicemail_buffering));
320         }
321 
322         @Override
setIsFetchingContent()323         public void setIsFetchingContent() {
324             disableUiElements();
325             mTextController.setPermanentText(getString(R.string.voicemail_fetching_content));
326         }
327 
328         @Override
setFetchContentTimeout()329         public void setFetchContentTimeout() {
330             disableUiElements();
331             mTextController.setPermanentText(getString(R.string.voicemail_fetching_timout));
332         }
333 
334         @Override
getDesiredClipPosition()335         public int getDesiredClipPosition() {
336             return mPlaybackSeek.getProgress();
337         }
338 
339         @Override
disableUiElements()340         public void disableUiElements() {
341             mRateIncreaseButton.setEnabled(false);
342             mRateDecreaseButton.setEnabled(false);
343             mStartStopButton.setEnabled(false);
344             mPlaybackSpeakerphone.setEnabled(false);
345             mPlaybackSeek.setProgress(0);
346             mPlaybackSeek.setEnabled(false);
347         }
348 
349         @Override
playbackError(Exception e)350         public void playbackError(Exception e) {
351             disableUiElements();
352             mTextController.setPermanentText(getString(R.string.voicemail_playback_error));
353             Log.e(TAG, "Could not play voicemail", e);
354         }
355 
356         @Override
enableUiElements()357         public void enableUiElements() {
358             mRateIncreaseButton.setEnabled(true);
359             mRateDecreaseButton.setEnabled(true);
360             mStartStopButton.setEnabled(true);
361             mPlaybackSpeakerphone.setEnabled(true);
362             mPlaybackSeek.setEnabled(true);
363         }
364 
365         @Override
sendFetchVoicemailRequest(Uri voicemailUri)366         public void sendFetchVoicemailRequest(Uri voicemailUri) {
367             Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri);
368             mApplicationContext.sendBroadcast(intent);
369         }
370 
371         @Override
queryHasContent(Uri voicemailUri)372         public boolean queryHasContent(Uri voicemailUri) {
373             ContentResolver contentResolver = mApplicationContext.getContentResolver();
374             Cursor cursor = contentResolver.query(
375                     voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
376             try {
377                 if (cursor != null && cursor.moveToNext()) {
378                     return cursor.getInt(cursor.getColumnIndexOrThrow(
379                             VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
380                 }
381             } finally {
382                 MoreCloseables.closeQuietly(cursor);
383             }
384             return false;
385         }
386 
getAudioManager()387         private AudioManager getAudioManager() {
388             return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE);
389         }
390 
391         @Override
isSpeakerPhoneOn()392         public boolean isSpeakerPhoneOn() {
393             return getAudioManager().isSpeakerphoneOn();
394         }
395 
396         @Override
setSpeakerPhoneOn(boolean on)397         public void setSpeakerPhoneOn(boolean on) {
398             getAudioManager().setSpeakerphoneOn(on);
399             if (on) {
400                 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on);
401             } else {
402                 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off);
403             }
404         }
405 
406         @Override
setVolumeControlStream(int streamType)407         public void setVolumeControlStream(int streamType) {
408             Activity activity = mActivityReference.get();
409             if (activity != null) {
410                 activity.setVolumeControlStream(streamType);
411             }
412         }
413     }
414 
415     /**
416      * Controls a TextView with dynamically changing text.
417      * <p>
418      * There are two methods here of interest,
419      * {@link TextViewWithMessagesController#setPermanentText(String)} and
420      * {@link TextViewWithMessagesController#setTemporaryText(String, long, TimeUnit)}.  The
421      * former is used to set the text on the text view immediately, and is used in our case for
422      * the countdown of duration remaining during voicemail playback.  The second is used to
423      * temporarily replace this countdown with a message, in our case faster voicemail speed or
424      * slower voicemail speed, before returning to the countdown display.
425      * <p>
426      * All the methods on this class must be called from the ui thread.
427      */
428     private static final class TextViewWithMessagesController {
429         private static final float VISIBLE = 1;
430         private static final float INVISIBLE = 0;
431         private static final long SHORT_ANIMATION_MS = 200;
432         private static final long LONG_ANIMATION_MS = 400;
433         private final Object mLock = new Object();
434         private final TextView mPermanentTextView;
435         private final TextView mTemporaryTextView;
436         @GuardedBy("mLock") private Runnable mRunnable;
437 
TextViewWithMessagesController(TextView permanentTextView, TextView temporaryTextView)438         public TextViewWithMessagesController(TextView permanentTextView,
439                 TextView temporaryTextView) {
440             mPermanentTextView = permanentTextView;
441             mTemporaryTextView = temporaryTextView;
442         }
443 
setPermanentText(String text)444         public void setPermanentText(String text) {
445             mPermanentTextView.setText(text);
446         }
447 
setTemporaryText(String text, long duration, TimeUnit units)448         public void setTemporaryText(String text, long duration, TimeUnit units) {
449             synchronized (mLock) {
450                 mTemporaryTextView.setText(text);
451                 mTemporaryTextView.animate().alpha(VISIBLE).setDuration(SHORT_ANIMATION_MS);
452                 mPermanentTextView.animate().alpha(INVISIBLE).setDuration(SHORT_ANIMATION_MS);
453                 mRunnable = new Runnable() {
454                     @Override
455                     public void run() {
456                         synchronized (mLock) {
457                             // We check for (mRunnable == this) becuase if not true, then another
458                             // setTemporaryText call has taken place in the meantime, and this
459                             // one is now defunct and needs to take no action.
460                             if (mRunnable == this) {
461                                 mRunnable = null;
462                                 mTemporaryTextView.animate()
463                                         .alpha(INVISIBLE).setDuration(LONG_ANIMATION_MS);
464                                 mPermanentTextView.animate()
465                                         .alpha(VISIBLE).setDuration(LONG_ANIMATION_MS);
466                             }
467                         }
468                     }
469                 };
470                 mTemporaryTextView.postDelayed(mRunnable, units.toMillis(duration));
471             }
472         }
473     }
474 }
475