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.annotation.NonNull; 20 import android.compat.annotation.UnsupportedAppUsage; 21 import android.os.BadParcelableException; 22 import android.os.Parcel; 23 import android.util.Printer; 24 import android.util.Slog; 25 26 import java.io.ByteArrayInputStream; 27 import java.io.ByteArrayOutputStream; 28 import java.util.ArrayList; 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 @UnsupportedAppUsage InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes)58 public InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes) { 59 if (subtypes == null) { 60 mCount = 0; 61 return; 62 } 63 mCount = subtypes.size(); 64 mInstance = subtypes.toArray(new InputMethodSubtype[mCount]); 65 } 66 67 /** 68 * Unmarshall an instance of {@link InputMethodSubtypeArray} from a given {@link Parcel} 69 * object. 70 * 71 * @param source A {@link Parcel} object from which {@link InputMethodSubtypeArray} will be 72 * unmarshalled. 73 */ InputMethodSubtypeArray(final Parcel source)74 public InputMethodSubtypeArray(final Parcel source) { 75 mCount = source.readInt(); 76 if (mCount < 0) { 77 throw new BadParcelableException("mCount must be non-negative."); 78 } 79 if (mCount > 0) { 80 mDecompressedSize = source.readInt(); 81 mCompressedData = source.createByteArray(); 82 } 83 } 84 85 /** 86 * Marshall the instance into a given {@link Parcel} object. 87 * 88 * <p>This methods may take a bit additional time to compress data lazily when called 89 * first time.</p> 90 * 91 * @param source A {@link Parcel} object to which {@link InputMethodSubtypeArray} will be 92 * marshalled. 93 */ writeToParcel(final Parcel dest)94 public void writeToParcel(final Parcel dest) { 95 if (mCount == 0) { 96 dest.writeInt(mCount); 97 return; 98 } 99 100 byte[] compressedData = mCompressedData; 101 int decompressedSize = mDecompressedSize; 102 if (compressedData == null && decompressedSize == 0) { 103 synchronized (mLockObject) { 104 compressedData = mCompressedData; 105 decompressedSize = mDecompressedSize; 106 if (compressedData == null && decompressedSize == 0) { 107 final byte[] decompressedData = marshall(mInstance); 108 compressedData = compress(decompressedData); 109 if (compressedData == null) { 110 decompressedSize = -1; 111 Slog.i(TAG, "Failed to compress data."); 112 } else { 113 decompressedSize = decompressedData.length; 114 } 115 mDecompressedSize = decompressedSize; 116 mCompressedData = compressedData; 117 } 118 } 119 } 120 121 if (compressedData != null && decompressedSize > 0) { 122 dest.writeInt(mCount); 123 dest.writeInt(decompressedSize); 124 dest.writeByteArray(compressedData); 125 } else { 126 Slog.i(TAG, "Unexpected state. Behaving as an empty array."); 127 dest.writeInt(0); 128 } 129 } 130 131 /** 132 * Return {@link InputMethodSubtype} specified with the given index. 133 * 134 * <p>This methods may take a bit additional time to decompress data lazily when called 135 * first time.</p> 136 * 137 * @param index The index of {@link InputMethodSubtype}. 138 */ get(final int index)139 public InputMethodSubtype get(final int index) { 140 if (index < 0 || mCount <= index) { 141 throw new ArrayIndexOutOfBoundsException(); 142 } 143 InputMethodSubtype[] instance = mInstance; 144 if (instance == null) { 145 synchronized (mLockObject) { 146 instance = mInstance; 147 if (instance == null) { 148 final byte[] decompressedData = 149 decompress(mCompressedData, mDecompressedSize); 150 // Clear the compressed data until {@link #getMarshalled()} is called. 151 mCompressedData = null; 152 mDecompressedSize = 0; 153 if (decompressedData != null) { 154 instance = unmarshall(decompressedData); 155 } else { 156 Slog.e(TAG, "Failed to decompress data. Returns null as fallback."); 157 instance = new InputMethodSubtype[mCount]; 158 } 159 mInstance = instance; 160 } 161 } 162 } 163 return instance[index]; 164 } 165 166 /** 167 * @return A list of {@link InputMethodInfo} copied from this array. 168 */ 169 @NonNull toList()170 public ArrayList<InputMethodSubtype> toList() { 171 final ArrayList<InputMethodSubtype> list = new ArrayList<>(mCount); 172 for (int i = 0; i < mCount; ++i) { 173 list.add(get(i)); 174 } 175 return list; 176 } 177 178 /** 179 * Return the number of {@link InputMethodSubtype} objects. 180 */ getCount()181 public int getCount() { 182 return mCount; 183 } 184 185 private final Object mLockObject = new Object(); 186 private final int mCount; 187 188 private volatile InputMethodSubtype[] mInstance; 189 private volatile byte[] mCompressedData; 190 private volatile int mDecompressedSize; 191 dump(@onNull Printer pw, @NonNull String prefix)192 void dump(@NonNull Printer pw, @NonNull String prefix) { 193 final var innerPrefix = prefix + " "; 194 for (int i = 0; i < mCount; i++) { 195 pw.println(prefix + "InputMethodSubtype #" + i + ":"); 196 final var subtype = get(i); 197 if (subtype != null) { 198 subtype.dump(pw, innerPrefix); 199 } else { 200 pw.println(innerPrefix + "missing subtype"); 201 } 202 } 203 } 204 marshall(final InputMethodSubtype[] array)205 private static byte[] marshall(final InputMethodSubtype[] array) { 206 Parcel parcel = null; 207 try { 208 parcel = Parcel.obtain(); 209 parcel.writeTypedArray(array, 0); 210 return parcel.marshall(); 211 } finally { 212 if (parcel != null) { 213 parcel.recycle(); 214 parcel = null; 215 } 216 } 217 } 218 unmarshall(final byte[] data)219 private static InputMethodSubtype[] unmarshall(final byte[] data) { 220 Parcel parcel = null; 221 try { 222 parcel = Parcel.obtain(); 223 parcel.unmarshall(data, 0, data.length); 224 parcel.setDataPosition(0); 225 return parcel.createTypedArray(InputMethodSubtype.CREATOR); 226 } finally { 227 if (parcel != null) { 228 parcel.recycle(); 229 parcel = null; 230 } 231 } 232 } 233 compress(final byte[] data)234 private static byte[] compress(final byte[] data) { 235 try (final ByteArrayOutputStream resultStream = new ByteArrayOutputStream(); 236 final GZIPOutputStream zipper = new GZIPOutputStream(resultStream)) { 237 zipper.write(data); 238 zipper.finish(); 239 return resultStream.toByteArray(); 240 } catch(Exception e) { 241 Slog.e(TAG, "Failed to compress the data.", e); 242 return null; 243 } 244 } 245 decompress(final byte[] data, final int expectedSize)246 private static byte[] decompress(final byte[] data, final int expectedSize) { 247 try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(data); 248 final GZIPInputStream unzipper = new GZIPInputStream(inputStream)) { 249 final byte [] result = new byte[expectedSize]; 250 int totalReadBytes = 0; 251 while (totalReadBytes < result.length) { 252 final int restBytes = result.length - totalReadBytes; 253 final int readBytes = unzipper.read(result, totalReadBytes, restBytes); 254 if (readBytes < 0) { 255 break; 256 } 257 totalReadBytes += readBytes; 258 } 259 if (expectedSize != totalReadBytes) { 260 return null; 261 } 262 return result; 263 } catch(Exception e) { 264 Slog.e(TAG, "Failed to decompress the data.", e); 265 return null; 266 } 267 } 268 } 269