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