content: /*
   * Copyright (C) 2015 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.app.backup;
  
  import android.os.ParcelFileDescriptor;
  import android.util.ArrayMap;
  import android.util.Log;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.DataInputStream;
  import java.io.DataOutputStream;
  import java.io.EOFException;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.zip.CRC32;
  import java.util.zip.DeflaterOutputStream;
  import java.util.zip.InflaterInputStream;
  
  /**
   * Utility class for writing BackupHelpers whose underlying data is a
   * fixed set of byte-array blobs.  The helper manages diff detection
   * and compression on the wire.
   *
   * @hide
   */
  public abstract class BlobBackupHelper implements BackupHelper {
      private static final String TAG = "BlobBackupHelper";
      private static final boolean DEBUG = false;
  
      private final int mCurrentBlobVersion;
      private final String[] mKeys;
  
      public BlobBackupHelper(int currentBlobVersion, String... keys) {
          mCurrentBlobVersion = currentBlobVersion;
          mKeys = keys;
      }
  
      // Client interface
  
      /**
       * Generate and return the byte array containing the backup payload describing
       * the current data state.  During a backup operation this method is called once
       * per key that was supplied to the helper's constructor.
       *
       * @return A byte array containing the data blob that the caller wishes to store,
       *     or {@code null} if the current state is empty or undefined.
       */
      abstract protected byte[] getBackupPayload(String key);
  
      /**
       * Given a byte array that was restored from backup, do whatever is appropriate
       * to apply that described state in the live system.  This method is called once
       * per key/value payload that was delivered for restore.  Typically data is delivered
       * for restore in lexical order by key, <i>not</i> in the order in which the keys
       * were supplied in the constructor.
       *
       * @param payload The byte array that was passed to {@link #getBackupPayload()}
       *     on the ancestral device.
       */
      abstract protected void applyRestoredPayload(String key, byte[] payload);
  
  
      // Internal implementation
  
      /*
       * State on-disk format:
       * [Int]    : overall blob version number
       * [Int=N] : number of keys represented in the state blob
       * N* :
       *     [String] key
       *     [Long]   blob checksum, calculated after compression
       */
      @SuppressWarnings("resource")
      private ArrayMap<String, Long> readOldState(ParcelFileDescriptor oldStateFd) {
          final ArrayMap<String, Long> state = new ArrayMap<String, Long>();
  
          FileInputStream fis = new FileInputStream(oldStateFd.getFileDescriptor());
          DataInputStream in = new DataInputStream(fis);
  
          try {
              int version = in.readInt();
              if (version <= mCurrentBlobVersion) {
                  final int numKeys = in.readInt();
                  if (DEBUG) {
                      Log.i(TAG, "  " + numKeys + " keys in state record");
                  }
                  for (int i = 0; i < numKeys; i++) {
                      String key = in.readUTF();
                      long checksum = in.readLong();
                      if (DEBUG) {
                          Log.i(TAG, "  key '" + key + "' checksum is " + checksum);
                      }
                      state.put(key, checksum);
                  }
              } else {
                  Log.w(TAG, "Prior state from unrecognized version " + version);
              }
          } catch (EOFException e) {
              // Empty file is expected on first backup,  so carry on. If the state
              // is truncated we just treat it the same way.
              if (DEBUG) {
                  Log.i(TAG, "Hit EOF reading prior state");
              }
              state.clear();
          } catch (Exception e) {
              Log.e(TAG, "Error examining prior backup state " + e.getMessage());
              state.clear();
          }
  
          return state;
      }
  
      /**
       * New overall state record
       */
      private void writeBackupState(ArrayMap<String, Long> state, ParcelFileDescriptor stateFile) {
          try {
              FileOutputStream fos = new FileOutputStream(stateFile.getFileDescriptor());
  
              // We explicitly don't close 'out' because we must not close the backing fd.
              // The FileOutputStream will not close it implicitly.
              @SuppressWarnings("resource")
              DataOutputStream out = new DataOutputStream(fos);
  
              out.writeInt(mCurrentBlobVersion);
  
              final int N = (state != null) ? state.size() : 0;
              out.writeInt(N);
              for (int i = 0; i < N; i++) {
                  final String key = state.keyAt(i);
                  final long checksum = state.valueAt(i).longValue();
                  if (DEBUG) {
                      Log.i(TAG, "  writing key " + key + " checksum = " + checksum);
                  }
                  out.writeUTF(key);
                  out.writeLong(checksum);
              }
          } catch (IOException e) {
              Log.e(TAG, "Unable to write updated state", e);
          }
      }
  
      // Also versions the deflated blob internally in case we need to revise it
      private byte[] deflate(byte[] data) {
          byte[] result = null;
          if (data != null) {
              try {
                  ByteArrayOutputStream sink = new ByteArrayOutputStream();
                  DataOutputStream headerOut = new DataOutputStream(sink);
  
                  // write the header directly to the sink ahead of the deflated payload
                  headerOut.writeInt(mCurrentBlobVersion);
  
                  DeflaterOutputStream out = new DeflaterOutputStream(sink);
                  out.write(data);
                  out.close();  // finishes and commits the compression run
                  result = sink.toByteArray();
                  if (DEBUG) {
                      Log.v(TAG, "Deflated " + data.length + " bytes to " + result.length);
                  }
              } catch (IOException e) {
                  Log.w(TAG, "Unable to process payload: " + e.getMessage());
              }
          }
          return result;
      }
  
      // Returns null if inflation failed
      private byte[] inflate(byte[] compressedData) {
          byte[] result = null;
          if (compressedData != null) {
              try {
                  ByteArrayInputStream source = new ByteArrayInputStream(compressedData);
                  DataInputStream headerIn = new DataInputStream(source);
                  int version = headerIn.readInt();
                  if (version > mCurrentBlobVersion) {
                      Log.w(TAG, "Saved payload from unrecognized version " + version);
                      return null;
                  }
  
                  InflaterInputStream in = new InflaterInputStream(source);
                  ByteArrayOutputStream inflated = new ByteArrayOutputStream();
                  byte[] buffer = new byte[4096];
                  int nRead;
                  while ((nRead = in.read(buffer)) > 0) {
                      inflated.write(buffer, 0, nRead);
                  }
                  in.close();
                  inflated.flush();
                  result = inflated.toByteArray();
                  if (DEBUG) {
                      Log.v(TAG, "Inflated " + compressedData.length + " bytes to " + result.length);
                  }
              } catch (IOException e) {
                  // result is still null here
                  Log.w(TAG, "Unable to process restored payload: " + e.getMessage());
              }
          }
          return result;
      }
  
      private long checksum(byte[] buffer) {
          if (buffer != null) {
              try {
                  CRC32 crc = new CRC32();
                  ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
                  byte[] buf = new byte[4096];
                  int nRead = 0;
                  while ((nRead = bis.read(buf)) >= 0) {
                      crc.update(buf, 0, nRead);
                  }
                  return crc.getValue();
              } catch (Exception e) {
                  // whoops; fall through with an explicitly bogus checksum
              }
          }
          return -1;
      }
  
      // BackupHelper interface
  
      @Override
      public void performBackup(ParcelFileDescriptor oldStateFd, BackupDataOutput data,
              ParcelFileDescriptor newStateFd) {
          if (DEBUG) {
              Log.i(TAG, "Performing backup for " + this.getClass().getName());
          }
  
          final ArrayMap<String, Long> oldState = readOldState(oldStateFd);
          final ArrayMap<String, Long> newState = new ArrayMap<String, Long>();
  
          try {
              for (String key : mKeys) {
                  final byte[] payload = deflate(getBackupPayload(key));
                  final long checksum = checksum(payload);
                  if (DEBUG) {
                      Log.i(TAG, "Key " + key + " backup checksum is " + checksum);
                  }
                  newState.put(key, checksum);
  
                  Long oldChecksum = oldState.get(key);
                  if (oldChecksum == null || checksum != oldChecksum.longValue()) {
                      if (DEBUG) {
                          Log.i(TAG, "Checksum has changed from " + oldChecksum + " to " + checksum
                                  + " for key " + key + ", writing");
                      }
                      if (payload != null) {
                          data.writeEntityHeader(key, payload.length);
                          data.writeEntityData(payload, payload.length);
                      } else {
                          // state's changed but there's no current payload => delete
                          data.writeEntityHeader(key, -1);
                      }
                  } else {
                      if (DEBUG) {
                          Log.i(TAG, "No change under key " + key + " => not writing");
                      }
                  }
              }
          } catch (Exception e) {
              Log.w(TAG,  "Unable to record notification state: " + e.getMessage());
              newState.clear();
          } finally {
              // Always rewrite the state even if nothing changed
              writeBackupState(newState, newStateFd);
          }
      }
  
      @Override
      public void restoreEntity(BackupDataInputStream data) {
          final String key = data.getKey();
          try {
              // known key?
              int which;
              for (which = 0; which < mKeys.length; which++) {
                  if (key.equals(mKeys[which])) {
                      break;
                  }
              }
              if (which >= mKeys.length) {
                  Log.e(TAG, "Unrecognized key " + key + ", ignoring");
                  return;
              }
  
              byte[] compressed = new byte[data.size()];
              data.read(compressed);
              byte[] payload = inflate(compressed);
              applyRestoredPayload(key, payload);
          } catch (Exception e) {
              Log.e(TAG, "Exception restoring entity " + key + " : " + e.getMessage());
          }
      }
  
      @Override
      public void writeNewStateDescription(ParcelFileDescriptor newState) {
          // Just ensure that we do a full backup the first time after a restore
          if (DEBUG) {
              Log.i(TAG, "Writing state description after restore");
          }
          writeBackupState(null, newState);
      }
  }
  