1 /*
2  * Copyright (C) 2011 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 package android.text.method;
17 
18 import android.content.Context;
19 import android.graphics.Rect;
20 import android.icu.text.CaseMap;
21 import android.icu.text.Edits;
22 import android.text.SpannableStringBuilder;
23 import android.text.Spanned;
24 import android.util.Log;
25 import android.view.View;
26 import android.widget.TextView;
27 
28 import java.util.Locale;
29 
30 /**
31  * Transforms source text into an ALL CAPS string, locale-aware.
32  *
33  * @hide
34  */
35 public class AllCapsTransformationMethod implements TransformationMethod2 {
36     private static final String TAG = "AllCapsTransformationMethod";
37 
38     private boolean mEnabled;
39     private Locale mLocale;
40 
AllCapsTransformationMethod(Context context)41     public AllCapsTransformationMethod(Context context) {
42         mLocale = context.getResources().getConfiguration().getLocales().get(0);
43     }
44 
45     @Override
getTransformation(CharSequence source, View view)46     public CharSequence getTransformation(CharSequence source, View view) {
47         if (!mEnabled) {
48             Log.w(TAG, "Caller did not enable length changes; not transforming text");
49             return source;
50         }
51 
52         if (source == null) {
53             return null;
54         }
55 
56         Locale locale = null;
57         if (view instanceof TextView) {
58             locale = ((TextView)view).getTextLocale();
59         }
60         if (locale == null) {
61             locale = mLocale;
62         }
63 
64         if (!(source instanceof Spanned)) { // No spans
65             return CaseMap.toUpper().apply(
66                     locale, source, new StringBuilder(),
67                     null /* we don't need the edits */);
68         }
69 
70         final Edits edits = new Edits();
71         final SpannableStringBuilder result = CaseMap.toUpper().apply(
72                 locale, source, new SpannableStringBuilder(), edits);
73         if (!edits.hasChanges()) {
74             // No changes happened while capitalizing. We can return the source as it was.
75             return source;
76         }
77 
78         final Edits.Iterator iterator = edits.getFineIterator();
79         final Spanned spanned = (Spanned) source;
80         final int sourceLength = source.length();
81         final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
82         for (Object span : spans) {
83             final int sourceStart = spanned.getSpanStart(span);
84             final int sourceEnd = spanned.getSpanEnd(span);
85             final int flags = spanned.getSpanFlags(span);
86             // Make sure the indexes are not at the end of the string, since in that case
87             // iterator.findSourceIndex() would fail.
88             final int destStart = sourceStart == sourceLength ? result.length() :
89                     mapToDest(iterator, sourceStart);
90             final int destEnd = sourceEnd == sourceLength ? result.length() :
91                     mapToDest(iterator, sourceEnd);
92             result.setSpan(span, destStart, destEnd, flags);
93         }
94         return result;
95     }
96 
mapToDest(Edits.Iterator iterator, int sourceIndex)97     private static int mapToDest(Edits.Iterator iterator, int sourceIndex) {
98         // Guaranteed to succeed if sourceIndex < source.length().
99         iterator.findSourceIndex(sourceIndex);
100         if (sourceIndex == iterator.sourceIndex()) {
101             return iterator.destinationIndex();
102         }
103         // We handle the situation differently depending on if we are in the changed slice or an
104         // unchanged one: In an unchanged slice, we can find the exact location the span
105         // boundary was before and map there.
106         //
107         // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
108         // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
109         // spans in the source overlapping in the result. (The choice for the end vs the beginning
110         // is somewhat arbitrary, but was taken because we except to see slightly more spans only
111         // affecting a base character compared to spans only affecting a combining character.)
112         if (iterator.hasChange()) {
113             return iterator.destinationIndex() + iterator.newLength();
114         } else {
115             // Move the index 1:1 along with this unchanged piece of text.
116             return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
117         }
118     }
119 
120     @Override
onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect)121     public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction,
122             Rect previouslyFocusedRect) {
123     }
124 
125     @Override
setLengthChangesAllowed(boolean allowLengthChanges)126     public void setLengthChangesAllowed(boolean allowLengthChanges) {
127         mEnabled = allowLengthChanges;
128     }
129 
130 }
131