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