summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/voicemail
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/dialer/voicemail')
-rw-r--r--src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java103
-rw-r--r--src/com/android/dialer/voicemail/VoicemailArchiveActivity.java160
-rw-r--r--src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java90
-rw-r--r--src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java346
-rw-r--r--src/com/android/dialer/voicemail/VoicemailAudioManager.java200
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java329
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java480
-rw-r--r--src/com/android/dialer/voicemail/WiredHeadsetManager.java88
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