/* * Copyright 2014-2016 CyberVision, Inc. * * 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 org.kaaproject.kaa.client.logging; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; import android.util.Log; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; public class AndroidSqLiteDqLogStorage implements LogStorage, LogStorageStatus { private static final String TAG = "AndroidSqLiteDqLogStorage"; private static final String CHANGES_QUERY_RESULT = "affected_row_count"; private static final String GET_CHANGES_QUERY = "SELECT changes() AS " + CHANGES_QUERY_RESULT; private final SQLiteOpenHelper dbHelper; private final SQLiteDatabase database; private long totalRecordCount; private long unmarkedRecordCount; private long unmarkedConsumedSize; private int currentBucketId = 1; private long currentBucketSize; private int currentRecordCount; private long maxBucketSize; private int maxRecordCount; private Map<Integer, Long> consumedMemoryStorage = new HashMap<>(); private SQLiteStatement insertStatement; private SQLiteStatement deleteByBucketIdStatement; private SQLiteStatement updateBucketStateStatement; private SQLiteStatement resetBucketIdStatement; /** * Instantiates the AndroidSqLiteDqLogStorage. */ public AndroidSqLiteDqLogStorage(Context context, long maxBucketSize, int maxRecordCount) { this(context, PersistentLogStorageConstants.DEFAULT_DB_NAME, maxBucketSize, maxRecordCount); } /** * Instantiates the AndroidSqLiteDqLogStorage. */ public AndroidSqLiteDqLogStorage(Context context, String dbName, long bucketSize, int recordCount) { Log.i(TAG, "Connecting to db with name: " + dbName); dbHelper = new DataCollectionDbHelper(context, dbName); database = dbHelper.getWritableDatabase(); this.maxRecordCount = recordCount; this.maxBucketSize = bucketSize; truncateIfBucketSizeIncompatible(); retrieveConsumedSizeAndVolume(); if (totalRecordCount > 0) { retrieveBucketId(); resetBucketsState(); } } @Override public BucketInfo addLogRecord(LogRecord record) { synchronized (database) { Log.d(TAG, "Adding a new log record..."); if (insertStatement == null) { try { insertStatement = database.compileStatement( PersistentLogStorageConstants.KAA_INSERT_NEW_RECORD); } catch (SQLiteException ex) { Log.e(TAG, "Can't create row insert statement", ex); throw new RuntimeException(ex); } } long leftConsumedSize = maxBucketSize - currentBucketSize; long leftRecordCount = maxRecordCount - currentRecordCount; if (leftConsumedSize < record.getSize() || leftRecordCount == 0) { moveToNextBucket(); } try { insertStatement.bindLong(1, currentBucketId); insertStatement.bindBlob(2, record.getData()); long insertedId = insertStatement.executeInsert(); if (insertedId >= 0) { currentBucketSize += record.getSize(); currentRecordCount++; unmarkedConsumedSize += record.getSize(); totalRecordCount++; unmarkedRecordCount++; Log.i(TAG, "Added a new log record, total record count: " + totalRecordCount + ", data: " + Arrays.toString(record.getData()) + "unmarked record count: " + unmarkedRecordCount); } else { Log.w(TAG, "No log record was added"); } } catch (SQLiteException ex) { Log.e(TAG, "Can't add a new record", ex); } } return new BucketInfo(currentBucketId, currentRecordCount); } @Override public LogStorageStatus getStatus() { return this; } @Override public LogBucket getNextBucket() { synchronized (database) { Log.d(TAG, "Creating a new record block"); LogBucket logBlock = null; Cursor cursor = null; List<LogRecord> logRecords = new LinkedList<>(); int bucketId = 0; try { cursor = database.rawQuery(PersistentLogStorageConstants.KAA_SELECT_MIN_BUCKET_ID, null); if (cursor.moveToFirst()) { bucketId = cursor.getInt(0); } } catch (SQLiteException ex) { Log.e(TAG, "Can't retrieve min bucket ID", ex); } finally { try { tryCloseCursor(cursor); } catch (SQLiteException ex) { Log.e(TAG, "Unable to close cursor", ex); } } try { long leftBucketSize = maxBucketSize; if (bucketId > 0) { cursor = database.rawQuery( PersistentLogStorageConstants.KAA_SELECT_LOG_RECORDS_BY_BUCKET_ID, new String[]{String.valueOf(bucketId)}); while (cursor.moveToNext()) { byte[] recordData = cursor.getBlob(0); logRecords.add(new LogRecord(recordData)); leftBucketSize -= recordData.length; } if (!logRecords.isEmpty()) { updateBucketState(bucketId); logBlock = new LogBucket(bucketId, logRecords); long logBlockSize = maxBucketSize - leftBucketSize; unmarkedConsumedSize -= logBlockSize; unmarkedRecordCount -= logRecords.size(); consumedMemoryStorage.put(logBlock.getBucketId(), logBlockSize); if (currentBucketId == bucketId) { moveToNextBucket(); } Log.i(TAG, "Created log block: id [" + logBlock.getBucketId() + "], size: " + logBlockSize + ". Log block record count: " + logBlock.getRecords().size() + ", total record count: " + totalRecordCount + ", unmarked record count: " + unmarkedRecordCount); } else { Log.i(TAG, "No unmarked log records found"); } } } catch (SQLiteException ex) { Log.e(TAG, "Can't retrieve unmarked records from storage", ex); } finally { try { tryCloseCursor(cursor); } catch (SQLiteException ex) { Log.e(TAG, "Unable to close cursor", ex); } } return logBlock; } } private void updateBucketState(int bucketId) { synchronized (database) { Log.v(TAG, "Updating bucket id [" + bucketId + "]"); try { if (updateBucketStateStatement == null) { updateBucketStateStatement = database.compileStatement( PersistentLogStorageConstants.KAA_UPDATE_BUCKET_ID); } updateBucketStateStatement.bindString( 1, PersistentLogStorageConstants.BUCKET_STATE_COLUMN); updateBucketStateStatement.bindLong(2, bucketId); updateBucketStateStatement.execute(); long affectedRows = getAffectedRowCount(); if (affectedRows > 0) { Log.i(TAG, "Successfully updated state for bucket ID [" + bucketId + "] for log records: " + affectedRows); } else { Log.w(TAG, "No log records were updated"); } } catch (SQLiteException ex) { Log.e(TAG, "Failed to update state for bucket [" + bucketId + "]", ex); } } } @Override public void removeBucket(int recordBlockId) { synchronized (database) { Log.d(TAG, "Removing record block with id [" + recordBlockId + "] from storage"); if (deleteByBucketIdStatement == null) { try { deleteByBucketIdStatement = database.compileStatement( PersistentLogStorageConstants.KAA_DELETE_BY_BUCKET_ID); } catch (SQLiteException ex) { Log.e(TAG, "Can't create record block deletion statement", ex); throw new RuntimeException(ex); } } try { deleteByBucketIdStatement.bindLong(1, recordBlockId); deleteByBucketIdStatement.execute(); long removedRecordsCount = getAffectedRowCount(); if (removedRecordsCount > 0) { totalRecordCount -= removedRecordsCount; Log.i(TAG, "Removed " + removedRecordsCount + " records from storage. Total log record count: " + totalRecordCount); } else { Log.i(TAG, "No records were removed from storage"); } } catch (SQLiteException ex) { Log.e(TAG, "Failed to remove record block with id [" + recordBlockId + "]", ex); } } } @Override public void rollbackBucket(int bucketId) { synchronized (database) { Log.d(TAG, "Notifying upload fail for bucket id: " + bucketId); if (resetBucketIdStatement == null) { try { resetBucketIdStatement = database.compileStatement( PersistentLogStorageConstants.KAA_RESET_BY_BUCKET_ID); } catch (SQLiteException ex) { Log.e(TAG, "Can't create bucket id reset statement", ex); throw new RuntimeException(ex); } } try { resetBucketIdStatement.bindLong(1, bucketId); resetBucketIdStatement.execute(); long affectedRows = getAffectedRowCount(); if (affectedRows > 0) { Log.i(TAG, "Total " + affectedRows + " log records reset for bucket id: [" + bucketId + "]"); long previouslyConsumedSize = consumedMemoryStorage.remove(bucketId); unmarkedConsumedSize += previouslyConsumedSize; unmarkedRecordCount += affectedRows; } else { Log.i(TAG, "No log records for bucket with id: [" + bucketId + "]"); } } catch (SQLiteException ex) { Log.e(TAG, "Failed to reset bucket with id [" + bucketId + "]", ex); } } } @Override public void close() { tryCloseStatement(insertStatement); tryCloseStatement(deleteByBucketIdStatement); tryCloseStatement(resetBucketIdStatement); tryCloseStatement(updateBucketStateStatement); if (database != null) { database.close(); } if (dbHelper != null) { dbHelper.close(); } } @Override public long getConsumedVolume() { return unmarkedConsumedSize; } @Override public long getRecordCount() { return unmarkedRecordCount; } private void moveToNextBucket() { this.currentBucketSize = 0; this.currentRecordCount = 0; this.currentBucketId++; } private void retrieveBucketId() { Cursor cursor = null; try { cursor = database.rawQuery(PersistentLogStorageConstants.KAA_SELECT_MAX_BUCKET_ID, null); if (cursor.moveToFirst()) { int currentBucketId = cursor.getInt(0); if (currentBucketId == 0) { Log.d(TAG, "Can't retrieve max bucket ID. Seems there is no logs"); return; } this.currentBucketId = ++currentBucketId; } } catch (SQLiteException ex) { Log.e(TAG, "Can't create select max bucket ID statement", ex); throw new RuntimeException("Can't create select max bucket ID statement"); } finally { try { tryCloseCursor(cursor); } catch (SQLiteException ex) { Log.e(TAG, "Unable to close cursor", ex); } } } private void retrieveConsumedSizeAndVolume() { synchronized (database) { Cursor cursor = null; try { cursor = database.rawQuery(PersistentLogStorageConstants.KAA_HOW_MANY_LOGS_IN_DB, null); if (cursor.moveToFirst()) { unmarkedRecordCount = totalRecordCount = cursor.getLong(0); unmarkedConsumedSize = cursor.getLong(1); Log.i(TAG, "Retrieved record count: " + totalRecordCount + "," + " consumed size: " + unmarkedConsumedSize); } else { Log.e(TAG, "Unable to retrieve consumed size and volume"); throw new RuntimeException("Unable to retrieve consumed size and volume"); } } finally { tryCloseCursor(cursor); } } } private void truncateIfBucketSizeIncompatible() { Cursor cursor = null; int lastSavedBucketSize = 0; int lastSavedRecordCount = 0; try { cursor = database.rawQuery(PersistentLogStorageConstants.KAA_SELECT_STORAGE_INFO, new String[]{PersistentLogStorageConstants.STORAGE_BUCKET_SIZE}); if (cursor.moveToFirst()) { lastSavedBucketSize = cursor.getInt(0); } } catch (SQLiteException ex) { Log.e(TAG, "Cannot retrieve storage param: bucketSize", ex); throw new RuntimeException("Cannot retrieve storage param: bucketSize"); } finally { tryCloseCursor(cursor); } try { cursor = database.rawQuery(PersistentLogStorageConstants.KAA_SELECT_STORAGE_INFO, new String[]{PersistentLogStorageConstants.STORAGE_RECORD_COUNT}); if (cursor.moveToFirst()) { lastSavedRecordCount = cursor.getInt(0); } } catch (SQLiteException ex) { Log.e(TAG, "Cannot retrieve storage param: recordCount", ex); throw new RuntimeException("Cannot retrieve storage param: recordCount"); } finally { tryCloseCursor(cursor); } try { if (lastSavedBucketSize != maxBucketSize || lastSavedRecordCount != maxRecordCount) { database.execSQL(PersistentLogStorageConstants.KAA_DELETE_ALL_DATA); } } catch (SQLiteException ex) { Log.e(TAG, "Can't prepare delete statement", ex); throw new RuntimeException("Can't prepare delete statement"); } finally { tryCloseCursor(cursor); } updateStorageParams(); } private void updateStorageParams() { SQLiteStatement updateInfoStatement = null; try { updateInfoStatement = database.compileStatement( PersistentLogStorageConstants.KAA_UPDATE_STORAGE_INFO); updateInfoStatement.bindString(1, PersistentLogStorageConstants.STORAGE_BUCKET_SIZE); updateInfoStatement.bindLong(2, maxBucketSize); updateInfoStatement.execute(); updateInfoStatement.bindString(1, PersistentLogStorageConstants.STORAGE_RECORD_COUNT); updateInfoStatement.bindLong(2, maxRecordCount); updateInfoStatement.execute(); } catch (SQLiteException ex) { Log.e(TAG, "Can't prepare update storage info statement", ex); throw new RuntimeException("Can't prepare update storage info statement"); } finally { tryCloseStatement(updateInfoStatement); } } private void resetBucketsState() { synchronized (database) { Log.d(TAG, "Resetting bucket ids on application start"); database.execSQL(PersistentLogStorageConstants.KAA_RESET_BUCKET_STATE_ON_START); long updatedRows = getAffectedRowCount(); Log.v(TAG, "Number of rows affected: " + updatedRows); } } private long getAffectedRowCount() { synchronized (database) { Cursor cursor = null; try { cursor = database.rawQuery(GET_CHANGES_QUERY, null); if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) { return cursor.getLong(cursor.getColumnIndex(CHANGES_QUERY_RESULT)); } else { return 0; } } finally { tryCloseCursor(cursor); } } } private void tryCloseCursor(Cursor cursor) { if (cursor != null) { cursor.close(); } } private void tryCloseStatement(SQLiteStatement statement) { if (statement != null) { statement.close(); } } }