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