1/*
2 * Copyright (C) 2006 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 android.text.method;
18
19import android.os.Handler;
20import android.os.SystemClock;
21import android.graphics.Rect;
22import android.view.View;
23import android.text.Editable;
24import android.text.GetChars;
25import android.text.NoCopySpan;
26import android.text.TextUtils;
27import android.text.TextWatcher;
28import android.text.Selection;
29import android.text.Spanned;
30import android.text.Spannable;
31import android.text.style.UpdateLayout;
32
33import java.lang.ref.WeakReference;
34
35public class PasswordTransformationMethod
36implements TransformationMethod, TextWatcher
37{
38    public CharSequence getTransformation(CharSequence source, View view) {
39        if (source instanceof Spannable) {
40            Spannable sp = (Spannable) source;
41
42            /*
43             * Remove any references to other views that may still be
44             * attached.  This will happen when you flip the screen
45             * while a password field is showing; there will still
46             * be references to the old EditText in the text.
47             */
48            ViewReference[] vr = sp.getSpans(0, sp.length(),
49                                             ViewReference.class);
50            for (int i = 0; i < vr.length; i++) {
51                sp.removeSpan(vr[i]);
52            }
53
54            removeVisibleSpans(sp);
55
56            sp.setSpan(new ViewReference(view), 0, 0,
57                       Spannable.SPAN_POINT_POINT);
58        }
59
60        return new PasswordCharSequence(source);
61    }
62
63    public static PasswordTransformationMethod getInstance() {
64        if (sInstance != null)
65            return sInstance;
66
67        sInstance = new PasswordTransformationMethod();
68        return sInstance;
69    }
70
71    public void beforeTextChanged(CharSequence s, int start,
72                                  int count, int after) {
73        // This callback isn't used.
74    }
75
76    public void onTextChanged(CharSequence s, int start,
77                              int before, int count) {
78        if (s instanceof Spannable) {
79            Spannable sp = (Spannable) s;
80            ViewReference[] vr = sp.getSpans(0, s.length(),
81                                             ViewReference.class);
82            if (vr.length == 0) {
83                return;
84            }
85
86            /*
87             * There should generally only be one ViewReference in the text,
88             * but make sure to look through all of them if necessary in case
89             * something strange is going on.  (We might still end up with
90             * multiple ViewReferences if someone moves text from one password
91             * field to another.)
92             */
93            View v = null;
94            for (int i = 0; v == null && i < vr.length; i++) {
95                v = vr[i].get();
96            }
97
98            if (v == null) {
99                return;
100            }
101
102            int pref = TextKeyListener.getInstance().getPrefs(v.getContext());
103            if ((pref & TextKeyListener.SHOW_PASSWORD) != 0) {
104                if (count > 0) {
105                    removeVisibleSpans(sp);
106
107                    if (count == 1) {
108                        sp.setSpan(new Visible(sp, this), start, start + count,
109                                   Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
110                    }
111                }
112            }
113        }
114    }
115
116    public void afterTextChanged(Editable s) {
117        // This callback isn't used.
118    }
119
120    public void onFocusChanged(View view, CharSequence sourceText,
121                               boolean focused, int direction,
122                               Rect previouslyFocusedRect) {
123        if (!focused) {
124            if (sourceText instanceof Spannable) {
125                Spannable sp = (Spannable) sourceText;
126
127                removeVisibleSpans(sp);
128            }
129        }
130    }
131
132    private static void removeVisibleSpans(Spannable sp) {
133        Visible[] old = sp.getSpans(0, sp.length(), Visible.class);
134        for (int i = 0; i < old.length; i++) {
135            sp.removeSpan(old[i]);
136        }
137    }
138
139    private static class PasswordCharSequence
140    implements CharSequence, GetChars
141    {
142        public PasswordCharSequence(CharSequence source) {
143            mSource = source;
144        }
145
146        public int length() {
147            return mSource.length();
148        }
149
150        public char charAt(int i) {
151            if (mSource instanceof Spanned) {
152                Spanned sp = (Spanned) mSource;
153
154                int st = sp.getSpanStart(TextKeyListener.ACTIVE);
155                int en = sp.getSpanEnd(TextKeyListener.ACTIVE);
156
157                if (i >= st && i < en) {
158                    return mSource.charAt(i);
159                }
160
161                Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
162
163                for (int a = 0; a < visible.length; a++) {
164                    if (sp.getSpanStart(visible[a].mTransformer) >= 0) {
165                        st = sp.getSpanStart(visible[a]);
166                        en = sp.getSpanEnd(visible[a]);
167
168                        if (i >= st && i < en) {
169                            return mSource.charAt(i);
170                        }
171                    }
172                }
173            }
174
175            return DOT;
176        }
177
178        public CharSequence subSequence(int start, int end) {
179            char[] buf = new char[end - start];
180
181            getChars(start, end, buf, 0);
182            return new String(buf);
183        }
184
185        public String toString() {
186            return subSequence(0, length()).toString();
187        }
188
189        public void getChars(int start, int end, char[] dest, int off) {
190            TextUtils.getChars(mSource, start, end, dest, off);
191
192            int st = -1, en = -1;
193            int nvisible = 0;
194            int[] starts = null, ends = null;
195
196            if (mSource instanceof Spanned) {
197                Spanned sp = (Spanned) mSource;
198
199                st = sp.getSpanStart(TextKeyListener.ACTIVE);
200                en = sp.getSpanEnd(TextKeyListener.ACTIVE);
201
202                Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
203                nvisible = visible.length;
204                starts = new int[nvisible];
205                ends = new int[nvisible];
206
207                for (int i = 0; i < nvisible; i++) {
208                    if (sp.getSpanStart(visible[i].mTransformer) >= 0) {
209                        starts[i] = sp.getSpanStart(visible[i]);
210                        ends[i] = sp.getSpanEnd(visible[i]);
211                    }
212                }
213            }
214
215            for (int i = start; i < end; i++) {
216                if (! (i >= st && i < en)) {
217                    boolean visible = false;
218
219                    for (int a = 0; a < nvisible; a++) {
220                        if (i >= starts[a] && i < ends[a]) {
221                            visible = true;
222                            break;
223                        }
224                    }
225
226                    if (!visible) {
227                        dest[i - start + off] = DOT;
228                    }
229                }
230            }
231        }
232
233        private CharSequence mSource;
234    }
235
236    private static class Visible
237    extends Handler
238    implements UpdateLayout, Runnable
239    {
240        public Visible(Spannable sp, PasswordTransformationMethod ptm) {
241            mText = sp;
242            mTransformer = ptm;
243            postAtTime(this, SystemClock.uptimeMillis() + 1500);
244        }
245
246        public void run() {
247            mText.removeSpan(this);
248        }
249
250        private Spannable mText;
251        private PasswordTransformationMethod mTransformer;
252    }
253
254    /**
255     * Used to stash a reference back to the View in the Editable so we
256     * can use it to check the settings.
257     */
258    private static class ViewReference extends WeakReference<View>
259            implements NoCopySpan {
260        public ViewReference(View v) {
261            super(v);
262        }
263    }
264
265    private static PasswordTransformationMethod sInstance;
266    private static char DOT = '\u2022';
267}
268