summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/app/voicemail
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/dialer/app/voicemail
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
Update dialer sources.
Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
Diffstat (limited to 'java/com/android/dialer/app/voicemail')
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailAudioManager.java252
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailErrorManager.java129
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java449
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java1050
-rw-r--r--java/com/android/dialer/app/voicemail/WiredHeadsetManager.java88
-rw-r--r--java/com/android/dialer/app/voicemail/error/AndroidManifest.xml5
-rw-r--r--java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java177
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java165
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java178
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java45
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailStatus.java260
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java114
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java25
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java25
-rw-r--r--java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java428
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml114
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml72
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/values/dimens.xml12
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/values/strings.xml176
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/values/styles.xml26
20 files changed, 3790 insertions, 0 deletions
diff --git a/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java b/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java
new file mode 100644
index 000000000..8d70cdbe7
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java
@@ -0,0 +1,252 @@
+/*
+ * 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.app.voicemail;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.telecom.CallAudioState;
+import com.android.dialer.common.LogUtil;
+import java.util.concurrent.RejectedExecutionException;
+
+/** This class manages all audio changes for voicemail playback. */
+public final class VoicemailAudioManager
+ implements OnAudioFocusChangeListener, WiredHeadsetManager.Listener {
+
+ private static final String TAG = "VoicemailAudioManager";
+
+ 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;
+ private boolean mBluetoothScoEnabled;
+
+ public VoicemailAudioManager(
+ Context context, VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+ mWiredHeadsetManager = new WiredHeadsetManager(context);
+ mWiredHeadsetManager.setListener(this);
+
+ mCallAudioState = getInitialAudioState();
+ LogUtil.i(
+ "VoicemailAudioManager.VoicemailAudioManager", "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.");
+ }
+ updateBluetoothScoState(true);
+ }
+
+ public void abandonAudioFocus() {
+ updateBluetoothScoState(false);
+ mAudioManager.abandonAudioFocus(this);
+ }
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ LogUtil.d("VoicemailAudioManager.onAudioFocusChange", "focusChange=" + focusChange);
+ mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN);
+ }
+
+ @Override
+ public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ LogUtil.i(
+ "VoicemailAudioManager.onWiredHeadsetPluggedInChanged",
+ "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();
+ }
+
+ /**
+ * Bluetooth SCO (Synchronous Connection-Oriented) is the "phone" bluetooth audio. The system will
+ * route to the bluetooth headset automatically if A2DP ("media") is available, but if the headset
+ * only supports SCO then dialer must route it manually.
+ */
+ private void updateBluetoothScoState(boolean hasAudioFocus) {
+ if (hasAudioFocus) {
+ if (hasMediaAudioCapability()) {
+ mBluetoothScoEnabled = false;
+ } else {
+ mBluetoothScoEnabled = true;
+ LogUtil.i(
+ "VoicemailAudioManager.updateBluetoothScoState",
+ "bluetooth device doesn't support media, using SCO instead");
+ }
+ } else {
+ mBluetoothScoEnabled = false;
+ }
+ applyBluetoothScoState();
+ }
+
+ private void applyBluetoothScoState() {
+ if (mBluetoothScoEnabled) {
+ mAudioManager.startBluetoothSco();
+ // The doc for startBluetoothSco() states it could take seconds to establish the SCO
+ // connection, so we should probably resume the playback after we've acquired SCO.
+ // In practice the delay is unnoticeable so this is ignored for simplicity.
+ mAudioManager.setBluetoothScoOn(true);
+ } else {
+ mAudioManager.setBluetoothScoOn(false);
+ mAudioManager.stopBluetoothSco();
+ }
+ }
+
+ private boolean hasMediaAudioCapability() {
+ for (AudioDeviceInfo info : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
+ if (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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) {
+ LogUtil.v(
+ "VoicemailAudioManager.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) {
+ LogUtil.w(
+ "VoicemailAudioManager.setAudioRoute",
+ "Asking to set to a route that is unsupported: " + newRoute);
+ return;
+ }
+
+ // 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) {
+ LogUtil.e(
+ "VoicemailAudioManager.selectWiredOrEarpiece",
+ "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;
+
+ LogUtil.i(
+ "VoicemailAudioManager.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);
+ // BluetoothSco is not handled by the system so it has to be reset.
+ applyBluetoothScoState();
+ }
+ }
+
+ private void turnOnSpeaker(boolean on) {
+ if (mAudioManager.isSpeakerphoneOn() != on) {
+ LogUtil.i("VoicemailAudioManager.turnOnSpeaker", "turning speaker phone on: " + on);
+ mAudioManager.setSpeakerphoneOn(on);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
new file mode 100644
index 000000000..939007adf
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
@@ -0,0 +1,129 @@
+/*
+ * 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.app.voicemail;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Handler;
+import com.android.dialer.app.calllog.CallLogAlertManager;
+import com.android.dialer.app.calllog.CallLogModalAlertManager;
+import com.android.dialer.app.voicemail.error.VoicemailErrorAlert;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessageCreator;
+import com.android.dialer.app.voicemail.error.VoicemailStatus;
+import com.android.dialer.app.voicemail.error.VoicemailStatusReader;
+import com.android.dialer.database.CallLogQueryHandler;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fetches voicemail status and generate {@link VoicemailStatus} for {@link VoicemailErrorAlert} to
+ * show.
+ */
+public class VoicemailErrorManager implements CallLogQueryHandler.Listener, VoicemailStatusReader {
+
+ private final Context context;
+ private final CallLogQueryHandler callLogQueryHandler;
+ private final VoicemailErrorAlert alertItem;
+
+ private final ContentObserver statusObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ maybeFetchStatus();
+ }
+ };
+
+ private boolean isForeground;
+ private boolean statusInvalidated;
+
+ public VoicemailErrorManager(
+ Context context,
+ CallLogAlertManager alertManager,
+ CallLogModalAlertManager modalAlertManager) {
+ this.context = context;
+ alertItem =
+ new VoicemailErrorAlert(
+ context, alertManager, modalAlertManager, new VoicemailErrorMessageCreator());
+ callLogQueryHandler = new CallLogQueryHandler(context, context.getContentResolver(), this);
+ maybeFetchStatus();
+ }
+
+ public ContentObserver getContentObserver() {
+ return statusObserver;
+ }
+
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ List<VoicemailStatus> statuses = new ArrayList<>();
+ while (statusCursor.moveToNext()) {
+ VoicemailStatus status = new VoicemailStatus(context, statusCursor);
+ if (status.isActive()) {
+ statuses.add(status);
+ }
+ }
+ alertItem.updateStatus(statuses, this);
+ // TODO: b/30668323 support error from multiple sources.
+ return;
+ }
+
+ @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;
+ }
+
+ public void onResume() {
+ isForeground = true;
+ if (statusInvalidated) {
+ maybeFetchStatus();
+ }
+ }
+
+ public void onPause() {
+ isForeground = false;
+ statusInvalidated = false;
+ }
+
+ @Override
+ public void refresh() {
+ maybeFetchStatus();
+ }
+
+ /**
+ * Fetch the status when the dialer is in foreground, or queue a fetch when the dialer resumes.
+ */
+ private void maybeFetchStatus() {
+ if (!isForeground) {
+ // Dialer is in the background, UI should not be updated. Reload the status when it resumes.
+ statusInvalidated = true;
+ return;
+ }
+ callLogQueryHandler.fetchVoicemailStatus();
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
new file mode 100644
index 000000000..fc6a37608
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2011 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.app.voicemail;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.support.annotation.VisibleForTesting;
+import android.support.design.widget.Snackbar;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.app.calllog.CallLogListItemViewHolder;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for details on the
+ * voicemail playback implementation.
+ *
+ * <p>This class is not thread-safe, it is thread-confined. All calls to all public methods on this
+ * class are expected to come from the main ui thread.
+ */
+@NotThreadSafe
+public class VoicemailPlaybackLayout extends LinearLayout
+ implements VoicemailPlaybackPresenter.PlaybackView,
+ CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
+
+ private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
+ private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
+
+ private Context mContext;
+ private CallLogListItemViewHolder mViewHolder;
+ private VoicemailPlaybackPresenter mPresenter;
+ /** Click listener to toggle speakerphone. */
+ private final View.OnClickListener mSpeakerphoneListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPresenter != null) {
+ mPresenter.toggleSpeakerphone();
+ }
+ }
+ };
+
+ private Uri mVoicemailUri;
+ private final View.OnClickListener mDeleteButtonListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_DELETE_ENTRY);
+ if (mPresenter == null) {
+ return;
+ }
+
+ // When the undo button is pressed, the viewHolder we have is no longer valid because when
+ // we hide the view it is binded to something else, and the layout is not updated for
+ // hidden items. copy the adapter position so we can update the view upon undo.
+ // TODO: refactor this so the view holder will always be valid.
+ final int adapterPosition = mViewHolder.getAdapterPosition();
+
+ mPresenter.pausePlayback();
+ mPresenter.onVoicemailDeleted(mViewHolder);
+
+ 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(adapterPosition);
+ handler.removeCallbacks(deleteCallback);
+ }
+ })
+ .setActionTextColor(
+ mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+ .show();
+ }
+ };
+ private boolean mIsPlaying = false;
+ /** Click listener to play or pause voicemail playback. */
+ private final View.OnClickListener mStartStopButtonListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mPresenter == null) {
+ return;
+ }
+
+ if (mIsPlaying) {
+ mPresenter.pausePlayback();
+ } else {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY);
+ mPresenter.resumePlayback();
+ }
+ }
+ };
+
+ private SeekBar mPlaybackSeek;
+ private ImageButton mStartStopButton;
+ private ImageButton mPlaybackSpeakerphone;
+ private ImageButton mDeleteButton;
+ private TextView mStateText;
+ private TextView mPositionText;
+ private TextView mTotalDurationText;
+ /** Handle state changes when the user manipulates the seek bar. */
+ private final OnSeekBarChangeListener mSeekBarChangeListener =
+ new OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (mPresenter != null) {
+ mPresenter.pausePlaybackForSeeking();
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mPresenter != null) {
+ mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress());
+ }
+ }
+
+ @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);
+ }
+ }
+ };
+
+ private PositionUpdater mPositionUpdater;
+ private Drawable mVoicemailSeekHandleEnabled;
+ private Drawable mVoicemailSeekHandleDisabled;
+
+ public VoicemailPlaybackLayout(Context context) {
+ this(context, null);
+ }
+
+ public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.voicemail_playback_layout, this);
+ }
+
+ public void setViewHolder(CallLogListItemViewHolder mViewHolder) {
+ this.mViewHolder = mViewHolder;
+ }
+
+ @Override
+ public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
+ mPresenter = presenter;
+ mVoicemailUri = voicemailUri;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
+ mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
+ mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
+ mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
+
+ mStateText = (TextView) findViewById(R.id.playback_state_text);
+ mStateText.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
+ mPositionText = (TextView) findViewById(R.id.playback_position_text);
+ mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
+
+ mPlaybackSeek.setOnSeekBarChangeListener(mSeekBarChangeListener);
+ mStartStopButton.setOnClickListener(mStartStopButtonListener);
+ mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
+ mDeleteButton.setOnClickListener(mDeleteButtonListener);
+
+ 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
+ public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
+ mIsPlaying = true;
+
+ mStartStopButton.setImageResource(R.drawable.ic_pause);
+
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ mPositionUpdater = null;
+ }
+ mPositionUpdater = new PositionUpdater(duration, executorService);
+ mPositionUpdater.startUpdating();
+ }
+
+ @Override
+ public void onPlaybackStopped() {
+ mIsPlaying = false;
+
+ mStartStopButton.setImageResource(R.drawable.ic_play_arrow);
+
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ mPositionUpdater = null;
+ }
+ }
+
+ @Override
+ public void onPlaybackError() {
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ }
+
+ disableUiElements();
+ mStateText.setText(getString(R.string.voicemail_playback_error));
+ }
+
+ @Override
+ public void onSpeakerphoneOn(boolean on) {
+ if (on) {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
+ // Speaker is now on, tapping button will turn it off.
+ mPlaybackSpeakerphone.setContentDescription(
+ mContext.getString(R.string.voicemail_speaker_off));
+ } else {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_down_24dp);
+ // Speaker is now off, tapping button will turn it on.
+ mPlaybackSpeakerphone.setContentDescription(
+ mContext.getString(R.string.voicemail_speaker_on));
+ }
+ }
+
+ @Override
+ public void setClipPosition(int positionMs, int durationMs) {
+ int seekBarPositionMs = Math.max(0, positionMs);
+ int seekBarMax = Math.max(seekBarPositionMs, durationMs);
+ if (mPlaybackSeek.getMax() != seekBarMax) {
+ mPlaybackSeek.setMax(seekBarMax);
+ }
+
+ mPlaybackSeek.setProgress(seekBarPositionMs);
+
+ mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
+ mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
+ }
+
+ @Override
+ public void setSuccess() {
+ mStateText.setText(null);
+ }
+
+ @Override
+ public void setIsFetchingContent() {
+ disableUiElements();
+ mStateText.setText(getString(R.string.voicemail_fetching_content));
+ }
+
+ @Override
+ public void setFetchContentTimeout() {
+ mStartStopButton.setEnabled(true);
+ mStateText.setText(getString(R.string.voicemail_fetching_timout));
+ }
+
+ @Override
+ public int getDesiredClipPosition() {
+ return mPlaybackSeek.getProgress();
+ }
+
+ @Override
+ public void disableUiElements() {
+ mStartStopButton.setEnabled(false);
+ resetSeekBar();
+ }
+
+ @Override
+ public void enableUiElements() {
+ mDeleteButton.setEnabled(true);
+ mStartStopButton.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);
+ }
+
+ /**
+ * Formats a number of milliseconds as something that looks like {@code 00:05}.
+ *
+ * <p>We always use four digits, two for minutes two for seconds. In the very unlikely event that
+ * the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
+ */
+ private String formatAsMinutesAndSeconds(int millis) {
+ int seconds = millis / 1000;
+ int minutes = seconds / 60;
+ seconds -= minutes * 60;
+ if (minutes > 99) {
+ minutes = 99;
+ }
+ return String.format("%02d:%02d", minutes, seconds);
+ }
+
+ @VisibleForTesting
+ public String getStateText() {
+ return mStateText.getText().toString();
+ }
+
+ /** Controls the animation of the playback slider. */
+ @ThreadSafe
+ private final class PositionUpdater implements Runnable {
+
+ /** Update rate for the slider, 30fps. */
+ private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
+
+ private final ScheduledExecutorService mExecutorService;
+ private final Object mLock = new Object();
+ private int mDurationMs;
+
+ @GuardedBy("mLock")
+ private ScheduledFuture<?> mScheduledFuture;
+
+ private Runnable mUpdateClipPositionRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ int currentPositionMs = 0;
+ synchronized (mLock) {
+ if (mScheduledFuture == null || mPresenter == null) {
+ // This task has been canceled. Just stop now.
+ return;
+ }
+ currentPositionMs = mPresenter.getMediaPlayerPosition();
+ }
+ setClipPosition(currentPositionMs, mDurationMs);
+ }
+ };
+
+ public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
+ mDurationMs = durationMs;
+ mExecutorService = executorService;
+ }
+
+ @Override
+ public void run() {
+ post(mUpdateClipPositionRunnable);
+ }
+
+ public void startUpdating() {
+ synchronized (mLock) {
+ cancelPendingRunnables();
+ mScheduledFuture =
+ mExecutorService.scheduleAtFixedRate(
+ this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public void stopUpdating() {
+ synchronized (mLock) {
+ cancelPendingRunnables();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void cancelPendingRunnables() {
+ if (mScheduledFuture != null) {
+ mScheduledFuture.cancel(true);
+ mScheduledFuture = null;
+ }
+ removeCallbacks(mUpdateClipPositionRunnable);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
new file mode 100644
index 000000000..657022291
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
@@ -0,0 +1,1050 @@
+/*
+ * Copyright (C) 2011 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.app.voicemail;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.FileProvider;
+import android.text.TextUtils;
+import android.view.WindowManager.LayoutParams;
+import android.webkit.MimeTypeMap;
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogListItemViewHolder;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.constants.Constants;
+import com.android.dialer.phonenumbercache.CallLogQuery;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled to
+ * assumptions about the behaviors and lifecycle of the call log, in particular in the {@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 #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 into this class from outside must be done from the main UI thread.
+ */
+@NotThreadSafe
+@VisibleForTesting
+@TargetApi(VERSION_CODES.M)
+public class VoicemailPlaybackPresenter
+ implements MediaPlayer.OnPreparedListener,
+ MediaPlayer.OnCompletionListener,
+ MediaPlayer.OnErrorListener {
+
+ public static final int PLAYBACK_REQUEST = 0;
+ 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;
+ private static final String VOICEMAIL_URI_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
+ private static final String IS_PREPARED_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
+ // If present in the saved instance bundle, we should not resume playback on create.
+ private static final String IS_PLAYING_STATE_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY";
+ // 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";
+ private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa";
+ private static VoicemailPlaybackPresenter sInstance;
+ private static ScheduledExecutorService mScheduledExecutorService;
+ /**
+ * The most recently cached duration. We cache this since we don't want to keep requesting it from
+ * the player, as this can easily lead to throwing {@link IllegalStateException} (any time the
+ * player is released, it's illegal to ask for the duration).
+ */
+ private final AtomicInteger mDuration = new AtomicInteger(0);
+
+ protected Context mContext;
+ private long mRowId;
+ protected Uri mVoicemailUri;
+ protected MediaPlayer mMediaPlayer;
+ // Used to run async tasks that need to interact with the UI.
+ protected AsyncTaskExecutor mAsyncTaskExecutor;
+ private Activity mActivity;
+ private PlaybackView mView;
+ 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;
+ /**
+ * Used to handle the result of a successful or time-out fetch result.
+ *
+ * <p>This variable is thread-contained, accessed only on the ui thread.
+ */
+ private FetchResultHandler mFetchResultHandler;
+
+ private PowerManager.WakeLock mProximityWakeLock;
+ private VoicemailAudioManager mVoicemailAudioManager;
+ private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
+
+ /** Initialize variables which are activity-independent and state-independent. */
+ protected VoicemailPlaybackPresenter(Activity activity) {
+ Context context = activity.getApplicationContext();
+ mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
+ mVoicemailAudioManager = new VoicemailAudioManager(context, this);
+ PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ mProximityWakeLock =
+ powerManager.newWakeLock(
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "VoicemailPlaybackPresenter");
+ }
+ }
+
+ /**
+ * Obtain singleton instance of this class. Use a single instance to provide a consistent listener
+ * to the AudioManager when requesting and abandoning audio focus.
+ *
+ * <p>Otherwise, after rotation the previous listener will still be active but a new listener will
+ * be provided to calls to the AudioManager, which is bad. For example, abandoning audio focus
+ * with the new listeners results in an AUDIO_FOCUS_GAIN callback to the previous listener, which
+ * is the opposite of the intended behavior.
+ */
+ @MainThread
+ public static VoicemailPlaybackPresenter getInstance(
+ Activity activity, Bundle savedInstanceState) {
+ if (sInstance == null) {
+ sInstance = new VoicemailPlaybackPresenter(activity);
+ }
+
+ sInstance.init(activity, savedInstanceState);
+ return sInstance;
+ }
+
+ private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
+ if (mScheduledExecutorService == null) {
+ mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
+ }
+ return mScheduledExecutorService;
+ }
+
+ /** Update variables which are activity-dependent or state-dependent. */
+ @MainThread
+ protected void init(Activity activity, Bundle savedInstanceState) {
+ Assert.isMainThread();
+ mActivity = activity;
+ mContext = activity;
+
+ if (savedInstanceState != null) {
+ // Restores playback state when activity is recreated, such as after rotation.
+ mVoicemailUri = savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
+ 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) {
+ mIsPrepared = false;
+ mIsPlaying = false;
+ }
+
+ if (mActivity != null) {
+ if (isPlaying()) {
+ mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+ }
+
+ /** Must be invoked when the parent Activity is saving it state. */
+ public void onSaveInstanceState(Bundle outState) {
+ if (mView != null) {
+ outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
+ 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);
+ }
+ }
+
+ /** Specify the view which this presenter controls and the voicemail to prepare to play. */
+ public void setPlaybackView(
+ PlaybackView view, long rowId, Uri voicemailUri, final boolean startPlayingImmediately) {
+ mRowId = rowId;
+ mView = view;
+ mView.setPresenter(this, voicemailUri);
+ mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
+
+ // 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);
+ } else {
+ if (!voicemailUri.equals(mVoicemailUri)) {
+ mVoicemailUri = voicemailUri;
+ mPosition = 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 (startPlayingImmediately) {
+ requestContent(PLAYBACK_REQUEST);
+ }
+ 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;
+ }
+ }
+ }
+
+ /** Reset the presenter for playback back to its original state. */
+ public void resetAll() {
+ pausePresenter(true);
+
+ mView = null;
+ mVoicemailUri = null;
+ }
+
+ /**
+ * 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 pausePresenter(boolean reset) {
+ pausePlayback();
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
+ disableProximitySensor(false /* waitForFarState */);
+
+ mIsPrepared = false;
+ mIsPlaying = false;
+
+ if (reset) {
+ // We want to reset the position whether or not the view is valid.
+ mPosition = 0;
+ }
+
+ if (mView != null) {
+ mView.onPlaybackStopped();
+ 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 (mActivity != null && mIsPrepared && mActivity.isChangingConfigurations()) {
+ // If an configuration change triggers the pause, retain the MediaPlayer.
+ LogUtil.d("VoicemailPlaybackPresenter.onPause", "configuration changed.");
+ return;
+ }
+
+ // Release the media player, otherwise there may be failures.
+ pausePresenter(false);
+ }
+
+ /** Must be invoked when the parent activity is destroyed. */
+ public void onDestroy() {
+ // Clear references to avoid leaks from the singleton instance.
+ mActivity = null;
+ mContext = null;
+
+ if (mScheduledExecutorService != null) {
+ mScheduledExecutorService.shutdown();
+ mScheduledExecutorService = null;
+ }
+
+ if (mFetchResultHandler != null) {
+ mFetchResultHandler.destroy();
+ mFetchResultHandler = null;
+ }
+ }
+
+ /** Checks to see if we have content available for this voicemail. */
+ protected void checkForContent(final OnContentCheckedListener callback) {
+ mAsyncTaskExecutor.submit(
+ Tasks.CHECK_FOR_CONTENT,
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Void... params) {
+ return queryHasContent(mVoicemailUri);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ callback.onContentChecked(hasContent);
+ }
+ });
+ }
+
+ private boolean queryHasContent(Uri voicemailUri) {
+ if (voicemailUri == null || mContext == null) {
+ return false;
+ }
+
+ ContentResolver contentResolver = mContext.getContentResolver();
+ Cursor cursor = contentResolver.query(voicemailUri, null, null, null, null);
+ try {
+ if (cursor != null && cursor.moveToNext()) {
+ 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 {
+ MoreCloseables.closeQuietly(cursor);
+ }
+ return false;
+ }
+
+ /**
+ * Makes a broadcast request to ask that a voicemail source fetch this content.
+ *
+ * <p>This method <b>must be called on the ui thread</b>.
+ *
+ * <p>This method will be called when we realise that we don't have content for this voicemail. It
+ * will trigger a broadcast to request that the content be downloaded. It will add a listener to
+ * the content resolver so that it will be notified when the has_content field changes. It will
+ * also set a timer. If the has_content field changes to true within the allowed time, we will
+ * 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
+ */
+ protected boolean requestContent(int code) {
+ if (mContext == null || mVoicemailUri == null) {
+ return false;
+ }
+
+ FetchResultHandler tempFetchResultHandler =
+ new FetchResultHandler(new Handler(), mVoicemailUri, code);
+
+ switch (code) {
+ default:
+ if (mFetchResultHandler != null) {
+ mFetchResultHandler.destroy();
+ }
+ mView.setIsFetchingContent();
+ mFetchResultHandler = tempFetchResultHandler;
+ break;
+ }
+
+ mAsyncTaskExecutor.submit(
+ Tasks.SEND_FETCH_REQUEST,
+ new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ try (Cursor cursor =
+ mContext
+ .getContentResolver()
+ .query(
+ mVoicemailUri,
+ new String[] {Voicemails.SOURCE_PACKAGE},
+ null,
+ null,
+ null)) {
+ String sourcePackage;
+ if (!hasContent(cursor)) {
+ LogUtil.e(
+ "VoicemailPlaybackPresenter.requestContent",
+ "mVoicemailUri does not return a SOURCE_PACKAGE");
+ sourcePackage = null;
+ } else {
+ sourcePackage = cursor.getString(0);
+ }
+ // Send voicemail fetch request.
+ Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
+ intent.setPackage(sourcePackage);
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.requestContent",
+ "Sending ACTION_FETCH_VOICEMAIL to " + sourcePackage);
+ mContext.sendBroadcast(intent);
+ }
+ return null;
+ }
+ });
+ return true;
+ }
+
+ /**
+ * Prepares the voicemail content for playback.
+ *
+ * <p>This method will be called once we know that our voicemail has content (according to the
+ * content provider). this method asynchronously tries to prepare the data source through the
+ * media player. If preparation is successful, the media player will {@link #onPrepared()}, and it
+ * will call {@link #onError()} otherwise.
+ */
+ protected void prepareContent() {
+ if (mView == null) {
+ return;
+ }
+ LogUtil.d("VoicemailPlaybackPresenter.prepareContent", null);
+
+ // Release the previous media player, otherwise there may be failures.
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
+ mView.disableUiElements();
+ mIsPrepared = false;
+
+ try {
+ mMediaPlayer = new MediaPlayer();
+ mMediaPlayer.setOnPreparedListener(this);
+ mMediaPlayer.setOnErrorListener(this);
+ mMediaPlayer.setOnCompletionListener(this);
+
+ mMediaPlayer.reset();
+ mMediaPlayer.setDataSource(mContext, mVoicemailUri);
+ mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM);
+ mMediaPlayer.prepareAsync();
+ } catch (IOException e) {
+ handleError(e);
+ }
+ }
+
+ /**
+ * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
+ */
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ if (mView == null || mContext == null) {
+ return;
+ }
+ LogUtil.d("VoicemailPlaybackPresenter.onPrepared", null);
+ mIsPrepared = true;
+
+ mDuration.set(mMediaPlayer.getDuration());
+
+ LogUtil.d("VoicemailPlaybackPresenter.onPrepared", "mPosition=" + mPosition);
+ mView.setClipPosition(mPosition, mDuration.get());
+ mView.enableUiElements();
+ mView.setSuccess();
+ mMediaPlayer.seekTo(mPosition);
+
+ if (mIsPlaying) {
+ resumePlayback();
+ } else {
+ pausePlayback();
+ }
+ }
+
+ /**
+ * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
+ * is an unknown file format that can't be played.
+ */
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
+ return true;
+ }
+
+ protected void handleError(Exception e) {
+ LogUtil.e("VoicemailPlaybackPresenter.handlerError", "could not play voicemail", e);
+
+ if (mIsPrepared) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mIsPrepared = false;
+ }
+
+ if (mView != null) {
+ mView.onPlaybackError();
+ }
+
+ mPosition = 0;
+ mIsPlaying = false;
+ }
+
+ /** After done playing the voicemail clip, reset the clip position to the start. */
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ pausePlayback();
+
+ // Reset the seekbar position to the beginning.
+ mPosition = 0;
+ if (mView != null) {
+ mediaPlayer.seekTo(0);
+ mView.setClipPosition(0, mDuration.get());
+ }
+ }
+
+ /**
+ * Only play voicemail when audio focus is granted. When it is lost (usually by another
+ * application requesting focus), pause playback. Audio focus gain/lost only triggers the focus is
+ * requested. Audio focus is requested when the user pressed play and abandoned when the user
+ * pressed pause or the audio has finished. Losing focus should not abandon focus as the voicemail
+ * should resume once the focus is returned.
+ *
+ * @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 (gainedFocus) {
+ resumePlayback();
+ } else {
+ pausePlayback(true);
+ }
+ }
+
+ /**
+ * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
+ * playing.
+ */
+ public void resumePlayback() {
+ if (mView == null) {
+ return;
+ }
+
+ if (!mIsPrepared) {
+ /*
+ * 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;
+ }
+
+ mIsPlaying = true;
+
+ mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ 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.
+ // Can throw RejectedExecutionException.
+ mVoicemailAudioManager.requestAudioFocus();
+ mMediaPlayer.start();
+ setSpeakerphoneOn(mIsSpeakerphoneOn);
+ mVoicemailAudioManager.setSpeakerphoneOn(mIsSpeakerphoneOn);
+ } catch (RejectedExecutionException e) {
+ handleError(e);
+ }
+ }
+
+ LogUtil.d("VoicemailPlaybackPresenter.resumePlayback", "resumed playback at %d.", mPosition);
+ mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
+ }
+
+ /** Pauses voicemail playback at the current position. Null-op if already paused. */
+ public void pausePlayback() {
+ pausePlayback(false);
+ }
+
+ private void pausePlayback(boolean keepFocus) {
+ if (!mIsPrepared) {
+ return;
+ }
+
+ mIsPlaying = false;
+
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ }
+
+ mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
+
+ LogUtil.d("VoicemailPlaybackPresenter.pausePlayback", "paused playback at %d.", mPosition);
+
+ if (mView != null) {
+ mView.onPlaybackStopped();
+ }
+
+ if (!keepFocus) {
+ mVoicemailAudioManager.abandonAudioFocus();
+ }
+ if (mActivity != null) {
+ mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ disableProximitySensor(true /* waitForFarState */);
+ }
+
+ /**
+ * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
+ * playing to know whether to resume playback once the user selects a new position.
+ */
+ public void pausePlaybackForSeeking() {
+ if (mMediaPlayer != null) {
+ mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
+ }
+ pausePlayback(true);
+ }
+
+ public void resumePlaybackAfterSeeking(int desiredPosition) {
+ mPosition = desiredPosition;
+ if (mShouldResumePlaybackAfterSeeking) {
+ mShouldResumePlaybackAfterSeeking = false;
+ resumePlayback();
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public void seek(int position) {
+ mPosition = position;
+ mMediaPlayer.seekTo(mPosition);
+ }
+
+ private void enableProximitySensor() {
+ if (mProximityWakeLock == null
+ || mIsSpeakerphoneOn
+ || !mIsPrepared
+ || mMediaPlayer == null
+ || !mMediaPlayer.isPlaying()) {
+ return;
+ }
+
+ if (!mProximityWakeLock.isHeld()) {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.enableProximitySensor", "acquiring proximity wake lock");
+ mProximityWakeLock.acquire();
+ } else {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.enableProximitySensor",
+ "proximity wake lock already acquired");
+ }
+ }
+
+ private void disableProximitySensor(boolean waitForFarState) {
+ if (mProximityWakeLock == null) {
+ return;
+ }
+ if (mProximityWakeLock.isHeld()) {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.disableProximitySensor", "releasing proximity wake lock");
+ int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
+ mProximityWakeLock.release(flags);
+ } else {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.disableProximitySensor",
+ "proximity wake lock already released");
+ }
+ }
+
+ /** This is for use by UI interactions only. It simplifies UI logic. */
+ public void toggleSpeakerphone() {
+ mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
+ setSpeakerphoneOn(!mIsSpeakerphoneOn);
+ }
+
+ public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
+ mOnVoicemailDeletedListener = listener;
+ }
+
+ public int getMediaPlayerPosition() {
+ return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
+ }
+
+ void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeleted(viewHolder, mVoicemailUri);
+ }
+ }
+
+ void onVoicemailDeleteUndo(int adapterPosition) {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeleteUndo(mRowId, adapterPosition, mVoicemailUri);
+ }
+ }
+
+ void onVoicemailDeletedInDatabase() {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase(mRowId, mVoicemailUri);
+ }
+ }
+
+ @VisibleForTesting
+ public boolean isPlaying() {
+ return mIsPlaying;
+ }
+
+ @VisibleForTesting
+ public boolean isSpeakerphoneOn() {
+ return 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) {
+ if (mView == null) {
+ return;
+ }
+
+ 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 */);
+ } else {
+ enableProximitySensor();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void clearInstance() {
+ sInstance = null;
+ }
+
+ /**
+ * Share voicemail to be opened by user selected apps. This method will collect information, copy
+ * voicemail to a temporary file in background and launch a chooser intent to share it.
+ */
+ @TargetApi(VERSION_CODES.M)
+ public void shareVoicemail() {
+ mAsyncTaskExecutor.submit(
+ Tasks.SHARE_VOICEMAIL,
+ new AsyncTask<Void, Void, Uri>() {
+ @Nullable
+ @Override
+ protected Uri doInBackground(Void... params) {
+ ContentResolver contentResolver = mContext.getContentResolver();
+ try (Cursor callLogInfo = getCallLogInfoCursor(contentResolver, mVoicemailUri);
+ Cursor contentInfo = getContentInfoCursor(contentResolver, mVoicemailUri)) {
+
+ if (hasContent(callLogInfo) && hasContent(contentInfo)) {
+ String cachedName = callLogInfo.getString(CallLogQuery.CACHED_NAME);
+ String number =
+ contentInfo.getString(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.NUMBER));
+ long date =
+ contentInfo.getLong(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.DATE));
+ String mimeType =
+ contentInfo.getString(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.MIME_TYPE));
+
+ // Copy voicemail content to a new file.
+ // Please see reference in third_party/java_src/android_app/dialer/java/com/android/
+ // dialer/app/res/xml/file_paths.xml for correct cache directory name.
+ File parentDir = new File(mContext.getCacheDir(), "my_cache");
+ if (!parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ File temporaryVoicemailFile =
+ new File(parentDir, getFileName(cachedName, number, mimeType, date));
+
+ try (InputStream inputStream = contentResolver.openInputStream(mVoicemailUri);
+ OutputStream outputStream =
+ contentResolver.openOutputStream(Uri.fromFile(temporaryVoicemailFile))) {
+ if (inputStream != null && outputStream != null) {
+ ByteStreams.copy(inputStream, outputStream);
+ return FileProvider.getUriForFile(
+ mContext,
+ Constants.get().getFileProviderAuthority(),
+ temporaryVoicemailFile);
+ }
+ } catch (IOException e) {
+ LogUtil.e(
+ "VoicemailAsyncTaskUtil.shareVoicemail",
+ "failed to copy voicemail content to new file: ",
+ e);
+ }
+ return null;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ if (uri == null) {
+ LogUtil.e("VoicemailAsyncTaskUtil.shareVoicemail", "failed to get voicemail");
+ } else {
+ mContext.startActivity(
+ Intent.createChooser(
+ getShareIntent(mContext, uri),
+ mContext.getResources().getText(R.string.call_log_action_share_voicemail)));
+ }
+ }
+ });
+ }
+
+ private static String getFileName(String cachedName, String number, String mimeType, long date) {
+ String callerName = TextUtils.isEmpty(cachedName) ? number : cachedName;
+ SimpleDateFormat simpleDateFormat =
+ new SimpleDateFormat(VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT, Locale.getDefault());
+
+ String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+
+ return callerName
+ + "_"
+ + simpleDateFormat.format(new Date(date))
+ + (TextUtils.isEmpty(fileExtension) ? "" : "." + fileExtension);
+ }
+
+ private static Intent getShareIntent(Context context, 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(context.getContentResolver().getType(voicemailFileUri));
+ return shareIntent;
+ }
+
+ private static boolean hasContent(@Nullable Cursor cursor) {
+ return cursor != null && cursor.moveToFirst();
+ }
+
+ @Nullable
+ private static Cursor getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
+ return contentResolver.query(
+ ContentUris.withAppendedId(
+ CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(voicemailUri)),
+ CallLogQuery.getProjection(),
+ null,
+ null,
+ null);
+ }
+
+ @Nullable
+ private static Cursor getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
+ return contentResolver.query(
+ voicemailUri,
+ new String[] {
+ VoicemailContract.Voicemails._ID,
+ VoicemailContract.Voicemails.NUMBER,
+ VoicemailContract.Voicemails.DATE,
+ VoicemailContract.Voicemails.MIME_TYPE,
+ },
+ null,
+ null,
+ null);
+ }
+
+ /** The enumeration of {@link AsyncTask} objects we use in this class. */
+ public enum Tasks {
+ CHECK_FOR_CONTENT,
+ CHECK_CONTENT_AFTER_CHANGE,
+ SHARE_VOICEMAIL,
+ SEND_FETCH_REQUEST
+ }
+
+ /** Contract describing the behaviour we need from the ui we are controlling. */
+ public interface PlaybackView {
+
+ int getDesiredClipPosition();
+
+ void disableUiElements();
+
+ void enableUiElements();
+
+ void onPlaybackError();
+
+ void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
+
+ void onPlaybackStopped();
+
+ void onSpeakerphoneOn(boolean on);
+
+ void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
+
+ void setSuccess();
+
+ void setFetchContentTimeout();
+
+ void setIsFetchingContent();
+
+ void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
+
+ void resetSeekBar();
+ }
+
+ public interface OnVoicemailDeletedListener {
+
+ void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri);
+
+ void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri);
+
+ void onVoicemailDeletedInDatabase(long rowId, Uri uri);
+ }
+
+ protected interface OnContentCheckedListener {
+
+ void onContentChecked(boolean hasContent);
+ }
+
+ @ThreadSafe
+ private class FetchResultHandler extends ContentObserver implements Runnable {
+
+ private final Handler mFetchResultHandler;
+ private final Uri mVoicemailUri;
+ private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
+
+ public FetchResultHandler(Handler handler, Uri uri, int code) {
+ super(handler);
+ mFetchResultHandler = handler;
+ mVoicemailUri = uri;
+ if (mContext != null) {
+ mContext.getContentResolver().registerContentObserver(mVoicemailUri, false, this);
+ mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
+ }
+ }
+
+ /** Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */
+ @Override
+ public void run() {
+ if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ if (mView != null) {
+ mView.setFetchContentTimeout();
+ }
+ }
+ }
+
+ public void destroy() {
+ if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ mFetchResultHandler.removeCallbacks(this);
+ }
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
+ mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
+ prepareContent();
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java b/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java
new file mode 100644
index 000000000..24d4c6ff7
--- /dev/null
+++ b/java/com/android/dialer/app/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.app.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();
+ 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);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
new file mode 100644
index 000000000..65d043034
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.app.voicemail.error">
+
+ <uses-permission android:name="android.permission.CALL_PHONE"/>
+</manifest>
diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
new file mode 100644
index 000000000..e36406d17
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
@@ -0,0 +1,177 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Create error message from {@link VoicemailStatus} for OMTP visual voicemail. This is also the
+ * default behavior if other message creator does not handle the status.
+ */
+public class OmtpVoicemailMessageCreator {
+
+ private static final float QUOTA_NEAR_FULL_THRESHOLD = 0.9f;
+ private static final float QUOTA_FULL_THRESHOLD = 0.99f;
+
+ @Nullable
+ public static VoicemailErrorMessage create(Context context, VoicemailStatus status) {
+ if (Status.CONFIGURATION_STATE_OK == status.configurationState
+ && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
+ && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
+
+ return checkQuota(context, status);
+ }
+ // Initial state when the source is activating. Other error might be written into data and
+ // notification channel during activation.
+ if (Status.CONFIGURATION_STATE_CONFIGURING == status.configurationState
+ && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
+ && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_activating_title),
+ context.getString(R.string.voicemail_error_activating_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context));
+ }
+
+ if (Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION == status.notificationChannelState) {
+ return createNoSignalMessage(context, status);
+ }
+
+ if (Status.CONFIGURATION_STATE_FAILED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_activation_failed_title),
+ context.getString(R.string.voicemail_error_activation_failed_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_no_data_title),
+ context.getString(R.string.voicemail_error_no_data_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_no_data_title),
+ context.getString(R.string.voicemail_error_no_data_cellular_required_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_bad_config_title),
+ context.getString(R.string.voicemail_error_bad_config_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_communication_title),
+ context.getString(R.string.voicemail_error_communication_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_SERVER_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_server_title),
+ context.getString(R.string.voicemail_error_server_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_server_connection_title),
+ context.getString(R.string.voicemail_error_server_connection_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ // This should be an assertion error, but there's a bug in NYC-DR (b/31069259) that will
+ // sometimes give status mixed from multiple SIMs. There's no meaningful message to be displayed
+ // from it, so just suppress the message.
+ LogUtil.e("OmtpVoicemailMessageCreator.create", "Unhandled status: " + status);
+ return null;
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage checkQuota(Context context, VoicemailStatus status) {
+ if (status.quotaOccupied != Status.QUOTA_UNAVAILABLE
+ && status.quotaTotal != Status.QUOTA_UNAVAILABLE) {
+ if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_FULL_THRESHOLD) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_inbox_full_title),
+ context.getString(R.string.voicemail_error_inbox_full_message));
+ }
+
+ if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_NEAR_FULL_THRESHOLD) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_inbox_near_full_title),
+ context.getString(R.string.voicemail_error_inbox_near_full_message));
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage createNoSignalMessage(
+ Context context, VoicemailStatus status) {
+ CharSequence title;
+ CharSequence description;
+ List<Action> actions = new ArrayList<>();
+ if (Status.CONFIGURATION_STATE_OK == status.configurationState) {
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED == status.dataChannelState) {
+ title = context.getString(R.string.voicemail_error_no_signal_title);
+ description =
+ context.getString(R.string.voicemail_error_no_signal_cellular_required_message);
+ } else {
+ title = context.getString(R.string.voicemail_error_no_signal_title);
+ if (status.isAirplaneMode) {
+ description = context.getString(R.string.voicemail_error_no_signal_airplane_mode_message);
+ } else {
+ description = context.getString(R.string.voicemail_error_no_signal_message);
+ }
+ actions.add(VoicemailErrorMessage.createSyncAction(context, status));
+ }
+ } else {
+ title = context.getString(R.string.voicemail_error_not_activate_no_signal_title);
+ if (status.isAirplaneMode) {
+ description =
+ context.getString(
+ R.string.voicemail_error_not_activate_no_signal_airplane_mode_message);
+ } else {
+ description = context.getString(R.string.voicemail_error_not_activate_no_signal_message);
+ actions.add(VoicemailErrorMessage.createRetryAction(context, status));
+ }
+ }
+ if (status.isAirplaneMode) {
+ actions.add(VoicemailErrorMessage.createChangeAirplaneModeAction(context));
+ }
+ return new VoicemailErrorMessage(title, description, actions);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java
new file mode 100644
index 000000000..d34a0f3c7
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java
@@ -0,0 +1,165 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.view.View;
+import android.widget.TextView;
+import com.android.dialer.app.alert.AlertManager;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.List;
+
+/**
+ * UI for the voicemail error message, which will be inserted to the top of the voicemail tab if any
+ * occurred.
+ */
+public class VoicemailErrorAlert {
+
+ private final Context context;
+ private final AlertManager alertManager;
+ private final VoicemailErrorMessageCreator messageCreator;
+
+ private final View view;
+ private final TextView header;
+ private final TextView details;
+ private final TextView primaryAction;
+ private final TextView secondaryAction;
+ private final TextView primaryActionRaised;
+ private final TextView secondaryActionRaised;
+ private final AlertManager modalAlertManager;
+ private View modalView;
+
+ public VoicemailErrorAlert(
+ Context context,
+ AlertManager alertManager,
+ AlertManager modalAlertManager,
+ VoicemailErrorMessageCreator messageCreator) {
+ this.context = context;
+ this.alertManager = alertManager;
+ this.modalAlertManager = modalAlertManager;
+ this.messageCreator = messageCreator;
+
+ view = alertManager.inflate(R.layout.voicemai_error_message_fragment);
+ header = (TextView) view.findViewById(R.id.error_card_header);
+ details = (TextView) view.findViewById(R.id.error_card_details);
+ primaryAction = (TextView) view.findViewById(R.id.primary_action);
+ secondaryAction = (TextView) view.findViewById(R.id.secondary_action);
+ primaryActionRaised = (TextView) view.findViewById(R.id.primary_action_raised);
+ secondaryActionRaised = (TextView) view.findViewById(R.id.secondary_action_raised);
+ }
+
+ public void updateStatus(List<VoicemailStatus> statuses, VoicemailStatusReader statusReader) {
+ LogUtil.i("VoicemailErrorAlert.updateStatus", "%d status", statuses.size());
+ VoicemailErrorMessage message = null;
+ view.setVisibility(View.VISIBLE);
+ for (VoicemailStatus status : statuses) {
+ message = messageCreator.create(context, status, statusReader);
+ if (message != null) {
+ break;
+ }
+ }
+
+ alertManager.clear();
+ modalAlertManager.clear();
+ if (message != null) {
+ LogUtil.i(
+ "VoicemailErrorAlert.updateStatus",
+ "isModal: %b, %s",
+ message.isModal(),
+ message.getTitle());
+ if (message.isModal()) {
+ if (message instanceof VoicemailTosMessage) {
+ modalView = getTosView(modalAlertManager, (VoicemailTosMessage) message);
+ } else {
+ throw new IllegalArgumentException("Modal message type is undefined!");
+ }
+ modalAlertManager.add(modalView);
+ } else {
+ loadMessage(message);
+ alertManager.add(view);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public View getView() {
+ return view;
+ }
+
+ @VisibleForTesting
+ public View getModalView() {
+ return modalView;
+ }
+
+ void loadMessage(VoicemailErrorMessage message) {
+ header.setText(message.getTitle());
+ details.setText(message.getDescription());
+ bindActions(message);
+ }
+
+ private View getTosView(AlertManager alertManager, VoicemailTosMessage message) {
+ View view = alertManager.inflate(R.layout.voicemail_tos_fragment);
+ TextView tosTitle = (TextView) view.findViewById(R.id.tos_message_title);
+ tosTitle.setText(message.getTitle());
+ TextView tosDetails = (TextView) view.findViewById(R.id.tos_message_details);
+ tosDetails.setText(message.getDescription());
+
+ Assert.checkArgument(message.getActions().size() == 2);
+ Action primaryAction = message.getActions().get(0);
+ TextView primaryButton = (TextView) view.findViewById(R.id.voicemail_tos_button_decline);
+ primaryButton.setText(primaryAction.getText());
+ primaryButton.setOnClickListener(primaryAction.getListener());
+ Action secondaryAction = message.getActions().get(1);
+ TextView secondaryButton = (TextView) view.findViewById(R.id.voicemail_tos_button_accept);
+ secondaryButton.setText(secondaryAction.getText());
+ secondaryButton.setOnClickListener(secondaryAction.getListener());
+ return view;
+ }
+
+ /**
+ * Attach actions to buttons until all buttons are assigned. If there are not enough actions the
+ * rest of the buttons will be removed. If there are more actions then buttons the extra actions
+ * will be dropped. {@link VoicemailErrorMessage#getActions()} will specify what actions should be
+ * shown and in what order.
+ */
+ private void bindActions(VoicemailErrorMessage message) {
+ TextView[] buttons = new TextView[] {primaryAction, secondaryAction};
+ TextView[] raisedButtons = new TextView[] {primaryActionRaised, secondaryActionRaised};
+ for (int i = 0; i < buttons.length; i++) {
+ if (message.getActions() != null && i < message.getActions().size()) {
+ VoicemailErrorMessage.Action action = message.getActions().get(i);
+ TextView button;
+ if (action.isRaised()) {
+ button = raisedButtons[i];
+ buttons[i].setVisibility(View.GONE);
+ } else {
+ button = buttons[i];
+ raisedButtons[i].setVisibility(View.GONE);
+ }
+ button.setText(action.getText());
+ button.setOnClickListener(action.getListener());
+ button.setVisibility(View.VISIBLE);
+ } else {
+ buttons[i].setVisibility(View.GONE);
+ raisedButtons[i].setVisibility(View.GONE);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
new file mode 100644
index 000000000..61572008b
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
@@ -0,0 +1,178 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.provider.VoicemailContract;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.CallUtil;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents an error determined from the current {@link
+ * android.provider.VoicemailContract.Status}. The message will contain a title, a description, and
+ * a list of actions that can be performed.
+ */
+public class VoicemailErrorMessage {
+
+ private final CharSequence title;
+ private final CharSequence description;
+ private final List<Action> actions;
+
+ private boolean modal;
+
+ /** Something the user can click on to resolve an error, such as retrying or calling voicemail */
+ public static class Action {
+
+ private final CharSequence text;
+ private final View.OnClickListener listener;
+ private final boolean raised;
+
+ public Action(CharSequence text, View.OnClickListener listener) {
+ this(text, listener, false);
+ }
+
+ public Action(CharSequence text, View.OnClickListener listener, boolean raised) {
+ this.text = text;
+ this.listener = listener;
+ this.raised = raised;
+ }
+
+ public CharSequence getText() {
+ return text;
+ }
+
+ public View.OnClickListener getListener() {
+ return listener;
+ }
+
+ public boolean isRaised() {
+ return raised;
+ }
+ }
+
+ public CharSequence getTitle() {
+ return title;
+ }
+
+ public CharSequence getDescription() {
+ return description;
+ }
+
+ @Nullable
+ public List<Action> getActions() {
+ return actions;
+ }
+
+ public boolean isModal() {
+ return modal;
+ }
+
+ public VoicemailErrorMessage setModal(boolean value) {
+ modal = value;
+ return this;
+ }
+
+ public VoicemailErrorMessage(CharSequence title, CharSequence description, Action... actions) {
+ this(title, description, Arrays.asList(actions));
+ }
+
+ public VoicemailErrorMessage(
+ CharSequence title, CharSequence description, @Nullable List<Action> actions) {
+ this.title = title;
+ this.description = description;
+ this.actions = actions;
+ }
+
+ @NonNull
+ public static Action createChangeAirplaneModeAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_turn_off_airplane_mode),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Settings.ACTION_AIRPLANE_MODE_SETTINGS);
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createSetPinAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_set_pin),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_ALERT_SET_PIN_CLICKED);
+ Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createCallVoicemailAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_call_voicemail),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_CALL, CallUtil.getVoicemailUri());
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createSyncAction(final Context context, final VoicemailStatus status) {
+ return new Action(
+ context.getString(R.string.voicemail_action_sync),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+ intent.setPackage(status.sourcePackage);
+ context.sendBroadcast(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createRetryAction(final Context context, final VoicemailStatus status) {
+ return new Action(
+ context.getString(R.string.voicemail_action_retry),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+ intent.setPackage(status.sourcePackage);
+ context.sendBroadcast(intent);
+ }
+ });
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
new file mode 100644
index 000000000..5ebef801d
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+
+/**
+ * Given a VoicemailStatus, {@link VoicemailErrorMessageCreator#create(Context, VoicemailStatus)}
+ * will return a {@link VoicemailErrorMessage} representing the message to be shown to the user, or
+ * <code>null</code> if no message should be shown.
+ */
+public class VoicemailErrorMessageCreator {
+
+ @Nullable
+ public VoicemailErrorMessage create(
+ Context context, VoicemailStatus status, VoicemailStatusReader statusReader) {
+ // Never return error message before NMR1. Voicemail status is not supported on those.
+ if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
+ return null;
+ }
+ switch (status.type) {
+ case Vvm3VoicemailMessageCreator.VVM_TYPE_VVM3:
+ return Vvm3VoicemailMessageCreator.create(context, status, statusReader);
+ default:
+ return OmtpVoicemailMessageCreator.create(context, status);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
new file mode 100644
index 000000000..a09941de2
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
@@ -0,0 +1,260 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import com.android.dialer.database.VoicemailStatusQuery;
+
+/** Structured data from {@link android.provider.VoicemailContract.Status} */
+public class VoicemailStatus {
+
+ public final String sourcePackage;
+ public final String type;
+
+ public final String phoneAccountComponentName;
+ public final String phoneAccountId;
+
+ @Nullable public final Uri settingsUri;
+ @Nullable public final Uri voicemailAccessUri;
+
+ public final int configurationState;
+ public final int dataChannelState;
+ public final int notificationChannelState;
+
+ public final int quotaOccupied;
+ public final int quotaTotal;
+
+ // System status
+
+ public final boolean isAirplaneMode;
+
+ /** Wraps the row currently pointed by <code>statusCursor</code> */
+ public VoicemailStatus(Context context, Cursor statusCursor) {
+ sourcePackage = getString(statusCursor, VoicemailStatusQuery.SOURCE_PACKAGE_INDEX, "");
+
+ settingsUri = getUri(statusCursor, VoicemailStatusQuery.SETTINGS_URI_INDEX);
+ voicemailAccessUri = getUri(statusCursor, VoicemailStatusQuery.VOICEMAIL_ACCESS_URI_INDEX);
+
+ configurationState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.CONFIGURATION_STATE_INDEX,
+ Status.CONFIGURATION_STATE_NOT_CONFIGURED);
+ dataChannelState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.DATA_CHANNEL_STATE_INDEX,
+ Status.DATA_CHANNEL_STATE_NO_CONNECTION);
+ notificationChannelState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.NOTIFICATION_CHANNEL_STATE_INDEX,
+ Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+
+ isAirplaneMode =
+ Settings.System.getInt(context.getContentResolver(), Global.AIRPLANE_MODE_ON, 0) != 0;
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ quotaOccupied =
+ getInt(statusCursor, VoicemailStatusQuery.QUOTA_OCCUPIED_INDEX, Status.QUOTA_UNAVAILABLE);
+ quotaTotal =
+ getInt(statusCursor, VoicemailStatusQuery.QUOTA_TOTAL_INDEX, Status.QUOTA_UNAVAILABLE);
+ } else {
+ quotaOccupied = Status.QUOTA_UNAVAILABLE;
+ quotaTotal = Status.QUOTA_UNAVAILABLE;
+ }
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ type =
+ getString(
+ statusCursor, VoicemailStatusQuery.SOURCE_TYPE_INDEX, TelephonyManager.VVM_TYPE_OMTP);
+ phoneAccountComponentName =
+ getString(statusCursor, VoicemailStatusQuery.PHONE_ACCOUNT_COMPONENT_NAME, "");
+ phoneAccountId = getString(statusCursor, VoicemailStatusQuery.PHONE_ACCOUNT_ID, "");
+ } else {
+ type = TelephonyManager.VVM_TYPE_OMTP;
+ phoneAccountComponentName = "";
+ phoneAccountId = "";
+ }
+ }
+
+ private VoicemailStatus(Builder builder) {
+ sourcePackage = builder.sourcePackage;
+ phoneAccountComponentName = builder.phoneAccountComponentName;
+ phoneAccountId = builder.phoneAccountId;
+ type = builder.type;
+ settingsUri = builder.settingsUri;
+ voicemailAccessUri = builder.voicemailAccessUri;
+ configurationState = builder.configurationState;
+ dataChannelState = builder.dataChannelState;
+ notificationChannelState = builder.notificationChannelState;
+ quotaOccupied = builder.quotaOccupied;
+ quotaTotal = builder.quotaTotal;
+ isAirplaneMode = builder.isAirplaneMode;
+ }
+
+ static class Builder {
+
+ private String sourcePackage = "";
+ private String type = TelephonyManager.VVM_TYPE_OMTP;
+ private String phoneAccountComponentName = "";
+ private String phoneAccountId = "";
+
+ @Nullable private Uri settingsUri;
+ @Nullable private Uri voicemailAccessUri;
+
+ private int configurationState = Status.CONFIGURATION_STATE_NOT_CONFIGURED;
+ private int dataChannelState = Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+ private int notificationChannelState = Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+
+ private int quotaOccupied = Status.QUOTA_UNAVAILABLE;
+ private int quotaTotal = Status.QUOTA_UNAVAILABLE;
+
+ private boolean isAirplaneMode;
+
+ public VoicemailStatus build() {
+ return new VoicemailStatus(this);
+ }
+
+ public Builder setSourcePackage(String sourcePackage) {
+ this.sourcePackage = sourcePackage;
+ return this;
+ }
+
+ public Builder setType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder setPhoneAccountComponentName(String name) {
+ this.phoneAccountComponentName = name;
+ return this;
+ }
+
+ public Builder setPhoneAccountId(String id) {
+ this.phoneAccountId = id;
+ return this;
+ }
+
+ public Builder setSettingsUri(Uri settingsUri) {
+ this.settingsUri = settingsUri;
+ return this;
+ }
+
+ public Builder setVoicemailAccessUri(Uri voicemailAccessUri) {
+ this.voicemailAccessUri = voicemailAccessUri;
+ return this;
+ }
+
+ public Builder setConfigurationState(int configurationState) {
+ this.configurationState = configurationState;
+ return this;
+ }
+
+ public Builder setDataChannelState(int dataChannelState) {
+ this.dataChannelState = dataChannelState;
+ return this;
+ }
+
+ public Builder setNotificationChannelState(int notificationChannelState) {
+ this.notificationChannelState = notificationChannelState;
+ return this;
+ }
+
+ public Builder setQuotaOccupied(int quotaOccupied) {
+ this.quotaOccupied = quotaOccupied;
+ return this;
+ }
+
+ public Builder setQuotaTotal(int quotaTotal) {
+ this.quotaTotal = quotaTotal;
+ return this;
+ }
+
+ public Builder setAirplaneMode(boolean isAirplaneMode) {
+ this.isAirplaneMode = isAirplaneMode;
+ return this;
+ }
+ }
+
+ public boolean isActive() {
+ switch (configurationState) {
+ case Status.CONFIGURATION_STATE_NOT_CONFIGURED:
+ case Status.CONFIGURATION_STATE_DISABLED:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "VoicemailStatus["
+ + "sourcePackage: "
+ + sourcePackage
+ + ", type:"
+ + type
+ + ", settingsUri: "
+ + settingsUri
+ + ", voicemailAccessUri: "
+ + voicemailAccessUri
+ + ", configurationState: "
+ + configurationState
+ + ", dataChannelState: "
+ + dataChannelState
+ + ", notificationChannelState: "
+ + notificationChannelState
+ + ", quotaOccupied: "
+ + quotaOccupied
+ + ", quotaTotal: "
+ + quotaTotal
+ + ", isAirplaneMode: "
+ + isAirplaneMode
+ + "]";
+ }
+
+ @Nullable
+ private static Uri getUri(Cursor cursor, int index) {
+ if (cursor.getString(index) != null) {
+ return Uri.parse(cursor.getString(index));
+ }
+ return null;
+ }
+
+ private static int getInt(Cursor cursor, int index, int defaultValue) {
+ if (cursor.isNull(index)) {
+ return defaultValue;
+ }
+ return cursor.getInt(index);
+ }
+
+ private static String getString(Cursor cursor, int index, String defaultValue) {
+ if (cursor.isNull(index)) {
+ return defaultValue;
+ }
+ return cursor.getString(index);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
new file mode 100644
index 000000000..6f411217c
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
@@ -0,0 +1,114 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Status;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+
+/**
+ * This class will detect the corruption in the voicemail status and log it so we can track how many
+ * users are affected.
+ */
+public class VoicemailStatusCorruptionHandler {
+
+ /** Where the check is made so logging can be done. */
+ public enum Source {
+ Activity,
+ Notification
+ }
+
+ private static final String CONFIG_VVM_STATUS_FIX_DISABLED = "vvm_status_fix_disabled";
+
+ public static void maybeFixVoicemailStatus(Context context, Cursor statusCursor, Source source) {
+
+ if (ConfigProviderBindings.get(context).getBoolean(CONFIG_VVM_STATUS_FIX_DISABLED, false)) {
+ return;
+ }
+
+ if (VERSION.SDK_INT != VERSION_CODES.N_MR1) {
+ // This issue is specific to N MR1, it is fixed in future SDK.
+ return;
+ }
+
+ if (statusCursor.getCount() == 0) {
+ return;
+ }
+
+ statusCursor.moveToFirst();
+ VoicemailStatus status = new VoicemailStatus(context, statusCursor);
+ PhoneAccountHandle phoneAccountHandle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(status.phoneAccountComponentName),
+ status.phoneAccountId);
+
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+
+ boolean visualVoicemailEnabled =
+ TelephonyManagerCompat.isVisualVoicemailEnabled(telephonyManager, phoneAccountHandle);
+ LogUtil.i(
+ "VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus",
+ "Source="
+ + source
+ + ", CONFIGURATION_STAIE="
+ + status.configurationState
+ + ", visualVoicemailEnabled="
+ + visualVoicemailEnabled);
+
+ // If visual voicemail is enabled, the CONFIGURATION_STATE should be either OK, PIN_NOT_SET,
+ // or other failure code. CONFIGURATION_STATE_NOT_CONFIGURED means that the client has been
+ // shut down improperly (b/32371710). The client should be reset or the VVM tab will be
+ // missing.
+ if (Status.CONFIGURATION_STATE_NOT_CONFIGURED == status.configurationState
+ && visualVoicemailEnabled) {
+ LogUtil.e(
+ "VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus",
+ "VVM3 voicemail status corrupted");
+
+ switch (source) {
+ case Activity:
+ Logger.get(context)
+ .logImpression(
+ DialerImpression.Type
+ .VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY);
+ break;
+ case Notification:
+ Logger.get(context)
+ .logImpression(
+ DialerImpression.Type
+ .VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION);
+ break;
+ default:
+ Assert.fail("this should never happen");
+ break;
+ }
+ // At this point we could attempt to work around the issue by disabling and re-enabling
+ // voicemail. Unfortunately this work around is buggy so we'll do nothing for now.
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java
new file mode 100644
index 000000000..fd9e7ef25
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java
@@ -0,0 +1,25 @@
+/*
+ * 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.app.voicemail.error;
+
+/**
+ * A source that is generating the voicemail status to show error messages, used by {@link
+ * VoicemailErrorMessageCreator} to inform the source that the status should be updated
+ */
+public interface VoicemailStatusReader {
+ void refresh();
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java
new file mode 100644
index 000000000..86b124419
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.app.voicemail.error;
+
+/** Voicemail TOS message. */
+public class VoicemailTosMessage extends VoicemailErrorMessage {
+
+ public VoicemailTosMessage(CharSequence title, CharSequence description, Action... actions) {
+ super(title, description, actions);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
new file mode 100644
index 000000000..6e9405cbf
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
@@ -0,0 +1,428 @@
+/*
+ * 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.app.voicemail.error;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import java.util.Locale;
+
+/**
+ * Create error message from {@link VoicemailStatus} for VVM3 visual voicemail. VVM3 is used only by
+ * Verizon Wireless.
+ */
+@RequiresApi(VERSION_CODES.N_MR1)
+public class Vvm3VoicemailMessageCreator {
+
+ public static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
+
+ // Copied from com.android.phone.vvm.omtp.protocol.Vvm3EventHandler
+ // TODO(b/28380841): unbundle VVM client so we can access these values directly
+ public static final int VMS_DNS_FAILURE = -9001;
+ public static final int VMG_DNS_FAILURE = -9002;
+ public static final int SPG_DNS_FAILURE = -9003;
+ public static final int VMS_NO_CELLULAR = -9004;
+ public static final int VMG_NO_CELLULAR = -9005;
+ public static final int SPG_NO_CELLULAR = -9006;
+ public static final int VMS_TIMEOUT = -9007;
+ public static final int VMG_TIMEOUT = -9008;
+ public static final int STATUS_SMS_TIMEOUT = -9009;
+
+ public static final int SUBSCRIBER_BLOCKED = -9990;
+ public static final int UNKNOWN_USER = -9991;
+ public static final int UNKNOWN_DEVICE = -9992;
+ public static final int INVALID_PASSWORD = -9993;
+ public static final int MAILBOX_NOT_INITIALIZED = -9994;
+ public static final int SERVICE_NOT_PROVISIONED = -9995;
+ public static final int SERVICE_NOT_ACTIVATED = -9996;
+ public static final int USER_BLOCKED = -9998;
+ public static final int IMAP_GETQUOTA_ERROR = -9997;
+ public static final int IMAP_SELECT_ERROR = -9989;
+ public static final int IMAP_ERROR = -9999;
+
+ public static final int VMG_INTERNAL_ERROR = -101;
+ public static final int VMG_DB_ERROR = -102;
+ public static final int VMG_COMMUNICATION_ERROR = -103;
+ public static final int SPG_URL_NOT_FOUND = -301;
+
+ // Non VVM3 codes:
+ public static final int VMG_UNKNOWN_ERROR = -1;
+ public static final int PIN_NOT_SET = -100;
+ public static final int SUBSCRIBER_UNKNOWN = -99;
+
+ private static final String ISO639_SPANISH = "es";
+ @VisibleForTesting static final String VVM3_TOS_ACCEPTANCE_FLAG_KEY = "vvm3_tos_acceptance_flag";
+
+ @Nullable
+ public static VoicemailErrorMessage create(
+ final Context context,
+ final VoicemailStatus status,
+ final VoicemailStatusReader statusReader) {
+ VoicemailErrorMessage tosMessage = maybeShowTosMessage(context, status, statusReader);
+ if (tosMessage != null) {
+ return tosMessage;
+ }
+
+ if (VMS_DNS_FAILURE == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_DNS_FAILURE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SPG_DNS_FAILURE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_spg_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_spg_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMS_NO_CELLULAR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_NO_CELLULAR == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SPG_NO_CELLULAR == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_spg_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_spg_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMS_TIMEOUT == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_TIMEOUT == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (STATUS_SMS_TIMEOUT == status.notificationChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_status_sms_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_status_sms_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SUBSCRIBER_BLOCKED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_subscriber_blocked_title),
+ getCustomerSupportString(context, R.string.vvm3_error_subscriber_blocked_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (UNKNOWN_USER == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_unknown_user_title),
+ getCustomerSupportString(context, R.string.vvm3_error_unknown_user_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (UNKNOWN_DEVICE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_unknown_device_title),
+ getCustomerSupportString(context, R.string.vvm3_error_unknown_device_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (INVALID_PASSWORD == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_invalid_password_title),
+ getCustomerSupportString(context, R.string.vvm3_error_invalid_password_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (MAILBOX_NOT_INITIALIZED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_mailbox_not_initialized_title),
+ getCustomerSupportString(context, R.string.vvm3_error_mailbox_not_initialized_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SERVICE_NOT_PROVISIONED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_service_not_provisioned_title),
+ getCustomerSupportString(context, R.string.vvm3_error_service_not_provisioned_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SERVICE_NOT_ACTIVATED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_service_not_activated_title),
+ getCustomerSupportString(context, R.string.vvm3_error_service_not_activated_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (USER_BLOCKED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_user_blocked_title),
+ getCustomerSupportString(context, R.string.vvm3_error_user_blocked_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SUBSCRIBER_UNKNOWN == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_subscriber_unknown_title),
+ getCustomerSupportString(context, R.string.vvm3_error_subscriber_unknown_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_GETQUOTA_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_getquota_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_getquota_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_SELECT_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_select_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_select_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (PIN_NOT_SET == status.configurationState) {
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_ALERT_SET_PIN_SHOWN);
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_pin_not_set_title),
+ getCustomerSupportString(context, R.string.voicemail_error_pin_not_set_message),
+ VoicemailErrorMessage.createSetPinAction(context));
+ }
+
+ return OmtpVoicemailMessageCreator.create(context, status);
+ }
+
+ @NonNull
+ private static CharSequence getCustomerSupportString(Context context, int id) {
+ // TODO: get number based on the country the user is currently in.
+ return ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ context.getResources(),
+ id,
+ context.getString(R.string.verizon_domestic_customer_support_display_number));
+ }
+
+ @NonNull
+ private static Action createCallCustomerSupportAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_call_customer_support),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent =
+ new Intent(
+ Intent.ACTION_CALL,
+ Uri.parse(
+ "tel:"
+ + context.getString(
+ R.string.verizon_domestic_customer_support_number)));
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage maybeShowTosMessage(
+ final Context context,
+ final VoicemailStatus status,
+ final VoicemailStatusReader statusReader) {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (preferences.getBoolean(VVM3_TOS_ACCEPTANCE_FLAG_KEY, false)) {
+ return null;
+ }
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_SHOWN);
+
+ CharSequence termsAndConditions;
+ CharSequence acceptText;
+ CharSequence declineText;
+ // TODO(b/29082671): use LocaleList
+ if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_SPANISH).getLanguage())) {
+ // Spanish
+ termsAndConditions = context.getString(R.string.verizon_terms_and_conditions_1_1_spanish);
+ acceptText = context.getString(R.string.verizon_terms_and_conditions_accept_spanish);
+ declineText = context.getString(R.string.verizon_terms_and_conditions_decline_spanish);
+ } else {
+ termsAndConditions = context.getString(R.string.verizon_terms_and_conditions_1_1_english);
+ acceptText = context.getString(R.string.verizon_terms_and_conditions_accept_english);
+ declineText = context.getString(R.string.verizon_terms_and_conditions_decline_english);
+ }
+
+ return new VoicemailTosMessage(
+ context.getString(R.string.verizon_terms_and_conditions_title),
+ context.getString(R.string.verizon_terms_and_conditions_message, termsAndConditions),
+ new Action(
+ declineText,
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtil.i("Vvm3VoicemailMessageCreator.maybeShowTosMessage", "decline clicked");
+ PhoneAccountHandle handle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(status.phoneAccountComponentName),
+ status.phoneAccountId);
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINE_CLICKED);
+ showDeclineTosDialog(context, handle, status);
+ }
+ }),
+ new Action(
+ acceptText,
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtil.i("Vvm3VoicemailMessageCreator.maybeShowTosMessage", "accept clicked");
+ preferences.edit().putBoolean(VVM3_TOS_ACCEPTANCE_FLAG_KEY, true).apply();
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_ACCEPTED);
+ statusReader.refresh();
+ }
+ },
+ true /* raised */))
+ .setModal(true);
+ }
+
+ private static void showDeclineTosDialog(
+ final Context context, final PhoneAccountHandle handle, VoicemailStatus status) {
+ if (PIN_NOT_SET == status.configurationState) {
+ LogUtil.i(
+ "Vvm3VoicemailMessageCreator.showDeclineTosDialog",
+ "PIN_NOT_SET, showing set PIN dialog");
+ showSetPinBeforeDeclineDialog(context);
+ return;
+ }
+ LogUtil.i(
+ "Vvm3VoicemailMessageCreator.showDeclineTosDialog",
+ "showing decline ToS dialog, status=" + status);
+ final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.verizon_terms_and_conditions_decline_dialog_message);
+ builder.setPositiveButton(
+ R.string.verizon_terms_and_conditions_decline_dialog_downgrade,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINED);
+ TelephonyManagerCompat.setVisualVoicemailEnabled(telephonyManager, handle, false);
+ }
+ });
+
+ builder.setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ builder.setCancelable(true);
+ builder.show();
+ }
+
+ private static void showSetPinBeforeDeclineDialog(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.verizon_terms_and_conditions_decline_set_pin_dialog_message);
+ builder.setPositiveButton(
+ R.string.verizon_terms_and_conditions_decline_set_pin_dialog_set_pin,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINE_CHANGE_PIN_SHOWN);
+ Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+ context.startActivity(intent);
+ }
+ });
+
+ builder.setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ builder.setCancelable(true);
+ builder.show();
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
new file mode 100644
index 000000000..0dfb1c2fd
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<android.support.v7.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/error_card"
+ style="@style/CallLogCardStyle"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/error_card_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/alert_main_padding"
+ android:layout_marginStart="@dimen/alert_main_padding"
+ android:layout_marginEnd="@dimen/alert_main_padding"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/error_card_header"
+ android:textStyle="bold"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/alert_title_padding"
+ android:layout_gravity="center_vertical"
+ android:singleLine="false"
+ android:textColor="@color/primary_text_color"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+
+ <TextView
+ android:id="@+id/error_card_details"
+ android:autoLink="web"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lineSpacingExtra="@dimen/alert_line_spacing"
+ android:singleLine="false"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/error_actions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:paddingTop="@dimen/alert_action_vertical_padding"
+ android:paddingBottom="@dimen/alert_action_vertical_padding"
+ android:paddingStart="@dimen/alert_action_horizontal_padding"
+ android:paddingEnd="@dimen/alert_action_horizontal_padding"
+ android:gravity="start"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/primary_action_raised"
+ style="@style/RaisedErrorActionStyle"
+ android:nextFocusLeft="@+id/promo_card"
+ android:nextFocusRight="@+id/primary_action"
+ android:clickable="true"
+ />
+
+ <TextView
+ android:id="@+id/primary_action"
+ style="@style/ErrorActionStyle"
+ android:background="?android:attr/selectableItemBackground"
+ android:nextFocusLeft="@+id/promo_card"
+ android:nextFocusRight="@+id/secondary_action"
+ android:clickable="true"
+ />
+
+ <TextView
+ android:id="@+id/secondary_action"
+ style="@style/ErrorActionStyle"
+ android:paddingEnd="@dimen/alert_action_between_padding"
+ android:background="?android:attr/selectableItemBackground"
+ android:nextFocusLeft="@+id/primary_action"
+ android:nextFocusRight="@+id/promo_card"
+ android:clickable="true"/>
+
+ <android.support.v4.widget.Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <TextView
+ android:id="@+id/secondary_action_raised"
+ style="@style/RaisedErrorActionStyle"
+ android:paddingEnd="@dimen/alert_action_between_padding"
+ android:layout_marginEnd="8dp"
+ android:nextFocusLeft="@+id/primary_action"
+ android:nextFocusRight="@+id/promo_card"
+ android:clickable="true"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</android.support.v7.widget.CardView>
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
new file mode 100644
index 000000000..2b9d17328
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <ScrollView
+ android:id="@+id/voicemail_tos_message"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/tos_message_title"
+ android:textStyle="bold"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="24dp"
+ android:paddingBottom="12dp"
+ android:text="@string/verizon_terms_and_conditions_title"
+ android:textColor="@color/primary_text_color"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+ <TextView
+ android:id="@+id/tos_message_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:autoLink="web"
+ android:text="@string/verizon_terms_and_conditions_1.1_english"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ </LinearLayout>
+ </ScrollView>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#D2D2D2"/>
+
+ <LinearLayout
+ android:id="@+id/voicemail_tos_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/voicemail_tos_button_decline"
+ style="@style/ErrorActionStyle"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/verizon_terms_and_conditions_decline_english"/>
+ <android.support.v4.widget.Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+ <TextView
+ android:id="@+id/voicemail_tos_button_accept"
+ style="@style/RaisedErrorActionStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/verizon_terms_and_conditions_accept_english"/>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml b/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml
new file mode 100644
index 000000000..20dd40a8f
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="alert_icon_size">24dp</dimen>
+ <dimen name="alert_start_padding">16dp</dimen>
+ <dimen name="alert_top_padding">21dp</dimen>
+ <dimen name="alert_main_padding">24dp</dimen>
+ <dimen name="alert_title_padding">12dp</dimen>
+ <dimen name="alert_action_vertical_padding">4dp</dimen>
+ <dimen name="alert_action_horizontal_padding">4dp</dimen>
+ <dimen name="alert_action_between_padding">11dp</dimen>
+ <dimen name="alert_line_spacing">4dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
new file mode 100644
index 000000000..1d39b9dcb
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
@@ -0,0 +1,176 @@
+<!--
+ ~ 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
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="voicemail_error_turn_off_airplane_mode_title">Turn off airplane mode</string>
+
+ <string name="voicemail_error_activating_title">Activating visual voicemail</string>
+ <string name="voicemail_error_activating_message">You might not receive voicemail notifications until visual voicemail is fully activated. Call voicemail to retrieve new messages until voicemail is fully activated.</string>
+
+ <string name="voicemail_error_not_activate_no_signal_title">Can\'t activate visual voicemail</string>
+ <string name="voicemail_error_not_activate_no_signal_message">Make sure your phone has cellular connection and try again.</string>
+ <string name="voicemail_error_not_activate_no_signal_airplane_mode_message">Turn off airplane mode and try again.</string>
+
+ <string name="voicemail_error_no_signal_title">No connection</string>
+ <string name="voicemail_error_no_signal_message">You won\'t be notified for new voicemails. If you\'re on Wi-Fi, you can check for voicemail by syncing now.</string>
+ <string name="voicemail_error_no_signal_airplane_mode_message">You won\'t be notified for new voicemails. Turn off airplane mode to sync your voicemail.</string>
+ <string name="voicemail_error_no_signal_cellular_required_message">Your phone needs a cellular data connection to check voicemail.</string>
+
+ <string name="voicemail_error_activation_failed_title">Can\'t activate visual voicemail</string>
+ <string name="voicemail_error_activation_failed_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_no_data_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_no_data_message">Try again when your Wi-Fi or cellular connection is better. You can still call to check voicemail.</string>
+ <string name="voicemail_error_no_data_cellular_required_message">Try again when your cellular data connection is better. You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_bad_config_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_bad_config_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_communication_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_communication_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_server_connection_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_server_connection_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_server_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_server_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_inbox_near_full_title">Inbox almost full</string>
+ <string name="voicemail_error_inbox_near_full_message">You won\'t be able to receive new voicemail if your inbox is full.</string>
+
+ <string name="voicemail_error_inbox_full_title">Can\'t receive new voicemails</string>
+ <string name="voicemail_error_inbox_full_message">Your inbox is full. Try deleting some messages to receive new voicemail.</string>
+
+
+ <string name="voicemail_error_pin_not_set_title">Set your voicemail PIN</string>
+ <string name="voicemail_error_pin_not_set_message">You\'ll need a voicemail PIN anytime you call to access your voicemail.</string>
+
+ <string name="voicemail_error_unknown_title">Unknown error</string>
+
+ <string name="voicemail_action_turn_off_airplane_mode">Airplane Mode Settings</string>
+ <string name="voicemail_action_set_pin">Set PIN</string>
+ <string name="voicemail_action_retry">Try Again</string>
+ <string name="voicemail_action_sync">Sync</string>
+ <string name="voicemail_action_call_voicemail">Call Voicemail</string>
+ <string name="voicemail_action_call_customer_support">Call Customer Support</string>
+
+ <string name="vvm3_error_vms_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_vms_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9001.</string>
+
+ <string name="vvm3_error_vmg_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_vmg_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9002.</string>
+
+ <string name="vvm3_error_spg_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_spg_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9003.</string>
+
+ <string name="vvm3_error_vms_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_vms_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9004.</string>
+
+ <string name="vvm3_error_vmg_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_vmg_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9005.</string>
+
+ <string name="vvm3_error_spg_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_spg_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9006.</string>
+
+ <string name="vvm3_error_vms_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_vms_timeout_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9007.</string>
+
+ <string name="vvm3_error_vmg_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_vmg_timeout_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9008.</string>
+
+ <string name="vvm3_error_status_sms_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_status_sms_timeout_message">Sorry, we\'re having trouble setting up your service. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9009.</string>
+
+ <string name="vvm3_error_subscriber_blocked_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_subscriber_blocked_message">Sorry, we\'re not able to connect to your voice mailbox at this time. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9990."</string>
+
+ <string name="vvm3_error_unknown_user_title">Set Up Voice Mail</string>
+ <string name="vvm3_error_unknown_user_message">Voicemail is not set up on your account. Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9991.</string>
+
+ <string name="vvm3_error_unknown_device_title">Voice Mail</string>
+ <string name="vvm3_error_unknown_device_message">Visual Voicemail cannot be used on this device. Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9992.</string>
+
+ <string name="vvm3_error_invalid_password_title">Something Went Wrong</string>
+ <string name="vvm3_error_invalid_password_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9993.</string>
+
+ <string name="vvm3_error_mailbox_not_initialized_title">Visual Voice Mail</string>
+ <string name="vvm3_error_mailbox_not_initialized_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9994.</string>
+
+ <string name="vvm3_error_service_not_provisioned_title">Visual Voice Mail</string>
+ <string name="vvm3_error_service_not_provisioned_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9995.</string>
+
+ <string name="vvm3_error_service_not_activated_title">Visual Voice Mail</string>
+ <string name="vvm3_error_service_not_activated_message">To activate Visual Voice Mail, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9996.</string>
+
+ <string name="vvm3_error_user_blocked_title">Something Went Wrong</string>
+ <string name="vvm3_error_user_blocked_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9998.</string>
+
+ <string name="vvm3_error_subscriber_unknown_title">Visual Voicemail is Disabled</string>
+ <string name="vvm3_error_subscriber_unknown_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> to activate visual voicemail.</string>
+
+ <string name="vvm3_error_imap_getquota_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_getquota_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9997.</string>
+
+ <string name="vvm3_error_imap_select_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_select_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9989.</string>
+
+ <string name="vvm3_error_imap_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9999.</string>
+
+ <string translatable="false" name="verizon_domestic_customer_support_number">+18009220204</string>
+ <string translatable="false" name="verizon_domestic_customer_support_display_number">(800) 922–0204</string>
+
+ <string name="verizon_terms_and_conditions_title">Visual Voicemail Terms and Conditions</string>
+ <string name="verizon_terms_and_conditions_message">You must accept Verizon Wireless\'s terms and conditions to use visual voicemail:\n\n%s</string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_1.1_english">
+Visual Voice Mail (VVM) is a service that provides access to voice mail messages directly on the device, without the need to call *86. This service requires traditional Voice Mail but does not support all traditional Voice Mail features, which you can access by dialing *86 from your handset. Use of this feature will be billed on a per-megabyte basis, or according to any data package you have. Mobile to mobile minutes do not apply. Standard rates apply to any calls, emails or messages initiated from Visual Voice Mail.\n
+\n
+You may disable VVM in settings. This will revert you to basic voice mail. In some cases you may need to call customer care to cancel and if you cancel Visual Voice Mail you may lose all stored voice mails and information.\n
+\n
+For the Premium Visual Voice Mail service, some voice messages may not be completely transcribed; incomplete messages will end with [...]. Only the first 45 seconds of each voice message will be transcribed, so for longer messages, you will need to listen to the voice message itself. Any profane or offensive language also will not be transcribed and will appear as [...] in the transcription.\n
+\n
+Speech recordings may be collected and stored for a period of 30 days, solely for the purpose of testing and improving transcription technology and performance, subject to the Verizon Wireless Privacy Policy, which can be found at http://www.verizon.com/about/privacy/policy/\n
+\n
+You understand that by selecting ACCEPT, your messages will be stored and anyone in possession of this device will have access to your voice mail. You further understand that your voice mail messages may be stored in electronic format on this device. To limit unauthorized access to your voice mail, you should consider locking your phone when not in use. Not available in all areas or over Wi-Fi.\n
+\n
+If you do not accept all of these terms and conditions, do not use Visual Voice Mail. </string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_1.1_spanish">
+El buzón de voz visual (VVM) es un servicio que permite acceder a los mensajes del buzón de voz directamente en el dispositivo, sin necesidad de llamar al *86. Este servicio requiere el buzón de voz tradicional, pero no admite todas las funciones del buzón de voz tradicional, a las que se puede acceder marcando *86 en el teléfono. El uso de esta función se factura por megabyte o conforme a cualquier paquete de datos que tenga. No se aplican los minutos de un dispositivo móvil a otro. Se aplican tarifas estándar a todos los correos electrónicos, las llamadas o los mensajes originados en el buzón de voz visual.\n
+\n
+Puede inhabilitar el VVM en la configuración. Esto le permite volver al buzón de voz básico. En algunos casos, es posible que deba llamar al servicio de atención al cliente para cancelar el buzón de voz visual. Si lo cancela, puede perder la información y los mensajes de voz almacenados.\n
+\n
+En el caso del servicio de buzón de voz visual premium, es posible que algunos mensajes no se transcriban totalmente; los mensajes incompletos finalizan con "[…]". Solo se transcriben los primeros 45 segundos de cada mensaje de voz, por lo que debe escuchar los mensajes de voz más largos. Tampoco se transcribe ninguna palabra ofensiva o profana; aparece como "[…]" en la transcripción.\n
+\n
+Es posible que reunamos y almacenemos grabaciones de voz durante 30 días, con el único fin de probar y mejorar el rendimiento y la tecnología de la transcripción, sujeto a la Política de privacidad de Verizon Wireless, disponible en http://www.verizon.com/about/privacy/policy/.\n
+\n
+Entiende que, al seleccionar ACEPTAR, sus mensajes se almacenarán, y cualquier persona que disponga de este dispositivo tendrá acceso al buzón de voz. Entiende, además, que los mensajes de voz pueden almacenarse en formato electrónico en este dispositivo. Para limitar el acceso no autorizado al buzón de voz, debe considerar el bloqueo del teléfono cuando no está en uso. No está disponible en todas las áreas ni mediante Wi-Fi.\n
+\n
+Si no acepta todos estos términos y condiciones, no use el buzón de voz visual.
+ </string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_accept_english">Accept</string>
+ <string translatable="false" name="verizon_terms_and_conditions_accept_spanish">Aceptar</string>
+ <string translatable="false" name="verizon_terms_and_conditions_decline_english">Decline</string>
+ <string translatable="false" name="verizon_terms_and_conditions_decline_spanish">Rechazar</string>
+
+ <string name="verizon_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string>
+ <string name="verizon_terms_and_conditions_decline_dialog_downgrade">Disable visual voicemail</string>
+
+ <string name="verizon_terms_and_conditions_decline_set_pin_dialog_message">Voicemail will only be accessible by calling *86. Set a new voicemail PIN to proceed.</string>
+ <string name="verizon_terms_and_conditions_decline_set_pin_dialog_set_pin">Set PIN</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/styles.xml b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
new file mode 100644
index 000000000..c4a8542f1
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="ErrorActionStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:gravity">end|center_vertical</item>
+ <item name="android:paddingStart">8dp</item>
+ <item name="android:paddingEnd">8dp</item>
+ <item name="android:layout_marginStart">8dp</item>
+ <item name="android:layout_marginEnd">8dp</item>
+ <item name="android:textColor">@color/dialtacts_theme_color</item>
+ <item name="android:fontFamily">"sans-serif-medium"</item>
+ <item name="android:focusable">true</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="RaisedErrorActionStyle" parent="Widget.AppCompat.Button.Colored">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:colorButtonNormal">@color/dialer_theme_color</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:layout_height">@dimen/call_log_action_height</item>
+ </style>
+</resources> \ No newline at end of file