From 3694b119843c161c3fdcf0820ec8bf755c79158f Mon Sep 17 00:00:00 2001 From: Sarmad Hashmi Date: Wed, 17 Feb 2016 17:39:36 -0800 Subject: Add files for voicemail archive tab. + Added VoicemailArchiveActivity which displays all voicemails archived by the user + VoicemailArchivePlaybackPresenter is the same as VoicemailPlaybackPresenter except that it does not need to request content from the voicemail server +Added tests (combined common methods and attributes for regular voicemail tab and voicemail archive used for tests) BUG=22797391 Change-Id: I4064eea945b9e257ec6c7ada35cf5ac1657dc866 --- .../dialer/voicemail/VoicemailArchiveActivity.java | 160 ++++++++++++ .../VoicemailArchivePlaybackPresenter.java | 85 +++++++ .../dialer/voicemail/VoicemailPlaybackLayout.java | 194 ++++++++++++-- .../voicemail/VoicemailPlaybackPresenter.java | 281 ++++++++++++++++++--- 4 files changed, 667 insertions(+), 53 deletions(-) create mode 100644 src/com/android/dialer/voicemail/VoicemailArchiveActivity.java create mode 100644 src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java (limited to 'src/com/android/dialer/voicemail') diff --git a/src/com/android/dialer/voicemail/VoicemailArchiveActivity.java b/src/com/android/dialer/voicemail/VoicemailArchiveActivity.java new file mode 100644 index 000000000..16b947cd3 --- /dev/null +++ b/src/com/android/dialer/voicemail/VoicemailArchiveActivity.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.voicemail; + +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.MenuItem; +import android.view.View; + +import com.android.contacts.common.GeoUtil; +import com.android.dialer.DialtactsActivity; +import com.android.dialer.R; +import com.android.dialer.TransactionSafeActivity; +import com.android.dialer.calllog.CallLogAdapter; +import com.android.dialer.calllog.CallLogQueryHandler; +import com.android.dialer.calllog.ContactInfoHelper; +import com.android.dialer.widget.EmptyContentView; +import com.android.dialerbind.ObjectFactory; + +/** + * This activity manages all the voicemails archived by the user. + */ +public class VoicemailArchiveActivity extends TransactionSafeActivity + implements CallLogAdapter.CallFetcher, CallLogQueryHandler.Listener { + private RecyclerView mRecyclerView; + private LinearLayoutManager mLayoutManager; + private EmptyContentView mEmptyListView; + private CallLogAdapter mAdapter; + private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + private CallLogQueryHandler mCallLogQueryHandler; + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!isSafeToCommitTransactions()) { + return true; + } + + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, DialtactsActivity.class); + // Clears any activities between VoicemailArchiveActivity and DialtactsActivity + // on the activity stack and reuses the existing instance of DialtactsActivity + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.call_log_fragment); + + // Make window opaque to reduce overdraw + getWindow().setBackgroundDrawable(null); + + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setElevation(0); + + mCallLogQueryHandler = new CallLogQueryHandler(this, getContentResolver(), this); + mVoicemailPlaybackPresenter = VoicemailArchivePlaybackPresenter + .getInstance(this, savedInstanceState); + + mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); + mRecyclerView.setHasFixedSize(true); + mLayoutManager = new LinearLayoutManager(this); + mRecyclerView.setLayoutManager(mLayoutManager); + mEmptyListView = (EmptyContentView) findViewById(R.id.empty_list_view); + mEmptyListView.setDescription(R.string.voicemail_archive_empty); + mEmptyListView.setImage(R.drawable.empty_call_log); + + mAdapter = ObjectFactory.newCallLogAdapter( + this, + this, + new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)), + mVoicemailPlaybackPresenter, + CallLogAdapter.ACTIVITY_TYPE_ARCHIVE); + mRecyclerView.setAdapter(mAdapter); + fetchCalls(); + } + + @Override + protected void onPause() { + mVoicemailPlaybackPresenter.onPause(); + mAdapter.onPause(); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + mAdapter.onResume(); + mVoicemailPlaybackPresenter.onResume(); + } + + @Override + public void onDestroy() { + mVoicemailPlaybackPresenter.onDestroy(); + mAdapter.changeCursor(null); + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mVoicemailPlaybackPresenter.onSaveInstanceState(outState); + } + + @Override + public void fetchCalls() { + mCallLogQueryHandler.fetchVoicemailArchive(); + } + + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) { + // Do nothing + } + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public boolean onCallsFetched(Cursor cursor) { + mAdapter.changeCursorVoicemail(cursor); + boolean showListView = cursor != null && cursor.getCount() > 0; + mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE); + mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE); + return true; + } +} diff --git a/src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java new file mode 100644 index 000000000..050b8ac62 --- /dev/null +++ b/src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.voicemail; + +import android.app.Activity; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import com.android.dialer.calllog.CallLogAsyncTaskUtil; +import com.android.dialer.database.VoicemailArchiveContract; +import java.io.FileNotFoundException; +import java.util.concurrent.TimeUnit; + +/** + * Similar to the {@link VoicemailPlaybackPresenter}, but for the archive voicemail tab. It checks + * whether the voicemail file exists locally before preparing it. + */ +public class VoicemailArchivePlaybackPresenter extends VoicemailPlaybackPresenter { + private static final String TAG = "VMPlaybackPresenter"; + private static VoicemailPlaybackPresenter sInstance; + + public VoicemailArchivePlaybackPresenter(Activity activity) { + super(activity); + } + + public static VoicemailPlaybackPresenter getInstance( + Activity activity, Bundle savedInstanceState) { + if (sInstance == null) { + sInstance = new VoicemailArchivePlaybackPresenter(activity); + } + + sInstance.init(activity, savedInstanceState); + return sInstance; + } + + @Override + protected void checkForContent(final OnContentCheckedListener callback) { + mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask() { + @Override + public Boolean doInBackground(Void... params) { + try { + // Check if the _data column of the archived voicemail is valid + if (mVoicemailUri != null) { + mContext.getContentResolver().openInputStream(mVoicemailUri); + return true; + } + } catch (FileNotFoundException e) { + Log.d(TAG, "Voicemail file not found for " + mVoicemailUri); + handleError(e); + } + return false; + } + + @Override + public void onPostExecute(Boolean hasContent) { + callback.onContentChecked(hasContent); + } + }); + } + + @Override + protected boolean requestContent(int code) { + if (mContext == null || mVoicemailUri == null) { + return false; + } + prepareContent(); + return true; + } +} diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java index 19b592d50..436fc7952 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java @@ -16,40 +16,44 @@ package com.android.dialer.voicemail; -import android.app.Activity; -import android.app.Fragment; +import android.content.ContentUris; import android.content.Context; +import android.content.Intent; +import android.database.Cursor; import android.graphics.drawable.Drawable; -import android.media.MediaPlayer; import android.net.Uri; -import android.os.Bundle; +import android.os.AsyncTask; import android.os.Handler; -import android.os.PowerManager; -import android.provider.VoicemailContract; import android.util.AttributeSet; -import android.util.Log; import android.support.design.widget.Snackbar; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; +import android.widget.Toast; import com.android.common.io.MoreCloseables; import com.android.dialer.PhoneCallDetails; import com.android.dialer.R; import com.android.dialer.calllog.CallLogAsyncTaskUtil; +import com.android.dialer.database.VoicemailArchiveContract; +import com.android.dialer.database.VoicemailArchiveContract.VoicemailArchive; +import com.android.dialer.util.AsyncTaskExecutor; +import com.android.dialer.util.AsyncTaskExecutors; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.ThreadSafe; @@ -67,6 +71,12 @@ public class VoicemailPlaybackLayout extends LinearLayout CallLogAsyncTaskUtil.CallLogAsyncTaskListener { private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName(); private static final int VOICEMAIL_DELETE_DELAY_MS = 3000; + private static final int VOICEMAIL_ARCHIVE_DELAY_MS = 3000; + + /** The enumeration of {@link AsyncTask} objects we use in this class. */ + public enum Tasks { + QUERY_ARCHIVED_STATUS + } /** * Controls the animation of the playback slider. @@ -202,7 +212,7 @@ public class VoicemailPlaybackLayout extends LinearLayout final Runnable deleteCallback = new Runnable() { @Override public void run() { - if (mVoicemailUri == deleteUri) { + if (Objects.equals(deleteUri, mVoicemailUri)) { CallLogAsyncTaskUtil.deleteVoicemail(mContext, deleteUri, VoicemailPlaybackLayout.this); } @@ -214,8 +224,6 @@ public class VoicemailPlaybackLayout extends LinearLayout // window. handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50); - final int actionTextColor = - mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color); Snackbar.make(VoicemailPlaybackLayout.this, R.string.snackbar_voicemail_deleted, Snackbar.LENGTH_LONG) .setDuration(VOICEMAIL_DELETE_DELAY_MS) @@ -227,21 +235,44 @@ public class VoicemailPlaybackLayout extends LinearLayout handler.removeCallbacks(deleteCallback); } }) - .setActionTextColor(actionTextColor) + .setActionTextColor( + mContext.getResources().getColor( + R.color.dialer_snackbar_action_text_color)) .show(); } }; + private final View.OnClickListener mArchiveButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mPresenter == null || isArchiving(mVoicemailUri)) { + return; + } + mIsArchiving.add(mVoicemailUri); + mPresenter.pausePlayback(); + updateArchiveUI(mVoicemailUri); + disableUiElements(); + mPresenter.archiveContent(mVoicemailUri, true); + } + }; + private Context mContext; private VoicemailPlaybackPresenter mPresenter; private Uri mVoicemailUri; - + private final AsyncTaskExecutor mAsyncTaskExecutor = + AsyncTaskExecutors.createAsyncTaskExecutor(); private boolean mIsPlaying = false; + /** + * Keeps track of which voicemails are currently being archived in order to update the voicemail + * card UI every time a user opens a new card. + */ + private static final ArrayList mIsArchiving = new ArrayList<>(); private SeekBar mPlaybackSeek; private ImageButton mStartStopButton; private ImageButton mPlaybackSpeakerphone; private ImageButton mDeleteButton; + private ImageButton mArchiveButton; private TextView mStateText; private TextView mPositionText; private TextView mTotalDurationText; @@ -256,7 +287,6 @@ public class VoicemailPlaybackLayout extends LinearLayout public VoicemailPlaybackLayout(Context context, AttributeSet attrs) { super(context, attrs); - mContext = context; LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -267,6 +297,8 @@ public class VoicemailPlaybackLayout extends LinearLayout public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) { mPresenter = presenter; mVoicemailUri = voicemailUri; + updateArchiveUI(mVoicemailUri); + updateArchiveButton(mVoicemailUri); } @Override @@ -277,6 +309,7 @@ public class VoicemailPlaybackLayout extends LinearLayout mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop); mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone); mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail); + mArchiveButton =(ImageButton) findViewById(R.id.archive_voicemail); mStateText = (TextView) findViewById(R.id.playback_state_text); mPositionText = (TextView) findViewById(R.id.playback_position_text); mTotalDurationText = (TextView) findViewById(R.id.total_duration_text); @@ -285,6 +318,7 @@ public class VoicemailPlaybackLayout extends LinearLayout mStartStopButton.setOnClickListener(mStartStopButtonListener); mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener); mDeleteButton.setOnClickListener(mDeleteButtonListener); + mArchiveButton.setOnClickListener(mArchiveButtonListener); mPositionText.setText(formatAsMinutesAndSeconds(0)); mTotalDurationText.setText(formatAsMinutesAndSeconds(0)); @@ -358,7 +392,6 @@ public class VoicemailPlaybackLayout extends LinearLayout mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs)); mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs)); - mStateText.setText(null); } @Override @@ -386,6 +419,7 @@ public class VoicemailPlaybackLayout extends LinearLayout @Override public void enableUiElements() { + mDeleteButton.setEnabled(true); mStartStopButton.setEnabled(true); mPlaybackSeek.setEnabled(true); mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled); @@ -429,6 +463,134 @@ public class VoicemailPlaybackLayout extends LinearLayout return String.format("%02d:%02d", minutes, seconds); } + /** + * Called when a voicemail archive succeeded. If the expanded voicemail was being + * archived, update the card UI. Either way, display a snackbar linking user to archive. + */ + @Override + public void onVoicemailArchiveSucceded(Uri voicemailUri) { + if (isArchiving(voicemailUri)) { + mIsArchiving.remove(voicemailUri); + if (Objects.equals(voicemailUri, mVoicemailUri)) { + onVoicemailArchiveResult(); + hideArchiveButton(); + } + } + + Snackbar.make(this, R.string.snackbar_voicemail_archived, + Snackbar.LENGTH_LONG) + .setDuration(VOICEMAIL_ARCHIVE_DELAY_MS) + .setAction(R.string.snackbar_voicemail_archived_goto, + new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(mContext, + VoicemailArchiveActivity.class); + mContext.startActivity(intent); + } + }) + .setActionTextColor( + mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color)) + .show(); + } + + /** + * If a voicemail archive failed, and the expanded card was being archived, update the card UI. + * Either way, display a toast saying the voicemail archive failed. + */ + @Override + public void onVoicemailArchiveFailed(Uri voicemailUri) { + if (isArchiving(voicemailUri)) { + mIsArchiving.remove(voicemailUri); + if (Objects.equals(voicemailUri, mVoicemailUri)) { + onVoicemailArchiveResult(); + } + } + String toastStr = mContext.getString(R.string.voicemail_archive_failed); + Toast.makeText(mContext, toastStr, Toast.LENGTH_SHORT).show(); + } + + public void hideArchiveButton() { + mArchiveButton.setVisibility(View.GONE); + mArchiveButton.setClickable(false); + mArchiveButton.setEnabled(false); + } + + /** + * Whenever a voicemail archive succeeds or fails, clear the text displayed in the voicemail + * card. + */ + private void onVoicemailArchiveResult() { + enableUiElements(); + mStateText.setText(null); + mArchiveButton.setColorFilter(null); + } + + /** + * Whether or not the voicemail with the given uri is being archived. + */ + private boolean isArchiving(@Nullable Uri uri) { + return uri != null && mIsArchiving.contains(uri); + } + + /** + * Show the proper text and hide the archive button if the voicemail is still being archived. + */ + private void updateArchiveUI(@Nullable Uri voicemailUri) { + if (!Objects.equals(voicemailUri, mVoicemailUri)) { + return; + } + if (isArchiving(voicemailUri)) { + // If expanded card was in the middle of archiving, disable buttons and display message + disableUiElements(); + mDeleteButton.setEnabled(false); + mArchiveButton.setColorFilter(getResources().getColor(R.color.setting_disabled_color)); + mStateText.setText(getString(R.string.voicemail_archiving_content)); + } else { + onVoicemailArchiveResult(); + } + } + + /** + * Hides the archive button if the voicemail has already been archived, shows otherwise. + * @param voicemailUri the URI of the voicemail for which the archive button needs to be updated + */ + private void updateArchiveButton(@Nullable final Uri voicemailUri) { + if (voicemailUri == null || + !Objects.equals(voicemailUri, mVoicemailUri) || isArchiving(voicemailUri) || + Objects.equals(voicemailUri.getAuthority(),VoicemailArchiveContract.AUTHORITY)) { + return; + } + mAsyncTaskExecutor.submit(Tasks.QUERY_ARCHIVED_STATUS, + new AsyncTask() { + @Override + public Boolean doInBackground(Void... params) { + Cursor cursor = mContext.getContentResolver().query(VoicemailArchive.CONTENT_URI, + null, VoicemailArchive.SERVER_ID + "=" + ContentUris.parseId(mVoicemailUri) + + " AND " + VoicemailArchive.ARCHIVED + "= 1", null, null); + boolean archived = cursor != null && cursor.getCount() > 0; + cursor.close(); + return archived; + } + + @Override + public void onPostExecute(Boolean archived) { + if (!Objects.equals(voicemailUri, mVoicemailUri)) { + return; + } + + if (archived) { + hideArchiveButton(); + } else { + mArchiveButton.setVisibility(View.VISIBLE); + mArchiveButton.setClickable(true); + mArchiveButton.setEnabled(true); + } + + } + }); + } + @VisibleForTesting public String getStateText() { return mStateText.getText().toString(); diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java index fcb35e57b..3151a5ea5 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java @@ -19,6 +19,9 @@ package com.android.dialer.voicemail; import com.google.common.annotations.VisibleForTesting; import android.app.Activity; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -30,20 +33,30 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; +import android.provider.CallLog; import android.provider.VoicemailContract; import android.support.annotation.Nullable; import android.util.Log; import android.view.WindowManager.LayoutParams; -import com.android.common.io.MoreCloseables; import com.android.dialer.calllog.CallLogAsyncTaskUtil; +import com.android.dialer.calllog.CallLogQuery; +import com.android.dialer.database.VoicemailArchiveContract; import com.android.dialer.util.AsyncTaskExecutor; import com.android.dialer.util.AsyncTaskExecutors; - +import com.android.common.io.MoreCloseables; +import com.android.dialer.util.TelecomUtil; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.io.ByteStreams; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -81,6 +94,8 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); void setFetchContentTimeout(); void setIsFetchingContent(); + void onVoicemailArchiveSucceded(Uri voicemailUri); + void onVoicemailArchiveFailed(Uri voicemailUri); void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); void resetSeekBar(); } @@ -95,10 +110,10 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene public enum Tasks { CHECK_FOR_CONTENT, CHECK_CONTENT_AFTER_CHANGE, + ARCHIVE_VOICEMAIL } - private interface OnContentCheckedListener { - + protected interface OnContentCheckedListener { void onContentChecked(boolean hasContent); } @@ -123,6 +138,8 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY"; private static final String IS_SPEAKERPHONE_ON_KEY = VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON"; + public static final int PLAYBACK_REQUEST = 0; + public static final int ARCHIVE_REQUEST = 1; /** * The most recently cached duration. We cache this since we don't want to keep requesting it @@ -134,11 +151,11 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene private static VoicemailPlaybackPresenter sInstance; private Activity mActivity; - private Context mContext; + protected Context mContext; private PlaybackView mView; - private Uri mVoicemailUri; + protected Uri mVoicemailUri; - private MediaPlayer mMediaPlayer; + protected MediaPlayer mMediaPlayer; private int mPosition; private boolean mIsPlaying; // MediaPlayer crashes on some method calls if not prepared but does not have a method which @@ -150,7 +167,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene private int mInitialOrientation; // Used to run async tasks that need to interact with the UI. - private AsyncTaskExecutor mAsyncTaskExecutor; + protected AsyncTaskExecutor mAsyncTaskExecutor; private static ScheduledExecutorService mScheduledExecutorService; /** * Used to handle the result of a successful or time-out fetch result. @@ -158,6 +175,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene * This variable is thread-contained, accessed only on the ui thread. */ private FetchResultHandler mFetchResultHandler; + private final List mArchiveResultHandlers = new ArrayList<>(); private Handler mHandler = new Handler(); private PowerManager.WakeLock mProximityWakeLock; private VoicemailAudioManager mVoicemailAudioManager; @@ -186,11 +204,10 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene /** * Initialize variables which are activity-independent and state-independent. */ - private VoicemailPlaybackPresenter(Activity activity) { + protected VoicemailPlaybackPresenter(Activity activity) { Context context = activity.getApplicationContext(); mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); mVoicemailAudioManager = new VoicemailAudioManager(context, this); - PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { @@ -202,7 +219,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene /** * Update variables which are activity-dependent or state-dependent. */ - private void init(Activity activity, Bundle savedInstanceState) { + protected void init(Activity activity, Bundle savedInstanceState) { mActivity = activity; mContext = activity; @@ -274,11 +291,9 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene public void onContentChecked(boolean hasContent) { if (hasContent) { prepareContent(); - } else { - if (mView != null) { - mView.resetSeekBar(); - mView.setClipPosition(0, mDuration.get()); - } + } else if (mView != null) { + mView.resetSeekBar(); + mView.setClipPosition(0, mDuration.get()); } } }); @@ -377,6 +392,13 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene mScheduledExecutorService = null; } + if (!mArchiveResultHandlers.isEmpty()) { + for (FetchResultHandler fetchResultHandler : mArchiveResultHandlers) { + fetchResultHandler.destroy(); + } + mArchiveResultHandlers.clear(); + } + if (mFetchResultHandler != null) { mFetchResultHandler.destroy(); mFetchResultHandler = null; @@ -386,7 +408,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene /** * Checks to see if we have content available for this voicemail. */ - private void checkForContent(final OnContentCheckedListener callback) { + protected void checkForContent(final OnContentCheckedListener callback) { mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask() { @Override public Boolean doInBackground(Void... params) { @@ -438,18 +460,26 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene * * @return whether issued request to fetch content */ - private boolean requestContent() { + protected boolean requestContent(int code) { if (mContext == null || mVoicemailUri == null) { return false; } - if (mFetchResultHandler != null) { - mFetchResultHandler.destroy(); - } - - mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri); + FetchResultHandler tempFetchResultHandler = + new FetchResultHandler(new Handler(), mVoicemailUri, code); - mView.setIsFetchingContent(); + switch (code) { + case ARCHIVE_REQUEST: + mArchiveResultHandlers.add(tempFetchResultHandler); + break; + default: + if (mFetchResultHandler != null) { + mFetchResultHandler.destroy(); + } + mView.setIsFetchingContent(); + mFetchResultHandler = tempFetchResultHandler; + break; + } // Send voicemail fetch request. Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri); @@ -461,14 +491,18 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene private class FetchResultHandler extends ContentObserver implements Runnable { private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true); private final Handler mFetchResultHandler; + private final Uri mVoicemailUri; + private final int mRequestCode; + private Uri mArchivedVoicemailUri; - public FetchResultHandler(Handler handler, Uri voicemailUri) { + public FetchResultHandler(Handler handler, Uri uri, int code) { super(handler); mFetchResultHandler = handler; - + mRequestCode = code; + mVoicemailUri = uri; if (mContext != null) { mContext.getContentResolver().registerContentObserver( - voicemailUri, false, this); + mVoicemailUri, false, this); mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS); } } @@ -481,7 +515,11 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene if (mIsWaitingForResult.getAndSet(false) && mContext != null) { mContext.getContentResolver().unregisterContentObserver(this); if (mView != null) { - mView.setFetchContentTimeout(); + if (mRequestCode == ARCHIVE_REQUEST) { + notifyUiOfArchiveResult(mVoicemailUri, false); + } else { + mView.setFetchContentTimeout(); + } } } } @@ -497,9 +535,16 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene public void onChange(boolean selfChange) { mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE, new AsyncTask() { + @Override public Boolean doInBackground(Void... params) { - return queryHasContent(mVoicemailUri); + boolean hasContent = queryHasContent(mVoicemailUri); + if (hasContent && mRequestCode == ARCHIVE_REQUEST) { + mArchivedVoicemailUri = + performArchiveVoicemailOnBackgroundThread(mVoicemailUri, true); + return mArchivedVoicemailUri != null; + } + return hasContent; } @Override @@ -507,7 +552,12 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) { mContext.getContentResolver().unregisterContentObserver( FetchResultHandler.this); - prepareContent(); + switch (mRequestCode) { + case ARCHIVE_REQUEST: + notifyUiOfArchiveResult(mVoicemailUri, true); + default: + prepareContent(); + } } } }); @@ -522,7 +572,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene * media player. If preparation is successful, the media player will {@link #onPrepared()}, * and it will call {@link #onError()} otherwise. */ - private void prepareContent() { + protected void prepareContent() { if (mView == null) { return; } @@ -564,10 +614,8 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene mIsPrepared = true; // Update the duration in the database if it was not previously retrieved - if (mDuration.get() == 0) { - CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri, - mMediaPlayer.getDuration() / 1000); - } + CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri, + TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getDuration())); mDuration.set(mMediaPlayer.getDuration()); @@ -593,7 +641,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene return true; } - private void handleError(Exception e) { + protected void handleError(Exception e) { Log.d(TAG, "handleError: Could not play voicemail " + e); if (mIsPrepared) { @@ -664,7 +712,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene if (!hasContent) { // No local content, download from server. Queue playing if the request was // issued, - mIsPlaying = requestContent(); + mIsPlaying = requestContent(PLAYBACK_REQUEST); } else { // Queue playing once the media play loaded the content. mIsPlaying = true; @@ -831,6 +879,17 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0; } + public void notifyUiOfArchiveResult(Uri voicemailUri, boolean archived) { + if (mView == null) { + return; + } + if (archived) { + mView.onVoicemailArchiveSucceded(voicemailUri); + } else { + mView.onVoicemailArchiveFailed(voicemailUri); + } + } + /* package */ void onVoicemailDeleted() { // Trampoline the event notification to the interested listener. if (mOnVoicemailDeletedListener != null) { @@ -859,6 +918,154 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene return mScheduledExecutorService; } + /** + * If voicemail has already been downloaded, go straight to archiving. Otherwise, request + * the voicemail content first. + */ + public void archiveContent(Uri voicemailUri, boolean archivedByUser) { + if (!mIsPrepared) { + requestContent(ARCHIVE_REQUEST); + } else { + startArchiveVoicemailTask(voicemailUri, archivedByUser); + } + } + + /** + * Asynchronous task used to archive a voicemail given its uri. + */ + private void startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser) { + mAsyncTaskExecutor.submit(Tasks.ARCHIVE_VOICEMAIL, new AsyncTask() { + @Override + public Uri doInBackground(Void... params) { + return performArchiveVoicemailOnBackgroundThread(voicemailUri, archivedByUser); + } + + @Override + public void onPostExecute(Uri archivedVoicemailUri) { + notifyUiOfArchiveResult(voicemailUri, archivedVoicemailUri != null); + } + }); + } + + /** + * Copy the voicemail information to the local dialer database, and copy + * the voicemail content to a local file in the dialer application's + * internal storage (voicemails directory). + * + * @param voicemailUri the uri of the voicemail to archive + * @return If archive was successful, archived voicemail URI, otherwise null. + */ + private Uri performArchiveVoicemailOnBackgroundThread(Uri voicemailUri, + boolean archivedByUser) { + Cursor callLogInfo = mContext.getContentResolver().query( + ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, + ContentUris.parseId(mVoicemailUri)), + CallLogQuery._PROJECTION, null, null, null); + Cursor contentInfo = mContext.getContentResolver().query( + voicemailUri, null, null, null, null); + + if (callLogInfo == null || contentInfo == null) { + return null; + } + + callLogInfo.moveToFirst(); + contentInfo.moveToFirst(); + + // Create values to insert into database + ContentValues values = new ContentValues(); + values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER, + contentInfo.getString(contentInfo.getColumnIndex( + VoicemailContract.Voicemails.NUMBER))); + + values.put(VoicemailArchiveContract.VoicemailArchive.DATE, + contentInfo.getLong(contentInfo.getColumnIndex( + VoicemailContract.Voicemails.DATE))); + + values.put(VoicemailArchiveContract.VoicemailArchive.DURATION, + contentInfo.getLong(contentInfo.getColumnIndex( + VoicemailContract.Voicemails.DURATION))); + + values.put(VoicemailArchiveContract.VoicemailArchive.MIME_TYPE, + contentInfo.getString(contentInfo.getColumnIndex( + VoicemailContract.Voicemails.MIME_TYPE))); + + values.put(VoicemailArchiveContract.VoicemailArchive.COUNTRY_ISO, + callLogInfo.getString(CallLogQuery.COUNTRY_ISO)); + + values.put(VoicemailArchiveContract.VoicemailArchive.GEOCODED_LOCATION, + callLogInfo.getString(CallLogQuery.GEOCODED_LOCATION)); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NAME, + callLogInfo.getString(CallLogQuery.CACHED_NAME)); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_TYPE, + callLogInfo.getInt(CallLogQuery.CACHED_NUMBER_TYPE)); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NUMBER_LABEL, + callLogInfo.getString(CallLogQuery.CACHED_NUMBER_LABEL)); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_LOOKUP_URI, + callLogInfo.getString(CallLogQuery.CACHED_LOOKUP_URI)); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_MATCHED_NUMBER, + callLogInfo.getString(CallLogQuery.CACHED_MATCHED_NUMBER)); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_NORMALIZED_NUMBER, + callLogInfo.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER)); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_FORMATTED_NUMBER, + callLogInfo.getString(CallLogQuery.CACHED_FORMATTED_NUMBER)); + + values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, archivedByUser); + + values.put(VoicemailArchiveContract.VoicemailArchive.NUMBER_PRESENTATION, + callLogInfo.getInt(CallLogQuery.NUMBER_PRESENTATION)); + + values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_COMPONENT_NAME, + callLogInfo.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME)); + + values.put(VoicemailArchiveContract.VoicemailArchive.ACCOUNT_ID, + callLogInfo.getString(CallLogQuery.ACCOUNT_ID)); + + values.put(VoicemailArchiveContract.VoicemailArchive.FEATURES, + callLogInfo.getInt(CallLogQuery.FEATURES)); + + values.put(VoicemailArchiveContract.VoicemailArchive.SERVER_ID, + contentInfo.getInt(contentInfo.getColumnIndex( + VoicemailContract.Voicemails._ID))); + + values.put(VoicemailArchiveContract.VoicemailArchive.TRANSCRIPTION, + contentInfo.getString(contentInfo.getColumnIndex( + VoicemailContract.Voicemails.TRANSCRIPTION))); + + values.put(VoicemailArchiveContract.VoicemailArchive.CACHED_PHOTO_URI, + callLogInfo.getLong(CallLogQuery.CACHED_PHOTO_URI)); + + callLogInfo.close(); + contentInfo.close(); + + // Insert info into dialer database + Uri archivedVoicemailUri = mContext.getContentResolver().insert( + VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, values); + try { + // Copy voicemail content to a local file + InputStream inputStream = mContext.getContentResolver() + .openInputStream(voicemailUri); + OutputStream outputStream = mContext.getContentResolver() + .openOutputStream(archivedVoicemailUri); + + ByteStreams.copy(inputStream, outputStream); + inputStream.close(); + outputStream.close(); + } catch (IOException e) { + // Roll back insert if new file creation failed + mContext.getContentResolver().delete(archivedVoicemailUri, null, null); + Log.w(TAG, "Failed to copy voicemail content to temporary file"); + return null; + } + return archivedVoicemailUri; + } + @VisibleForTesting public boolean isPlaying() { return mIsPlaying; -- cgit v1.2.3