content: /*
   * 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.annotation.UnsupportedAppUsage;
  import android.os.Parcel;
  import android.util.Slog;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  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.
       */
      @UnsupportedAppUsage
      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) {
          try (final ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
                  final GZIPOutputStream zipper = new GZIPOutputStream(resultStream)) {
              zipper.write(data);
              zipper.finish();
              return resultStream.toByteArray();
          } catch(Exception e) {
              Slog.e(TAG, "Failed to compress the data.", e);
              return null;
          }
      }
  
      private static byte[] decompress(final byte[] data, final int expectedSize) {
          try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
                  final GZIPInputStream 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(Exception e) {
              Slog.e(TAG, "Failed to decompress the data.", e);
              return null;
          }
      }
  }
  