summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
authorYohei Yukawa <yukawa@google.com>2014-03-03 15:38:18 +0900
committerYohei Yukawa <yukawa@google.com>2014-03-06 11:25:57 +0900
commitf06569561fe1c6e898debf8bb9f37331a9f87323 (patch)
treec9c391d4d0ac5987eb33bd5e47be4c03a2ba0cbb /core/java
parent4b131046aa5685112b8f198a989daef13279cadb (diff)
downloadframeworks_base-f06569561fe1c6e898debf8bb9f37331a9f87323.zip
frameworks_base-f06569561fe1c6e898debf8bb9f37331a9f87323.tar.gz
frameworks_base-f06569561fe1c6e898debf8bb9f37331a9f87323.tar.bz2
Introduce InputMethodSubtypeArray for memory efficient IPCs
This CL introduces InputMethodSubtypeArray which compresses multiple instances of InputMethodSubtype to reduce the risk of TransactionTooLargeException during IPCs. There are some IMEs which rapidly adding new subtypes into their supported language list. One problem here is that each instance of InputMethodInfo internally owns the list of supported subtypes. Basically it requires additional 200 ~ 300 bytes for each subtype when InputMethodInfo is transffered via IPCs. We should keep the size less than 100 KB in typical scenario. With this CL, the list of InputMethodSubtype is marshalled with GZIP compression. Approximately one InputMethodInfo is marshalled within 10 KB even when it has 100 subtypes. No negative performance impact is observed so far. The cost of decompression seems to be compensated by another optimization in this CL. Actually marshalling cost is reduced with this CL by caching the compressed data on demand. BUG: 12954290 Change-Id: Ibb2940fcc02f3b3b51ba6bbe127d646fd7de7c45
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/view/inputmethod/InputMethodInfo.java27
-rw-r--r--core/java/android/view/inputmethod/InputMethodSubtypeArray.java278
-rw-r--r--core/java/com/android/internal/view/IInputMethodManager.aidl2
3 files changed, 294 insertions, 13 deletions
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 9f2bf33..f8160c8 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -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);
}
/**
diff --git a/core/java/android/view/inputmethod/InputMethodSubtypeArray.java b/core/java/android/view/inputmethod/InputMethodSubtypeArray.java
new file mode 100644
index 0000000..5bef71f
--- /dev/null
+++ b/core/java/android/view/inputmethod/InputMethodSubtypeArray.java
@@ -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
+ }
+ }
+ }
+}
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 325a27d..e51345c 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -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);