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.os.Parcelable; 21 import android.util.AndroidRuntimeException; 22 import android.util.Slog; 23 24 import java.io.ByteArrayInputStream; 25 import java.io.ByteArrayOutputStream; 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.io.OutputStream; 29 import java.util.List; 30 import java.util.zip.GZIPInputStream; 31 import 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 */ 47 public 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