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
17package android.view.inputmethod;
18
19import android.os.Parcel;
20import android.os.Parcelable;
21import android.util.AndroidRuntimeException;
22import android.util.Slog;
23
24import java.io.ByteArrayInputStream;
25import java.io.ByteArrayOutputStream;
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.OutputStream;
29import java.util.List;
30import java.util.zip.GZIPInputStream;
31import 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 */
47public 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    public InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes) {
58        if (subtypes == null) {
59            mCount = 0;
60            return;
61        }
62        mCount = subtypes.size();
63        mInstance = subtypes.toArray(new InputMethodSubtype[mCount]);
64    }
65
66    /**
67     * Unmarshall an instance of {@link InputMethodSubtypeArray} from a given {@link Parcel}
68     * object.
69     *
70     * @param source A {@link Parcel} object from which {@link InputMethodSubtypeArray} will be
71     * unmarshalled.
72     */
73    public InputMethodSubtypeArray(final Parcel source) {
74        mCount = source.readInt();
75        if (mCount > 0) {
76            mDecompressedSize = source.readInt();
77            mCompressedData = source.createByteArray();
78        }
79    }
80
81    /**
82     * Marshall the instance into a given {@link Parcel} object.
83     *
84     * <p>This methods may take a bit additional time to compress data lazily when called
85     * first time.</p>
86     *
87     * @param source A {@link Parcel} object to which {@link InputMethodSubtypeArray} will be
88     * marshalled.
89     */
90    public void writeToParcel(final Parcel dest) {
91        if (mCount == 0) {
92            dest.writeInt(mCount);
93            return;
94        }
95
96        byte[] compressedData = mCompressedData;
97        int decompressedSize = mDecompressedSize;
98        if (compressedData == null && decompressedSize == 0) {
99            synchronized (mLockObject) {
100                compressedData = mCompressedData;
101                decompressedSize = mDecompressedSize;
102                if (compressedData == null && decompressedSize == 0) {
103                    final byte[] decompressedData = marshall(mInstance);
104                    compressedData = compress(decompressedData);
105                    if (compressedData == null) {
106                        decompressedSize = -1;
107                        Slog.i(TAG, "Failed to compress data.");
108                    } else {
109                        decompressedSize = decompressedData.length;
110                    }
111                    mDecompressedSize = decompressedSize;
112                    mCompressedData = compressedData;
113                }
114            }
115        }
116
117        if (compressedData != null && decompressedSize > 0) {
118            dest.writeInt(mCount);
119            dest.writeInt(decompressedSize);
120            dest.writeByteArray(compressedData);
121        } else {
122            Slog.i(TAG, "Unexpected state. Behaving as an empty array.");
123            dest.writeInt(0);
124        }
125    }
126
127    /**
128     * Return {@link InputMethodSubtype} specified with the given index.
129     *
130     * <p>This methods may take a bit additional time to decompress data lazily when called
131     * first time.</p>
132     *
133     * @param index The index of {@link InputMethodSubtype}.
134     */
135    public InputMethodSubtype get(final int index) {
136        if (index < 0 || mCount <= index) {
137            throw new ArrayIndexOutOfBoundsException();
138        }
139        InputMethodSubtype[] instance = mInstance;
140        if (instance == null) {
141            synchronized (mLockObject) {
142                instance = mInstance;
143                if (instance == null) {
144                    final byte[] decompressedData =
145                          decompress(mCompressedData, mDecompressedSize);
146                    // Clear the compressed data until {@link #getMarshalled()} is called.
147                    mCompressedData = null;
148                    mDecompressedSize = 0;
149                    if (decompressedData != null) {
150                        instance = unmarshall(decompressedData);
151                    } else {
152                        Slog.e(TAG, "Failed to decompress data. Returns null as fallback.");
153                        instance = new InputMethodSubtype[mCount];
154                    }
155                    mInstance = instance;
156                }
157            }
158        }
159        return instance[index];
160    }
161
162    /**
163     * Return the number of {@link InputMethodSubtype} objects.
164     */
165    public int getCount() {
166        return mCount;
167    }
168
169    private final Object mLockObject = new Object();
170    private final int mCount;
171
172    private volatile InputMethodSubtype[] mInstance;
173    private volatile byte[] mCompressedData;
174    private volatile int mDecompressedSize;
175
176    private static byte[] marshall(final InputMethodSubtype[] array) {
177        Parcel parcel = null;
178        try {
179            parcel = Parcel.obtain();
180            parcel.writeTypedArray(array, 0);
181            return parcel.marshall();
182        } finally {
183            if (parcel != null) {
184                parcel.recycle();
185                parcel = null;
186            }
187        }
188    }
189
190    private static InputMethodSubtype[] unmarshall(final byte[] data) {
191        Parcel parcel = null;
192        try {
193            parcel = Parcel.obtain();
194            parcel.unmarshall(data, 0, data.length);
195            parcel.setDataPosition(0);
196            return parcel.createTypedArray(InputMethodSubtype.CREATOR);
197        } finally {
198            if (parcel != null) {
199                parcel.recycle();
200                parcel = null;
201            }
202        }
203    }
204
205    private static byte[] compress(final byte[] data) {
206        ByteArrayOutputStream resultStream = null;
207        GZIPOutputStream zipper = null;
208        try {
209            resultStream = new ByteArrayOutputStream();
210            zipper = new GZIPOutputStream(resultStream);
211            zipper.write(data);
212        } catch(IOException e) {
213            return null;
214        } finally {
215            try {
216                if (zipper != null) {
217                    zipper.close();
218                }
219            } catch (IOException e) {
220                zipper = null;
221                Slog.e(TAG, "Failed to close the stream.", e);
222                // swallowed, not propagated back to the caller
223            }
224            try {
225                if (resultStream != null) {
226                    resultStream.close();
227                }
228            } catch (IOException e) {
229                resultStream = null;
230                Slog.e(TAG, "Failed to close the stream.", e);
231                // swallowed, not propagated back to the caller
232            }
233        }
234        return resultStream != null ? resultStream.toByteArray() : null;
235    }
236
237    private static byte[] decompress(final byte[] data, final int expectedSize) {
238        ByteArrayInputStream inputStream = null;
239        GZIPInputStream unzipper = null;
240        try {
241            inputStream = new ByteArrayInputStream(data);
242            unzipper = new GZIPInputStream(inputStream);
243            final byte [] result = new byte[expectedSize];
244            int totalReadBytes = 0;
245            while (totalReadBytes < result.length) {
246                final int restBytes = result.length - totalReadBytes;
247                final int readBytes = unzipper.read(result, totalReadBytes, restBytes);
248                if (readBytes < 0) {
249                    break;
250                }
251                totalReadBytes += readBytes;
252            }
253            if (expectedSize != totalReadBytes) {
254                return null;
255            }
256            return result;
257        } catch(IOException e) {
258            return null;
259        } finally {
260            try {
261                if (unzipper != null) {
262                    unzipper.close();
263                }
264            } catch (IOException e) {
265                Slog.e(TAG, "Failed to close the stream.", e);
266                // swallowed, not propagated back to the caller
267            }
268            try {
269                if (inputStream != null) {
270                  inputStream.close();
271                }
272            } catch (IOException e) {
273                Slog.e(TAG, "Failed to close the stream.", e);
274                // swallowed, not propagated back to the caller
275            }
276        }
277    }
278}
279