1 /*
2  * Copyright (C) 2010 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.services.telephony.sip;
18 
19 import android.app.AlertDialog;
20 import android.app.Dialog;
21 import android.app.DialogFragment;
22 import android.content.Intent;
23 import android.net.sip.SipProfile;
24 import android.os.Bundle;
25 import android.os.Parcelable;
26 import android.preference.CheckBoxPreference;
27 import android.preference.EditTextPreference;
28 import android.preference.ListPreference;
29 import android.preference.Preference;
30 import android.preference.PreferenceActivity;
31 import android.preference.PreferenceGroup;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.view.KeyEvent;
35 import android.view.Menu;
36 import android.view.MenuItem;
37 import android.widget.Button;
38 import android.widget.Toast;
39 
40 import java.io.IOException;
41 import java.lang.reflect.Method;
42 import java.util.Arrays;
43 
44 /**
45  * The activity class for editing a new or existing SIP profile.
46  */
47 public class SipEditor extends PreferenceActivity
48         implements Preference.OnPreferenceChangeListener {
49     private static final String PREFIX = "[SipEditor] ";
50     private static final boolean VERBOSE = false; /* STOP SHIP if true */
51 
52     private static final int MENU_SAVE = Menu.FIRST;
53     private static final int MENU_DISCARD = Menu.FIRST + 1;
54     private static final int MENU_REMOVE = Menu.FIRST + 2;
55 
56     private static final String KEY_PROFILE = "profile";
57     private static final String GET_METHOD_PREFIX = "get";
58     private static final char SCRAMBLED = '*';
59     private static final int NA = 0;
60 
61     private AdvancedSettings mAdvancedSettings;
62     private SipPreferences mSipPreferences;
63     private boolean mDisplayNameSet;
64     private boolean mHomeButtonClicked;
65     private boolean mUpdateRequired;
66 
67     private SipProfileDb mProfileDb;
68     private SipProfile mOldProfile;
69     private Button mRemoveButton;
70     private SipAccountRegistry mSipAccountRegistry;
71 
72     /**
73      * Dialog fragment class to be used for displaying an alert dialog.
74      */
75     public static class AlertDialogFragment extends DialogFragment {
76         private static final String KEY_MESSAGE = "message";
77 
78         /**
79          * Initialize the AlertDialogFragment instance.
80          *
81          * @param message the dialog message to display.
82          * @return the AlertDialogFragment.
83          */
newInstance(String message)84         public static AlertDialogFragment newInstance(String message) {
85             AlertDialogFragment frag = new AlertDialogFragment();
86             Bundle args = new Bundle();
87             args.putString(KEY_MESSAGE, message);
88             frag.setArguments(args);
89             return frag;
90         }
91 
92         @Override
onCreateDialog(Bundle savedInstanceState)93         public Dialog onCreateDialog(Bundle savedInstanceState) {
94             String message = getArguments().getString(KEY_MESSAGE);
95 
96             return new AlertDialog.Builder(getActivity())
97                     .setTitle(android.R.string.dialog_alert_title)
98                     .setIconAttribute(android.R.attr.alertDialogIcon)
99                     .setMessage(message)
100                     .setPositiveButton(R.string.alert_dialog_ok, null)
101                     .create();
102         }
103     }
104 
105     enum PreferenceKey {
106         Username(R.string.username, 0, R.string.default_preference_summary_username),
107         Password(R.string.password, 0, R.string.default_preference_summary_password),
108         DomainAddress(R.string.domain_address, 0,
109                 R.string.default_preference_summary_domain_address),
110         DisplayName(R.string.display_name, 0, R.string.display_name_summary),
111         ProxyAddress(R.string.proxy_address, 0, R.string.optional_summary),
112         Port(R.string.port, R.string.default_port, R.string.default_port),
113         Transport(R.string.transport, R.string.default_transport, NA),
114         SendKeepAlive(R.string.send_keepalive, R.string.sip_system_decide, NA),
115         AuthUserName(R.string.auth_username, 0, R.string.optional_summary);
116 
117         final int text;
118         final int initValue;
119         final int defaultSummary;
120         Preference preference;
121 
122         /**
123          * @param key The key name of the preference.
124          * @param initValue The initial value of the preference.
125          * @param defaultSummary The default summary value of the preference
126          *        when the preference value is empty.
127          */
PreferenceKey(int text, int initValue, int defaultSummary)128         PreferenceKey(int text, int initValue, int defaultSummary) {
129             this.text = text;
130             this.initValue = initValue;
131             this.defaultSummary = defaultSummary;
132         }
133 
getValue()134         String getValue() {
135             if (preference instanceof EditTextPreference) {
136                 return ((EditTextPreference) preference).getText();
137             } else if (preference instanceof ListPreference) {
138                 return ((ListPreference) preference).getValue();
139             }
140             throw new RuntimeException("getValue() for the preference " + this);
141         }
142 
setValue(String value)143         void setValue(String value) {
144             if (preference instanceof EditTextPreference) {
145                 String oldValue = getValue();
146                 ((EditTextPreference) preference).setText(value);
147                 if (this != Password) {
148                     if (VERBOSE) {
149                         log(this + ": setValue() " + value + ": " + oldValue + " --> " +
150                                 getValue());
151                     }
152                 }
153             } else if (preference instanceof ListPreference) {
154                 ((ListPreference) preference).setValue(value);
155             }
156 
157             if (TextUtils.isEmpty(value)) {
158                 preference.setSummary(defaultSummary);
159             } else if (this == Password) {
160                 preference.setSummary(scramble(value));
161             } else if ((this == DisplayName)
162                     && value.equals(getDefaultDisplayName())) {
163                 preference.setSummary(defaultSummary);
164             } else {
165                 preference.setSummary(value);
166             }
167         }
168     }
169 
170     @Override
onResume()171     public void onResume() {
172         super.onResume();
173         mHomeButtonClicked = false;
174         if (!SipUtil.isPhoneIdle(this)) {
175             mAdvancedSettings.show();
176             getPreferenceScreen().setEnabled(false);
177             if (mRemoveButton != null) mRemoveButton.setEnabled(false);
178         } else {
179             getPreferenceScreen().setEnabled(true);
180             if (mRemoveButton != null) mRemoveButton.setEnabled(true);
181         }
182     }
183 
184     @Override
onCreate(Bundle savedInstanceState)185     public void onCreate(Bundle savedInstanceState) {
186         if (VERBOSE) log("onCreate, start profile editor");
187         super.onCreate(savedInstanceState);
188 
189         mSipPreferences = new SipPreferences(this);
190         mProfileDb = new SipProfileDb(this);
191         mSipAccountRegistry = SipAccountRegistry.getInstance();
192 
193         setContentView(R.layout.sip_settings_ui);
194         addPreferencesFromResource(R.xml.sip_edit);
195 
196         SipProfile p = mOldProfile = (SipProfile) ((savedInstanceState == null)
197                 ? getIntent().getParcelableExtra(SipSettings.KEY_SIP_PROFILE)
198                 : savedInstanceState.getParcelable(KEY_PROFILE));
199 
200         PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
201         for (int i = 0, n = screen.getPreferenceCount(); i < n; i++) {
202             setupPreference(screen.getPreference(i));
203         }
204 
205         if (p == null) {
206             screen.setTitle(R.string.sip_edit_new_title);
207         }
208 
209         mAdvancedSettings = new AdvancedSettings();
210 
211         loadPreferencesFromProfile(p);
212     }
213 
214     @Override
onPause()215     public void onPause() {
216         if (VERBOSE) log("onPause, finishing: " + isFinishing());
217         if (!isFinishing()) {
218             mHomeButtonClicked = true;
219             validateAndSetResult();
220         }
221         super.onPause();
222     }
223 
224     @Override
onCreateOptionsMenu(Menu menu)225     public boolean onCreateOptionsMenu(Menu menu) {
226         super.onCreateOptionsMenu(menu);
227         menu.add(0, MENU_DISCARD, 0, R.string.sip_menu_discard)
228                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
229         menu.add(0, MENU_SAVE, 0, R.string.sip_menu_save)
230                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
231         menu.add(0, MENU_REMOVE, 0, R.string.remove_sip_account)
232                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
233         return true;
234     }
235 
236     @Override
onPrepareOptionsMenu(Menu menu)237     public boolean onPrepareOptionsMenu(Menu menu) {
238         MenuItem removeMenu = menu.findItem(MENU_REMOVE);
239         removeMenu.setVisible(mOldProfile != null);
240         menu.findItem(MENU_SAVE).setEnabled(mUpdateRequired);
241         return super.onPrepareOptionsMenu(menu);
242     }
243 
244     @Override
onOptionsItemSelected(MenuItem item)245     public boolean onOptionsItemSelected(MenuItem item) {
246         switch (item.getItemId()) {
247             case MENU_SAVE:
248                 validateAndSetResult();
249                 return true;
250 
251             case MENU_DISCARD:
252                 finish();
253                 return true;
254 
255             case MENU_REMOVE: {
256                 setRemovedProfileAndFinish();
257                 return true;
258             }
259             case android.R.id.home: {
260                 finish();
261                 return true;
262             }
263         }
264         return super.onOptionsItemSelected(item);
265     }
266 
267     @Override
onKeyDown(int keyCode, KeyEvent event)268     public boolean onKeyDown(int keyCode, KeyEvent event) {
269         switch (keyCode) {
270             case KeyEvent.KEYCODE_BACK:
271                 validateAndSetResult();
272                 return true;
273         }
274         return super.onKeyDown(keyCode, event);
275     }
276 
277     /**
278      * Saves a {@link SipProfile} and registers the associated
279      * {@link android.telecom.PhoneAccount}.
280      *
281      * @param p The {@link SipProfile} to register.
282      * @param enableProfile {@code true} if profile should be enabled, too.
283      * @throws IOException Exception resulting from profile save.
284      */
saveAndRegisterProfile(SipProfile p, boolean enableProfile)285     private void saveAndRegisterProfile(SipProfile p, boolean enableProfile) throws IOException {
286         if (p == null) return;
287         mProfileDb.saveProfile(p);
288         mSipAccountRegistry.startSipService(this, p.getProfileName(), enableProfile);
289     }
290 
291     /**
292      * Deletes a {@link SipProfile} and un-registers the associated
293      * {@link android.telecom.PhoneAccount}.
294      *
295      * @param p The {@link SipProfile} to delete.
296      */
deleteAndUnregisterProfile(SipProfile p)297     private void deleteAndUnregisterProfile(SipProfile p) throws IOException {
298         if (p == null) return;
299         mProfileDb.deleteProfile(p);
300         mSipAccountRegistry.stopSipService(this, p.getProfileName());
301     }
302 
setRemovedProfileAndFinish()303     private void setRemovedProfileAndFinish() {
304         Intent intent = new Intent(this, SipSettings.class);
305         setResult(RESULT_FIRST_USER, intent);
306         Toast.makeText(this, R.string.removing_account, Toast.LENGTH_SHORT)
307                 .show();
308         replaceProfile(mOldProfile, null);
309         // do finish() in replaceProfile() in a background thread
310     }
311 
showAlert(Throwable e)312     private void showAlert(Throwable e) {
313         String msg = e.getMessage();
314         if (TextUtils.isEmpty(msg)) msg = e.toString();
315         showAlert(msg);
316     }
317 
showAlert(final String message)318     private void showAlert(final String message) {
319         if (mHomeButtonClicked) {
320             if (VERBOSE) log("Home button clicked, don't show dialog: " + message);
321             return;
322         }
323 
324         AlertDialogFragment newFragment = AlertDialogFragment.newInstance(message);
325         newFragment.show(getFragmentManager(), null);
326     }
327 
isEditTextEmpty(PreferenceKey key)328     private boolean isEditTextEmpty(PreferenceKey key) {
329         EditTextPreference pref = (EditTextPreference) key.preference;
330         return TextUtils.isEmpty(pref.getText())
331                 || pref.getSummary().equals(getString(key.defaultSummary));
332     }
333 
validateAndSetResult()334     private void validateAndSetResult() {
335         boolean allEmpty = true;
336         CharSequence firstEmptyFieldTitle = null;
337         for (PreferenceKey key : PreferenceKey.values()) {
338             Preference p = key.preference;
339             if (p instanceof EditTextPreference) {
340                 EditTextPreference pref = (EditTextPreference) p;
341                 boolean fieldEmpty = isEditTextEmpty(key);
342                 if (allEmpty && !fieldEmpty) allEmpty = false;
343 
344                 // use default value if display name is empty
345                 if (fieldEmpty) {
346                     switch (key) {
347                         case DisplayName:
348                             pref.setText(getDefaultDisplayName());
349                             break;
350                         case AuthUserName:
351                         case ProxyAddress:
352                             // optional; do nothing
353                             break;
354                         case Port:
355                             pref.setText(getString(R.string.default_port));
356                             break;
357                         default:
358                             if (firstEmptyFieldTitle == null) {
359                                 firstEmptyFieldTitle = pref.getTitle();
360                             }
361                     }
362                 } else if (key == PreferenceKey.Port) {
363                     int port;
364                     try {
365                         port = Integer.parseInt(PreferenceKey.Port.getValue());
366                     } catch (NumberFormatException e) {
367                         showAlert(getString(R.string.not_a_valid_port));
368                         return;
369                     }
370                     if ((port < 1000) || (port > 65534)) {
371                         showAlert(getString(R.string.not_a_valid_port));
372                         return;
373                     }
374                 }
375             }
376         }
377 
378         if (!mUpdateRequired) {
379             finish();
380             return;
381         } else if (allEmpty) {
382             showAlert(getString(R.string.all_empty_alert));
383             return;
384         } else if (firstEmptyFieldTitle != null) {
385             showAlert(getString(R.string.empty_alert, firstEmptyFieldTitle));
386             return;
387         }
388         try {
389             SipProfile profile = createSipProfile();
390             Intent intent = new Intent(this, SipSettings.class);
391             intent.putExtra(SipSettings.KEY_SIP_PROFILE, (Parcelable) profile);
392             setResult(RESULT_OK, intent);
393             Toast.makeText(this, R.string.saving_account, Toast.LENGTH_SHORT).show();
394 
395             replaceProfile(mOldProfile, profile);
396             // do finish() in replaceProfile() in a background thread
397         } catch (Exception e) {
398             log("validateAndSetResult, can not create new SipProfile, exception: " + e);
399             showAlert(e);
400         }
401     }
402 
replaceProfile(final SipProfile oldProfile, final SipProfile newProfile)403     private void replaceProfile(final SipProfile oldProfile, final SipProfile newProfile) {
404         // Replace profile in a background thread as it takes time to access the
405         // storage; do finish() once everything goes fine.
406         // newProfile may be null if the old profile is to be deleted rather
407         // than being modified.
408         new Thread(new Runnable() {
409             public void run() {
410                 try {
411                     deleteAndUnregisterProfile(oldProfile);
412                     boolean autoEnableNewProfile = oldProfile == null;
413                     saveAndRegisterProfile(newProfile, autoEnableNewProfile);
414                     finish();
415                 } catch (Exception e) {
416                     log("replaceProfile, can not save/register new SipProfile, exception: " + e);
417                     showAlert(e);
418                 }
419             }
420         }, "SipEditor").start();
421     }
422 
getProfileName()423     private String getProfileName() {
424         return PreferenceKey.Username.getValue() + "@"
425                 + PreferenceKey.DomainAddress.getValue();
426     }
427 
createSipProfile()428     private SipProfile createSipProfile() throws Exception {
429         return new SipProfile.Builder(
430                 PreferenceKey.Username.getValue(),
431                 PreferenceKey.DomainAddress.getValue())
432                 .setProfileName(getProfileName())
433                 .setPassword(PreferenceKey.Password.getValue())
434                 .setOutboundProxy(PreferenceKey.ProxyAddress.getValue())
435                 .setProtocol(PreferenceKey.Transport.getValue())
436                 .setDisplayName(PreferenceKey.DisplayName.getValue())
437                 .setPort(Integer.parseInt(PreferenceKey.Port.getValue()))
438                 .setSendKeepAlive(isAlwaysSendKeepAlive())
439                 .setAutoRegistration(
440                         mSipPreferences.isReceivingCallsEnabled())
441                 .setAuthUserName(PreferenceKey.AuthUserName.getValue())
442                 .build();
443     }
444 
onPreferenceChange(Preference pref, Object newValue)445     public boolean onPreferenceChange(Preference pref, Object newValue) {
446         if (!mUpdateRequired) {
447             mUpdateRequired = true;
448         }
449 
450         if (pref instanceof CheckBoxPreference) {
451             invalidateOptionsMenu();
452             return true;
453         }
454         String value = (newValue == null) ? "" : newValue.toString();
455         if (TextUtils.isEmpty(value)) {
456             pref.setSummary(getPreferenceKey(pref).defaultSummary);
457         } else if (pref == PreferenceKey.Password.preference) {
458             pref.setSummary(scramble(value));
459         } else {
460             pref.setSummary(value);
461         }
462 
463         if (pref == PreferenceKey.DisplayName.preference) {
464             ((EditTextPreference) pref).setText(value);
465             checkIfDisplayNameSet();
466         }
467 
468         // SAVE menu should be enabled once the user modified some preference.
469         invalidateOptionsMenu();
470         return true;
471     }
472 
getPreferenceKey(Preference pref)473     private PreferenceKey getPreferenceKey(Preference pref) {
474         for (PreferenceKey key : PreferenceKey.values()) {
475             if (key.preference == pref) return key;
476         }
477         throw new RuntimeException("not possible to reach here");
478     }
479 
loadPreferencesFromProfile(SipProfile p)480     private void loadPreferencesFromProfile(SipProfile p) {
481         if (p != null) {
482             if (VERBOSE) log("loadPreferencesFromProfile, existing profile: " + p.getProfileName());
483             try {
484                 Class profileClass = SipProfile.class;
485                 for (PreferenceKey key : PreferenceKey.values()) {
486                     Method meth = profileClass.getMethod(GET_METHOD_PREFIX
487                             + getString(key.text), (Class[])null);
488                     if (key == PreferenceKey.SendKeepAlive) {
489                         boolean value = ((Boolean) meth.invoke(p, (Object[]) null)).booleanValue();
490                         key.setValue(getString(value
491                                 ? R.string.sip_always_send_keepalive
492                                 : R.string.sip_system_decide));
493                     } else {
494                         Object value = meth.invoke(p, (Object[])null);
495                         key.setValue((value == null) ? "" : value.toString());
496                     }
497                 }
498                 checkIfDisplayNameSet();
499             } catch (Exception e) {
500                 log("loadPreferencesFromProfile, can not load pref from profile, exception: " + e);
501             }
502         } else {
503             if (VERBOSE) log("loadPreferencesFromProfile, edit a new profile");
504             for (PreferenceKey key : PreferenceKey.values()) {
505                 key.preference.setOnPreferenceChangeListener(this);
506 
507                 // FIXME: android:defaultValue in preference xml file doesn't
508                 // work. Even if we setValue() for each preference in the case
509                 // of (p != null), the dialog still shows android:defaultValue,
510                 // not the value set by setValue(). This happens if
511                 // android:defaultValue is not empty. Is it a bug?
512                 if (key.initValue != 0) {
513                     key.setValue(getString(key.initValue));
514                 }
515             }
516             mDisplayNameSet = false;
517         }
518     }
519 
isAlwaysSendKeepAlive()520     private boolean isAlwaysSendKeepAlive() {
521         ListPreference pref = (ListPreference) PreferenceKey.SendKeepAlive.preference;
522         return getString(R.string.sip_always_send_keepalive).equals(pref.getValue());
523     }
524 
setCheckBox(PreferenceKey key, boolean checked)525     private void setCheckBox(PreferenceKey key, boolean checked) {
526         CheckBoxPreference pref = (CheckBoxPreference) key.preference;
527         pref.setChecked(checked);
528     }
529 
setupPreference(Preference pref)530     private void setupPreference(Preference pref) {
531         pref.setOnPreferenceChangeListener(this);
532         for (PreferenceKey key : PreferenceKey.values()) {
533             String name = getString(key.text);
534             if (name.equals(pref.getKey())) {
535                 key.preference = pref;
536                 return;
537             }
538         }
539     }
540 
checkIfDisplayNameSet()541     private void checkIfDisplayNameSet() {
542         String displayName = PreferenceKey.DisplayName.getValue();
543         mDisplayNameSet = !TextUtils.isEmpty(displayName)
544                 && !displayName.equals(getDefaultDisplayName());
545         if (VERBOSE) log("checkIfDisplayNameSet, displayName set: " + mDisplayNameSet);
546         if (mDisplayNameSet) {
547             PreferenceKey.DisplayName.preference.setSummary(displayName);
548         } else {
549             PreferenceKey.DisplayName.setValue("");
550         }
551     }
552 
getDefaultDisplayName()553     private static String getDefaultDisplayName() {
554         return PreferenceKey.Username.getValue();
555     }
556 
scramble(String s)557     private static String scramble(String s) {
558         char[] cc = new char[s.length()];
559         Arrays.fill(cc, SCRAMBLED);
560         return new String(cc);
561     }
562 
563     private class AdvancedSettings implements Preference.OnPreferenceClickListener {
564         private Preference mAdvancedSettingsTrigger;
565         private Preference[] mPreferences;
566         private boolean mShowing = false;
567 
AdvancedSettings()568         AdvancedSettings() {
569             mAdvancedSettingsTrigger = getPreferenceScreen().findPreference(
570                     getString(R.string.advanced_settings));
571             mAdvancedSettingsTrigger.setOnPreferenceClickListener(this);
572 
573             loadAdvancedPreferences();
574         }
575 
loadAdvancedPreferences()576         private void loadAdvancedPreferences() {
577             PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
578 
579             addPreferencesFromResource(R.xml.sip_advanced_edit);
580             PreferenceGroup group = (PreferenceGroup) screen.findPreference(
581                     getString(R.string.advanced_settings_container));
582             screen.removePreference(group);
583 
584             mPreferences = new Preference[group.getPreferenceCount()];
585             int order = screen.getPreferenceCount();
586             for (int i = 0, n = mPreferences.length; i < n; i++) {
587                 Preference pref = group.getPreference(i);
588                 pref.setOrder(order++);
589                 setupPreference(pref);
590                 mPreferences[i] = pref;
591             }
592         }
593 
show()594         void show() {
595             mShowing = true;
596             mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_hide);
597             PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
598             for (Preference pref : mPreferences) {
599                 screen.addPreference(pref);
600                 if (VERBOSE) {
601                     log("AdvancedSettings.show, pref: " + pref.getKey() + ", order: " +
602                             pref.getOrder());
603                 }
604             }
605         }
606 
hide()607         private void hide() {
608             mShowing = false;
609             mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_show);
610             PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
611             for (Preference pref : mPreferences) {
612                 screen.removePreference(pref);
613             }
614         }
615 
onPreferenceClick(Preference preference)616         public boolean onPreferenceClick(Preference preference) {
617             if (VERBOSE) log("AdvancedSettings.onPreferenceClick");
618             if (!mShowing) {
619                 show();
620             } else {
621                 hide();
622             }
623             return true;
624         }
625     }
626 
log(String msg)627     private static void log(String msg) {
628         Log.d(SipUtil.LOG_TAG, PREFIX + msg);
629     }
630 }
631