1 /*
2  * Copyright (C) 2008 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;
18 
19 import static android.view.View.LAYOUT_DIRECTION_LOCALE;
20 import static android.view.View.TEXT_DIRECTION_LOCALE;
21 
22 import android.app.Activity;
23 import android.app.AlertDialog;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.Intent;
27 import android.content.res.TypedArray;
28 import android.preference.EditTextPreference;
29 import android.provider.ContactsContract.CommonDataKinds.Phone;
30 import android.telephony.PhoneNumberUtils;
31 import android.text.BidiFormatter;
32 import android.text.TextDirectionHeuristics;
33 import android.text.TextUtils;
34 import android.text.method.ArrowKeyMovementMethod;
35 import android.text.method.DialerKeyListener;
36 import android.util.AttributeSet;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.EditText;
40 import android.widget.ImageButton;
41 import android.widget.TextView;
42 
43 import com.android.internal.telephony.CommandsInterface;
44 
45 public class EditPhoneNumberPreference extends EditTextPreference {
46 
47     //allowed modes for this preference.
48     /** simple confirmation (OK / CANCEL) */
49     private static final int CM_CONFIRM = 0;
50     /** toggle [(ENABLE / CANCEL) or (DISABLE / CANCEL)], use isToggled() to see requested state.*/
51     private static final int CM_ACTIVATION = 1;
52 
53     private int mConfirmationMode;
54 
55     //String constants used in storing the value of the preference
56     // The preference is backed by a string that holds the encoded value, which reads:
57     //  <VALUE_ON | VALUE_OFF><VALUE_SEPARATOR><mPhoneNumber>
58     // for example, an enabled preference with a number of 6502345678 would read:
59     //  "1:6502345678"
60     private static final String VALUE_SEPARATOR = ":";
61     private static final String VALUE_OFF = "0";
62     private static final String VALUE_ON = "1";
63 
64     //UI layout
65     private ImageButton mContactPickButton;
66 
67     //Listeners
68     /** Called when focus is changed between fields */
69     private View.OnFocusChangeListener mDialogFocusChangeListener;
70     /** Called when the Dialog is closed. */
71     private OnDialogClosedListener mDialogOnClosedListener;
72     /**
73      * Used to indicate that we are going to request for a
74      * default number. for the dialog.
75      */
76     private GetDefaultNumberListener mGetDefaultNumberListener;
77 
78     //Activity values
79     private Activity mParentActivity;
80     private Intent mContactListIntent;
81     /** Arbitrary activity-assigned preference id value */
82     private int mPrefId;
83 
84     //similar to toggle preference
85     private CharSequence mEnableText;
86     private CharSequence mDisableText;
87     private CharSequence mChangeNumberText;
88     private CharSequence mSummaryOn;
89     private CharSequence mSummaryOff;
90 
91     // button that was clicked on dialog close.
92     private int mButtonClicked;
93 
94     //relevant (parsed) value of the mText
95     private String mPhoneNumber;
96     private boolean mChecked;
97 
98     private boolean mIsUnknownStatus;
99 
100     /**
101      * Interface for the dialog closed listener, related to
102      * DialogPreference.onDialogClosed(), except we also pass in a buttonClicked
103      * value indicating which of the three possible buttons were pressed.
104      */
105     public interface OnDialogClosedListener {
onDialogClosed(EditPhoneNumberPreference preference, int buttonClicked)106         void onDialogClosed(EditPhoneNumberPreference preference, int buttonClicked);
107     }
108 
109     /**
110      * Interface for the default number setting listener.  Handles requests for
111      * the default display number for the dialog.
112      */
113     public interface GetDefaultNumberListener {
114         /**
115          * Notify that we are looking for a default display value.
116          * @return null if there is no contribution from this interface,
117          *  indicating that the orignal value of mPhoneNumber should be
118          *  displayed unchanged.
119          */
onGetDefaultNumber(EditPhoneNumberPreference preference)120         String onGetDefaultNumber(EditPhoneNumberPreference preference);
121     }
122 
123     /*
124      * Constructors
125      */
EditPhoneNumberPreference(Context context, AttributeSet attrs)126     public EditPhoneNumberPreference(Context context, AttributeSet attrs) {
127         super(context, attrs);
128 
129         setDialogLayoutResource(R.layout.pref_dialog_editphonenumber);
130 
131         //create intent to bring up contact list
132         mContactListIntent = new Intent(Intent.ACTION_PICK);
133         mContactListIntent.setType(Phone.CONTENT_TYPE);
134 
135         //get the edit phone number default settings
136         TypedArray a = context.obtainStyledAttributes(attrs,
137                 R.styleable.EditPhoneNumberPreference, 0, R.style.EditPhoneNumberPreference);
138         mEnableText = a.getString(R.styleable.EditPhoneNumberPreference_enableButtonText);
139         mDisableText = a.getString(R.styleable.EditPhoneNumberPreference_disableButtonText);
140         mChangeNumberText = a.getString(R.styleable.EditPhoneNumberPreference_changeNumButtonText);
141         mConfirmationMode = a.getInt(R.styleable.EditPhoneNumberPreference_confirmMode, 0);
142         a.recycle();
143 
144         //get the summary settings, use CheckBoxPreference as the standard.
145         a = context.obtainStyledAttributes(attrs, android.R.styleable.CheckBoxPreference, 0, 0);
146         mSummaryOn = a.getString(android.R.styleable.CheckBoxPreference_summaryOn);
147         mSummaryOff = a.getString(android.R.styleable.CheckBoxPreference_summaryOff);
148         a.recycle();
149     }
150 
EditPhoneNumberPreference(Context context)151     public EditPhoneNumberPreference(Context context) {
152         this(context, null);
153     }
154 
155 
156     /*
157      * Methods called on UI bindings
158      */
159     @Override
160     //called when we're binding the view to the preference.
onBindView(View view)161     protected void onBindView(View view) {
162         super.onBindView(view);
163 
164         // Sync the summary view
165         TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
166         if (summaryView != null) {
167             CharSequence sum;
168             int vis;
169 
170             //set summary depending upon mode
171             if (mConfirmationMode == CM_ACTIVATION) {
172                 if (mChecked) {
173                     sum = (mSummaryOn == null) ? getSummary() : mSummaryOn;
174                 } else {
175                     sum = (mSummaryOff == null) ? getSummary() : mSummaryOff;
176                 }
177             } else {
178                 sum = getSummary();
179             }
180 
181             if (sum != null) {
182                 summaryView.setText(sum);
183                 vis = View.VISIBLE;
184             } else {
185                 vis = View.GONE;
186             }
187 
188             if (vis != summaryView.getVisibility()) {
189                 summaryView.setVisibility(vis);
190             }
191         }
192     }
193 
194     //called when we're binding the dialog to the preference's view.
195     @Override
onBindDialogView(View view)196     protected void onBindDialogView(View view) {
197         // default the button clicked to be the cancel button.
198         mButtonClicked = DialogInterface.BUTTON_NEGATIVE;
199 
200         super.onBindDialogView(view);
201 
202         //get the edittext component within the number field
203         EditText editText = getEditText();
204         //get the contact pick button within the number field
205         mContactPickButton = (ImageButton) view.findViewById(R.id.select_contact);
206 
207         //setup number entry
208         if (editText != null) {
209             // see if there is a means to get a default number,
210             // and set it accordingly.
211             if (mGetDefaultNumberListener != null) {
212                 String defaultNumber = mGetDefaultNumberListener.onGetDefaultNumber(this);
213                 if (defaultNumber != null) {
214                     mPhoneNumber = defaultNumber;
215                 }
216             }
217             editText.setText(BidiFormatter.getInstance().unicodeWrap(
218                     mPhoneNumber, TextDirectionHeuristics.LOCALE));
219             editText.setTextDirection(TEXT_DIRECTION_LOCALE);
220             editText.setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
221             editText.setMovementMethod(ArrowKeyMovementMethod.getInstance());
222             editText.setKeyListener(DialerKeyListener.getInstance());
223             editText.setOnFocusChangeListener(mDialogFocusChangeListener);
224         }
225 
226         //set contact picker
227         if (mContactPickButton != null) {
228             mContactPickButton.setOnClickListener(new View.OnClickListener() {
229                 public void onClick(View v) {
230                     if (mParentActivity != null) {
231                         mParentActivity.startActivityForResult(mContactListIntent, mPrefId);
232                     }
233                 }
234             });
235         }
236     }
237 
238     /**
239      * Overriding EditTextPreference's onAddEditTextToDialogView.
240      *
241      * This method attaches the EditText to the container specific to this
242      * preference's dialog layout.
243      */
244     @Override
onAddEditTextToDialogView(View dialogView, EditText editText)245     protected void onAddEditTextToDialogView(View dialogView, EditText editText) {
246 
247         // look for the container object
248         ViewGroup container = (ViewGroup) dialogView
249                 .findViewById(R.id.edit_container);
250 
251         // add the edittext to the container.
252         if (container != null) {
253             container.addView(editText, ViewGroup.LayoutParams.MATCH_PARENT,
254                     ViewGroup.LayoutParams.WRAP_CONTENT);
255         }
256     }
257 
258     //control the appearance of the dialog depending upon the mode.
259     @Override
onPrepareDialogBuilder(AlertDialog.Builder builder)260     protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
261         // modified so that we just worry about the buttons being
262         // displayed, since there is no need to hide the edittext
263         // field anymore.
264         if (mConfirmationMode == CM_ACTIVATION) {
265             if (mIsUnknownStatus) {
266                 builder.setPositiveButton(mEnableText, this);
267                 builder.setNeutralButton(mDisableText, this);
268                 if (mPrefId == CommandsInterface.CF_REASON_ALL) {
269                     builder.setPositiveButton(null, null);
270                 }
271             } else if (mChecked) {
272                 builder.setPositiveButton(mChangeNumberText, this);
273                 builder.setNeutralButton(mDisableText, this);
274             } else {
275                 builder.setPositiveButton(mEnableText, this);
276                 builder.setNeutralButton(null, null);
277             }
278         }
279         // set the call icon on the title.
280         builder.setIcon(R.mipmap.ic_launcher_phone);
281     }
282 
283 
284     /*
285      * Listeners and other state setting methods
286      */
287     //set the on focus change listener to be assigned to the Dialog's edittext field.
setDialogOnFocusChangeListener(View.OnFocusChangeListener l)288     public void setDialogOnFocusChangeListener(View.OnFocusChangeListener l) {
289         mDialogFocusChangeListener = l;
290     }
291 
292     //set the listener to be called wht the dialog is closed.
setDialogOnClosedListener(OnDialogClosedListener l)293     public void setDialogOnClosedListener(OnDialogClosedListener l) {
294         mDialogOnClosedListener = l;
295     }
296 
297     //set the link back to the parent activity, so that we may run the contact picker.
setParentActivity(Activity parent, int identifier)298     public void setParentActivity(Activity parent, int identifier) {
299         mParentActivity = parent;
300         mPrefId = identifier;
301         mGetDefaultNumberListener = null;
302     }
303 
304     //set the link back to the parent activity, so that we may run the contact picker.
305     //also set the default number listener.
setParentActivity(Activity parent, int identifier, GetDefaultNumberListener l)306     public void setParentActivity(Activity parent, int identifier, GetDefaultNumberListener l) {
307         mParentActivity = parent;
308         mPrefId = identifier;
309         mGetDefaultNumberListener = l;
310     }
311 
312     /*
313      * Notification handlers
314      */
315     //Notify the preference that the pick activity is complete.
onPickActivityResult(String pickedValue)316     public void onPickActivityResult(String pickedValue) {
317         EditText editText = getEditText();
318         if (editText != null) {
319             editText.setText(pickedValue);
320         }
321     }
322 
323     //called when the dialog is clicked.
324     @Override
onClick(DialogInterface dialog, int which)325     public void onClick(DialogInterface dialog, int which) {
326         // The neutral button (button3) is always the toggle.
327         if ((mConfirmationMode == CM_ACTIVATION) && (which == DialogInterface.BUTTON_NEUTRAL)
328              && !mIsUnknownStatus) {
329             //flip the toggle if we are in the correct mode.
330             setToggled(!isToggled());
331         }
332         // record the button that was clicked.
333         mButtonClicked = which;
334         super.onClick(dialog, which);
335     }
336 
337     @Override
338     //When the dialog is closed, perform the relevant actions, including setting
339     // phone numbers and calling the close action listener.
onDialogClosed(boolean positiveResult)340     protected void onDialogClosed(boolean positiveResult) {
341         // A positive result is technically either button1 or button3.
342         if ((mButtonClicked == DialogInterface.BUTTON_POSITIVE) ||
343                 (mButtonClicked == DialogInterface.BUTTON_NEUTRAL)){
344             setPhoneNumber(getEditText().getText().toString());
345             super.onDialogClosed(positiveResult);
346             setText(getStringValue());
347         } else {
348             super.onDialogClosed(positiveResult);
349         }
350 
351         // send the clicked button over to the listener.
352         if (mDialogOnClosedListener != null) {
353             mDialogOnClosedListener.onDialogClosed(this, mButtonClicked);
354         }
355     }
356 
357 
358     /*
359      * Toggle handling code.
360      */
361     //return the toggle value.
isToggled()362     public boolean isToggled() {
363         return mChecked;
364     }
365 
366     //set the toggle value.
367     // return the current preference to allow for chaining preferences.
setToggled(boolean checked)368     public EditPhoneNumberPreference setToggled(boolean checked) {
369         mChecked = checked;
370         setText(getStringValue());
371         notifyChanged();
372 
373         return this;
374     }
375 
376 
377     /**
378      * Phone number handling code
379      */
getPhoneNumber()380     public String getPhoneNumber() {
381         // return the phone number, after it has been stripped of all
382         // irrelevant text.
383         return PhoneNumberUtils.stripSeparators(mPhoneNumber);
384     }
385 
386     /** The phone number including any formatting characters */
getRawPhoneNumber()387     protected String getRawPhoneNumber() {
388         return mPhoneNumber;
389     }
390 
391     //set the phone number value.
392     // return the current preference to allow for chaining preferences.
setPhoneNumber(String number)393     public EditPhoneNumberPreference setPhoneNumber(String number) {
394         mPhoneNumber = number;
395         setText(getStringValue());
396         notifyChanged();
397 
398         return this;
399     }
400 
401 
402     /*
403      * Other code relevant to preference framework
404      */
405     //when setting default / initial values, make sure we're setting things correctly.
406     @Override
onSetInitialValue(boolean restoreValue, Object defaultValue)407     protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
408         setValueFromString(restoreValue ? getPersistedString(getStringValue())
409                 : (String) defaultValue);
410     }
411 
412     /**
413      * Decides how to disable dependents.
414      */
415     @Override
shouldDisableDependents()416     public boolean shouldDisableDependents() {
417         // There is really only one case we care about, but for consistency
418         // we fill out the dependency tree for all of the cases.  If this
419         // is in activation mode (CF), we look for the encoded toggle value
420         // in the string.  If this in confirm mode (VM), then we just
421         // examine the number field.
422         // Note: The toggle value is stored in the string in an encoded
423         // manner (refer to setValueFromString and getStringValue below).
424         boolean shouldDisable = false;
425         if ((mConfirmationMode == CM_ACTIVATION) && (mEncodedText != null)) {
426             String[] inValues = mEncodedText.split(":", 2);
427             shouldDisable = inValues[0].equals(VALUE_ON);
428         } else {
429             shouldDisable = (TextUtils.isEmpty(mPhoneNumber) && (mConfirmationMode == CM_CONFIRM));
430         }
431         return shouldDisable;
432     }
433 
434     /**
435      * Override persistString so that we can get a hold of the EditTextPreference's
436      * text field.
437      */
438     private String mEncodedText = null;
439     @Override
persistString(String value)440     protected boolean persistString(String value) {
441         mEncodedText = value;
442         return super.persistString(value);
443     }
444 
445 
446     /*
447      * Summary On handling code
448      */
449     //set the Summary for the on state (relevant only in CM_ACTIVATION mode)
setSummaryOn(CharSequence summary)450     public EditPhoneNumberPreference setSummaryOn(CharSequence summary) {
451         mSummaryOn = summary;
452         if (isToggled()) {
453             notifyChanged();
454         }
455         return this;
456     }
457 
458     //set the Summary for the on state, given a string resource id
459     // (relevant only in CM_ACTIVATION mode)
setSummaryOn(int summaryResId)460     public EditPhoneNumberPreference setSummaryOn(int summaryResId) {
461         return setSummaryOn(getContext().getString(summaryResId));
462     }
463 
464     //get the summary string for the on state
getSummaryOn()465     public CharSequence getSummaryOn() {
466         return mSummaryOn;
467     }
468 
469 
470     /*
471      * Summary Off handling code
472      */
473     //set the Summary for the off state (relevant only in CM_ACTIVATION mode)
setSummaryOff(CharSequence summary)474     public EditPhoneNumberPreference setSummaryOff(CharSequence summary) {
475         mSummaryOff = summary;
476         if (!isToggled()) {
477             notifyChanged();
478         }
479         return this;
480     }
481 
482     //set the Summary for the off state, given a string resource id
483     // (relevant only in CM_ACTIVATION mode)
setSummaryOff(int summaryResId)484     public EditPhoneNumberPreference setSummaryOff(int summaryResId) {
485         return setSummaryOff(getContext().getString(summaryResId));
486     }
487 
488     //get the summary string for the off state
getSummaryOff()489     public CharSequence getSummaryOff() {
490         return mSummaryOff;
491     }
492 
493 
494     /*
495      * Methods to get and set from encoded strings.
496      */
497     //set the values given an encoded string.
setValueFromString(String value)498     protected void setValueFromString(String value) {
499         String[] inValues = value.split(":", 2);
500         setToggled(inValues[0].equals(VALUE_ON));
501         setPhoneNumber(inValues[1]);
502     }
503 
504     //retrieve the state of this preference in the form of an encoded string
getStringValue()505     protected String getStringValue() {
506         return ((isToggled() ? VALUE_ON : VALUE_OFF) + VALUE_SEPARATOR + getPhoneNumber());
507     }
508 
509     /**
510      * Externally visible method to bring up the dialog.
511      *
512      * Generally used when we are navigating the user to this preference.
513      */
showPhoneNumberDialog()514     public void showPhoneNumberDialog() {
515         showDialog(null);
516     }
517 
setUnknownStatus(boolean isUnknown)518     public void setUnknownStatus(boolean isUnknown) {
519         mIsUnknownStatus = isUnknown;
520     }
521 
isUnknownStatus()522     public boolean isUnknownStatus() {
523         return mIsUnknownStatus;
524     }
525 }
526