Merge "Introduce InputMethodSubtypeArray for memory efficient IPCs"
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
278
core/java/android/view/inputmethod/InputMethodSubtypeArray.java
Normal file
278
core/java/android/view/inputmethod/InputMethodSubtypeArray.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user