Merge "Introduce InputMethodSubtypeArray for memory efficient IPCs"

This commit is contained in:
Yohei Yukawa
2014-03-07 08:27:23 +00:00
committed by Android (Google) Code Review
5 changed files with 372 additions and 14 deletions

View File

@@ -37,6 +37,7 @@ import android.util.Printer;
import android.util.Slog;
import android.util.Xml;
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
import android.view.inputmethod.InputMethodSubtypeArray;
import java.io.IOException;
import java.util.ArrayList;
@@ -86,9 +87,9 @@ public final class InputMethodInfo implements Parcelable {
final int mIsDefaultResId;
/**
* The array of the subtypes.
* An array-like container of the subtypes.
*/
private final ArrayList<InputMethodSubtype> mSubtypes = new ArrayList<InputMethodSubtype>();
private final InputMethodSubtypeArray mSubtypes;
private final boolean mIsAuxIme;
@@ -138,6 +139,7 @@ public final class InputMethodInfo implements Parcelable {
int isDefaultResId = 0;
XmlResourceParser parser = null;
final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
try {
parser = si.loadXmlMetaData(pm, InputMethod.SERVICE_META_DATA);
if (parser == null) {
@@ -206,7 +208,7 @@ public final class InputMethodInfo implements Parcelable {
if (!subtype.isAuxiliary()) {
isAuxIme = false;
}
mSubtypes.add(subtype);
subtypes.add(subtype);
}
}
} catch (NameNotFoundException e) {
@@ -216,7 +218,7 @@ public final class InputMethodInfo implements Parcelable {
if (parser != null) parser.close();
}
if (mSubtypes.size() == 0) {
if (subtypes.size() == 0) {
isAuxIme = false;
}
@@ -225,14 +227,15 @@ public final class InputMethodInfo implements Parcelable {
final int N = additionalSubtypes.size();
for (int i = 0; i < N; ++i) {
final InputMethodSubtype subtype = additionalSubtypes.get(i);
if (!mSubtypes.contains(subtype)) {
mSubtypes.add(subtype);
if (!subtypes.contains(subtype)) {
subtypes.add(subtype);
} else {
Slog.w(TAG, "Duplicated subtype definition found: "
+ subtype.getLocale() + ", " + subtype.getMode());
}
}
}
mSubtypes = new InputMethodSubtypeArray(subtypes);
mSettingsActivityName = settingsActivityComponent;
mIsDefaultResId = isDefaultResId;
mIsAuxIme = isAuxIme;
@@ -246,7 +249,7 @@ public final class InputMethodInfo implements Parcelable {
mIsAuxIme = source.readInt() == 1;
mSupportsSwitchingToNextInputMethod = source.readInt() == 1;
mService = ResolveInfo.CREATOR.createFromParcel(source);
source.readTypedList(mSubtypes, InputMethodSubtype.CREATOR);
mSubtypes = new InputMethodSubtypeArray(source);
mForceDefault = false;
}
@@ -272,9 +275,7 @@ public final class InputMethodInfo implements Parcelable {
mSettingsActivityName = settingsActivity;
mIsDefaultResId = isDefaultResId;
mIsAuxIme = isAuxIme;
if (subtypes != null) {
mSubtypes.addAll(subtypes);
}
mSubtypes = new InputMethodSubtypeArray(subtypes);
mForceDefault = forceDefault;
mSupportsSwitchingToNextInputMethod = true;
}
@@ -364,7 +365,7 @@ public final class InputMethodInfo implements Parcelable {
* composed of {@link #getPackageName} and the class name returned here.
*
* <p>A null will be returned if there is no settings activity associated
* with the input method.
* with the input method.</p>
*/
public String getSettingsActivity() {
return mSettingsActivityName;
@@ -374,7 +375,7 @@ public final class InputMethodInfo implements Parcelable {
* Return the count of the subtypes of Input Method.
*/
public int getSubtypeCount() {
return mSubtypes.size();
return mSubtypes.getCount();
}
/**
@@ -479,7 +480,7 @@ public final class InputMethodInfo implements Parcelable {
dest.writeInt(mIsAuxIme ? 1 : 0);
dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0);
mService.writeToParcel(dest, flags);
dest.writeTypedList(mSubtypes);
mSubtypes.writeToParcel(dest);
}
/**

View File

@@ -0,0 +1,278 @@
/*
* 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.os.Parcelable;
import android.util.AndroidRuntimeException;
import android.util.Slog;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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) {
ByteArrayOutputStream resultStream = null;
GZIPOutputStream zipper = null;
try {
resultStream = new ByteArrayOutputStream();
zipper = new GZIPOutputStream(resultStream);
zipper.write(data);
} catch(IOException e) {
return null;
} finally {
try {
if (zipper != null) {
zipper.close();
}
} catch (IOException e) {
zipper = null;
Slog.e(TAG, "Failed to close the stream.", e);
// swallowed, not propagated back to the caller
}
try {
if (resultStream != null) {
resultStream.close();
}
} catch (IOException e) {
resultStream = null;
Slog.e(TAG, "Failed to close the stream.", e);
// swallowed, not propagated back to the caller
}
}
return resultStream != null ? resultStream.toByteArray() : null;
}
private static byte[] decompress(final byte[] data, final int expectedSize) {
ByteArrayInputStream inputStream = null;
GZIPInputStream unzipper = null;
try {
inputStream = new ByteArrayInputStream(data);
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(IOException e) {
return null;
} finally {
try {
if (unzipper != null) {
unzipper.close();
}
} catch (IOException e) {
Slog.e(TAG, "Failed to close the stream.", e);
// swallowed, not propagated back to the caller
}
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
Slog.e(TAG, "Failed to close the stream.", e);
// swallowed, not propagated back to the caller
}
}
}
}

View File

@@ -32,7 +32,9 @@ import com.android.internal.view.IInputMethodClient;
* this file.
*/
interface IInputMethodManager {
// TODO: Use ParceledListSlice instead
List<InputMethodInfo> getInputMethodList();
// TODO: Use ParceledListSlice instead
List<InputMethodInfo> getEnabledInputMethodList();
List<InputMethodSubtype> getEnabledInputMethodSubtypeList(in String imiId,
boolean allowsImplicitlySelectedSubtypes);

View File

@@ -21,4 +21,4 @@ if [[ $rebuild == true ]]; then
$COMMAND
fi
adb shell am instrument -w -e class android.os.InputMethodTest com.android.frameworks.coretests.inputmethod/android.test.InstrumentationTestRunner
adb shell am instrument -w -e class android.os.InputMethodTest,android.os.InputMethodSubtypeArrayTest com.android.frameworks.coretests.inputmethod/android.test.InstrumentationTestRunner

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 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.os;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.InputMethodSubtypeArray;
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
import java.util.ArrayList;
public class InputMethodSubtypeArrayTest extends InstrumentationTestCase {
@SmallTest
public void testInstanciate() throws Exception {
final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
subtypes.add(createDummySubtype(0, "en_US"));
subtypes.add(createDummySubtype(1, "en_US"));
subtypes.add(createDummySubtype(2, "ja_JP"));
final InputMethodSubtypeArray array = new InputMethodSubtypeArray(subtypes);
assertEquals(subtypes.size(), array.getCount());
assertEquals(subtypes.get(0), array.get(0));
assertEquals(subtypes.get(1), array.get(1));
assertEquals(subtypes.get(2), array.get(2));
final InputMethodSubtypeArray clonedArray = cloneViaParcel(array);
assertEquals(subtypes.size(), clonedArray.getCount());
assertEquals(subtypes.get(0), clonedArray.get(0));
assertEquals(subtypes.get(1), clonedArray.get(1));
assertEquals(subtypes.get(2), clonedArray.get(2));
final InputMethodSubtypeArray clonedClonedArray = cloneViaParcel(clonedArray);
assertEquals(clonedArray.getCount(), clonedClonedArray.getCount());
assertEquals(clonedArray.get(0), clonedClonedArray.get(0));
assertEquals(clonedArray.get(1), clonedClonedArray.get(1));
assertEquals(clonedArray.get(2), clonedClonedArray.get(2));
}
InputMethodSubtypeArray cloneViaParcel(final InputMethodSubtypeArray original) {
Parcel parcel = null;
try {
parcel = Parcel.obtain();
original.writeToParcel(parcel);
parcel.setDataPosition(0);
return new InputMethodSubtypeArray(parcel);
} finally {
if (parcel != null) {
parcel.recycle();
}
}
}
private static InputMethodSubtype createDummySubtype(final int id, final String locale) {
final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
return builder.setSubtypeNameResId(0)
.setSubtypeIconResId(0)
.setSubtypeId(id)
.setSubtypeLocale(locale)
.setIsAsciiCapable(true)
.build();
}
}