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