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.util.Slog;
21
22import java.io.ByteArrayInputStream;
23import java.io.ByteArrayOutputStream;
24import java.util.List;
25import java.util.zip.GZIPInputStream;
26import 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 */
42public 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     */
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     */
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     */
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     */
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     */
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
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
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
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
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