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