/* * Copyright (C) 2017 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.listui; import android.app.FragmentManager; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.VoicemailContract; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.v4.util.Pair; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.voicemail.listui.NewVoicemailViewHolder.NewVoicemailViewHolderListener; import com.android.dialer.voicemail.model.VoicemailEntry; import java.util.Locale; /** * The view of the media player that is visible when a {@link NewVoicemailViewHolder} is expanded. */ public class NewVoicemailMediaPlayerView extends LinearLayout { private ImageButton playButton; private ImageButton pauseButton; private ImageButton speakerButton; private ImageButton phoneButton; private ImageButton deleteButton; private TextView currentSeekBarPosition; private SeekBar seekBarView; private TextView totalDurationView; private Uri voicemailUri; private FragmentManager fragmentManager; private NewVoicemailViewHolder newVoicemailViewHolder; private NewVoicemailMediaPlayer mediaPlayer; private NewVoicemailViewHolderListener newVoicemailViewHolderListener; public NewVoicemailMediaPlayerView(Context context, AttributeSet attrs) { super(context, attrs); LogUtil.enterBlock("NewVoicemailMediaPlayer"); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.new_voicemail_media_player_layout, this); } @Override protected void onFinishInflate() { super.onFinishInflate(); LogUtil.enterBlock("NewVoicemailMediaPlayer.onFinishInflate"); initializeMediaPlayerButtonsAndViews(); setupListenersForMediaPlayerButtons(); } private void initializeMediaPlayerButtonsAndViews() { playButton = findViewById(R.id.playButton); pauseButton = findViewById(R.id.pauseButton); currentSeekBarPosition = findViewById(R.id.playback_position_text); seekBarView = findViewById(R.id.playback_seek); speakerButton = findViewById(R.id.speakerButton); phoneButton = findViewById(R.id.phoneButton); deleteButton = findViewById(R.id.deleteButton); totalDurationView = findViewById(R.id.playback_seek_total_duration); } private void setupListenersForMediaPlayerButtons() { playButton.setOnClickListener(playButtonListener); pauseButton.setOnClickListener(pauseButtonListener); seekBarView.setOnSeekBarChangeListener(seekbarChangeListener); speakerButton.setOnClickListener(speakerButtonListener); phoneButton.setOnClickListener(phoneButtonListener); deleteButton.setOnClickListener(deleteButtonListener); } public void reset() { LogUtil.i("NewVoicemailMediaPlayer.reset", "the uri for this is " + voicemailUri); voicemailUri = null; } /** * Can be called either when binding happens on the {@link NewVoicemailViewHolder} from {@link * NewVoicemailAdapter} or when a user expands a {@link NewVoicemailViewHolder}. During the * binding, since {@link NewVoicemailMediaPlayerView} is part of {@link NewVoicemailViewHolder}, * we have to ensure that during the binding the values from the {@link NewVoicemailAdapter} are * also propogated down to the {@link NewVoicemailMediaPlayerView} via {@link * NewVoicemailViewHolder}. In the case of when the {@link NewVoicemailViewHolder} is expanded, * the most recent value and states from the {@link NewVoicemailAdapter} are set for the expanded * {@link NewVoicemailMediaPlayerView}. * * @param viewHolder * @param voicemailEntryFromAdapter are the voicemail related values from the {@link * AnnotatedCallLog} converted into {@link VoicemailEntry} format. * @param fragmentManager * @param mp the media player passed down from the adapter * @param listener */ void bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView( NewVoicemailViewHolder viewHolder, @NonNull VoicemailEntry voicemailEntryFromAdapter, @NonNull FragmentManager fragmentManager, NewVoicemailMediaPlayer mp, NewVoicemailViewHolderListener listener) { Assert.isNotNull(voicemailEntryFromAdapter); Uri uri = Uri.parse(voicemailEntryFromAdapter.voicemailUri()); Assert.isNotNull(viewHolder); Assert.isNotNull(uri); Assert.isNotNull(listener); Assert.isNotNull(totalDurationView); Assert.checkArgument(uri.equals(viewHolder.getViewHolderVoicemailUri())); LogUtil.i( "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", "Updating the viewholder:%d mediaPlayerView with uri value:%s", viewHolder.getViewHolderId(), uri.toString()); this.fragmentManager = fragmentManager; newVoicemailViewHolder = viewHolder; newVoicemailViewHolderListener = listener; mediaPlayer = mp; voicemailUri = uri; totalDurationView.setText( VoicemailEntryText.getVoicemailDuration(getContext(), voicemailEntryFromAdapter)); // Not sure if these are needed, but it'll ensure that onInflate() has atleast happened. initializeMediaPlayerButtonsAndViews(); setupListenersForMediaPlayerButtons(); // During the binding we only send a request to the adapter to tell us what the // state of the media player should be and call that function. // This could be the paused state, or the playing state of the resume state. // Our job here is only to send the request upto the adapter and have it decide what we should // do. LogUtil.i( "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", "Updating media player values for id:" + viewHolder.getViewHolderId()); // During the binding make sure that the first time we just set the mediaplayer view // This does not take care of the constant update if (mp.isPlaying() && mp.getLastPlayedOrPlayingVoicemailUri().equals(voicemailUri)) { Assert.checkArgument( mp.getLastPlayedOrPlayingVoicemailUri() .equals(mp.getLastPreparedOrPreparingToPlayVoicemailUri())); LogUtil.i( "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", "show playing state"); playButton.setVisibility(GONE); pauseButton.setVisibility(VISIBLE); currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mp.getCurrentPosition())); if (seekBarView.getMax() != mp.getDuration()) { seekBarView.setMax(mp.getDuration()); } seekBarView.setProgress(mp.getCurrentPosition()); } else if (mediaPlayer.isPaused() && mp.getLastPausedVoicemailUri().equals(voicemailUri)) { LogUtil.i( "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", "show paused state"); Assert.checkArgument(viewHolder.getViewHolderVoicemailUri().equals(voicemailUri)); playButton.setVisibility(VISIBLE); pauseButton.setVisibility(GONE); currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mp.getCurrentPosition())); if (seekBarView.getMax() != mp.getDuration()) { seekBarView.setMax(mp.getDuration()); } seekBarView.setProgress(mp.getCurrentPosition()); } else { LogUtil.i( "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", "show reset state"); playButton.setVisibility(VISIBLE); pauseButton.setVisibility(GONE); seekBarView.setProgress(0); seekBarView.setMax(100); currentSeekBarPosition.setText(formatAsMinutesAndSeconds(0)); } } private final OnSeekBarChangeListener seekbarChangeListener = new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBarfromProgress, int progress, boolean fromUser) { // TODO(uabdullah): Only for debugging purposes, to be removed. if (progress < 100) { LogUtil.i( "NewVoicemailMediaPlayer.seekbarChangeListener", "onProgressChanged, progress:%d, seekbarMax: %d, fromUser:%b", progress, seekBarfromProgress.getMax(), fromUser); } if (fromUser) { mediaPlayer.seekTo(progress); currentSeekBarPosition.setText(formatAsMinutesAndSeconds(progress)); } } @Override // TODO(uabdullah): Handle this case public void onStartTrackingTouch(SeekBar seekBar) { LogUtil.i("NewVoicemailMediaPlayer.onStartTrackingTouch", "does nothing for now"); } @Override // TODO(uabdullah): Handle this case public void onStopTrackingTouch(SeekBar seekBar) { LogUtil.i("NewVoicemailMediaPlayer.onStopTrackingTouch", "does nothing for now"); } }; private final View.OnClickListener pauseButtonListener = new View.OnClickListener() { @Override public void onClick(View view) { LogUtil.i( "NewVoicemailMediaPlayer.pauseButtonListener", "pauseMediaPlayerAndSetPausedStateOfViewHolder button for voicemailUri: %s", voicemailUri.toString()); Assert.checkArgument(playButton.getVisibility() == GONE); Assert.checkArgument(mediaPlayer != null); Assert.checkArgument( mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals((voicemailUri)), "the voicemail being played is the only voicemail that should" + " be paused. last played voicemail:%s, uri:%s", mediaPlayer.getLastPlayedOrPlayingVoicemailUri().toString(), voicemailUri.toString()); Assert.checkArgument( newVoicemailViewHolder.getViewHolderVoicemailUri().equals(voicemailUri), "viewholder uri and mediaplayer view should be the same."); newVoicemailViewHolderListener.pauseViewHolder(newVoicemailViewHolder); } }; private final View.OnClickListener playButtonListener = new View.OnClickListener() { @Override public void onClick(View view) { LogUtil.i( "NewVoicemailMediaPlayer.playButtonListener", "play button for voicemailUri: %s", voicemailUri.toString()); if (mediaPlayer.getLastPausedVoicemailUri() != null && mediaPlayer .getLastPausedVoicemailUri() .toString() .contentEquals(voicemailUri.toString())) { LogUtil.i( "NewVoicemailMediaPlayer.playButtonListener", "resume playing voicemailUri: %s", voicemailUri.toString()); newVoicemailViewHolderListener.resumePausedViewHolder(newVoicemailViewHolder); } else { playVoicemailWhenAvailableLocally(); } } }; /** * Plays the voicemail when we are able to play the voicemail locally from the device. This * involves checking if the voicemail is available to play locally, if it is, then we setup the * Media Player to play the voicemail. If the voicemail is not available, then we need download * the voicemail from the voicemail server to the device, and then have the Media player play it. */ private void playVoicemailWhenAvailableLocally() { LogUtil.enterBlock("playVoicemailWhenAvailableLocally"); Worker, Pair> checkVoicemailHasContent = this::queryVoicemailHasContent; SuccessListener> checkVoicemailHasContentCallBack = this::prepareMediaPlayer; DialerExecutorComponent.get(getContext()) .dialerExecutorFactory() .createUiTaskBuilder(fragmentManager, "lookup_voicemail_content", checkVoicemailHasContent) .onSuccess(checkVoicemailHasContentCallBack) .build() .executeSerial(new Pair<>(getContext(), voicemailUri)); } private Pair queryVoicemailHasContent(Pair contextUriPair) { Context context = contextUriPair.first; Uri uri = contextUriPair.second; try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { return new Pair<>( cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.HAS_CONTENT)) == 1, uri); } return new Pair<>(false, uri); } } /** * If the voicemail is available to play locally, setup the media player to play it. Otherwise * send a request to download the voicemail and then play it. */ private void prepareMediaPlayer(Pair booleanUriPair) { boolean voicemailAvailableLocally = booleanUriPair.first; Uri uri = booleanUriPair.second; LogUtil.i( "NewVoicemailMediaPlayer.prepareMediaPlayer", "voicemail available locally: %b for voicemailUri: %s", voicemailAvailableLocally, uri.toString()); if (voicemailAvailableLocally) { try { Assert.checkArgument(mediaPlayer != null, "media player should not have been null"); mediaPlayer.prepareMediaPlayerAndPlayVoicemailWhenReady(getContext(), uri); } catch (Exception e) { LogUtil.e( "NewVoicemailMediaPlayer.prepareMediaPlayer", "Exception when mediaPlayer.prepareMediaPlayerAndPlayVoicemailWhenReady" + "(getContext(), uri)\n" + e + "\n uri:" + uri + "context should not be null, its value is :" + getContext()); } } else { // TODO(a bug): Add logic for downloading voicemail content from the server. LogUtil.i( "NewVoicemailMediaPlayer.prepareVoicemailForMediaPlayer", "need to download content"); } } private final View.OnClickListener speakerButtonListener = new View.OnClickListener() { @Override public void onClick(View view) { LogUtil.i( "NewVoicemailMediaPlayer.speakerButtonListener", "speaker request for voicemailUri: %s", voicemailUri.toString()); } }; private final View.OnClickListener phoneButtonListener = new View.OnClickListener() { @Override public void onClick(View view) { LogUtil.i( "NewVoicemailMediaPlayer.phoneButtonListener", "speaker request for voicemailUri: %s", voicemailUri.toString()); } }; private final View.OnClickListener deleteButtonListener = new View.OnClickListener() { @Override public void onClick(View view) { LogUtil.i( "NewVoicemailMediaPlayer.deleteButtonListener", "delete voicemailUri %s", voicemailUri.toString()); } }; /** * This is only called to update the media player view of the seekbar, and the duration and the * play button. For constant updates the adapter should seek track. This is the state when a * voicemail is playing. */ public void updateSeekBarDurationAndShowPlayButton(NewVoicemailMediaPlayer mp) { if (!mp.isPlaying()) { return; } playButton.setVisibility(GONE); pauseButton.setVisibility(VISIBLE); Assert.checkArgument( mp.equals(mediaPlayer), "there should only be one instance of a media player"); Assert.checkArgument( mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri().equals(voicemailUri)); Assert.checkArgument(mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals(voicemailUri)); Assert.isNotNull(mediaPlayer, "media player should have been set on bind"); Assert.checkArgument(mediaPlayer.isPlaying()); Assert.checkArgument(mediaPlayer.getCurrentPosition() >= 0); Assert.checkArgument(mediaPlayer.getDuration() >= 0); Assert.checkArgument(playButton.getVisibility() == GONE); Assert.checkArgument(pauseButton.getVisibility() == VISIBLE); Assert.checkArgument(seekBarView.getVisibility() == VISIBLE); Assert.checkArgument(currentSeekBarPosition.getVisibility() == VISIBLE); currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mediaPlayer.getCurrentPosition())); if (seekBarView.getMax() != mediaPlayer.getDuration()) { seekBarView.setMax(mediaPlayer.getDuration()); } seekBarView.setProgress(mediaPlayer.getCurrentPosition()); } /** * What the default state of an expanded media player view should look like. * * @param currentlyExpandedViewHolderOnScreen * @param mediaPlayer */ public void setToResetState( NewVoicemailViewHolder currentlyExpandedViewHolderOnScreen, NewVoicemailMediaPlayer mediaPlayer) { LogUtil.i( "NewVoicemailMediaPlayer.setToResetState", "update the seekbar for viewholder id:%d, mediaplayer view uri:%s, play button " + "visible:%b, pause button visible:%b", currentlyExpandedViewHolderOnScreen.getViewHolderId(), String.valueOf(voicemailUri), playButton.getVisibility() == VISIBLE, pauseButton.getVisibility() == VISIBLE); if (playButton.getVisibility() == GONE) { playButton.setVisibility(VISIBLE); pauseButton.setVisibility(GONE); } Assert.checkArgument(playButton.getVisibility() == VISIBLE); Assert.checkArgument(pauseButton.getVisibility() == GONE); Assert.checkArgument( !mediaPlayer.isPlaying(), "when resetting an expanded " + "state, there should be no voicemail playing"); Assert.checkArgument( mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals(Uri.EMPTY), "reset should have been called before updating its media player view"); currentSeekBarPosition.setText(formatAsMinutesAndSeconds(0)); seekBarView.setProgress(0); seekBarView.setMax(100); } public void setToPausedState(Uri toPausedState, NewVoicemailMediaPlayer mp) { LogUtil.i( "NewVoicemailMediaPlayer.setToPausedState", "toPausedState uri:%s, play button visible:%b, pause button visible:%b", toPausedState == null ? "null" : voicemailUri.toString(), playButton.getVisibility() == VISIBLE, pauseButton.getVisibility() == VISIBLE); playButton.setVisibility(VISIBLE); pauseButton.setVisibility(GONE); currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mediaPlayer.getCurrentPosition())); if (seekBarView.getMax() != mediaPlayer.getDuration()) { seekBarView.setMax(mediaPlayer.getDuration()); } seekBarView.setProgress(mediaPlayer.getCurrentPosition()); Assert.checkArgument(voicemailUri.equals(toPausedState)); Assert.checkArgument(!mp.isPlaying()); Assert.checkArgument( mp.equals(mediaPlayer), "there should only be one instance of a media player"); Assert.checkArgument( this.mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri().equals(voicemailUri)); Assert.checkArgument( this.mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals(voicemailUri)); Assert.checkArgument(this.mediaPlayer.getLastPausedVoicemailUri().equals(voicemailUri)); Assert.isNotNull(this.mediaPlayer, "media player should have been set on bind"); Assert.checkArgument(this.mediaPlayer.getCurrentPosition() >= 0); Assert.checkArgument(this.mediaPlayer.getDuration() >= 0); Assert.checkArgument(playButton.getVisibility() == VISIBLE); Assert.checkArgument(pauseButton.getVisibility() == GONE); Assert.checkArgument(seekBarView.getVisibility() == VISIBLE); Assert.checkArgument(currentSeekBarPosition.getVisibility() == VISIBLE); } @NonNull public Uri getVoicemailUri() { return voicemailUri; } private String formatAsMinutesAndSeconds(int millis) { int seconds = millis / 1000; int minutes = seconds / 60; seconds -= minutes * 60; if (minutes > 99) { minutes = 99; } return String.format(Locale.US, "%02d:%02d", minutes, seconds); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) void setFragmentManager(FragmentManager fragmentManager) { this.fragmentManager = fragmentManager; } }