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 */ 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