content: /*
   * Copyright (C) 2017 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 com.android.server.net.watchlist;
  
  import android.annotation.Nullable;
  import android.content.ContentValues;
  import android.content.Context;
  import android.database.Cursor;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteOpenHelper;
  import android.os.Environment;
  import android.util.Pair;
  
  import com.android.internal.util.HexDump;
  
  import java.io.File;
  import java.util.ArrayList;
  import java.util.GregorianCalendar;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  
  /**
   * Helper class to process watchlist read / save watchlist reports.
   */
  class WatchlistReportDbHelper extends SQLiteOpenHelper {
  
      private static final String TAG = "WatchlistReportDbHelper";
  
      private static final String NAME = "watchlist_report.db";
      private static final int VERSION = 2;
  
      private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
  
      private static class WhiteListReportContract {
          private static final String TABLE = "records";
          private static final String APP_DIGEST = "app_digest";
          private static final String CNC_DOMAIN = "cnc_domain";
          private static final String TIMESTAMP = "timestamp";
      }
  
      private static final String CREATE_TABLE_MODEL = "CREATE TABLE "
              + WhiteListReportContract.TABLE + "("
              + WhiteListReportContract.APP_DIGEST + " BLOB,"
              + WhiteListReportContract.CNC_DOMAIN + " TEXT,"
              + WhiteListReportContract.TIMESTAMP + " INTEGER DEFAULT 0" + " )";
  
      private static final int INDEX_DIGEST = 0;
      private static final int INDEX_CNC_DOMAIN = 1;
      private static final int INDEX_TIMESTAMP = 2;
  
      private static final String[] DIGEST_DOMAIN_PROJECTION =
              new String[] {
                      WhiteListReportContract.APP_DIGEST,
                      WhiteListReportContract.CNC_DOMAIN
              };
  
      private static WatchlistReportDbHelper sInstance;
  
      /**
       * Aggregated watchlist records.
       */
      public static class AggregatedResult {
          // A list of digests that visited c&c domain or ip before.
          final Set<String> appDigestList;
  
          // The c&c domain or ip visited before.
          @Nullable final String cncDomainVisited;
  
          // A list of app digests and c&c domain visited.
          final HashMap<String, String> appDigestCNCList;
  
          public AggregatedResult(Set<String> appDigestList, String cncDomainVisited,
                  HashMap<String, String> appDigestCNCList) {
              this.appDigestList = appDigestList;
              this.cncDomainVisited = cncDomainVisited;
              this.appDigestCNCList = appDigestCNCList;
          }
      }
  
      static File getSystemWatchlistDbFile() {
          return new File(Environment.getDataSystemDirectory(), NAME);
      }
  
      private WatchlistReportDbHelper(Context context) {
          super(context, getSystemWatchlistDbFile().getAbsolutePath(), null, VERSION);
          // Memory optimization - close idle connections after 30s of inactivity
          setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
      }
  
      public static synchronized WatchlistReportDbHelper getInstance(Context context) {
          if (sInstance != null) {
              return sInstance;
          }
          sInstance = new WatchlistReportDbHelper(context);
          return sInstance;
      }
  
      @Override
      public void onCreate(SQLiteDatabase db) {
          db.execSQL(CREATE_TABLE_MODEL);
      }
  
      @Override
      public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
          // TODO: For now, drop older tables and recreate new ones.
          db.execSQL("DROP TABLE IF EXISTS " + WhiteListReportContract.TABLE);
          onCreate(db);
      }
  
      /**
       * Insert new watchlist record.
       *
       * @param appDigest The digest of an app.
       * @param cncDomain C&C domain that app visited.
       * @return True if success.
       */
      public boolean insertNewRecord(byte[] appDigest, String cncDomain,
              long timestamp) {
          final SQLiteDatabase db = getWritableDatabase();
          final ContentValues values = new ContentValues();
          values.put(WhiteListReportContract.APP_DIGEST, appDigest);
          values.put(WhiteListReportContract.CNC_DOMAIN, cncDomain);
          values.put(WhiteListReportContract.TIMESTAMP, timestamp);
          return db.insert(WhiteListReportContract.TABLE, null, values) != -1;
      }
  
      /**
       * Aggregate all records in database before input timestamp, and return a
       * rappor encoded result.
       */
      @Nullable
      public AggregatedResult getAggregatedRecords(long untilTimestamp) {
          final String selectStatement = WhiteListReportContract.TIMESTAMP + " < ?";
  
          final SQLiteDatabase db = getReadableDatabase();
          Cursor c = null;
          try {
              c = db.query(true /* distinct */,
                      WhiteListReportContract.TABLE, DIGEST_DOMAIN_PROJECTION, selectStatement,
                      new String[]{Long.toString(untilTimestamp)}, null, null,
                      null, null);
              if (c == null) {
                  return null;
              }
              final HashSet<String> appDigestList = new HashSet<>();
              final HashMap<String, String> appDigestCNCList = new HashMap<>();
              String cncDomainVisited = null;
              while (c.moveToNext()) {
                  // We use hex string here as byte[] cannot be a key in HashMap.
                  String digestHexStr = HexDump.toHexString(c.getBlob(INDEX_DIGEST));
                  String cncDomain = c.getString(INDEX_CNC_DOMAIN);
  
                  appDigestList.add(digestHexStr);
                  if (cncDomainVisited != null) {
                      cncDomainVisited = cncDomain;
                  }
                  appDigestCNCList.put(digestHexStr, cncDomain);
              }
              return new AggregatedResult(appDigestList, cncDomainVisited, appDigestCNCList);
          } finally {
              if (c != null) {
                  c.close();
              }
          }
      }
  
      /**
       * Remove all the records before input timestamp.
       *
       * @return True if success.
       */
      public boolean cleanup(long untilTimestamp) {
          final SQLiteDatabase db = getWritableDatabase();
          final String clause = WhiteListReportContract.TIMESTAMP + "< " + untilTimestamp;
          return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
      }
  }