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