• 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 android.content.ContentUris;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.database.Cursor;
23 import android.graphics.drawable.Drawable;
24 import android.net.Uri;
25 import android.os.AsyncTask;
26 import android.os.Handler;
27 import android.util.AttributeSet;
28 import android.support.design.widget.Snackbar;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.widget.ImageButton;
32 import android.widget.LinearLayout;
33 import android.widget.SeekBar;
34 import android.widget.SeekBar.OnSeekBarChangeListener;
35 import android.widget.Space;
36 import android.widget.TextView;
37 import android.widget.Toast;
38 
39 import com.android.common.io.MoreCloseables;
40 import com.android.dialer.PhoneCallDetails;
41 import com.android.dialer.R;
42 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
43 
44 import com.android.dialer.database.VoicemailArchiveContract;
45 import com.android.dialer.database.VoicemailArchiveContract.VoicemailArchive;
46 import com.android.dialer.util.AsyncTaskExecutor;
47 import com.android.dialer.util.AsyncTaskExecutors;
48 import com.android.dialerbind.ObjectFactory;
49 import com.google.common.annotations.VisibleForTesting;
50 
51 import java.util.ArrayList;
52 import java.util.HashMap;
53 import java.util.Objects;
54 import java.util.concurrent.TimeUnit;
55 import java.util.concurrent.ScheduledFuture;
56 import java.util.concurrent.ScheduledExecutorService;
57 
58 import javax.annotation.Nullable;
59 import javax.annotation.concurrent.GuardedBy;
60 import javax.annotation.concurrent.NotThreadSafe;
61 import javax.annotation.concurrent.ThreadSafe;
62 
63 /**
64  * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for
65  * details on the voicemail playback implementation.
66  *
67  * This class is not thread-safe, it is thread-confined. All calls to all public
68  * methods on this class are expected to come from the main ui thread.
69  */
70 @NotThreadSafe
71 public class VoicemailPlaybackLayout extends LinearLayout
72         implements VoicemailPlaybackPresenter.PlaybackView,
73         CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
74     private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
75     private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
76     private static final int VOICEMAIL_ARCHIVE_DELAY_MS = 3000;
77 
78     /** The enumeration of {@link AsyncTask} objects we use in this class. */
79     public enum Tasks {
80         QUERY_ARCHIVED_STATUS
81     }
82 
83     /**
84      * Controls the animation of the playback slider.
85      */
86     @ThreadSafe
87     private final class PositionUpdater implements Runnable {
88 
89         /** Update rate for the slider, 30fps. */
90         private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
91 
92         private int mDurationMs;
93         private final ScheduledExecutorService mExecutorService;
94         private final Object mLock = new Object();
95         @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture;
96 
97         private Runnable mUpdateClipPositionRunnable = new Runnable() {
98             @Override
99             public void run() {
100                 int currentPositionMs = 0;
101                 synchronized (mLock) {
102                     if (mScheduledFuture == null || mPresenter == null) {
103                         // This task has been canceled. Just stop now.
104                         return;
105                     }
106                     currentPositionMs = mPresenter.getMediaPlayerPosition();
107                 }
108                 setClipPosition(currentPositionMs, mDurationMs);
109             }
110         };
111 
PositionUpdater(int durationMs, ScheduledExecutorService executorService)112         public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
113             mDurationMs = durationMs;
114             mExecutorService = executorService;
115         }
116 
117         @Override
run()118         public void run() {
119             post(mUpdateClipPositionRunnable);
120         }
121 
startUpdating()122         public void startUpdating() {
123             synchronized (mLock) {
124                 cancelPendingRunnables();
125                 mScheduledFuture = mExecutorService.scheduleAtFixedRate(
126                         this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
127             }
128         }
129 
stopUpdating()130         public void stopUpdating() {
131             synchronized (mLock) {
132                 cancelPendingRunnables();
133             }
134         }
135 
cancelPendingRunnables()136         private void cancelPendingRunnables() {
137             if (mScheduledFuture != null) {
138                 mScheduledFuture.cancel(true);
139                 mScheduledFuture = null;
140             }
141             removeCallbacks(mUpdateClipPositionRunnable);
142         }
143     }
144 
145     /**
146      * Handle state changes when the user manipulates the seek bar.
147      */
148     private final OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() {
149         @Override
150         public void onStartTrackingTouch(SeekBar seekBar) {
151             if (mPresenter != null) {
152                 mPresenter.pausePlaybackForSeeking();
153             }
154         }
155 
156         @Override
157         public void onStopTrackingTouch(SeekBar seekBar) {
158             if (mPresenter != null) {
159                 mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress());
160             }
161         }
162 
163         @Override
164         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
165             setClipPosition(progress, seekBar.getMax());
166             // Update the seek position if user manually changed it. This makes sure position gets
167             // updated when user use volume button to seek playback in talkback mode.
168             if (fromUser) {
169                 mPresenter.seek(progress);
170             }
171         }
172     };
173 
174     /**
175      * Click listener to toggle speakerphone.
176      */
177     private final View.OnClickListener mSpeakerphoneListener = new View.OnClickListener() {
178         @Override
179         public void onClick(View v) {
180             if (mPresenter != null) {
181                 mPresenter.toggleSpeakerphone();
182             }
183         }
184     };
185 
186     /**
187      * Click listener to play or pause voicemail playback.
188      */
189     private final View.OnClickListener mStartStopButtonListener = new View.OnClickListener() {
190         @Override
191         public void onClick(View view) {
192             if (mPresenter == null) {
193                 return;
194             }
195 
196             if (mIsPlaying) {
197                 mPresenter.pausePlayback();
198             } else {
199                 mPresenter.resumePlayback();
200             }
201         }
202     };
203 
204     private final View.OnClickListener mDeleteButtonListener = new View.OnClickListener() {
205         @Override
206         public void onClick(View view ) {
207             if (mPresenter == null) {
208                 return;
209             }
210             mPresenter.pausePlayback();
211             mPresenter.onVoicemailDeleted();
212 
213             final Uri deleteUri = mVoicemailUri;
214             final Runnable deleteCallback = new Runnable() {
215                 @Override
216                 public void run() {
217                     if (Objects.equals(deleteUri, mVoicemailUri)) {
218                         CallLogAsyncTaskUtil.deleteVoicemail(mContext, deleteUri,
219                                 VoicemailPlaybackLayout.this);
220                     }
221                 }
222             };
223 
224             final Handler handler = new Handler();
225             // Add a little buffer time in case the user clicked "undo" at the end of the delay
226             // window.
227             handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
228 
229             Snackbar.make(VoicemailPlaybackLayout.this, R.string.snackbar_voicemail_deleted,
230                             Snackbar.LENGTH_LONG)
231                     .setDuration(VOICEMAIL_DELETE_DELAY_MS)
232                     .setAction(R.string.snackbar_voicemail_deleted_undo,
233                             new View.OnClickListener() {
234                                 @Override
235                                 public void onClick(View view) {
236                                     mPresenter.onVoicemailDeleteUndo();
237                                         handler.removeCallbacks(deleteCallback);
238                                 }
239                             })
240                     .setActionTextColor(
241                             mContext.getResources().getColor(
242                                     R.color.dialer_snackbar_action_text_color))
243                     .show();
244         }
245     };
246 
247     private final View.OnClickListener mArchiveButtonListener = new View.OnClickListener() {
248         @Override
249         public void onClick(View v) {
250             if (mPresenter == null || isArchiving(mVoicemailUri)) {
251                 return;
252             }
253             mIsArchiving.add(mVoicemailUri);
254             mPresenter.pausePlayback();
255             updateArchiveUI(mVoicemailUri);
256             disableUiElements();
257             mPresenter.archiveContent(mVoicemailUri, true);
258         }
259     };
260 
261     private final View.OnClickListener mShareButtonListener = new View.OnClickListener() {
262         @Override
263         public void onClick(View v) {
264             if (mPresenter == null || isArchiving(mVoicemailUri)) {
265                 return;
266             }
267             disableUiElements();
268             mPresenter.archiveContent(mVoicemailUri, false);
269         }
270     };
271 
272     private Context mContext;
273     private VoicemailPlaybackPresenter mPresenter;
274     private Uri mVoicemailUri;
275     private final AsyncTaskExecutor mAsyncTaskExecutor =
276             AsyncTaskExecutors.createAsyncTaskExecutor();
277     private boolean mIsPlaying = false;
278     /**
279      * Keeps track of which voicemails are currently being archived in order to update the voicemail
280      * card UI every time a user opens a new card.
281      */
282     private static final ArrayList<Uri> mIsArchiving = new ArrayList<>();
283 
284     private SeekBar mPlaybackSeek;
285     private ImageButton mStartStopButton;
286     private ImageButton mPlaybackSpeakerphone;
287     private ImageButton mDeleteButton;
288     private ImageButton mArchiveButton;
289     private ImageButton mShareButton;
290 
291     private Space mArchiveSpace;
292     private Space mShareSpace;
293 
294     private TextView mStateText;
295     private TextView mPositionText;
296     private TextView mTotalDurationText;
297 
298     private PositionUpdater mPositionUpdater;
299     private Drawable mVoicemailSeekHandleEnabled;
300     private Drawable mVoicemailSeekHandleDisabled;
301 
VoicemailPlaybackLayout(Context context)302     public VoicemailPlaybackLayout(Context context) {
303         this(context, null);
304     }
305 
VoicemailPlaybackLayout(Context context, AttributeSet attrs)306     public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
307         super(context, attrs);
308         mContext = context;
309         LayoutInflater inflater =
310                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
311         inflater.inflate(R.layout.voicemail_playback_layout, this);
312     }
313 
314     @Override
setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)315     public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
316         mPresenter = presenter;
317         mVoicemailUri = voicemailUri;
318         if (ObjectFactory.isVoicemailArchiveEnabled(mContext)) {
319             updateArchiveUI(mVoicemailUri);
320             updateArchiveButton(mVoicemailUri);
321         }
322 
323         if (ObjectFactory.isVoicemailShareEnabled(mContext)) {
324             // Show share button and space before it
325             mShareSpace.setVisibility(View.VISIBLE);
326             mShareButton.setVisibility(View.VISIBLE);
327         }
328     }
329 
330     @Override
onFinishInflate()331     protected void onFinishInflate() {
332         super.onFinishInflate();
333 
334         mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
335         mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
336         mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
337         mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
338         mArchiveButton =(ImageButton) findViewById(R.id.archive_voicemail);
339         mShareButton = (ImageButton) findViewById(R.id.share_voicemail);
340 
341         mArchiveSpace = (Space) findViewById(R.id.space_before_archive_voicemail);
342         mShareSpace = (Space) findViewById(R.id.space_before_share_voicemail);
343 
344         mStateText = (TextView) findViewById(R.id.playback_state_text);
345         mPositionText = (TextView) findViewById(R.id.playback_position_text);
346         mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
347 
348         mPlaybackSeek.setOnSeekBarChangeListener(mSeekBarChangeListener);
349         mStartStopButton.setOnClickListener(mStartStopButtonListener);
350         mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
351         mDeleteButton.setOnClickListener(mDeleteButtonListener);
352         mArchiveButton.setOnClickListener(mArchiveButtonListener);
353         mShareButton.setOnClickListener(mShareButtonListener);
354 
355         mPositionText.setText(formatAsMinutesAndSeconds(0));
356         mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
357 
358         mVoicemailSeekHandleEnabled = getResources().getDrawable(
359                 R.drawable.ic_voicemail_seek_handle, mContext.getTheme());
360         mVoicemailSeekHandleDisabled = getResources().getDrawable(
361                 R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme());
362     }
363 
364     @Override
onPlaybackStarted(int duration, ScheduledExecutorService executorService)365     public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
366         mIsPlaying = true;
367 
368         mStartStopButton.setImageResource(R.drawable.ic_pause);
369 
370         if (mPositionUpdater != null) {
371             mPositionUpdater.stopUpdating();
372             mPositionUpdater = null;
373         }
374         mPositionUpdater = new PositionUpdater(duration, executorService);
375         mPositionUpdater.startUpdating();
376     }
377 
378     @Override
onPlaybackStopped()379     public void onPlaybackStopped() {
380         mIsPlaying = false;
381 
382         mStartStopButton.setImageResource(R.drawable.ic_play_arrow);
383 
384         if (mPositionUpdater != null) {
385             mPositionUpdater.stopUpdating();
386             mPositionUpdater = null;
387         }
388     }
389 
390     @Override
onPlaybackError()391     public void onPlaybackError() {
392         if (mPositionUpdater != null) {
393             mPositionUpdater.stopUpdating();
394         }
395 
396         disableUiElements();
397         mStateText.setText(getString(R.string.voicemail_playback_error));
398     }
399 
400     @Override
onSpeakerphoneOn(boolean on)401     public void onSpeakerphoneOn(boolean on) {
402         if (on) {
403             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
404             // Speaker is now on, tapping button will turn it off.
405             mPlaybackSpeakerphone.setContentDescription(
406                     mContext.getString(R.string.voicemail_speaker_off));
407         } else {
408             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_down_24dp);
409             // Speaker is now off, tapping button will turn it on.
410             mPlaybackSpeakerphone.setContentDescription(
411                     mContext.getString(R.string.voicemail_speaker_on));
412         }
413     }
414 
415     @Override
setClipPosition(int positionMs, int durationMs)416     public void setClipPosition(int positionMs, int durationMs) {
417         int seekBarPositionMs = Math.max(0, positionMs);
418         int seekBarMax = Math.max(seekBarPositionMs, durationMs);
419         if (mPlaybackSeek.getMax() != seekBarMax) {
420             mPlaybackSeek.setMax(seekBarMax);
421         }
422 
423         mPlaybackSeek.setProgress(seekBarPositionMs);
424 
425         mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
426         mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
427     }
428 
429     @Override
setSuccess()430     public void setSuccess() {
431         mStateText.setText(null);
432     }
433 
434     @Override
setIsFetchingContent()435     public void setIsFetchingContent() {
436         disableUiElements();
437         mStateText.setText(getString(R.string.voicemail_fetching_content));
438     }
439 
440     @Override
setFetchContentTimeout()441     public void setFetchContentTimeout() {
442         mStartStopButton.setEnabled(true);
443         mStateText.setText(getString(R.string.voicemail_fetching_timout));
444     }
445 
446     @Override
getDesiredClipPosition()447     public int getDesiredClipPosition() {
448         return mPlaybackSeek.getProgress();
449     }
450 
451     @Override
disableUiElements()452     public void disableUiElements() {
453         mStartStopButton.setEnabled(false);
454         resetSeekBar();
455     }
456 
457     @Override
enableUiElements()458     public void enableUiElements() {
459         mDeleteButton.setEnabled(true);
460         mStartStopButton.setEnabled(true);
461         mPlaybackSeek.setEnabled(true);
462         mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
463     }
464 
465     @Override
resetSeekBar()466     public void resetSeekBar() {
467         mPlaybackSeek.setProgress(0);
468         mPlaybackSeek.setEnabled(false);
469         mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
470     }
471 
472     @Override
onDeleteCall()473     public void onDeleteCall() {}
474 
475     @Override
onDeleteVoicemail()476     public void onDeleteVoicemail() {
477         mPresenter.onVoicemailDeletedInDatabase();
478     }
479 
480     @Override
onGetCallDetails(PhoneCallDetails[] details)481     public void onGetCallDetails(PhoneCallDetails[] details) {}
482 
getString(int resId)483     private String getString(int resId) {
484         return mContext.getString(resId);
485     }
486 
487     /**
488      * Formats a number of milliseconds as something that looks like {@code 00:05}.
489      * <p>
490      * We always use four digits, two for minutes two for seconds.  In the very unlikely event
491      * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
492      */
formatAsMinutesAndSeconds(int millis)493     private String formatAsMinutesAndSeconds(int millis) {
494         int seconds = millis / 1000;
495         int minutes = seconds / 60;
496         seconds -= minutes * 60;
497         if (minutes > 99) {
498             minutes = 99;
499         }
500         return String.format("%02d:%02d", minutes, seconds);
501     }
502 
503     /**
504      * Called when a voicemail archive succeeded. If the expanded voicemail was being
505      * archived, update the card UI. Either way, display a snackbar linking user to archive.
506      */
507     @Override
onVoicemailArchiveSucceded(Uri voicemailUri)508     public void onVoicemailArchiveSucceded(Uri voicemailUri) {
509         if (isArchiving(voicemailUri)) {
510             mIsArchiving.remove(voicemailUri);
511             if (Objects.equals(voicemailUri, mVoicemailUri)) {
512                 onVoicemailArchiveResult();
513                 hideArchiveButton();
514             }
515         }
516 
517         Snackbar.make(this, R.string.snackbar_voicemail_archived,
518                 Snackbar.LENGTH_LONG)
519                 .setDuration(VOICEMAIL_ARCHIVE_DELAY_MS)
520                 .setAction(R.string.snackbar_voicemail_archived_goto,
521                         new View.OnClickListener() {
522                             @Override
523                             public void onClick(View view) {
524                                 Intent intent = new Intent(mContext,
525                                         VoicemailArchiveActivity.class);
526                                 mContext.startActivity(intent);
527                             }
528                         })
529                 .setActionTextColor(
530                         mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color))
531                 .show();
532     }
533 
534     /**
535      * If a voicemail archive failed, and the expanded card was being archived, update the card UI.
536      * Either way, display a toast saying the voicemail archive failed.
537      */
538     @Override
onVoicemailArchiveFailed(Uri voicemailUri)539     public void onVoicemailArchiveFailed(Uri voicemailUri) {
540         if (isArchiving(voicemailUri)) {
541             mIsArchiving.remove(voicemailUri);
542             if (Objects.equals(voicemailUri, mVoicemailUri)) {
543                 onVoicemailArchiveResult();
544             }
545         }
546         String toastStr = mContext.getString(R.string.voicemail_archive_failed);
547         Toast.makeText(mContext, toastStr, Toast.LENGTH_SHORT).show();
548     }
549 
hideArchiveButton()550     public void hideArchiveButton() {
551         mArchiveSpace.setVisibility(View.GONE);
552         mArchiveButton.setVisibility(View.GONE);
553         mArchiveButton.setClickable(false);
554         mArchiveButton.setEnabled(false);
555     }
556 
557     /**
558      * Whenever a voicemail archive succeeds or fails, clear the text displayed in the voicemail
559      * card.
560      */
onVoicemailArchiveResult()561     private void onVoicemailArchiveResult() {
562         enableUiElements();
563         mStateText.setText(null);
564         mArchiveButton.setColorFilter(null);
565     }
566 
567     /**
568      * Whether or not the voicemail with the given uri is being archived.
569      */
isArchiving(@ullable Uri uri)570     private boolean isArchiving(@Nullable Uri uri) {
571         return uri != null && mIsArchiving.contains(uri);
572     }
573 
574     /**
575      * Show the proper text and hide the archive button if the voicemail is still being archived.
576      */
updateArchiveUI(@ullable Uri voicemailUri)577     private void updateArchiveUI(@Nullable Uri voicemailUri) {
578         if (!Objects.equals(voicemailUri, mVoicemailUri)) {
579             return;
580         }
581         if (isArchiving(voicemailUri)) {
582             // If expanded card was in the middle of archiving, disable buttons and display message
583             disableUiElements();
584             mDeleteButton.setEnabled(false);
585             mArchiveButton.setColorFilter(getResources().getColor(R.color.setting_disabled_color));
586             mStateText.setText(getString(R.string.voicemail_archiving_content));
587         } else {
588             onVoicemailArchiveResult();
589         }
590     }
591 
592     /**
593      * Hides the archive button if the voicemail has already been archived, shows otherwise.
594      * @param voicemailUri the URI of the voicemail for which the archive button needs to be updated
595      */
updateArchiveButton(@ullable final Uri voicemailUri)596     private void updateArchiveButton(@Nullable final Uri voicemailUri) {
597         if (voicemailUri == null ||
598                 !Objects.equals(voicemailUri, mVoicemailUri) || isArchiving(voicemailUri) ||
599                 Objects.equals(voicemailUri.getAuthority(),VoicemailArchiveContract.AUTHORITY)) {
600             return;
601         }
602         mAsyncTaskExecutor.submit(Tasks.QUERY_ARCHIVED_STATUS,
603                 new AsyncTask<Void, Void, Boolean>() {
604             @Override
605             public Boolean doInBackground(Void... params) {
606                 Cursor cursor = mContext.getContentResolver().query(VoicemailArchive.CONTENT_URI,
607                         null, VoicemailArchive.SERVER_ID + "=" + ContentUris.parseId(mVoicemailUri)
608                         + " AND " + VoicemailArchive.ARCHIVED + "= 1", null, null);
609                 boolean archived = cursor != null && cursor.getCount() > 0;
610                 cursor.close();
611                 return archived;
612             }
613 
614             @Override
615             public void onPostExecute(Boolean archived) {
616                 if (!Objects.equals(voicemailUri, mVoicemailUri)) {
617                     return;
618                 }
619 
620                 if (archived) {
621                     hideArchiveButton();
622                 } else {
623                     mArchiveSpace.setVisibility(View.VISIBLE);
624                     mArchiveButton.setVisibility(View.VISIBLE);
625                     mArchiveButton.setClickable(true);
626                     mArchiveButton.setEnabled(true);
627                 }
628 
629             }
630         });
631     }
632 
633     @VisibleForTesting
getStateText()634     public String getStateText() {
635         return mStateText.getText().toString();
636     }
637 }
638