diff options
Diffstat (limited to 'src/com/android/dialer/voicemail')
8 files changed, 1644 insertions, 152 deletions
diff --git a/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java b/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java new file mode 100644 index 000000000..80a0368bd --- /dev/null +++ b/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java @@ -0,0 +1,103 @@ +package com.android.dialer.voicemail; + + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; + +import com.android.dialer.calllog.CallLogQueryHandler; + +/** + * Helper class to check whether visual voicemail is enabled. + * + * Call isVisualVoicemailEnabled() to retrieve the result. + * + * The result is cached and saved in a SharedPreferences, stored as a boolean in + * PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER. Every time a new instance is created, it will try to + * restore the cached result from the SharedPreferences. + * + * Call asyncUpdate() to make a CallLogQuery to check the actual status. This is a async call so + * isVisualVoicemailEnabled() will not be affected immediately. + * + * If the status has changed as a result of asyncUpdate(), + * Callback.onVisualVoicemailEnabledStatusChanged() will be called with the new value. + */ +public class VisualVoicemailEnabledChecker implements CallLogQueryHandler.Listener { + + public static final String PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER = + "has_active_voicemail_provider"; + private SharedPreferences mPrefs; + private boolean mHasActiveVoicemailProvider; + private CallLogQueryHandler mCallLogQueryHandler; + private VoicemailStatusHelper mVoicemailStatusHelper; + private Context mContext; + + public interface Callback { + + /** + * Callback to notify enabled status has changed to the @param newValue + */ + void onVisualVoicemailEnabledStatusChanged(boolean newValue); + } + + private Callback mCallback; + + public VisualVoicemailEnabledChecker(Context context, @Nullable Callback callback) { + mContext = context; + mCallback = callback; + mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); + mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); + mHasActiveVoicemailProvider = mPrefs.getBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, + false); + } + + /** + * @return whether visual voicemail is enabled. Result is cached, call asyncUpdate() to + * update the result. + */ + public boolean isVisualVoicemailEnabled() { + return mHasActiveVoicemailProvider; + } + + /** + * Perform an async query into the system to check the status of visual voicemail. + * If the status has changed, Callback.onVisualVoicemailEnabledStatusChanged() will be called. + */ + public void asyncUpdate() { + mCallLogQueryHandler = + new CallLogQueryHandler(mContext, mContext.getContentResolver(), this); + mCallLogQueryHandler.fetchVoicemailStatus(); + } + + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) { + boolean hasActiveVoicemailProvider = + mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0; + if (hasActiveVoicemailProvider != mHasActiveVoicemailProvider) { + mHasActiveVoicemailProvider = hasActiveVoicemailProvider; + mPrefs.edit().putBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, + mHasActiveVoicemailProvider); + if (mCallback != null) { + mCallback.onVisualVoicemailEnabledStatusChanged(mHasActiveVoicemailProvider); + } + } + } + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public boolean onCallsFetched(Cursor combinedCursor) { + // Do nothing + return false; + } +} 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..5f73d1689 --- /dev/null +++ b/src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java @@ -0,0 +1,90 @@ +/* + * 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<Void, Void, Boolean>() { + @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); + } + return false; + } + + @Override + public void onPostExecute(Boolean hasContent) { + callback.onContentChecked(hasContent); + } + }); + } + + @Override + protected void startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser) { + // If a user wants to share an archived voicemail, no need for archiving, just go straight + // to share intent. + if (!archivedByUser) { + sendShareIntent(voicemailUri); + } + } + + @Override + protected boolean requestContent(int code) { + handleError(new FileNotFoundException("Voicemail archive file does not exist")); + return false; // No way for archive tab to request content + } +} diff --git a/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java b/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java new file mode 100644 index 000000000..7abf9a72c --- /dev/null +++ b/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java @@ -0,0 +1,346 @@ +/* + * 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 com.android.contacts.common.testing.NeededForTesting; +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.google.common.base.Preconditions; +import com.google.common.io.ByteStreams; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.CallLog; +import android.provider.VoicemailContract; +import android.util.Log; +import com.android.common.io.MoreCloseables; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.annotation.Nullable; + +/** + * Class containing asynchronous tasks for voicemails. + */ +@NeededForTesting +public class VoicemailAsyncTaskUtil { + private static final String TAG = "VoicemailAsyncTaskUtil"; + + /** The enumeration of {@link AsyncTask} objects we use in this class. */ + public enum Tasks { + GET_VOICEMAIL_FILE_PATH, + SET_VOICEMAIL_ARCHIVE_STATUS, + ARCHIVE_VOICEMAIL_CONTENT + } + + @NeededForTesting + public interface OnArchiveVoicemailListener { + /** + * Called after the voicemail has been archived. + * + * @param archivedVoicemailUri the URI of the archived voicemail + */ + void onArchiveVoicemail(@Nullable Uri archivedVoicemailUri); + } + + @NeededForTesting + public interface OnSetVoicemailArchiveStatusListener { + /** + * Called after the voicemail archived_by_user column is updated. + * + * @param success whether the update was successful or not + */ + void onSetVoicemailArchiveStatus(boolean success); + } + + @NeededForTesting + public interface OnGetArchivedVoicemailFilePathListener { + /** + * Called after the voicemail file path is obtained. + * + * @param filePath the file path of the archived voicemail + */ + void onGetArchivedVoicemailFilePath(@Nullable String filePath); + } + + private final ContentResolver mResolver; + private final AsyncTaskExecutor mAsyncTaskExecutor; + + @NeededForTesting + public VoicemailAsyncTaskUtil(ContentResolver contentResolver) { + mResolver = Preconditions.checkNotNull(contentResolver); + mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); + } + + /** + * Returns the archived voicemail file path. + */ + @NeededForTesting + public void getVoicemailFilePath( + final OnGetArchivedVoicemailFilePathListener listener, + final Uri voicemailUri) { + Preconditions.checkNotNull(listener); + Preconditions.checkNotNull(voicemailUri); + mAsyncTaskExecutor.submit(Tasks.GET_VOICEMAIL_FILE_PATH, + new AsyncTask<Void, Void, String>() { + @Nullable + @Override + protected String doInBackground(Void... params) { + try (Cursor cursor = mResolver.query(voicemailUri, + new String[]{VoicemailArchiveContract.VoicemailArchive._DATA}, + null, null, null)) { + if (hasContent(cursor)) { + return cursor.getString(cursor.getColumnIndex( + VoicemailArchiveContract.VoicemailArchive._DATA)); + } + } + return null; + } + + @Override + protected void onPostExecute(String filePath) { + listener.onGetArchivedVoicemailFilePath(filePath); + } + }); + } + + /** + * Updates the archived_by_user flag of the archived voicemail. + */ + @NeededForTesting + public void setVoicemailArchiveStatus( + final OnSetVoicemailArchiveStatusListener listener, + final Uri voicemailUri, + final boolean archivedByUser) { + Preconditions.checkNotNull(listener); + Preconditions.checkNotNull(voicemailUri); + mAsyncTaskExecutor.submit(Tasks.SET_VOICEMAIL_ARCHIVE_STATUS, + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... params) { + ContentValues values = new ContentValues(1); + values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, + archivedByUser); + return mResolver.update(voicemailUri, values, null, null) > 0; + } + + @Override + protected void onPostExecute(Boolean success) { + listener.onSetVoicemailArchiveStatus(success); + } + }); + } + + /** + * Checks if a voicemail has already been archived, if so, return the previously archived URI. + * Otherwise, copy the voicemail information to the local dialer database. If archive was + * successful, archived voicemail URI is returned to listener, otherwise null. + */ + @NeededForTesting + public void archiveVoicemailContent( + final OnArchiveVoicemailListener listener, + final Uri voicemailUri) { + Preconditions.checkNotNull(listener); + Preconditions.checkNotNull(voicemailUri); + mAsyncTaskExecutor.submit(Tasks.ARCHIVE_VOICEMAIL_CONTENT, + new AsyncTask<Void, Void, Uri>() { + @Nullable + @Override + protected Uri doInBackground(Void... params) { + Uri archivedVoicemailUri = getArchivedVoicemailUri(voicemailUri); + + // If previously archived, return uri, otherwise archive everything. + if (archivedVoicemailUri != null) { + return archivedVoicemailUri; + } + + // Combine call log and voicemail content info. + ContentValues values = getVoicemailContentValues(voicemailUri); + if (values == null) { + return null; + } + + Uri insertedVoicemailUri = mResolver.insert( + VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, values); + if (insertedVoicemailUri == null) { + return null; + } + + // Copy voicemail content to a new file. + boolean copiedFile = false; + try (InputStream inputStream = mResolver.openInputStream(voicemailUri); + OutputStream outputStream = + mResolver.openOutputStream(insertedVoicemailUri)) { + if (inputStream != null && outputStream != null) { + ByteStreams.copy(inputStream, outputStream); + copiedFile = true; + return insertedVoicemailUri; + } + } catch (IOException e) { + Log.w(TAG, "Failed to copy voicemail content to new file: " + + e.toString()); + } finally { + if (!copiedFile) { + // Roll back insert if the voicemail content was not copied. + mResolver.delete(insertedVoicemailUri, null, null); + } + } + return null; + } + + @Override + protected void onPostExecute(Uri archivedVoicemailUri) { + listener.onArchiveVoicemail(archivedVoicemailUri); + } + }); + } + + /** + * Helper method to get the archived URI of a voicemail. + * + * @param voicemailUri a {@link android.provider.VoicemailContract.Voicemails#CONTENT_URI} URI. + * @return the URI of the archived voicemail or {@code null} + */ + @Nullable + private Uri getArchivedVoicemailUri(Uri voicemailUri) { + try (Cursor cursor = getArchiveExistsCursor(voicemailUri)) { + if (hasContent(cursor)) { + return VoicemailArchiveContract.VoicemailArchive + .buildWithId(cursor.getInt(cursor.getColumnIndex( + VoicemailArchiveContract.VoicemailArchive._ID))); + } + } + return null; + } + + /** + * Helper method to make a copy of all the values needed to display a voicemail. + * + * @param voicemailUri a {@link VoicemailContract.Voicemails#CONTENT_URI} URI. + * @return the combined call log and voicemail values for the given URI, or {@code null} + */ + @Nullable + private ContentValues getVoicemailContentValues(Uri voicemailUri) { + try (Cursor callLogInfo = getCallLogInfoCursor(voicemailUri); + Cursor contentInfo = getContentInfoCursor(voicemailUri)) { + + if (hasContent(callLogInfo) && hasContent(contentInfo)) { + // Create values to insert into database. + ContentValues values = new ContentValues(); + + // Insert voicemail call log info. + 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.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.CACHED_PHOTO_URI, + callLogInfo.getString(CallLogQuery.CACHED_PHOTO_URI)); + + // Insert voicemail content info. + values.put(VoicemailArchiveContract.VoicemailArchive.SERVER_ID, + contentInfo.getInt(contentInfo.getColumnIndex( + VoicemailContract.Voicemails._ID))); + 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.TRANSCRIPTION, + contentInfo.getString(contentInfo.getColumnIndex( + VoicemailContract.Voicemails.TRANSCRIPTION))); + + // Achived is false by default because it is updated after insertion. + values.put(VoicemailArchiveContract.VoicemailArchive.ARCHIVED, false); + + return values; + } + } + return null; + } + + private boolean hasContent(@Nullable Cursor cursor) { + return cursor != null && cursor.moveToFirst(); + } + + @Nullable + private Cursor getCallLogInfoCursor(Uri voicemailUri) { + return mResolver.query( + ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, + ContentUris.parseId(voicemailUri)), + CallLogQuery._PROJECTION, null, null, null); + } + + @Nullable + private Cursor getContentInfoCursor(Uri voicemailUri) { + return mResolver.query(voicemailUri, + new String[] { + VoicemailContract.Voicemails._ID, + VoicemailContract.Voicemails.NUMBER, + VoicemailContract.Voicemails.DATE, + VoicemailContract.Voicemails.DURATION, + VoicemailContract.Voicemails.MIME_TYPE, + VoicemailContract.Voicemails.TRANSCRIPTION, + }, null, null, null); + } + + @Nullable + private Cursor getArchiveExistsCursor(Uri voicemailUri) { + return mResolver.query(VoicemailArchiveContract.VoicemailArchive.CONTENT_URI, + new String[] {VoicemailArchiveContract.VoicemailArchive._ID}, + VoicemailArchiveContract.VoicemailArchive.SERVER_ID + "=" + + ContentUris.parseId(voicemailUri), + null, + null); + } +} diff --git a/src/com/android/dialer/voicemail/VoicemailAudioManager.java b/src/com/android/dialer/voicemail/VoicemailAudioManager.java new file mode 100644 index 000000000..fe6cf5f45 --- /dev/null +++ b/src/com/android/dialer/voicemail/VoicemailAudioManager.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2015 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.Context; +import android.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.telecom.CallAudioState; +import android.util.Log; + +import java.util.concurrent.RejectedExecutionException; + +/** + * This class manages all audio changes for voicemail playback. + */ +final class VoicemailAudioManager implements OnAudioFocusChangeListener, + WiredHeadsetManager.Listener { + private static final String TAG = VoicemailAudioManager.class.getSimpleName(); + + public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; + + private AudioManager mAudioManager; + private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + private WiredHeadsetManager mWiredHeadsetManager; + private boolean mWasSpeakerOn; + private CallAudioState mCallAudioState; + + public VoicemailAudioManager(Context context, + VoicemailPlaybackPresenter voicemailPlaybackPresenter) { + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; + mWiredHeadsetManager = new WiredHeadsetManager(context); + mWiredHeadsetManager.setListener(this); + + mCallAudioState = getInitialAudioState(); + Log.i(TAG, "Initial audioState = " + mCallAudioState); + } + + public void requestAudioFocus() { + int result = mAudioManager.requestAudioFocus( + this, + PLAYBACK_STREAM, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + throw new RejectedExecutionException("Could not capture audio focus."); + } + } + + public void abandonAudioFocus() { + mAudioManager.abandonAudioFocus(this); + } + + @Override + public void onAudioFocusChange(int focusChange) { + Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange); + mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN); + } + + @Override + public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) { + Log.i(TAG, "wired headset was plugged in changed: " + oldIsPluggedIn + + " -> "+ newIsPluggedIn); + + if (oldIsPluggedIn == newIsPluggedIn) { + return; + } + + int newRoute = mCallAudioState.getRoute(); // start out with existing route + if (newIsPluggedIn) { + newRoute = CallAudioState.ROUTE_WIRED_HEADSET; + } else { + if (mWasSpeakerOn) { + newRoute = CallAudioState.ROUTE_SPEAKER; + } else { + newRoute = CallAudioState.ROUTE_EARPIECE; + } + } + + mVoicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER); + + // We need to call this every time even if we do not change the route because the supported + // routes changed either to include or not include WIRED_HEADSET. + setSystemAudioState( + new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes())); + } + + public void setSpeakerphoneOn(boolean on) { + setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE); + } + + public boolean isWiredHeadsetPluggedIn() { + return mWiredHeadsetManager.isPluggedIn(); + } + + public void registerReceivers() { + // Receivers is plural because we expect to add bluetooth support. + mWiredHeadsetManager.registerReceiver(); + } + + public void unregisterReceivers() { + mWiredHeadsetManager.unregisterReceiver(); + } + + /** + * Change the audio route, for example from earpiece to speakerphone. + * + * @param route The new audio route to use. See {@link CallAudioState}. + */ + void setAudioRoute(int route) { + Log.v(TAG, "setAudioRoute, route: " + CallAudioState.audioRouteToString(route)); + + // Change ROUTE_WIRED_OR_EARPIECE to a single entry. + int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask()); + + // If route is unsupported, do nothing. + if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) { + Log.w(TAG, "Asking to set to a route that is unsupported: " + newRoute); + return; + } + + if (mCallAudioState.getRoute() != newRoute) { + // Remember the new speaker state so it can be restored when the user plugs and unplugs + // a headset. + mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER; + setSystemAudioState(new CallAudioState(false /* muted */, newRoute, + mCallAudioState.getSupportedRouteMask())); + } + } + + private CallAudioState getInitialAudioState() { + int supportedRouteMask = calculateSupportedRoutes(); + int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, + supportedRouteMask); + return new CallAudioState(false /* muted */, route, supportedRouteMask); + } + + private int calculateSupportedRoutes() { + int routeMask = CallAudioState.ROUTE_SPEAKER; + if (mWiredHeadsetManager.isPluggedIn()) { + routeMask |= CallAudioState.ROUTE_WIRED_HEADSET; + } else { + routeMask |= CallAudioState.ROUTE_EARPIECE; + } + return routeMask; + } + + private int selectWiredOrEarpiece(int route, int supportedRouteMask) { + // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of + // ROUTE_WIRED_OR_EARPIECE so that callers don't have to make a call to check which is + // supported before calling setAudioRoute. + if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) { + route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask; + if (route == 0) { + Log.wtf(TAG, "One of wired headset or earpiece should always be valid."); + // assume earpiece in this case. + route = CallAudioState.ROUTE_EARPIECE; + } + } + return route; + } + + private void setSystemAudioState(CallAudioState callAudioState) { + CallAudioState oldAudioState = mCallAudioState; + mCallAudioState = callAudioState; + + Log.i(TAG, "setSystemAudioState: changing from " + oldAudioState + " to " + + mCallAudioState); + + // Audio route. + if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) { + turnOnSpeaker(true); + } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE || + mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) { + // Just handle turning off the speaker, the system will handle switching between wired + // headset and earpiece. + turnOnSpeaker(false); + } + } + + private void turnOnSpeaker(boolean on) { + if (mAudioManager.isSpeakerphoneOn() != on) { + Log.i(TAG, "turning speaker phone on: " + on); + mAudioManager.setSpeakerphoneOn(on); + } + } +} diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java index c918d7944..d4d294e8d 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java @@ -16,35 +16,46 @@ package com.android.dialer.voicemail; -import android.app.Activity; -import android.app.Fragment; +import android.content.ContentUris; import android.content.Context; -import android.media.MediaPlayer; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Bundle; -import android.os.PowerManager; -import android.provider.VoicemailContract; +import android.os.AsyncTask; +import android.os.Handler; 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.Space; 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.google.common.base.Preconditions; +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.android.dialerbind.ObjectFactory; +import com.google.common.annotations.VisibleForTesting; +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; @@ -58,8 +69,16 @@ import javax.annotation.concurrent.ThreadSafe; */ @NotThreadSafe public class VoicemailPlaybackLayout extends LinearLayout - implements VoicemailPlaybackPresenter.PlaybackView { + implements VoicemailPlaybackPresenter.PlaybackView, + 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. @@ -145,6 +164,11 @@ public class VoicemailPlaybackLayout extends LinearLayout @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { setClipPosition(progress, seekBar.getMax()); + // Update the seek position if user manually changed it. This makes sure position gets + // updated when user use volume button to seek playback in talkback mode. + if (fromUser) { + mPresenter.seek(progress); + } } }; @@ -155,7 +179,7 @@ public class VoicemailPlaybackLayout extends LinearLayout @Override public void onClick(View v) { if (mPresenter != null) { - onSpeakerphoneOn(!mPresenter.isSpeakerphoneOn()); + mPresenter.toggleSpeakerphone(); } } }; @@ -185,26 +209,96 @@ public class VoicemailPlaybackLayout extends LinearLayout return; } mPresenter.pausePlayback(); - CallLogAsyncTaskUtil.deleteVoicemail(mContext, mVoicemailUri, null); mPresenter.onVoicemailDeleted(); + + final Uri deleteUri = mVoicemailUri; + final Runnable deleteCallback = new Runnable() { + @Override + public void run() { + if (Objects.equals(deleteUri, mVoicemailUri)) { + CallLogAsyncTaskUtil.deleteVoicemail(mContext, deleteUri, + VoicemailPlaybackLayout.this); + } + } + }; + + final Handler handler = new Handler(); + // Add a little buffer time in case the user clicked "undo" at the end of the delay + // window. + handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50); + + Snackbar.make(VoicemailPlaybackLayout.this, R.string.snackbar_voicemail_deleted, + Snackbar.LENGTH_LONG) + .setDuration(VOICEMAIL_DELETE_DELAY_MS) + .setAction(R.string.snackbar_voicemail_deleted_undo, + new View.OnClickListener() { + @Override + public void onClick(View view) { + mPresenter.onVoicemailDeleteUndo(); + handler.removeCallbacks(deleteCallback); + } + }) + .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 final View.OnClickListener mShareButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mPresenter == null || isArchiving(mVoicemailUri)) { + return; + } + disableUiElements(); + mPresenter.archiveContent(mVoicemailUri, false); } }; 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<Uri> mIsArchiving = new ArrayList<>(); private SeekBar mPlaybackSeek; private ImageButton mStartStopButton; private ImageButton mPlaybackSpeakerphone; private ImageButton mDeleteButton; + private ImageButton mArchiveButton; + private ImageButton mShareButton; + + private Space mArchiveSpace; + private Space mShareSpace; + private TextView mStateText; private TextView mPositionText; private TextView mTotalDurationText; private PositionUpdater mPositionUpdater; + private Drawable mVoicemailSeekHandleEnabled; + private Drawable mVoicemailSeekHandleDisabled; public VoicemailPlaybackLayout(Context context) { this(context, null); @@ -212,7 +306,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); @@ -223,6 +316,16 @@ public class VoicemailPlaybackLayout extends LinearLayout public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) { mPresenter = presenter; mVoicemailUri = voicemailUri; + if (ObjectFactory.isVoicemailArchiveEnabled(mContext)) { + updateArchiveUI(mVoicemailUri); + updateArchiveButton(mVoicemailUri); + } + + if (ObjectFactory.isVoicemailShareEnabled(mContext)) { + // Show share button and space before it + mShareSpace.setVisibility(View.VISIBLE); + mShareButton.setVisibility(View.VISIBLE); + } } @Override @@ -233,6 +336,12 @@ 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); + mShareButton = (ImageButton) findViewById(R.id.share_voicemail); + + mArchiveSpace = (Space) findViewById(R.id.space_before_archive_voicemail); + mShareSpace = (Space) findViewById(R.id.space_before_share_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); @@ -241,6 +350,16 @@ public class VoicemailPlaybackLayout extends LinearLayout mStartStopButton.setOnClickListener(mStartStopButtonListener); mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener); mDeleteButton.setOnClickListener(mDeleteButtonListener); + mArchiveButton.setOnClickListener(mArchiveButtonListener); + mShareButton.setOnClickListener(mShareButtonListener); + + mPositionText.setText(formatAsMinutesAndSeconds(0)); + mTotalDurationText.setText(formatAsMinutesAndSeconds(0)); + + mVoicemailSeekHandleEnabled = getResources().getDrawable( + R.drawable.ic_voicemail_seek_handle, mContext.getTheme()); + mVoicemailSeekHandleDisabled = getResources().getDrawable( + R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme()); } @Override @@ -249,10 +368,6 @@ public class VoicemailPlaybackLayout extends LinearLayout mStartStopButton.setImageResource(R.drawable.ic_pause); - if (mPresenter != null) { - onSpeakerphoneOn(mPresenter.isSpeakerphoneOn()); - } - if (mPositionUpdater != null) { mPositionUpdater.stopUpdating(); mPositionUpdater = null; @@ -283,12 +398,8 @@ public class VoicemailPlaybackLayout extends LinearLayout mStateText.setText(getString(R.string.voicemail_playback_error)); } - + @Override public void onSpeakerphoneOn(boolean on) { - if (mPresenter != null) { - mPresenter.setSpeakerphoneOn(on); - } - if (on) { mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp); // Speaker is now on, tapping button will turn it off. @@ -314,13 +425,6 @@ public class VoicemailPlaybackLayout extends LinearLayout mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs)); mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs)); - mStateText.setText(null); - } - - @Override - public void setIsBuffering() { - disableUiElements(); - mStateText.setText(getString(R.string.voicemail_buffering)); } @Override @@ -331,7 +435,7 @@ public class VoicemailPlaybackLayout extends LinearLayout @Override public void setFetchContentTimeout() { - disableUiElements(); + mStartStopButton.setEnabled(true); mStateText.setText(getString(R.string.voicemail_fetching_timout)); } @@ -343,21 +447,35 @@ public class VoicemailPlaybackLayout extends LinearLayout @Override public void disableUiElements() { mStartStopButton.setEnabled(false); - mPlaybackSpeakerphone.setEnabled(false); - mPlaybackSeek.setProgress(0); - mPlaybackSeek.setEnabled(false); - - mPositionText.setText(formatAsMinutesAndSeconds(0)); - mTotalDurationText.setText(formatAsMinutesAndSeconds(0)); + resetSeekBar(); } @Override public void enableUiElements() { + mDeleteButton.setEnabled(true); mStartStopButton.setEnabled(true); - mPlaybackSpeakerphone.setEnabled(true); mPlaybackSeek.setEnabled(true); + mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled); } + @Override + public void resetSeekBar() { + mPlaybackSeek.setProgress(0); + mPlaybackSeek.setEnabled(false); + mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled); + } + + @Override + public void onDeleteCall() {} + + @Override + public void onDeleteVoicemail() { + mPresenter.onVoicemailDeletedInDatabase(); + } + + @Override + public void onGetCallDetails(PhoneCallDetails[] details) {} + private String getString(int resId) { return mContext.getString(resId); } @@ -377,4 +495,139 @@ 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() { + mArchiveSpace.setVisibility(View.GONE); + 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<Void, Void, Boolean>() { + @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 { + mArchiveSpace.setVisibility(View.VISIBLE); + 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 ed6cc8b43..5924fb453 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java @@ -16,14 +16,14 @@ package com.android.dialer.voicemail; +import com.google.common.annotations.VisibleForTesting; + import android.app.Activity; import android.content.Context; import android.content.ContentResolver; import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; -import android.media.AudioManager; -import android.media.AudioManager.OnAudioFocusChangeListener; import android.media.MediaPlayer; import android.net.Uri; import android.os.AsyncTask; @@ -31,25 +31,24 @@ import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.provider.VoicemailContract; +import android.support.v4.content.FileProvider; import android.util.Log; -import android.view.View; import android.view.WindowManager.LayoutParams; -import android.widget.SeekBar; import com.android.dialer.R; +import com.android.dialer.calllog.CallLogAsyncTaskUtil; import com.android.dialer.util.AsyncTaskExecutor; import com.android.dialer.util.AsyncTaskExecutors; - import com.android.common.io.MoreCloseables; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; +import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -62,7 +61,7 @@ import javax.annotation.concurrent.ThreadSafe; * {@link CallLogFragment} and {@link CallLogAdapter}. * <p> * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single - * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. This + * instance can be reused for different such layouts, using {@link #setPlaybackView}. This * is to facilitate reuse across different voicemail call log entries. * <p> * This class is not thread safe. The thread policy for this class is thread-confinement, all calls @@ -70,11 +69,10 @@ import javax.annotation.concurrent.ThreadSafe; */ @NotThreadSafe @VisibleForTesting -public class VoicemailPlaybackPresenter - implements OnAudioFocusChangeListener, MediaPlayer.OnPreparedListener, +public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { - private static final String TAG = VoicemailPlaybackPresenter.class.getSimpleName(); + private static final String TAG = "VmPlaybackPresenter"; /** Contract describing the behaviour we need from the ui we are controlling. */ public interface PlaybackView { @@ -87,26 +85,35 @@ public class VoicemailPlaybackPresenter void onSpeakerphoneOn(boolean on); void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); void setFetchContentTimeout(); - void setIsBuffering(); void setIsFetchingContent(); + void onVoicemailArchiveSucceded(Uri voicemailUri); + void onVoicemailArchiveFailed(Uri voicemailUri); void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); + void resetSeekBar(); } public interface OnVoicemailDeletedListener { void onVoicemailDeleted(Uri uri); + void onVoicemailDeleteUndo(); + void onVoicemailDeletedInDatabase(); } /** The enumeration of {@link AsyncTask} objects we use in this class. */ public enum Tasks { CHECK_FOR_CONTENT, CHECK_CONTENT_AFTER_CHANGE, + ARCHIVE_VOICEMAIL + } + + protected interface OnContentCheckedListener { + void onContentChecked(boolean hasContent); } private static final String[] HAS_CONTENT_PROJECTION = new String[] { VoicemailContract.Voicemails.HAS_CONTENT, + VoicemailContract.Voicemails.DURATION }; - public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; private static final int NUMBER_OF_THREADS_IN_POOL = 2; // Time to wait for content to be fetched before timing out. private static final long FETCH_CONTENT_TIMEOUT_MS = 20000; @@ -121,6 +128,11 @@ public class VoicemailPlaybackPresenter // If present in the saved instance bundle, indicates where to set the playback slider. private static final String CLIP_POSITION_KEY = 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; + public static final int SHARE_REQUEST = 2; /** * The most recently cached duration. We cache this since we don't want to keep requesting it @@ -132,22 +144,23 @@ public class VoicemailPlaybackPresenter 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 // exposes its prepared state. Store this locally, so we can check and prevent crashes. private boolean mIsPrepared; + private boolean mIsSpeakerphoneOn; private boolean mShouldResumePlaybackAfterSeeking; 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. @@ -155,11 +168,13 @@ public class VoicemailPlaybackPresenter * This variable is thread-contained, accessed only on the ui thread. */ private FetchResultHandler mFetchResultHandler; + private final List<FetchResultHandler> mArchiveResultHandlers = new ArrayList<>(); private Handler mHandler = new Handler(); private PowerManager.WakeLock mProximityWakeLock; - private AudioManager mAudioManager; + private VoicemailAudioManager mVoicemailAudioManager; private OnVoicemailDeletedListener mOnVoicemailDeletedListener; + private final VoicemailAsyncTaskUtil mVoicemailAsyncTaskUtil; /** * Obtain singleton instance of this class. Use a single instance to provide a consistent @@ -183,11 +198,11 @@ public class VoicemailPlaybackPresenter /** * Initialize variables which are activity-independent and state-independent. */ - private VoicemailPlaybackPresenter(Activity activity) { + protected VoicemailPlaybackPresenter(Activity activity) { Context context = activity.getApplicationContext(); mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - + mVoicemailAudioManager = new VoicemailAudioManager(context, this); + mVoicemailAsyncTaskUtil = new VoicemailAsyncTaskUtil(context.getContentResolver()); PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { @@ -199,12 +214,12 @@ public class VoicemailPlaybackPresenter /** * 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; mInitialOrientation = mContext.getResources().getConfiguration().orientation; - mActivity.setVolumeControlStream(VoicemailPlaybackPresenter.PLAYBACK_STREAM); + mActivity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM); if (savedInstanceState != null) { // Restores playback state when activity is recreated, such as after rotation. @@ -212,6 +227,7 @@ public class VoicemailPlaybackPresenter mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY); mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0); mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false); + mIsSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false); } if (mMediaPlayer == null) { @@ -229,6 +245,7 @@ public class VoicemailPlaybackPresenter outState.putBoolean(IS_PREPARED_KEY, mIsPrepared); outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); + outState.putBoolean(IS_SPEAKERPHONE_ON_KEY, mIsSpeakerphoneOn); } } @@ -240,28 +257,48 @@ public class VoicemailPlaybackPresenter mView = view; mView.setPresenter(this, voicemailUri); - if (mMediaPlayer != null && voicemailUri.equals(mVoicemailUri)) { - // Handles case where MediaPlayer was retained after an orientation change. + // Handles cases where the same entry is binded again when scrolling in list, or where + // the MediaPlayer was retained after an orientation change. + if (mMediaPlayer != null && mIsPrepared && voicemailUri.equals(mVoicemailUri)) { + // If the voicemail card was rebinded, we need to set the position to the appropriate + // point. Since we retain the media player, we can just set it to the position of the + // media player. + mPosition = mMediaPlayer.getCurrentPosition(); onPrepared(mMediaPlayer); - mView.onSpeakerphoneOn(isSpeakerphoneOn()); } else { if (!voicemailUri.equals(mVoicemailUri)) { + mVoicemailUri = voicemailUri; mPosition = 0; + // Default to earpiece. + setSpeakerphoneOn(false); + mVoicemailAudioManager.setSpeakerphoneOn(false); + } else { + // Update the view to the current speakerphone state. + mView.onSpeakerphoneOn(mIsSpeakerphoneOn); } - - mVoicemailUri = voicemailUri; - mDuration.set(0); + /* + * Check to see if the content field in the DB is set. If set, we proceed to + * prepareContent() method. We get the duration of the voicemail from the query and set + * it if the content is not available. + */ + checkForContent(new OnContentCheckedListener() { + @Override + public void onContentChecked(boolean hasContent) { + if (hasContent) { + prepareContent(); + } else if (mView != null) { + mView.resetSeekBar(); + mView.setClipPosition(0, mDuration.get()); + } + } + }); if (startPlayingImmediately) { // Since setPlaybackView can get called during the view binding process, we don't // want to reset mIsPlaying to false if the user is currently playing the // voicemail and the view is rebound. mIsPlaying = startPlayingImmediately; - checkForContent(); } - - // Default to earpiece. - mView.onSpeakerphoneOn(false); } } @@ -269,16 +306,20 @@ public class VoicemailPlaybackPresenter * Reset the presenter for playback back to its original state. */ public void resetAll() { - reset(); + pausePresenter(true); mView = null; mVoicemailUri = null; } /** - * Reset the presenter such that it is as if the voicemail has not been played. + * When navigating away from voicemail playback, we need to release the media player, + * pause the UI and save the position. + * + * @param reset {@code true} if we want to reset the position of the playback, {@code false} if + * we want to retain the current position (in case we return to the voicemail). */ - public void reset() { + public void pausePresenter(boolean reset) { if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer = null; @@ -288,19 +329,35 @@ public class VoicemailPlaybackPresenter mIsPrepared = false; mIsPlaying = false; - mPosition = 0; - mDuration.set(0); + + if (reset) { + // We want to reset the position whether or not the view is valid. + mPosition = 0; + } if (mView != null) { mView.onPlaybackStopped(); - mView.setClipPosition(0, mDuration.get()); + if (reset) { + mView.setClipPosition(0, mDuration.get()); + } else { + mPosition = mView.getDesiredClipPosition(); + } } } /** + * Must be invoked when the parent activity is resumed. + */ + public void onResume() { + mVoicemailAudioManager.registerReceivers(); + } + + /** * Must be invoked when the parent activity is paused. */ public void onPause() { + mVoicemailAudioManager.unregisterReceivers(); + if (mContext != null && mIsPrepared && mInitialOrientation != mContext.getResources().getConfiguration().orientation) { // If an orientation change triggers the pause, retain the MediaPlayer. @@ -309,11 +366,12 @@ public class VoicemailPlaybackPresenter } // Release the media player, otherwise there may be failures. - reset(); + pausePresenter(false); if (mActivity != null) { mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); } + } /** @@ -329,6 +387,13 @@ public class VoicemailPlaybackPresenter mScheduledExecutorService = null; } + if (!mArchiveResultHandlers.isEmpty()) { + for (FetchResultHandler fetchResultHandler : mArchiveResultHandlers) { + fetchResultHandler.destroy(); + } + mArchiveResultHandlers.clear(); + } + if (mFetchResultHandler != null) { mFetchResultHandler.destroy(); mFetchResultHandler = null; @@ -337,16 +402,8 @@ public class VoicemailPlaybackPresenter /** * Checks to see if we have content available for this voicemail. - * <p> - * This method will be called once, after the fragment has been created, before we know if the - * voicemail we've been asked to play has any content available. - * <p> - * Notify the user that we are fetching the content, then check to see if the content field in - * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make - * a request to fetch the content asynchronously via {@link #requestContent()}. */ - private void checkForContent() { - mView.setIsFetchingContent(); + protected void checkForContent(final OnContentCheckedListener callback) { mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() { @Override public Boolean doInBackground(Void... params) { @@ -355,11 +412,7 @@ public class VoicemailPlaybackPresenter @Override public void onPostExecute(Boolean hasContent) { - if (hasContent) { - prepareContent(); - } else { - requestContent(); - } + callback.onContentChecked(hasContent); } }); } @@ -371,10 +424,14 @@ public class VoicemailPlaybackPresenter ContentResolver contentResolver = mContext.getContentResolver(); Cursor cursor = contentResolver.query( - voicemailUri, HAS_CONTENT_PROJECTION, null, null, null); + voicemailUri, null, null, null, null); try { if (cursor != null && cursor.moveToNext()) { - return cursor.getInt(cursor.getColumnIndexOrThrow( + int duration = cursor.getInt(cursor.getColumnIndex( + VoicemailContract.Voicemails.DURATION)); + // Convert database duration (seconds) into mDuration (milliseconds) + mDuration.set(duration > 0 ? duration * 1000 : 0); + return cursor.getInt(cursor.getColumnIndex( VoicemailContract.Voicemails.HAS_CONTENT)) == 1; } } finally { @@ -395,31 +452,51 @@ public class VoicemailPlaybackPresenter * proceed to {@link #prepareContent()}. If the has_content field does not * become true within the allowed time, we will update the ui to reflect the fact that content * was not available. + * + * @return whether issued request to fetch content */ - private void requestContent() { - if (mFetchResultHandler != null) { - mFetchResultHandler.destroy(); + protected boolean requestContent(int code) { + if (mContext == null || mVoicemailUri == null) { + return false; } - mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri); + FetchResultHandler tempFetchResultHandler = + new FetchResultHandler(new Handler(), mVoicemailUri, code); + + 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); mContext.sendBroadcast(intent); + return true; } @ThreadSafe 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; - 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); } } @@ -448,6 +525,7 @@ public class VoicemailPlaybackPresenter public void onChange(boolean selfChange) { mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE, new AsyncTask<Void, Void, Boolean>() { + @Override public Boolean doInBackground(Void... params) { return queryHasContent(mVoicemailUri); @@ -459,6 +537,11 @@ public class VoicemailPlaybackPresenter mContext.getContentResolver().unregisterContentObserver( FetchResultHandler.this); prepareContent(); + if (mRequestCode == ARCHIVE_REQUEST) { + startArchiveVoicemailTask(mVoicemailUri, true /* archivedByUser */); + } else if (mRequestCode == SHARE_REQUEST) { + startArchiveVoicemailTask(mVoicemailUri, false /* archivedByUser */); + } } } }); @@ -473,7 +556,7 @@ public class VoicemailPlaybackPresenter * 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; } @@ -485,7 +568,7 @@ public class VoicemailPlaybackPresenter mMediaPlayer = null; } - mView.setIsBuffering(); + mView.disableUiElements(); mIsPrepared = false; try { @@ -496,7 +579,7 @@ public class VoicemailPlaybackPresenter mMediaPlayer.reset(); mMediaPlayer.setDataSource(mContext, mVoicemailUri); - mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM); + mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM); mMediaPlayer.prepareAsync(); } catch (IOException e) { handleError(e); @@ -514,12 +597,15 @@ public class VoicemailPlaybackPresenter Log.d(TAG, "onPrepared"); mIsPrepared = true; + // Update the duration in the database if it was not previously retrieved + CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri, + TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getDuration())); + mDuration.set(mMediaPlayer.getDuration()); - mPosition = mMediaPlayer.getCurrentPosition(); - mView.enableUiElements(); Log.d(TAG, "onPrepared: mPosition=" + mPosition); mView.setClipPosition(mPosition, mDuration.get()); + mView.enableUiElements(); mMediaPlayer.seekTo(mPosition); if (mIsPlaying) { @@ -539,7 +625,7 @@ public class VoicemailPlaybackPresenter return true; } - private void handleError(Exception e) { + protected void handleError(Exception e) { Log.d(TAG, "handleError: Could not play voicemail " + e); if (mIsPrepared) { @@ -570,15 +656,22 @@ public class VoicemailPlaybackPresenter } } - @Override - public void onAudioFocusChange(int focusChange) { - Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange); - boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT - || focusChange == AudioManager.AUDIOFOCUS_LOSS; - if (mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_LOSS) { - pausePlayback(); - } else if (!mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_GAIN) { + /** + * Only play voicemail when audio focus is granted. When it is lost (usually by another + * application requesting focus), pause playback. + * + * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise. + */ + public void onAudioFocusChange(boolean gainedFocus) { + if (mIsPlaying == gainedFocus) { + // Nothing new here, just exit. + return; + } + + if (!mIsPlaying) { resumePlayback(); + } else { + pausePlayback(); } } @@ -587,15 +680,30 @@ public class VoicemailPlaybackPresenter * playing. */ public void resumePlayback() { - if (mView == null || mContext == null) { + if (mView == null) { return; } if (!mIsPrepared) { - // If we haven't downloaded the voicemail yet, attempt to download it. - checkForContent(); - mIsPlaying = true; - + /* + * Check content before requesting content to avoid duplicated requests. It is possible + * that the UI doesn't know content has arrived if the fetch took too long causing a + * timeout, but succeeded. + */ + checkForContent(new OnContentCheckedListener() { + @Override + public void onContentChecked(boolean hasContent) { + if (!hasContent) { + // No local content, download from server. Queue playing if the request was + // issued, + mIsPlaying = requestContent(PLAYBACK_REQUEST); + } else { + // Queue playing once the media play loaded the content. + mIsPlaying = true; + prepareContent(); + } + } + }); return; } @@ -604,20 +712,15 @@ public class VoicemailPlaybackPresenter if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { // Clamp the start position between 0 and the duration. mPosition = Math.max(0, Math.min(mPosition, mDuration.get())); + mMediaPlayer.seekTo(mPosition); try { // Grab audio focus. - int result = mAudioManager.requestAudioFocus( - this, - PLAYBACK_STREAM, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - throw new RejectedExecutionException("Could not capture audio focus."); - } - // Can throw RejectedExecutionException. + mVoicemailAudioManager.requestAudioFocus(); mMediaPlayer.start(); + setSpeakerphoneOn(mIsSpeakerphoneOn); } catch (RejectedExecutionException e) { handleError(e); } @@ -625,11 +728,6 @@ public class VoicemailPlaybackPresenter Log.d(TAG, "Resumed playback at " + mPosition + "."); mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance()); - if (isSpeakerphoneOn()) { - mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - enableProximitySensor(); - } } /** @@ -653,7 +751,8 @@ public class VoicemailPlaybackPresenter if (mView != null) { mView.onPlaybackStopped(); } - mAudioManager.abandonAudioFocus(this); + + mVoicemailAudioManager.abandonAudioFocus(); if (mActivity != null) { mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -680,8 +779,17 @@ public class VoicemailPlaybackPresenter } } + /** + * Seek to position. This is called when user manually seek the playback. It could be either + * by touch or volume button while in talkback mode. + * @param position + */ + public void seek(int position) { + mPosition = position; + } + private void enableProximitySensor() { - if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared + if (mProximityWakeLock == null || mIsSpeakerphoneOn || !mIsPrepared || mMediaPlayer == null || !mMediaPlayer.isPlaying()) { return; } @@ -707,26 +815,46 @@ public class VoicemailPlaybackPresenter } } + /** + * This is for use by UI interactions only. It simplifies UI logic. + */ + public void toggleSpeakerphone() { + mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn); + setSpeakerphoneOn(!mIsSpeakerphoneOn); + } + + /** + * This method only handles app-level changes to the speakerphone. Audio layer changes should + * be handled separately. This is so that the VoicemailAudioManager can trigger changes to + * the presenter without the presenter triggering the audio manager and duplicating actions. + */ public void setSpeakerphoneOn(boolean on) { - mAudioManager.setSpeakerphoneOn(on); + if (mView == null) { + return; + } - if (on) { - disableProximitySensor(false /* waitForFarState */); - if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) { - mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } else { - enableProximitySensor(); - if (mActivity != null) { - mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); + mView.onSpeakerphoneOn(on); + + mIsSpeakerphoneOn = on; + + // This should run even if speakerphone is not being toggled because we may be switching + // from earpiece to headphone and vise versa. Also upon initial setup the default audio + // source is the earpiece, so we want to trigger the proximity sensor. + if (mIsPlaying) { + if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) { + disableProximitySensor(false /* waitForFarState */); + if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) { + mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } else { + enableProximitySensor(); + if (mActivity != null) { + mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); + } } } } - public boolean isSpeakerphoneOn() { - return mAudioManager.isSpeakerphoneOn(); - } - public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) { mOnVoicemailDeletedListener = listener; } @@ -735,13 +863,38 @@ public class VoicemailPlaybackPresenter 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 + // Trampoline the event notification to the interested listener. if (mOnVoicemailDeletedListener != null) { mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri); } } + /* package */ void onVoicemailDeleteUndo() { + // Trampoline the event notification to the interested listener. + if (mOnVoicemailDeletedListener != null) { + mOnVoicemailDeletedListener.onVoicemailDeleteUndo(); + } + } + + /* package */ void onVoicemailDeletedInDatabase() { + // Trampoline the event notification to the interested listener. + if (mOnVoicemailDeletedListener != null) { + mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase(); + } + } + private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { if (mScheduledExecutorService == null) { mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL); @@ -749,8 +902,107 @@ public class VoicemailPlaybackPresenter return mScheduledExecutorService; } + /** + * If voicemail has already been downloaded, go straight to archiving. Otherwise, request + * the voicemail content first. + */ + public void archiveContent(final Uri voicemailUri, final boolean archivedByUser) { + checkForContent(new OnContentCheckedListener() { + @Override + public void onContentChecked(boolean hasContent) { + if (!hasContent) { + requestContent(archivedByUser ? ARCHIVE_REQUEST : SHARE_REQUEST); + } else { + startArchiveVoicemailTask(voicemailUri, archivedByUser); + } + } + }); + } + + /** + * Asynchronous task used to archive a voicemail given its uri. + */ + protected void startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser) { + mVoicemailAsyncTaskUtil.archiveVoicemailContent( + new VoicemailAsyncTaskUtil.OnArchiveVoicemailListener() { + @Override + public void onArchiveVoicemail(final Uri archivedVoicemailUri) { + if (archivedVoicemailUri == null) { + notifyUiOfArchiveResult(voicemailUri, false); + return; + } + + if (archivedByUser) { + setArchivedVoicemailStatusAndUpdateUI(voicemailUri, + archivedVoicemailUri, true); + } else { + sendShareIntent(archivedVoicemailUri); + } + } + }, voicemailUri); + } + + /** + * Sends the intent for sharing the voicemail file. + */ + protected void sendShareIntent(final Uri voicemailUri) { + mVoicemailAsyncTaskUtil.getVoicemailFilePath( + new VoicemailAsyncTaskUtil.OnGetArchivedVoicemailFilePathListener() { + @Override + public void onGetArchivedVoicemailFilePath(String filePath) { + mView.enableUiElements(); + if (filePath == null) { + mView.setFetchContentTimeout(); + return; + } + Uri voicemailFileUri = FileProvider.getUriForFile( + mContext, + mContext.getString(R.string.contacts_file_provider_authority), + new File(filePath)); + mContext.startActivity(Intent.createChooser( + getShareIntent(voicemailFileUri), + mContext.getResources().getText( + R.string.call_log_share_voicemail))); + } + }, voicemailUri); + } + + /** Sets archived_by_user field to the given boolean and updates the URI. */ + private void setArchivedVoicemailStatusAndUpdateUI( + final Uri voicemailUri, + final Uri archivedVoicemailUri, + boolean status) { + mVoicemailAsyncTaskUtil.setVoicemailArchiveStatus( + new VoicemailAsyncTaskUtil.OnSetVoicemailArchiveStatusListener() { + @Override + public void onSetVoicemailArchiveStatus(boolean success) { + notifyUiOfArchiveResult(voicemailUri, success); + } + }, archivedVoicemailUri, status); + } + + private Intent getShareIntent(Uri voicemailFileUri) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + shareIntent.setType(mContext.getContentResolver() + .getType(voicemailFileUri)); + return shareIntent; + } + @VisibleForTesting public boolean isPlaying() { return mIsPlaying; } + + @VisibleForTesting + public boolean isSpeakerphoneOn() { + return mIsSpeakerphoneOn; + } + + @VisibleForTesting + public void clearInstance() { + sInstance = null; + } } diff --git a/src/com/android/dialer/voicemail/WiredHeadsetManager.java b/src/com/android/dialer/voicemail/WiredHeadsetManager.java new file mode 100644 index 000000000..7351f4f01 --- /dev/null +++ b/src/com/android/dialer/voicemail/WiredHeadsetManager.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2015 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.util.Log; + +/** Listens for and caches headset state. */ +class WiredHeadsetManager { + private static final String TAG = WiredHeadsetManager.class.getSimpleName(); + + interface Listener { + void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn); + } + + /** Receiver for wired headset plugged and unplugged events. */ + private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (AudioManager.ACTION_HEADSET_PLUG.equals(intent.getAction())) { + boolean isPluggedIn = intent.getIntExtra("state", 0) == 1; + Log.v(TAG, "ACTION_HEADSET_PLUG event, plugged in: " + isPluggedIn); + onHeadsetPluggedInChanged(isPluggedIn); + } + } + } + + private final WiredHeadsetBroadcastReceiver mReceiver; + private boolean mIsPluggedIn; + private Listener mListener; + private Context mContext; + + WiredHeadsetManager(Context context) { + mContext = context; + mReceiver = new WiredHeadsetBroadcastReceiver(); + + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mIsPluggedIn = audioManager.isWiredHeadsetOn(); + + } + + void setListener(Listener listener) { + mListener = listener; + } + + boolean isPluggedIn() { + return mIsPluggedIn; + } + + void registerReceiver() { + // Register for misc other intent broadcasts. + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + mContext.registerReceiver(mReceiver, intentFilter); + } + + void unregisterReceiver() { + mContext.unregisterReceiver(mReceiver); + } + + private void onHeadsetPluggedInChanged(boolean isPluggedIn) { + if (mIsPluggedIn != isPluggedIn) { + Log.v(TAG, "onHeadsetPluggedInChanged, mIsPluggedIn: " + mIsPluggedIn + " -> " + + isPluggedIn); + boolean oldIsPluggedIn = mIsPluggedIn; + mIsPluggedIn = isPluggedIn; + if (mListener != null) { + mListener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn); + } + } + } +}
\ No newline at end of file |