1 /**
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.phone.settings;
18 
19 import android.app.Dialog;
20 import android.content.DialogInterface;
21 import android.content.Intent;
22 import android.database.Cursor;
23 import android.os.AsyncResult;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.Message;
27 import android.os.UserManager;
28 import android.preference.Preference;
29 import android.preference.PreferenceActivity;
30 import android.preference.PreferenceScreen;
31 import android.preference.SwitchPreference;
32 import android.provider.ContactsContract.CommonDataKinds;
33 import android.provider.Settings;
34 import android.telephony.TelephonyManager;
35 import android.text.BidiFormatter;
36 import android.text.TextDirectionHeuristics;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.view.MenuItem;
40 import android.widget.ListAdapter;
41 import android.widget.Toast;
42 
43 import com.android.internal.telephony.CallForwardInfo;
44 import com.android.internal.telephony.Phone;
45 import com.android.internal.telephony.PhoneConstants;
46 import com.android.internal.telephony.util.NotificationChannelController;
47 import com.android.phone.EditPhoneNumberPreference;
48 import com.android.phone.PhoneGlobals;
49 import com.android.phone.R;
50 import com.android.phone.SubscriptionInfoHelper;
51 
52 import java.util.Collection;
53 import java.util.HashMap;
54 import java.util.HashSet;
55 import java.util.Iterator;
56 import java.util.Map;
57 
58 public class VoicemailSettingsActivity extends PreferenceActivity
59         implements DialogInterface.OnClickListener,
60                 Preference.OnPreferenceChangeListener,
61                 EditPhoneNumberPreference.OnDialogClosedListener,
62                 EditPhoneNumberPreference.GetDefaultNumberListener{
63     private static final String LOG_TAG = VoicemailSettingsActivity.class.getSimpleName();
64     private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
65 
66     /**
67      * Intent action to bring up Voicemail Provider settings
68      * DO NOT RENAME. There are existing apps which use this intent value.
69      */
70     public static final String ACTION_ADD_VOICEMAIL =
71             "com.android.phone.CallFeaturesSetting.ADD_VOICEMAIL";
72 
73     /**
74      * Intent action to bring up the {@code VoicemailSettingsActivity}.
75      * DO NOT RENAME. There are existing apps which use this intent value.
76      */
77     public static final String ACTION_CONFIGURE_VOICEMAIL =
78             "com.android.phone.CallFeaturesSetting.CONFIGURE_VOICEMAIL";
79 
80     // Extra put in the return from VM provider config containing voicemail number to set
81     public static final String VM_NUMBER_EXTRA = "com.android.phone.VoicemailNumber";
82     // Extra put in the return from VM provider config containing call forwarding number to set
83     public static final String FWD_NUMBER_EXTRA = "com.android.phone.ForwardingNumber";
84     // Extra put in the return from VM provider config containing call forwarding number to set
85     public static final String FWD_NUMBER_TIME_EXTRA = "com.android.phone.ForwardingNumberTime";
86     // If the VM provider returns non null value in this extra we will force the user to
87     // choose another VM provider
88     public static final String SIGNOUT_EXTRA = "com.android.phone.Signout";
89 
90     /**
91      * String Extra put into ACTION_ADD_VOICEMAIL call to indicate which provider should be hidden
92      * in the list of providers presented to the user. This allows a provider which is being
93      * disabled (e.g. GV user logging out) to force the user to pick some other provider.
94      */
95     public static final String IGNORE_PROVIDER_EXTRA = "com.android.phone.ProviderToIgnore";
96 
97     /**
98      * String Extra put into ACTION_ADD_VOICEMAIL to indicate that the voicemail setup screen should
99      * be opened.
100      */
101     public static final String SETUP_VOICEMAIL_EXTRA = "com.android.phone.SetupVoicemail";
102 
103     // TODO: Define these preference keys in XML.
104     private static final String BUTTON_VOICEMAIL_KEY = "button_voicemail_key";
105     private static final String BUTTON_VOICEMAIL_PROVIDER_KEY = "button_voicemail_provider_key";
106     private static final String BUTTON_VOICEMAIL_SETTING_KEY = "button_voicemail_setting_key";
107 
108     /** Event for Async voicemail change call */
109     private static final int EVENT_VOICEMAIL_CHANGED        = 500;
110     private static final int EVENT_FORWARDING_CHANGED       = 501;
111     private static final int EVENT_FORWARDING_GET_COMPLETED = 502;
112 
113     /** Handle to voicemail pref */
114     private static final int VOICEMAIL_PREF_ID = 1;
115     private static final int VOICEMAIL_PROVIDER_CFG_ID = 2;
116 
117     /**
118      * Results of reading forwarding settings
119      */
120     private CallForwardInfo[] mForwardingReadResults = null;
121 
122     /**
123      * Result of forwarding number change.
124      * Keys are reasons (eg. unconditional forwarding).
125      */
126     private Map<Integer, AsyncResult> mForwardingChangeResults = null;
127 
128     /**
129      * Expected CF read result types.
130      * This set keeps track of the CF types for which we've issued change
131      * commands so we can tell when we've received all of the responses.
132      */
133     private Collection<Integer> mExpectedChangeResultReasons = null;
134 
135     /**
136      * Result of vm number change
137      */
138     private AsyncResult mVoicemailChangeResult = null;
139 
140     /**
141      * Previous VM provider setting so we can return to it in case of failure.
142      */
143     private String mPreviousVMProviderKey = null;
144 
145     /**
146      * Id of the dialog being currently shown.
147      */
148     private int mCurrentDialogId = 0;
149 
150     /**
151      * Flag indicating that we are invoking settings for the voicemail provider programmatically
152      * due to vm provider change.
153      */
154     private boolean mVMProviderSettingsForced = false;
155 
156     /**
157      * Flag indicating that we are making changes to vm or fwd numbers
158      * due to vm provider change.
159      */
160     private boolean mChangingVMorFwdDueToProviderChange = false;
161 
162     /**
163      * True if we are in the process of vm & fwd number change and vm has already been changed.
164      * This is used to decide what to do in case of rollback.
165      */
166     private boolean mVMChangeCompletedSuccessfully = false;
167 
168     /**
169      * True if we had full or partial failure setting forwarding numbers and so need to roll them
170      * back.
171      */
172     private boolean mFwdChangesRequireRollback = false;
173 
174     /**
175      * Id of error msg to display to user once we are done reverting the VM provider to the previous
176      * one.
177      */
178     private int mVMOrFwdSetError = 0;
179 
180     /** string to hold old voicemail number as it is being updated. */
181     private String mOldVmNumber;
182 
183     // New call forwarding settings and vm number we will be setting
184     // Need to save these since before we get to saving we need to asynchronously
185     // query the existing forwarding settings.
186     private CallForwardInfo[] mNewFwdSettings;
187     private String mNewVMNumber;
188 
189     /**
190      * Used to indicate that the voicemail preference should be shown.
191      */
192     private boolean mShowVoicemailPreference = false;
193 
194     private boolean mForeground;
195     private Phone mPhone;
196     private SubscriptionInfoHelper mSubscriptionInfoHelper;
197 
198     private EditPhoneNumberPreference mSubMenuVoicemailSettings = null;
199     private VoicemailProviderListPreference mVoicemailProviders;
200     private PreferenceScreen mVoicemailSettings;
201     private Preference mVoicemailNotificationPreference;
202 
203     //*********************************************************************************************
204     // Preference Activity Methods
205     //*********************************************************************************************
206 
207     @Override
onCreate(Bundle icicle)208     protected void onCreate(Bundle icicle) {
209         super.onCreate(icicle);
210         // Make sure we are running as the primary user only
211         UserManager userManager = getApplicationContext().getSystemService(UserManager.class);
212         if (!userManager.isPrimaryUser()) {
213            Toast.makeText(this, R.string.voice_number_setting_primary_user_only,
214                    Toast.LENGTH_SHORT).show();
215            finish();
216            return;
217         }
218         // Show the voicemail preference in onResume if the calling intent specifies the
219         // ACTION_ADD_VOICEMAIL action.
220         mShowVoicemailPreference = (icicle == null) &&
221                 TextUtils.equals(getIntent().getAction(), ACTION_ADD_VOICEMAIL);
222 
223         mSubscriptionInfoHelper = new SubscriptionInfoHelper(this, getIntent());
224         mSubscriptionInfoHelper.setActionBarTitle(
225                 getActionBar(), getResources(), R.string.voicemail_settings_with_label);
226         mPhone = mSubscriptionInfoHelper.getPhone();
227         addPreferencesFromResource(R.xml.voicemail_settings);
228 
229         mVoicemailNotificationPreference =
230                 findPreference(getString(R.string.voicemail_notifications_key));
231         final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
232         intent.putExtra(Settings.EXTRA_CHANNEL_ID,
233                 NotificationChannelController.CHANNEL_ID_VOICE_MAIL);
234         intent.putExtra(Settings.EXTRA_APP_PACKAGE, mPhone.getContext().getPackageName());
235         mVoicemailNotificationPreference.setIntent(intent);
236     }
237 
238     @Override
onResume()239     protected void onResume() {
240         super.onResume();
241         mForeground = true;
242 
243         PreferenceScreen prefSet = getPreferenceScreen();
244 
245         if (mSubMenuVoicemailSettings == null) {
246             mSubMenuVoicemailSettings =
247                     (EditPhoneNumberPreference) findPreference(BUTTON_VOICEMAIL_KEY);
248         }
249         if (mSubMenuVoicemailSettings != null) {
250             mSubMenuVoicemailSettings.setParentActivity(this, VOICEMAIL_PREF_ID, this);
251             mSubMenuVoicemailSettings.setDialogOnClosedListener(this);
252             mSubMenuVoicemailSettings.setDialogTitle(R.string.voicemail_settings_number_label);
253         }
254 
255         mVoicemailProviders = (VoicemailProviderListPreference) findPreference(
256                 BUTTON_VOICEMAIL_PROVIDER_KEY);
257         mVoicemailProviders.init(mPhone, getIntent());
258         mVoicemailProviders.setOnPreferenceChangeListener(this);
259         mPreviousVMProviderKey = mVoicemailProviders.getValue();
260 
261         mVoicemailSettings = (PreferenceScreen) findPreference(BUTTON_VOICEMAIL_SETTING_KEY);
262 
263         maybeHidePublicSettings();
264 
265         updateVMPreferenceWidgets(mVoicemailProviders.getValue());
266 
267         // check the intent that started this activity and pop up the voicemail
268         // dialog if we've been asked to.
269         // If we have at least one non default VM provider registered then bring up
270         // the selection for the VM provider, otherwise bring up a VM number dialog.
271         // We only bring up the dialog the first time we are called (not after orientation change)
272         if (mShowVoicemailPreference) {
273             if (DBG) log("ACTION_ADD_VOICEMAIL Intent is thrown");
274             if (mVoicemailProviders.hasMoreThanOneVoicemailProvider()) {
275                 if (DBG) log("Voicemail data has more than one provider.");
276                 simulatePreferenceClick(mVoicemailProviders);
277             } else {
278                 onPreferenceChange(mVoicemailProviders, VoicemailProviderListPreference.DEFAULT_KEY);
279                 mVoicemailProviders.setValue(VoicemailProviderListPreference.DEFAULT_KEY);
280             }
281             mShowVoicemailPreference = false;
282         }
283 
284         updateVoiceNumberField();
285         mVMProviderSettingsForced = false;
286     }
287 
288     /**
289      * Hides a subset of voicemail settings if required by the intent extra. This is used by the
290      * default dialer to show "advanced" voicemail settings from its own custom voicemail settings
291      * UI.
292      */
maybeHidePublicSettings()293     private void maybeHidePublicSettings() {
294         if(!getIntent().getBooleanExtra(TelephonyManager.EXTRA_HIDE_PUBLIC_SETTINGS, false)){
295             return;
296         }
297         if (DBG) {
298             log("maybeHidePublicSettings: settings hidden by EXTRA_HIDE_PUBLIC_SETTINGS");
299         }
300         PreferenceScreen preferenceScreen = getPreferenceScreen();
301         preferenceScreen.removePreference(mVoicemailNotificationPreference);
302     }
303 
304     @Override
onPause()305     public void onPause() {
306         super.onPause();
307         mForeground = false;
308     }
309 
310     @Override
onOptionsItemSelected(MenuItem item)311     public boolean onOptionsItemSelected(MenuItem item) {
312         if (item.getItemId() == android.R.id.home) {
313             onBackPressed();
314             return true;
315         }
316         return super.onOptionsItemSelected(item);
317     }
318 
319     @Override
onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference)320     public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
321         if (preference == mSubMenuVoicemailSettings) {
322             return true;
323         } else if (preference.getKey().equals(mVoicemailSettings.getKey())) {
324             // Check key instead of comparing reference because closing the voicemail notification
325             // ringtone dialog invokes onResume(), but leaves the old preference screen up,
326             // TODO: Revert to checking reference after migrating voicemail to its own activity.
327             if (DBG) log("onPreferenceTreeClick: Voicemail Settings Preference is clicked.");
328 
329             final Dialog dialog = ((PreferenceScreen) preference).getDialog();
330             if (dialog != null) {
331                 dialog.getActionBar().setDisplayHomeAsUpEnabled(false);
332             }
333 
334             mSubMenuVoicemailSettings =
335                     (EditPhoneNumberPreference) findPreference(BUTTON_VOICEMAIL_KEY);
336             mSubMenuVoicemailSettings.setParentActivity(this, VOICEMAIL_PREF_ID, this);
337             mSubMenuVoicemailSettings.setDialogOnClosedListener(this);
338             mSubMenuVoicemailSettings.setDialogTitle(R.string.voicemail_settings_number_label);
339             updateVoiceNumberField();
340 
341             if (preference.getIntent() != null) {
342                 if (DBG) log("Invoking cfg intent " + preference.getIntent().getPackage());
343 
344                 // onActivityResult() will be responsible for resetting some of variables.
345                 this.startActivityForResult(preference.getIntent(), VOICEMAIL_PROVIDER_CFG_ID);
346                 return true;
347             } else {
348                 if (DBG) log("onPreferenceTreeClick(). No intent; use default behavior in xml.");
349 
350                 // onActivityResult() will not be called, so reset variables here.
351                 mPreviousVMProviderKey = VoicemailProviderListPreference.DEFAULT_KEY;
352                 mVMProviderSettingsForced = false;
353                 return false;
354             }
355         }
356         return false;
357     }
358 
359     /**
360      * Implemented to support onPreferenceChangeListener to look for preference changes.
361      *
362      * @param preference is the preference to be changed
363      * @param objValue should be the value of the selection, NOT its localized
364      * display value.
365      */
366     @Override
onPreferenceChange(Preference preference, Object objValue)367     public boolean onPreferenceChange(Preference preference, Object objValue) {
368         if (DBG) log("onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\"");
369 
370         if (preference == mVoicemailProviders) {
371             final String newProviderKey = (String) objValue;
372 
373             // If previous provider key and the new one is same, we don't need to handle it.
374             if (mPreviousVMProviderKey.equals(newProviderKey)) {
375                 if (DBG) log("No change is made to the VM provider setting.");
376                 return true;
377             }
378             updateVMPreferenceWidgets(newProviderKey);
379 
380             final VoicemailProviderSettings newProviderSettings =
381                     VoicemailProviderSettingsUtil.load(this, newProviderKey);
382 
383             // If the user switches to a voice mail provider and we have numbers stored for it we
384             // will automatically change the phone's voice mail and forwarding number to the stored
385             // ones. Otherwise we will bring up provider's configuration UI.
386             if (newProviderSettings == null) {
387                 // Force the user into a configuration of the chosen provider
388                 Log.w(LOG_TAG, "Saved preferences not found - invoking config");
389                 mVMProviderSettingsForced = true;
390                 simulatePreferenceClick(mVoicemailSettings);
391             } else {
392                 if (DBG) log("Saved preferences found - switching to them");
393                 // Set this flag so if we get a failure we revert to previous provider
394                 mChangingVMorFwdDueToProviderChange = true;
395                 saveVoiceMailAndForwardingNumber(newProviderKey, newProviderSettings);
396             }
397         }
398         // Always let the preference setting proceed.
399         return true;
400     }
401 
402     /**
403      * Implemented for EditPhoneNumberPreference.GetDefaultNumberListener.
404      * This method set the default values for the various
405      * EditPhoneNumberPreference dialogs.
406      */
407     @Override
onGetDefaultNumber(EditPhoneNumberPreference preference)408     public String onGetDefaultNumber(EditPhoneNumberPreference preference) {
409         if (preference == mSubMenuVoicemailSettings) {
410             // update the voicemail number field, which takes care of the
411             // mSubMenuVoicemailSettings itself, so we should return null.
412             if (DBG) log("updating default for voicemail dialog");
413             updateVoiceNumberField();
414             return null;
415         }
416 
417         String vmDisplay = mPhone.getVoiceMailNumber();
418         if (TextUtils.isEmpty(vmDisplay)) {
419             // if there is no voicemail number, we just return null to
420             // indicate no contribution.
421             return null;
422         }
423 
424         // Return the voicemail number prepended with "VM: "
425         if (DBG) log("updating default for call forwarding dialogs");
426         return getString(R.string.voicemail_abbreviated) + " " + vmDisplay;
427     }
428 
429     @Override
onActivityResult(int requestCode, int resultCode, Intent data)430     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
431         if (DBG) {
432             log("onActivityResult: requestCode: " + requestCode
433                     + ", resultCode: " + resultCode
434                     + ", data: " + data);
435         }
436 
437         // there are cases where the contact picker may end up sending us more than one
438         // request.  We want to ignore the request if we're not in the correct state.
439         if (requestCode == VOICEMAIL_PROVIDER_CFG_ID) {
440             boolean failure = false;
441 
442             // No matter how the processing of result goes lets clear the flag
443             if (DBG) log("mVMProviderSettingsForced: " + mVMProviderSettingsForced);
444             final boolean isVMProviderSettingsForced = mVMProviderSettingsForced;
445             mVMProviderSettingsForced = false;
446 
447             String vmNum = null;
448             if (resultCode != RESULT_OK) {
449                 if (DBG) log("onActivityResult: vm provider cfg result not OK.");
450                 failure = true;
451             } else {
452                 if (data == null) {
453                     if (DBG) log("onActivityResult: vm provider cfg result has no data");
454                     failure = true;
455                 } else {
456                     if (data.getBooleanExtra(SIGNOUT_EXTRA, false)) {
457                         if (DBG) log("Provider requested signout");
458                         if (isVMProviderSettingsForced) {
459                             if (DBG) log("Going back to previous provider on signout");
460                             switchToPreviousVoicemailProvider();
461                         } else {
462                             final String victim = mVoicemailProviders.getKey();
463                             if (DBG) log("Relaunching activity and ignoring " + victim);
464                             Intent i = new Intent(ACTION_ADD_VOICEMAIL);
465                             i.putExtra(IGNORE_PROVIDER_EXTRA, victim);
466                             i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
467                             this.startActivity(i);
468                         }
469                         return;
470                     }
471                     vmNum = data.getStringExtra(VM_NUMBER_EXTRA);
472                     if (vmNum == null || vmNum.length() == 0) {
473                         if (DBG) log("onActivityResult: vm provider cfg result has no vmnum");
474                         failure = true;
475                     }
476                 }
477             }
478             if (failure) {
479                 if (DBG) log("Failure in return from voicemail provider.");
480                 if (isVMProviderSettingsForced) {
481                     switchToPreviousVoicemailProvider();
482                 }
483 
484                 return;
485             }
486             mChangingVMorFwdDueToProviderChange = isVMProviderSettingsForced;
487             final String fwdNum = data.getStringExtra(FWD_NUMBER_EXTRA);
488 
489             // TODO: It would be nice to load the current network setting for this and
490             // send it to the provider when it's config is invoked so it can use this as default
491             final int fwdNumTime = data.getIntExtra(FWD_NUMBER_TIME_EXTRA, 20);
492 
493             if (DBG) log("onActivityResult: cfg result has forwarding number " + fwdNum);
494             saveVoiceMailAndForwardingNumber(mVoicemailProviders.getKey(),
495                     new VoicemailProviderSettings(vmNum, fwdNum, fwdNumTime));
496             return;
497         }
498 
499         if (requestCode == VOICEMAIL_PREF_ID) {
500             if (resultCode != RESULT_OK) {
501                 if (DBG) log("onActivityResult: contact picker result not OK.");
502                 return;
503             }
504 
505             Cursor cursor = null;
506             try {
507                 cursor = getContentResolver().query(data.getData(),
508                     new String[] { CommonDataKinds.Phone.NUMBER }, null, null, null);
509                 if ((cursor == null) || (!cursor.moveToFirst())) {
510                     if (DBG) log("onActivityResult: bad contact data, no results found.");
511                     return;
512                 }
513                 if (mSubMenuVoicemailSettings != null) {
514                     mSubMenuVoicemailSettings.onPickActivityResult(cursor.getString(0));
515                 } else {
516                     Log.w(LOG_TAG, "VoicemailSettingsActivity destroyed while setting contacts.");
517                 }
518                 return;
519             } finally {
520                 if (cursor != null) {
521                     cursor.close();
522                 }
523             }
524         }
525 
526         super.onActivityResult(requestCode, resultCode, data);
527     }
528 
529     /**
530      * Simulates user clicking on a passed preference.
531      * Usually needed when the preference is a dialog preference and we want to invoke
532      * a dialog for this preference programmatically.
533      * TODO: figure out if there is a cleaner way to cause preference dlg to come up
534      */
simulatePreferenceClick(Preference preference)535     private void simulatePreferenceClick(Preference preference) {
536         // Go through settings until we find our setting
537         // and then simulate a click on it to bring up the dialog
538         final ListAdapter adapter = getPreferenceScreen().getRootAdapter();
539         for (int idx = 0; idx < adapter.getCount(); idx++) {
540             if (adapter.getItem(idx) == preference) {
541                 getPreferenceScreen().onItemClick(this.getListView(),
542                         null, idx, adapter.getItemId(idx));
543                 break;
544             }
545         }
546     }
547 
548 
549     //*********************************************************************************************
550     // Activity Dialog Methods
551     //*********************************************************************************************
552 
553     @Override
onPrepareDialog(int id, Dialog dialog)554     protected void onPrepareDialog(int id, Dialog dialog) {
555         super.onPrepareDialog(id, dialog);
556         mCurrentDialogId = id;
557     }
558 
559     // dialog creation method, called by showDialog()
560     @Override
onCreateDialog(int dialogId)561     protected Dialog onCreateDialog(int dialogId) {
562         return VoicemailDialogUtil.getDialog(this, dialogId);
563     }
564 
565     @Override
onDialogClosed(EditPhoneNumberPreference preference, int buttonClicked)566     public void onDialogClosed(EditPhoneNumberPreference preference, int buttonClicked) {
567         if (DBG) log("onDialogClosed: Button clicked is " + buttonClicked);
568 
569         if (buttonClicked == DialogInterface.BUTTON_NEGATIVE) {
570             return;
571         }
572 
573         if (preference == mSubMenuVoicemailSettings) {
574             VoicemailProviderSettings newSettings = new VoicemailProviderSettings(
575                     mSubMenuVoicemailSettings.getPhoneNumber(),
576                     VoicemailProviderSettings.NO_FORWARDING);
577             saveVoiceMailAndForwardingNumber(mVoicemailProviders.getKey(), newSettings);
578         }
579     }
580 
581     /**
582      * Wrapper around showDialog() that will silently do nothing if we're
583      * not in the foreground.
584      *
585      * This is useful here because most of the dialogs we display from
586      * this class are triggered by asynchronous events (like
587      * success/failure messages from the telephony layer) and it's
588      * possible for those events to come in even after the user has gone
589      * to a different screen.
590      */
591     // TODO: this is too brittle: it's still easy to accidentally add new
592     // code here that calls showDialog() directly (which will result in a
593     // WindowManager$BadTokenException if called after the activity has
594     // been stopped.)
595     //
596     // It would be cleaner to do the "if (mForeground)" check in one
597     // central place, maybe by using a single Handler for all asynchronous
598     // events (and have *that* discard events if we're not in the
599     // foreground.)
600     //
601     // Unfortunately it's not that simple, since we sometimes need to do
602     // actual work to handle these events whether or not we're in the
603     // foreground (see the Handler code in mSetOptionComplete for
604     // example.)
605     //
606     // TODO: It's a bit worrisome that we don't do anything in error cases when we're not in the
607     // foreground. Consider displaying a toast instead.
showDialogIfForeground(int id)608     private void showDialogIfForeground(int id) {
609         if (mForeground) {
610             showDialog(id);
611         }
612     }
613 
dismissDialogSafely(int id)614     private void dismissDialogSafely(int id) {
615         try {
616             dismissDialog(id);
617         } catch (IllegalArgumentException e) {
618             // This is expected in the case where we were in the background
619             // at the time we would normally have shown the dialog, so we didn't
620             // show it.
621         }
622     }
623 
624     // This is a method implemented for DialogInterface.OnClickListener.
625     // Used with the error dialog to close the app, voicemail dialog to just dismiss.
626     // Close button is mapped to BUTTON_POSITIVE for the errors that close the activity,
627     // while those that are mapped to BUTTON_NEUTRAL only move the preference focus.
onClick(DialogInterface dialog, int which)628     public void onClick(DialogInterface dialog, int which) {
629         if (DBG) log("onClick: button clicked is " + which);
630 
631         dialog.dismiss();
632         switch (which){
633             case DialogInterface.BUTTON_NEGATIVE:
634                 if (mCurrentDialogId == VoicemailDialogUtil.FWD_GET_RESPONSE_ERROR_DIALOG) {
635                     // We failed to get current forwarding settings and the user
636                     // does not wish to continue.
637                     switchToPreviousVoicemailProvider();
638                 }
639                 break;
640             case DialogInterface.BUTTON_POSITIVE:
641                 if (mCurrentDialogId == VoicemailDialogUtil.FWD_GET_RESPONSE_ERROR_DIALOG) {
642                     // We failed to get current forwarding settings but the user
643                     // wishes to continue changing settings to the new vm provider
644                     setVoicemailNumberWithCarrier();
645                 } else {
646                     finish();
647                 }
648                 return;
649             default:
650                 // just let the dialog close and go back to the input
651         }
652 
653         // In all dialogs, all buttons except BUTTON_POSITIVE lead to the end of user interaction
654         // with settings UI. If we were called to explicitly configure voice mail then
655         // we finish the settings activity here to come back to whatever the user was doing.
656         final String action = getIntent() != null ? getIntent().getAction() : null;
657         if (ACTION_ADD_VOICEMAIL.equals(action)) {
658             finish();
659         }
660     }
661 
662 
663     //*********************************************************************************************
664     // Voicemail Methods
665     //*********************************************************************************************
666 
667     /**
668      * TODO: Refactor to make it easier to understand what's done in the different stages.
669      */
saveVoiceMailAndForwardingNumber( String key, VoicemailProviderSettings newSettings)670     private void saveVoiceMailAndForwardingNumber(
671             String key, VoicemailProviderSettings newSettings) {
672         if (DBG) log("saveVoiceMailAndForwardingNumber: " + newSettings.toString());
673         mNewVMNumber = newSettings.getVoicemailNumber();
674         mNewVMNumber = (mNewVMNumber == null) ? "" : mNewVMNumber;
675         mNewFwdSettings = newSettings.getForwardingSettings();
676 
677         // Call forwarding is not suppported on CDMA.
678         if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
679             if (DBG) log("Ignoring forwarding setting since this is CDMA phone");
680             mNewFwdSettings = VoicemailProviderSettings.NO_FORWARDING;
681         }
682 
683         // Throw a warning if the voicemail is the same and we did not change forwarding.
684         if (mNewVMNumber.equals(mOldVmNumber)
685                 && mNewFwdSettings == VoicemailProviderSettings.NO_FORWARDING) {
686             showDialogIfForeground(VoicemailDialogUtil.VM_NOCHANGE_ERROR_DIALOG);
687             return;
688         }
689 
690         VoicemailProviderSettingsUtil.save(this, key, newSettings);
691         mVMChangeCompletedSuccessfully = false;
692         mFwdChangesRequireRollback = false;
693         mVMOrFwdSetError = 0;
694 
695         if (mNewFwdSettings == VoicemailProviderSettings.NO_FORWARDING
696                 || key.equals(mPreviousVMProviderKey)) {
697             if (DBG) log("Set voicemail number. No changes to forwarding number.");
698             setVoicemailNumberWithCarrier();
699         } else {
700             if (DBG) log("Reading current forwarding settings.");
701             int numSettingsReasons = VoicemailProviderSettings.FORWARDING_SETTINGS_REASONS.length;
702             mForwardingReadResults = new CallForwardInfo[numSettingsReasons];
703             for (int i = 0; i < mForwardingReadResults.length; i++) {
704                 mPhone.getCallForwardingOption(
705                         VoicemailProviderSettings.FORWARDING_SETTINGS_REASONS[i],
706                         mGetOptionComplete.obtainMessage(EVENT_FORWARDING_GET_COMPLETED, i, 0));
707             }
708             showDialogIfForeground(VoicemailDialogUtil.VM_FWD_READING_DIALOG);
709         }
710     }
711 
712     private final Handler mGetOptionComplete = new Handler() {
713         @Override
714         public void handleMessage(Message msg) {
715             AsyncResult result = (AsyncResult) msg.obj;
716             switch (msg.what) {
717                 case EVENT_FORWARDING_GET_COMPLETED:
718                     handleForwardingSettingsReadResult(result, msg.arg1);
719                     break;
720             }
721         }
722     };
723 
handleForwardingSettingsReadResult(AsyncResult ar, int idx)724     private void handleForwardingSettingsReadResult(AsyncResult ar, int idx) {
725         if (DBG) Log.d(LOG_TAG, "handleForwardingSettingsReadResult: " + idx);
726 
727         Throwable error = null;
728         if (ar.exception != null) {
729             error = ar.exception;
730             if (DBG) Log.d(LOG_TAG, "FwdRead: ar.exception=" + error.getMessage());
731         }
732         if (ar.userObj instanceof Throwable) {
733             error = (Throwable) ar.userObj;
734             if (DBG) Log.d(LOG_TAG, "FwdRead: userObj=" + error.getMessage());
735         }
736 
737         // We may have already gotten an error and decided to ignore the other results.
738         if (mForwardingReadResults == null) {
739             if (DBG) Log.d(LOG_TAG, "Ignoring fwd reading result: " + idx);
740             return;
741         }
742 
743         // In case of error ignore other results, show an error dialog
744         if (error != null) {
745             if (DBG) Log.d(LOG_TAG, "Error discovered for fwd read : " + idx);
746             mForwardingReadResults = null;
747             dismissDialogSafely(VoicemailDialogUtil.VM_FWD_READING_DIALOG);
748             showDialogIfForeground(VoicemailDialogUtil.FWD_GET_RESPONSE_ERROR_DIALOG);
749             return;
750         }
751 
752         // Get the forwarding info.
753         mForwardingReadResults[idx] = CallForwardInfoUtil.getCallForwardInfo(
754                 (CallForwardInfo[]) ar.result,
755                 VoicemailProviderSettings.FORWARDING_SETTINGS_REASONS[idx]);
756 
757         // Check if we got all the results already
758         boolean done = true;
759         for (int i = 0; i < mForwardingReadResults.length; i++) {
760             if (mForwardingReadResults[i] == null) {
761                 done = false;
762                 break;
763             }
764         }
765 
766         if (done) {
767             if (DBG) Log.d(LOG_TAG, "Done receiving fwd info");
768             dismissDialogSafely(VoicemailDialogUtil.VM_FWD_READING_DIALOG);
769 
770             if (mPreviousVMProviderKey.equals(VoicemailProviderListPreference.DEFAULT_KEY)) {
771                 VoicemailProviderSettingsUtil.save(mPhone.getContext(),
772                         VoicemailProviderListPreference.DEFAULT_KEY,
773                         new VoicemailProviderSettings(mOldVmNumber, mForwardingReadResults));
774             }
775             saveVoiceMailAndForwardingNumberStage2();
776         }
777     }
778 
resetForwardingChangeState()779     private void resetForwardingChangeState() {
780         mForwardingChangeResults = new HashMap<Integer, AsyncResult>();
781         mExpectedChangeResultReasons = new HashSet<Integer>();
782     }
783 
784     // Called after we are done saving the previous forwarding settings if we needed.
saveVoiceMailAndForwardingNumberStage2()785     private void saveVoiceMailAndForwardingNumberStage2() {
786         mForwardingChangeResults = null;
787         mVoicemailChangeResult = null;
788 
789         resetForwardingChangeState();
790         for (int i = 0; i < mNewFwdSettings.length; i++) {
791             CallForwardInfo fi = mNewFwdSettings[i];
792             CallForwardInfo fiForReason =
793                     CallForwardInfoUtil.infoForReason(mForwardingReadResults, fi.reason);
794             final boolean doUpdate = CallForwardInfoUtil.isUpdateRequired(fiForReason, fi);
795 
796             if (doUpdate) {
797                 if (DBG) log("Setting fwd #: " + i + ": " + fi.toString());
798                 mExpectedChangeResultReasons.add(i);
799 
800                 CallForwardInfoUtil.setCallForwardingOption(mPhone, fi,
801                         mSetOptionComplete.obtainMessage(
802                                 EVENT_FORWARDING_CHANGED, fi.reason, 0));
803             }
804         }
805         showDialogIfForeground(VoicemailDialogUtil.VM_FWD_SAVING_DIALOG);
806     }
807 
808 
809     /**
810      * Callback to handle option update completions
811      */
812     private final Handler mSetOptionComplete = new Handler() {
813         @Override
814         public void handleMessage(Message msg) {
815             AsyncResult result = (AsyncResult) msg.obj;
816             boolean done = false;
817             switch (msg.what) {
818                 case EVENT_VOICEMAIL_CHANGED:
819                     mVoicemailChangeResult = result;
820                     mVMChangeCompletedSuccessfully = isVmChangeSuccess();
821                     PhoneGlobals.getInstance().refreshMwiIndicator(
822                             mSubscriptionInfoHelper.getSubId());
823                     done = true;
824                     break;
825                 case EVENT_FORWARDING_CHANGED:
826                     mForwardingChangeResults.put(msg.arg1, result);
827                     if (result.exception != null) {
828                         Log.w(LOG_TAG, "Error in setting fwd# " + msg.arg1 + ": " +
829                                 result.exception.getMessage());
830                     }
831                     if (isForwardingCompleted()) {
832                         if (isFwdChangeSuccess()) {
833                             if (DBG) log("Overall fwd changes completed ok, starting vm change");
834                             setVoicemailNumberWithCarrier();
835                         } else {
836                             Log.w(LOG_TAG, "Overall fwd changes completed in failure. " +
837                                     "Check if we need to try rollback for some settings.");
838                             mFwdChangesRequireRollback = false;
839                             Iterator<Map.Entry<Integer,AsyncResult>> it =
840                                 mForwardingChangeResults.entrySet().iterator();
841                             while (it.hasNext()) {
842                                 Map.Entry<Integer,AsyncResult> entry = it.next();
843                                 if (entry.getValue().exception == null) {
844                                     // If at least one succeeded we have to revert
845                                     Log.i(LOG_TAG, "Rollback will be required");
846                                     mFwdChangesRequireRollback = true;
847                                     break;
848                                 }
849                             }
850                             if (!mFwdChangesRequireRollback) {
851                                 Log.i(LOG_TAG, "No rollback needed.");
852                             }
853                             done = true;
854                         }
855                     }
856                     break;
857                 default:
858                     // TODO: should never reach this, may want to throw exception
859             }
860 
861             if (done) {
862                 if (DBG) log("All VM provider related changes done");
863                 if (mForwardingChangeResults != null) {
864                     dismissDialogSafely(VoicemailDialogUtil.VM_FWD_SAVING_DIALOG);
865                 }
866                 handleSetVmOrFwdMessage();
867             }
868         }
869     };
870 
871     /**
872      * Callback to handle option revert completions
873      */
874     private final Handler mRevertOptionComplete = new Handler() {
875         @Override
876         public void handleMessage(Message msg) {
877             AsyncResult result = (AsyncResult) msg.obj;
878             switch (msg.what) {
879                 case EVENT_VOICEMAIL_CHANGED:
880                     if (DBG) log("VM revert complete msg");
881                     mVoicemailChangeResult = result;
882                     break;
883 
884                 case EVENT_FORWARDING_CHANGED:
885                     if (DBG) log("FWD revert complete msg ");
886                     mForwardingChangeResults.put(msg.arg1, result);
887                     if (result.exception != null) {
888                         if (DBG) log("Error in reverting fwd# " + msg.arg1 + ": " +
889                                 result.exception.getMessage());
890                     }
891                     break;
892 
893                 default:
894                     // TODO: should never reach this, may want to throw exception
895             }
896 
897             final boolean done = (!mVMChangeCompletedSuccessfully || mVoicemailChangeResult != null)
898                     && (!mFwdChangesRequireRollback || isForwardingCompleted());
899             if (done) {
900                 if (DBG) log("All VM reverts done");
901                 dismissDialogSafely(VoicemailDialogUtil.VM_REVERTING_DIALOG);
902                 onRevertDone();
903             }
904         }
905     };
906 
setVoicemailNumberWithCarrier()907     private void setVoicemailNumberWithCarrier() {
908         if (DBG) log("save voicemail #: " + mNewVMNumber);
909 
910         mVoicemailChangeResult = null;
911         mPhone.setVoiceMailNumber(
912                 mPhone.getVoiceMailAlphaTag().toString(),
913                 mNewVMNumber,
914                 Message.obtain(mSetOptionComplete, EVENT_VOICEMAIL_CHANGED));
915     }
916 
switchToPreviousVoicemailProvider()917     private void switchToPreviousVoicemailProvider() {
918         if (DBG) log("switchToPreviousVoicemailProvider " + mPreviousVMProviderKey);
919 
920         if (mPreviousVMProviderKey == null) {
921             return;
922         }
923 
924         if (mVMChangeCompletedSuccessfully || mFwdChangesRequireRollback) {
925             showDialogIfForeground(VoicemailDialogUtil.VM_REVERTING_DIALOG);
926             final VoicemailProviderSettings prevSettings =
927                     VoicemailProviderSettingsUtil.load(this, mPreviousVMProviderKey);
928             if (prevSettings == null) {
929                 Log.e(LOG_TAG, "VoicemailProviderSettings for the key \""
930                         + mPreviousVMProviderKey + "\" is null but should be loaded.");
931                 return;
932             }
933 
934             if (mVMChangeCompletedSuccessfully) {
935                 mNewVMNumber = prevSettings.getVoicemailNumber();
936                 Log.i(LOG_TAG, "VM change is already completed successfully."
937                         + "Have to revert VM back to " + mNewVMNumber + " again.");
938                 mPhone.setVoiceMailNumber(
939                         mPhone.getVoiceMailAlphaTag().toString(),
940                         mNewVMNumber,
941                         Message.obtain(mRevertOptionComplete, EVENT_VOICEMAIL_CHANGED));
942             }
943 
944             if (mFwdChangesRequireRollback) {
945                 Log.i(LOG_TAG, "Requested to rollback forwarding changes.");
946 
947                 final CallForwardInfo[] prevFwdSettings = prevSettings.getForwardingSettings();
948                 if (prevFwdSettings != null) {
949                     Map<Integer, AsyncResult> results = mForwardingChangeResults;
950                     resetForwardingChangeState();
951                     for (int i = 0; i < prevFwdSettings.length; i++) {
952                         CallForwardInfo fi = prevFwdSettings[i];
953                         if (DBG) log("Reverting fwd #: " + i + ": " + fi.toString());
954                         // Only revert the settings for which the update succeeded.
955                         AsyncResult result = results.get(fi.reason);
956                         if (result != null && result.exception == null) {
957                             mExpectedChangeResultReasons.add(fi.reason);
958                             CallForwardInfoUtil.setCallForwardingOption(mPhone, fi,
959                                     mRevertOptionComplete.obtainMessage(
960                                             EVENT_FORWARDING_CHANGED, i, 0));
961                         }
962                     }
963                 }
964             }
965         } else {
966             if (DBG) log("No need to revert");
967             onRevertDone();
968         }
969     }
970 
971 
972     //*********************************************************************************************
973     // Voicemail Handler Helpers
974     //*********************************************************************************************
975 
976     /**
977      * Updates the look of the VM preference widgets based on current VM provider settings.
978      * Note that the provider name is loaded fxrorm the found activity via loadLabel in
979      * {@link VoicemailProviderListPreference#initVoiceMailProviders()} in order for it to be
980      * localizable.
981      */
updateVMPreferenceWidgets(String currentProviderSetting)982     private void updateVMPreferenceWidgets(String currentProviderSetting) {
983         final String key = currentProviderSetting;
984         final VoicemailProviderListPreference.VoicemailProvider provider =
985                 mVoicemailProviders.getVoicemailProvider(key);
986 
987         /* This is the case when we are coming up on a freshly wiped phone and there is no
988          persisted value for the list preference mVoicemailProviders.
989          In this case we want to show the UI asking the user to select a voicemail provider as
990          opposed to silently falling back to default one. */
991         if (provider == null) {
992             if (DBG) log("updateVMPreferenceWidget: key: " + key + " -> null.");
993 
994             mVoicemailProviders.setSummary(getString(R.string.sum_voicemail_choose_provider));
995             mVoicemailSettings.setEnabled(false);
996             mVoicemailSettings.setIntent(null);
997         } else {
998             if (DBG) log("updateVMPreferenceWidget: key: " + key + " -> " + provider.toString());
999 
1000             final String providerName = provider.name;
1001             mVoicemailProviders.setSummary(providerName);
1002             mVoicemailSettings.setEnabled(true);
1003             mVoicemailSettings.setIntent(provider.intent);
1004         }
1005     }
1006 
1007     /**
1008      * Update the voicemail number from what we've recorded on the sim.
1009      */
updateVoiceNumberField()1010     private void updateVoiceNumberField() {
1011         if (DBG) log("updateVoiceNumberField()");
1012 
1013         mOldVmNumber = mPhone.getVoiceMailNumber();
1014         if (TextUtils.isEmpty(mOldVmNumber)) {
1015             mSubMenuVoicemailSettings.setPhoneNumber("");
1016             mSubMenuVoicemailSettings.setSummary(getString(R.string.voicemail_number_not_set));
1017         } else {
1018             mSubMenuVoicemailSettings.setPhoneNumber(mOldVmNumber);
1019             mSubMenuVoicemailSettings.setSummary(BidiFormatter.getInstance().unicodeWrap(
1020                     mOldVmNumber, TextDirectionHeuristics.LTR));
1021         }
1022     }
1023 
handleSetVmOrFwdMessage()1024     private void handleSetVmOrFwdMessage() {
1025         if (DBG) log("handleSetVMMessage: set VM request complete");
1026 
1027         if (!isFwdChangeSuccess()) {
1028             handleVmOrFwdSetError(VoicemailDialogUtil.FWD_SET_RESPONSE_ERROR_DIALOG);
1029         } else if (!isVmChangeSuccess()) {
1030             handleVmOrFwdSetError(VoicemailDialogUtil.VM_RESPONSE_ERROR_DIALOG);
1031         } else {
1032             handleVmAndFwdSetSuccess(VoicemailDialogUtil.VM_CONFIRM_DIALOG);
1033         }
1034     }
1035 
1036     /**
1037      * Called when Voicemail Provider or its forwarding settings failed. Rolls back partly made
1038      * changes to those settings and show "failure" dialog.
1039      *
1040      * @param dialogId ID of the dialog to show for the specific error case. Either
1041      *     {@link #FWD_SET_RESPONSE_ERROR_DIALOG} or {@link #VM_RESPONSE_ERROR_DIALOG}
1042      */
handleVmOrFwdSetError(int dialogId)1043     private void handleVmOrFwdSetError(int dialogId) {
1044         if (mChangingVMorFwdDueToProviderChange) {
1045             mVMOrFwdSetError = dialogId;
1046             mChangingVMorFwdDueToProviderChange = false;
1047             switchToPreviousVoicemailProvider();
1048             return;
1049         }
1050         mChangingVMorFwdDueToProviderChange = false;
1051         showDialogIfForeground(dialogId);
1052         updateVoiceNumberField();
1053     }
1054 
1055     /**
1056      * Called when Voicemail Provider and its forwarding settings were successfully finished.
1057      * This updates a bunch of variables and show "success" dialog.
1058      */
handleVmAndFwdSetSuccess(int dialogId)1059     private void handleVmAndFwdSetSuccess(int dialogId) {
1060         if (DBG) log("handleVmAndFwdSetSuccess: key is " + mVoicemailProviders.getKey());
1061 
1062         mPreviousVMProviderKey = mVoicemailProviders.getKey();
1063         mChangingVMorFwdDueToProviderChange = false;
1064         showDialogIfForeground(dialogId);
1065         updateVoiceNumberField();
1066     }
1067 
onRevertDone()1068     private void onRevertDone() {
1069         if (DBG) log("onRevertDone: Changing provider key back to " + mPreviousVMProviderKey);
1070 
1071         updateVMPreferenceWidgets(mPreviousVMProviderKey);
1072         updateVoiceNumberField();
1073         if (mVMOrFwdSetError != 0) {
1074             showDialogIfForeground(mVMOrFwdSetError);
1075             mVMOrFwdSetError = 0;
1076         }
1077     }
1078 
1079 
1080     //*********************************************************************************************
1081     // Voicemail State Helpers
1082     //*********************************************************************************************
1083 
1084     /**
1085      * Return true if there is a change result for every reason for which we expect a result.
1086      */
isForwardingCompleted()1087     private boolean isForwardingCompleted() {
1088         if (mForwardingChangeResults == null) {
1089             return true;
1090         }
1091 
1092         for (Integer reason : mExpectedChangeResultReasons) {
1093             if (mForwardingChangeResults.get(reason) == null) {
1094                 return false;
1095             }
1096         }
1097 
1098         return true;
1099     }
1100 
isFwdChangeSuccess()1101     private boolean isFwdChangeSuccess() {
1102         if (mForwardingChangeResults == null) {
1103             return true;
1104         }
1105 
1106         for (AsyncResult result : mForwardingChangeResults.values()) {
1107             Throwable exception = result.exception;
1108             if (exception != null) {
1109                 String msg = exception.getMessage();
1110                 msg = (msg != null) ? msg : "";
1111                 Log.w(LOG_TAG, "Failed to change forwarding setting. Reason: " + msg);
1112                 return false;
1113             }
1114         }
1115         return true;
1116     }
1117 
isVmChangeSuccess()1118     private boolean isVmChangeSuccess() {
1119         if (mVoicemailChangeResult.exception != null) {
1120             String msg = mVoicemailChangeResult.exception.getMessage();
1121             msg = (msg != null) ? msg : "";
1122             Log.w(LOG_TAG, "Failed to change voicemail. Reason: " + msg);
1123             return false;
1124         }
1125         return true;
1126     }
1127 
log(String msg)1128     private static void log(String msg) {
1129         Log.d(LOG_TAG, msg);
1130     }
1131 }
1132