1 /*
2  * Copyright (C) 2007-2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package android.view.inputmethod;
18 
19 import android.annotation.NonNull;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.os.BadParcelableException;
22 import android.os.Parcel;
23 import android.util.Printer;
24 import android.util.Slog;
25 
26 import java.io.ByteArrayInputStream;
27 import java.io.ByteArrayOutputStream;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.zip.GZIPInputStream;
31 import java.util.zip.GZIPOutputStream;
32 
33 /**
34  * An array-like container that stores multiple instances of {@link InputMethodSubtype}.
35  *
36  * <p>This container is designed to reduce the risk of {@link TransactionTooLargeException}
37  * when one or more instancess of {@link InputMethodInfo} are transferred through IPC.
38  * Basically this class does following three tasks.</p>
39  * <ul>
40  * <li>Applying compression for the marshalled data</li>
41  * <li>Lazily unmarshalling objects</li>
42  * <li>Caching the marshalled data when appropriate</li>
43  * </ul>
44  *
45  * @hide
46  */
47 public class InputMethodSubtypeArray {
48     private final static String TAG = "InputMethodSubtypeArray";
49 
50     /**
51      * Create a new instance of {@link InputMethodSubtypeArray} from an existing list of
52      * {@link InputMethodSubtype}.
53      *
54      * @param subtypes A list of {@link InputMethodSubtype} from which
55      * {@link InputMethodSubtypeArray} will be created.
56      */
57     @UnsupportedAppUsage
InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes)58     public InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes) {
59         if (subtypes == null) {
60             mCount = 0;
61             return;
62         }
63         mCount = subtypes.size();
64         mInstance = subtypes.toArray(new InputMethodSubtype[mCount]);
65     }
66 
67     /**
68      * Unmarshall an instance of {@link InputMethodSubtypeArray} from a given {@link Parcel}
69      * object.
70      *
71      * @param source A {@link Parcel} object from which {@link InputMethodSubtypeArray} will be
72      * unmarshalled.
73      */
InputMethodSubtypeArray(final Parcel source)74     public InputMethodSubtypeArray(final Parcel source) {
75         mCount = source.readInt();
76         if (mCount < 0) {
77             throw new BadParcelableException("mCount must be non-negative.");
78         }
79         if (mCount > 0) {
80             mDecompressedSize = source.readInt();
81             mCompressedData = source.createByteArray();
82         }
83     }
84 
85     /**
86      * Marshall the instance into a given {@link Parcel} object.
87      *
88      * <p>This methods may take a bit additional time to compress data lazily when called
89      * first time.</p>
90      *
91      * @param source A {@link Parcel} object to which {@link InputMethodSubtypeArray} will be
92      * marshalled.
93      */
writeToParcel(final Parcel dest)94     public void writeToParcel(final Parcel dest) {
95         if (mCount == 0) {
96             dest.writeInt(mCount);
97             return;
98         }
99 
100         byte[] compressedData = mCompressedData;
101         int decompressedSize = mDecompressedSize;
102         if (compressedData == null && decompressedSize == 0) {
103             synchronized (mLockObject) {
104                 compressedData = mCompressedData;
105                 decompressedSize = mDecompressedSize;
106                 if (compressedData == null && decompressedSize == 0) {
107                     final byte[] decompressedData = marshall(mInstance);
108                     compressedData = compress(decompressedData);
109                     if (compressedData == null) {
110                         decompressedSize = -1;
111                         Slog.i(TAG, "Failed to compress data.");
112                     } else {
113                         decompressedSize = decompressedData.length;
114                     }
115                     mDecompressedSize = decompressedSize;
116                     mCompressedData = compressedData;
117                 }
118             }
119         }
120 
121         if (compressedData != null && decompressedSize > 0) {
122             dest.writeInt(mCount);
123             dest.writeInt(decompressedSize);
124             dest.writeByteArray(compressedData);
125         } else {
126             Slog.i(TAG, "Unexpected state. Behaving as an empty array.");
127             dest.writeInt(0);
128         }
129     }
130 
131     /**
132      * Return {@link InputMethodSubtype} specified with the given index.
133      *
134      * <p>This methods may take a bit additional time to decompress data lazily when called
135      * first time.</p>
136      *
137      * @param index The index of {@link InputMethodSubtype}.
138      */
get(final int index)139     public InputMethodSubtype get(final int index) {
140         if (index < 0 || mCount <= index) {
141             throw new ArrayIndexOutOfBoundsException();
142         }
143         InputMethodSubtype[] instance = mInstance;
144         if (instance == null) {
145             synchronized (mLockObject) {
146                 instance = mInstance;
147                 if (instance == null) {
148                     final byte[] decompressedData =
149                           decompress(mCompressedData, mDecompressedSize);
150                     // Clear the compressed data until {@link #getMarshalled()} is called.
151                     mCompressedData = null;
152                     mDecompressedSize = 0;
153                     if (decompressedData != null) {
154                         instance = unmarshall(decompressedData);
155                     } else {
156                         Slog.e(TAG, "Failed to decompress data. Returns null as fallback.");
157                         instance = new InputMethodSubtype[mCount];
158                     }
159                     mInstance = instance;
160                 }
161             }
162         }
163         return instance[index];
164     }
165 
166     /**
167      * @return A list of {@link InputMethodInfo} copied from this array.
168      */
169     @NonNull
toList()170     public ArrayList<InputMethodSubtype> toList() {
171         final ArrayList<InputMethodSubtype> list = new ArrayList<>(mCount);
172         for (int i = 0; i < mCount; ++i) {
173             list.add(get(i));
174         }
175         return list;
176     }
177 
178     /**
179      * Return the number of {@link InputMethodSubtype} objects.
180      */
getCount()181     public int getCount() {
182         return mCount;
183     }
184 
185     private final Object mLockObject = new Object();
186     private final int mCount;
187 
188     private volatile InputMethodSubtype[] mInstance;
189     private volatile byte[] mCompressedData;
190     private volatile int mDecompressedSize;
191 
dump(@onNull Printer pw, @NonNull String prefix)192     void dump(@NonNull Printer pw, @NonNull String prefix) {
193         final var innerPrefix = prefix + "  ";
194         for (int i = 0; i < mCount; i++) {
195             pw.println(prefix + "InputMethodSubtype #" + i + ":");
196             final var subtype = get(i);
197             if (subtype != null) {
198                 subtype.dump(pw, innerPrefix);
199             } else {
200                 pw.println(innerPrefix + "missing subtype");
201             }
202         }
203     }
204 
marshall(final InputMethodSubtype[] array)205     private static byte[] marshall(final InputMethodSubtype[] array) {
206         Parcel parcel = null;
207         try {
208             parcel = Parcel.obtain();
209             parcel.writeTypedArray(array, 0);
210             return parcel.marshall();
211         } finally {
212             if (parcel != null) {
213                 parcel.recycle();
214                 parcel = null;
215             }
216         }
217     }
218 
unmarshall(final byte[] data)219     private static InputMethodSubtype[] unmarshall(final byte[] data) {
220         Parcel parcel = null;
221         try {
222             parcel = Parcel.obtain();
223             parcel.unmarshall(data, 0, data.length);
224             parcel.setDataPosition(0);
225             return parcel.createTypedArray(InputMethodSubtype.CREATOR);
226         } finally {
227             if (parcel != null) {
228                 parcel.recycle();
229                 parcel = null;
230             }
231         }
232     }
233 
compress(final byte[] data)234     private static byte[] compress(final byte[] data) {
235         try (final ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
236                 final GZIPOutputStream zipper = new GZIPOutputStream(resultStream)) {
237             zipper.write(data);
238             zipper.finish();
239             return resultStream.toByteArray();
240         } catch(Exception e) {
241             Slog.e(TAG, "Failed to compress the data.", e);
242             return null;
243         }
244     }
245 
decompress(final byte[] data, final int expectedSize)246     private static byte[] decompress(final byte[] data, final int expectedSize) {
247         try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
248                 final GZIPInputStream unzipper = new GZIPInputStream(inputStream)) {
249             final byte [] result = new byte[expectedSize];
250             int totalReadBytes = 0;
251             while (totalReadBytes < result.length) {
252                 final int restBytes = result.length - totalReadBytes;
253                 final int readBytes = unzipper.read(result, totalReadBytes, restBytes);
254                 if (readBytes < 0) {
255                     break;
256                 }
257                 totalReadBytes += readBytes;
258             }
259             if (expectedSize != totalReadBytes) {
260                 return null;
261             }
262             return result;
263         } catch(Exception e) {
264             Slog.e(TAG, "Failed to decompress the data.", e);
265             return null;
266         }
267     }
268 }
269