/* * This source is part of the * _____ ___ ____ * __ / / _ \/ _ | / __/___ _______ _ * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/ * \___/_/|_/_/ |_/_/ (_)___/_/ \_, / * /___/ * repository. * * Copyright (C) 2015 Carmen Alvarez (c@rmen.ca) * * 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 ca.rmen.android.networkmonitor.app.dbops.backend.clean; import android.content.Context; import android.database.Cursor; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.SparseArray; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import ca.rmen.android.networkmonitor.Constants; import ca.rmen.android.networkmonitor.R; import ca.rmen.android.networkmonitor.app.dbops.ProgressListener; import ca.rmen.android.networkmonitor.app.dbops.backend.DBOperation; import ca.rmen.android.networkmonitor.provider.NetMonColumns; import ca.rmen.android.networkmonitor.util.Log; /** * Reduces groups of 3 or more consecutive rows with identical data (except the timestamp) into a single row. */ public class DBCompress implements DBOperation { private static final String TAG = Constants.TAG + DBCompress.class.getSimpleName(); private final Context mContext; private final AtomicBoolean mIsCanceled = new AtomicBoolean(false); public DBCompress(Context context) { mContext = context; } @Override public void execute(ProgressListener listener) { Log.v(TAG, "compress DB"); Cursor c = mContext.getContentResolver().query(NetMonColumns.CONTENT_URI, null, null, null, BaseColumns._ID); SparseArray<String> previousRow = null; List<Integer> rowIdsToDelete = new ArrayList<>(); int idLastRow = 0; int posLastNewRow = 0; if (c != null) { try { int rowCount = c.getCount(); int columnCount = c.getColumnCount(); // We will not include the _id and _timestamp fields when comparing rows. int timestampIndex = c.getColumnIndex(NetMonColumns.TIMESTAMP); int idIndex = c.getColumnIndex(BaseColumns._ID); while (c.moveToNext() && !mIsCanceled.get()) { int position = c.getPosition(); int id = c.getInt(idIndex); SparseArray<String> currentRow = readRow(c, columnCount, timestampIndex, idIndex); if (previousRow != null) { boolean rowsAreEqual = areEqual(previousRow, currentRow); if (rowsAreEqual) { // If we've seen at least 3 consecutive identical rows, // delete the previous row. // Ex: if rows 21, 22, 23, and 24 are identical, we'll add // the ids of rows 22 and 23 to the list. if (position - posLastNewRow >= 2) { rowIdsToDelete.add(idLastRow); } } else { posLastNewRow = position; } } if (listener != null) listener.onProgress(position, rowCount); idLastRow = id; previousRow = currentRow; } } finally { c.close(); } } int numRowsDeleted = 0; int numRowsToDelete = rowIdsToDelete.size(); Log.v(TAG, "compress DB: " + numRowsToDelete + " rows to delete"); StringBuilder inClause = new StringBuilder(); for (int i = 0; i < numRowsToDelete; i++) { inClause.append(rowIdsToDelete.get(i)); if (i % 100 == 0 || i == numRowsToDelete - 1) { String whereClause = BaseColumns._ID + " in (" + inClause + ")"; numRowsDeleted += mContext.getContentResolver().delete(NetMonColumns.CONTENT_URI, whereClause, null); Log.v(TAG, "compress DB: deleted " + numRowsDeleted + " rows"); inClause = new StringBuilder(); } else { inClause.append(","); } } if (listener != null) { if (numRowsToDelete >= 0) { if(mIsCanceled.get()) listener.onError(mContext.getString(R.string.compress_notif_canceled_content)); else listener.onComplete(mContext.getResources().getQuantityString(R.plurals.compress_notif_complete_content, numRowsDeleted, numRowsDeleted)); } else { listener.onError(mContext.getString(R.string.compress_notif_error_content)); } } } private static boolean areEqual(SparseArray<String> o1, SparseArray<String> o2) { if (o1.size() != o2.size()) return false; for (int i = 0; i < o1.size(); i++) { if (o1.keyAt(i) != (o2.keyAt(i))) return false; if (!TextUtils.equals(o1.get(i), o2.get(i))) return false; } return true; } @Override public void cancel() { mIsCanceled.set(true); } /** * @return a map of the row's values: the key is the column index, and the value the string representation of a cell. */ private static SparseArray<String> readRow(Cursor c, int columnCount, int timestampIndex, int idIndex) { SparseArray<String> result = new SparseArray<>(); for (int i = 0; i < columnCount; i++) { if (i == timestampIndex || i == idIndex) continue; result.put(i, c.getString(i)); } return result; } }