Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 660ceddb authored by Yohei Yukawa's avatar Yohei Yukawa Committed by Android Git Automerger
Browse files

am ae90d762: Merge "Introduce InputMethodSubtypeArray for memory efficient IPCs"

* commit 'ae90d762':
  Introduce InputMethodSubtypeArray for memory efficient IPCs
parents 3d95971b ae90d762
Loading
Loading
Loading
Loading
+14 −13
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
/*
 * 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
            }
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -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);
+1 −1
Original line number Diff line number Diff line
@@ -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
+77 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}