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