Home | History | Annotate | Download | only in inputmethod
      1 /*
      2  * Copyright (C) 2007-2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package android.view.inputmethod;
     18 
     19 import android.os.Parcel;
     20 import android.os.Parcelable;
     21 import android.util.AndroidRuntimeException;
     22 import android.util.Slog;
     23 
     24 import java.io.ByteArrayInputStream;
     25 import java.io.ByteArrayOutputStream;
     26 import java.io.IOException;
     27 import java.io.InputStream;
     28 import java.io.OutputStream;
     29 import java.util.List;
     30 import java.util.zip.GZIPInputStream;
     31 import java.util.zip.GZIPOutputStream;
     32 
     33 /**
     34  * An array-like container that stores multiple instances of {@link InputMethodSubtype}.
     35  *
     36  * <p>This container is designed to reduce the risk of {@link TransactionTooLargeException}
     37  * when one or more instancess of {@link InputMethodInfo} are transferred through IPC.
     38  * Basically this class does following three tasks.</p>
     39  * <ul>
     40  * <li>Applying compression for the marshalled data</li>
     41  * <li>Lazily unmarshalling objects</li>
     42  * <li>Caching the marshalled data when appropriate</li>
     43  * </ul>
     44  *
     45  * @hide
     46  */
     47 public class InputMethodSubtypeArray {
     48     private final static String TAG = "InputMethodSubtypeArray";
     49 
     50     /**
     51      * Create a new instance of {@link InputMethodSubtypeArray} from an existing list of
     52      * {@link InputMethodSubtype}.
     53      *
     54      * @param subtypes A list of {@link InputMethodSubtype} from which
     55      * {@link InputMethodSubtypeArray} will be created.
     56      */
     57     public InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes) {
     58         if (subtypes == null) {
     59             mCount = 0;
     60             return;
     61         }
     62         mCount = subtypes.size();
     63         mInstance = subtypes.toArray(new InputMethodSubtype[mCount]);
     64     }
     65 
     66     /**
     67      * Unmarshall an instance of {@link InputMethodSubtypeArray} from a given {@link Parcel}
     68      * object.
     69      *
     70      * @param source A {@link Parcel} object from which {@link InputMethodSubtypeArray} will be
     71      * unmarshalled.
     72      */
     73     public InputMethodSubtypeArray(final Parcel source) {
     74         mCount = source.readInt();
     75         if (mCount > 0) {
     76             mDecompressedSize = source.readInt();
     77             mCompressedData = source.createByteArray();
     78         }
     79     }
     80 
     81     /**
     82      * Marshall the instance into a given {@link Parcel} object.
     83      *
     84      * <p>This methods may take a bit additional time to compress data lazily when called
     85      * first time.</p>
     86      *
     87      * @param source A {@link Parcel} object to which {@link InputMethodSubtypeArray} will be
     88      * marshalled.
     89      */
     90     public void writeToParcel(final Parcel dest) {
     91         if (mCount == 0) {
     92             dest.writeInt(mCount);
     93             return;
     94         }
     95 
     96         byte[] compressedData = mCompressedData;
     97         int decompressedSize = mDecompressedSize;
     98         if (compressedData == null && decompressedSize == 0) {
     99             synchronized (mLockObject) {
    100                 compressedData = mCompressedData;
    101                 decompressedSize = mDecompressedSize;
    102                 if (compressedData == null && decompressedSize == 0) {
    103                     final byte[] decompressedData = marshall(mInstance);
    104                     compressedData = compress(decompressedData);
    105                     if (compressedData == null) {
    106                         decompressedSize = -1;
    107                         Slog.i(TAG, "Failed to compress data.");
    108                     } else {
    109                         decompressedSize = decompressedData.length;
    110                     }
    111                     mDecompressedSize = decompressedSize;
    112                     mCompressedData = compressedData;
    113                 }
    114             }
    115         }
    116 
    117         if (compressedData != null && decompressedSize > 0) {
    118             dest.writeInt(mCount);
    119             dest.writeInt(decompressedSize);
    120             dest.writeByteArray(compressedData);
    121         } else {
    122             Slog.i(TAG, "Unexpected state. Behaving as an empty array.");
    123             dest.writeInt(0);
    124         }
    125     }
    126 
    127     /**
    128      * Return {@link InputMethodSubtype} specified with the given index.
    129      *
    130      * <p>This methods may take a bit additional time to decompress data lazily when called
    131      * first time.</p>
    132      *
    133      * @param index The index of {@link InputMethodSubtype}.
    134      */
    135     public InputMethodSubtype get(final int index) {
    136         if (index < 0 || mCount <= index) {
    137             throw new ArrayIndexOutOfBoundsException();
    138         }
    139         InputMethodSubtype[] instance = mInstance;
    140         if (instance == null) {
    141             synchronized (mLockObject) {
    142                 instance = mInstance;
    143                 if (instance == null) {
    144                     final byte[] decompressedData =
    145                           decompress(mCompressedData, mDecompressedSize);
    146                     // Clear the compressed data until {@link #getMarshalled()} is called.
    147                     mCompressedData = null;
    148                     mDecompressedSize = 0;
    149                     if (decompressedData != null) {
    150                         instance = unmarshall(decompressedData);
    151                     } else {
    152                         Slog.e(TAG, "Failed to decompress data. Returns null as fallback.");
    153                         instance = new InputMethodSubtype[mCount];
    154                     }
    155                     mInstance = instance;
    156                 }
    157             }
    158         }
    159         return instance[index];
    160     }
    161 
    162     /**
    163      * Return the number of {@link InputMethodSubtype} objects.
    164      */
    165     public int getCount() {
    166         return mCount;
    167     }
    168 
    169     private final Object mLockObject = new Object();
    170     private final int mCount;
    171 
    172     private volatile InputMethodSubtype[] mInstance;
    173     private volatile byte[] mCompressedData;
    174     private volatile int mDecompressedSize;
    175 
    176     private static byte[] marshall(final InputMethodSubtype[] array) {
    177         Parcel parcel = null;
    178         try {
    179             parcel = Parcel.obtain();
    180             parcel.writeTypedArray(array, 0);
    181             return parcel.marshall();
    182         } finally {
    183             if (parcel != null) {
    184                 parcel.recycle();
    185                 parcel = null;
    186             }
    187         }
    188     }
    189 
    190     private static InputMethodSubtype[] unmarshall(final byte[] data) {
    191         Parcel parcel = null;
    192         try {
    193             parcel = Parcel.obtain();
    194             parcel.unmarshall(data, 0, data.length);
    195             parcel.setDataPosition(0);
    196             return parcel.createTypedArray(InputMethodSubtype.CREATOR);
    197         } finally {
    198             if (parcel != null) {
    199                 parcel.recycle();
    200                 parcel = null;
    201             }
    202         }
    203     }
    204 
    205     private static byte[] compress(final byte[] data) {
    206         ByteArrayOutputStream resultStream = null;
    207         GZIPOutputStream zipper = null;
    208         try {
    209             resultStream = new ByteArrayOutputStream();
    210             zipper = new GZIPOutputStream(resultStream);
    211             zipper.write(data);
    212         } catch(IOException e) {
    213             return null;
    214         } finally {
    215             try {
    216                 if (zipper != null) {
    217                     zipper.close();
    218                 }
    219             } catch (IOException e) {
    220                 zipper = null;
    221                 Slog.e(TAG, "Failed to close the stream.", e);
    222                 // swallowed, not propagated back to the caller
    223             }
    224             try {
    225                 if (resultStream != null) {
    226                     resultStream.close();
    227                 }
    228             } catch (IOException e) {
    229                 resultStream = null;
    230                 Slog.e(TAG, "Failed to close the stream.", e);
    231                 // swallowed, not propagated back to the caller
    232             }
    233         }
    234         return resultStream != null ? resultStream.toByteArray() : null;
    235     }
    236 
    237     private static byte[] decompress(final byte[] data, final int expectedSize) {
    238         ByteArrayInputStream inputStream = null;
    239         GZIPInputStream unzipper = null;
    240         try {
    241             inputStream = new ByteArrayInputStream(data);
    242             unzipper = new GZIPInputStream(inputStream);
    243             final byte [] result = new byte[expectedSize];
    244             int totalReadBytes = 0;
    245             while (totalReadBytes < result.length) {
    246                 final int restBytes = result.length - totalReadBytes;
    247                 final int readBytes = unzipper.read(result, totalReadBytes, restBytes);
    248                 if (readBytes < 0) {
    249                     break;
    250                 }
    251                 totalReadBytes += readBytes;
    252             }
    253             if (expectedSize != totalReadBytes) {
    254                 return null;
    255             }
    256             return result;
    257         } catch(IOException e) {
    258             return null;
    259         } finally {
    260             try {
    261                 if (unzipper != null) {
    262                     unzipper.close();
    263                 }
    264             } catch (IOException e) {
    265                 Slog.e(TAG, "Failed to close the stream.", e);
    266                 // swallowed, not propagated back to the caller
    267             }
    268             try {
    269                 if (inputStream != null) {
    270                   inputStream.close();
    271                 }
    272             } catch (IOException e) {
    273                 Slog.e(TAG, "Failed to close the stream.", e);
    274                 // swallowed, not propagated back to the caller
    275             }
    276         }
    277     }
    278 }
    279