/*
* Copyright (C) 2007-2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package android.view.inputmethod;
import android.os.Parcel;
import android.util.Slog;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* An array-like container that stores multiple instances of {@link InputMethodSubtype}.
*
* <p>This container is designed to reduce the risk of {@link TransactionTooLargeException}
* when one or more instancess of {@link InputMethodInfo} are transferred through IPC.
* Basically this class does following three tasks.</p>
* <ul>
* <li>Applying compression for the marshalled data</li>
* <li>Lazily unmarshalling objects</li>
* <li>Caching the marshalled data when appropriate</li>
* </ul>
*
* @hide
*/
public class InputMethodSubtypeArray {
private final static String TAG = "InputMethodSubtypeArray";
/**
* Create a new instance of {@link InputMethodSubtypeArray} from an existing list of
* {@link InputMethodSubtype}.
*
* @param subtypes A list of {@link InputMethodSubtype} from which
* {@link InputMethodSubtypeArray} will be created.
*/
public InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes) {
if (subtypes == null) {
mCount = 0;
return;
}
mCount = subtypes.size();
mInstance = subtypes.toArray(new InputMethodSubtype[mCount]);
}
/**
* Unmarshall an instance of {@link InputMethodSubtypeArray} from a given {@link Parcel}
* object.
*
* @param source A {@link Parcel} object from which {@link InputMethodSubtypeArray} will be
* unmarshalled.
*/
public InputMethodSubtypeArray(final Parcel source) {
mCount = source.readInt();
if (mCount > 0) {
mDecompressedSize = source.readInt();
mCompressedData = source.createByteArray();
}
}
/**
* Marshall the instance into a given {@link Parcel} object.
*
* <p>This methods may take a bit additional time to compress data lazily when called
* first time.</p>
*
* @param source A {@link Parcel} object to which {@link InputMethodSubtypeArray} will be
* marshalled.
*/
public void writeToParcel(final Parcel dest) {
if (mCount == 0) {
dest.writeInt(mCount);
return;
}
byte[] compressedData = mCompressedData;
int decompressedSize = mDecompressedSize;
if (compressedData == null && decompressedSize == 0) {
synchronized (mLockObject) {
compressedData = mCompressedData;
decompressedSize = mDecompressedSize;
if (compressedData == null && decompressedSize == 0) {
final byte[] decompressedData = marshall(mInstance);
compressedData = compress(decompressedData);
if (compressedData == null) {
decompressedSize = -1;
Slog.i(TAG, "Failed to compress data.");
} else {
decompressedSize = decompressedData.length;
}
mDecompressedSize = decompressedSize;
mCompressedData = compressedData;
}
}
}
if (compressedData != null && decompressedSize > 0) {
dest.writeInt(mCount);
dest.writeInt(decompressedSize);
dest.writeByteArray(compressedData);
} else {
Slog.i(TAG, "Unexpected state. Behaving as an empty array.");
dest.writeInt(0);
}
}
/**
* Return {@link InputMethodSubtype} specified with the given index.
*
* <p>This methods may take a bit additional time to decompress data lazily when called
* first time.</p>
*
* @param index The index of {@link InputMethodSubtype}.
*/
public InputMethodSubtype get(final int index) {
if (index < 0 || mCount <= index) {
throw new ArrayIndexOutOfBoundsException();
}
InputMethodSubtype[] instance = mInstance;
if (instance == null) {
synchronized (mLockObject) {
instance = mInstance;
if (instance == null) {
final byte[] decompressedData =
decompress(mCompressedData, mDecompressedSize);
// Clear the compressed data until {@link #getMarshalled()} is called.
mCompressedData = null;
mDecompressedSize = 0;
if (decompressedData != null) {
instance = unmarshall(decompressedData);
} else {
Slog.e(TAG, "Failed to decompress data. Returns null as fallback.");
instance = new InputMethodSubtype[mCount];
}
mInstance = instance;
}
}
}
return instance[index];
}
/**
* Return the number of {@link InputMethodSubtype} objects.
*/
public int getCount() {
return mCount;
}
private final Object mLockObject = new Object();
private final int mCount;
private volatile InputMethodSubtype[] mInstance;
private volatile byte[] mCompressedData;
private volatile int mDecompressedSize;
private static byte[] marshall(final InputMethodSubtype[] array) {
Parcel parcel = null;
try {
parcel = Parcel.obtain();
parcel.writeTypedArray(array, 0);
return parcel.marshall();
} finally {
if (parcel != null) {
parcel.recycle();
parcel = null;
}
}
}
private static InputMethodSubtype[] unmarshall(final byte[] data) {
Parcel parcel = null;
try {
parcel = Parcel.obtain();
parcel.unmarshall(data, 0, data.length);
parcel.setDataPosition(0);
return parcel.createTypedArray(InputMethodSubtype.CREATOR);
} finally {
if (parcel != null) {
parcel.recycle();
parcel = null;
}
}
}
private static byte[] compress(final byte[] data) {
try (final ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
final GZIPOutputStream zipper = new GZIPOutputStream(resultStream)) {
zipper.write(data);
zipper.finish();
return resultStream.toByteArray();
} catch(Exception e) {
Slog.e(TAG, "Failed to compress the data.", e);
return null;
}
}
private static byte[] decompress(final byte[] data, final int expectedSize) {
try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
final GZIPInputStream unzipper = new GZIPInputStream(inputStream)) {
final byte [] result = new byte[expectedSize];
int totalReadBytes = 0;
while (totalReadBytes < result.length) {
final int restBytes = result.length - totalReadBytes;
final int readBytes = unzipper.read(result, totalReadBytes, restBytes);
if (readBytes < 0) {
break;
}
totalReadBytes += readBytes;
}
if (expectedSize != totalReadBytes) {
return null;
}
return result;
} catch(Exception e) {
Slog.e(TAG, "Failed to decompress the data.", e);
return null;
}
}
}