/** Copyright 2015 Tim Engler, Rareventure LLC This file is part of Tiny Travel Tracker. Tiny Travel Tracker is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Tiny Travel Tracker is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>. */ package com.rareventure.android.database; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.http.client.CircularRedirectException; import android.database.Cursor; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import com.rareventure.android.DbUtil; import com.rareventure.android.SuperThread; import com.rareventure.android.database.Cache.DatastoreAccessor; import com.rareventure.android.encryption.EncryptedRow; import com.rareventure.gps2.GTG; import com.rareventure.gps2.database.TAssert; import com.rareventure.gps2.database.cache.AreaPanelCache; import com.rareventure.util.CircularList; import com.rareventure.util.Pair; import com.rareventure.util.ReadWriteThreadManager; public abstract class Cache<T extends CachableRow> { public static interface DatastoreAccessor<T extends CachableRow> { /** * * @return the next row id for inserting, * usually one plus maximum id */ int getNextRowId(); void updateRow(T row); void insertRow(T row); /** * * @param outRow * @param id * @return true if the row was retrieved */ boolean getRow(T outRow, int id); void softUpdateRow(T row); boolean needsSoftUpdate(); } private HashMap<Integer, T> idToCache; private HashMap<Integer, T> idToDirtyRow; public int misses; public int hits; private int maxCacheSize; private int nextRowId = Integer.MIN_VALUE; private CircularList<T> cache; private DatastoreAccessor<T> da; public Cache(DatastoreAccessor<T> da, int maxCacheSize) { this.da = da; this.maxCacheSize = maxCacheSize; cache = new CircularList<T>(maxCacheSize); clear(); } public synchronized void clear() { cache.clear(); //PERF: convert to SparseArray(), note we will need to implement entrySet() idToCache = new HashMap<Integer, T>(); idToDirtyRow = new HashMap<Integer, T>(); nextRowId = da.getNextRowId(); } public synchronized int getDirtyRowCount() { return this.idToDirtyRow.size(); } public synchronized boolean isEmpty() { return idToCache.isEmpty(); } public final synchronized T newRow() { T row = allocateRow(); row.isDirty = true; row.isInserted = false; row.id = nextRowId++; idToDirtyRow.put(row.id, row); return row; } public synchronized void notifyRowUpdated(T row) { if(!row.isDirty) { //ignore rows that don't have an id yet if(row.id == -1) return; idToDirtyRow.put(row.id, row); row.isDirty = true; row.referencedRecently = true; } } //TODO 4: implement delete (if you want) public void writeDirtyRows() { writeDirtyRows(null); } /** * Commits dirty rows to the database. * This is meant only to be called by the same thread that is inserting or * updating the rows * * WARNING: Again, writing to the data while calling this method is not thread safe */ public void writeDirtyRows(ReadWriteThreadManager rwtm) { /* ttt_installer:remove_line */Log.d(GTG.TAG,"Commiting "+idToDirtyRow.size()+" rows for "+this); ArrayList<Entry<Integer, T>> al = new ArrayList<Entry<Integer, T>>(idToDirtyRow.entrySet()); //sort it by id because timmy tables demand that inserts be done in ascending // consecutive order Collections.sort(al, new Comparator<Entry<Integer, T>>() { @Override public int compare(Entry<Integer, T> lhs, Entry<Integer, T> rhs) { return lhs.getKey() - rhs.getKey(); } }); if(da.needsSoftUpdate()) { for(Map.Entry<Integer,T> e : al) { SuperThread.abortOrPauseIfNecessary(); T r = e.getValue(); if(r.isInserted) { // Log.d(GTG.TAG,"soft update for "+r.id); da.softUpdateRow(r); } if(rwtm != null && rwtm.isReadingThreadsActive()) { rwtm.pauseForReadingThreads(); } } } for(Map.Entry<Integer,T> e : al) { SuperThread.abortOrPauseIfNecessary(); T r = e.getValue(); if(r.isInserted) { // Log.d(GTG.TAG,"hard update for "+r.id); da.updateRow(r); } else da.insertRow(r); synchronized (this) { r.isDirty = false; r.isInserted = true; //Log.d("GTS", "i = "+e.getKey()+" id is "+r.id); //put the dirty rows in the cache, because they probably will be used r.referencedRecently = true; if(!idToCache.containsKey(r.id)) putRowInCache(r); } if(rwtm != null && rwtm.isReadingThreadsActive()) { rwtm.pauseForReadingThreads(); } } //TODO 4 the below explanation is no longer the case, fix? //note that we don't clear the rows here. This is to work well with timmy tables, which //don't support access to row being committed } public void clearDirtyRows() { idToDirtyRow.clear(); } abstract protected T allocateRow(); protected T allocateTopRow() { return allocateRow(); } public synchronized T getRow(int id) { T row = getRowNoFail(id); if(row == null) TAssert.fail("can't find cache row for id "+id); return row; } public synchronized T getRowNoFail(int id) { T er = idToDirtyRow.get(id); if(er != null) return er; //now check the proper cache T cacheRow = idToCache.get(id); //if not in the cache if(cacheRow == null) { misses++; //get row from the db cacheRow = getCacheRowFromDb(id); if(cacheRow == null) return null; //put it into the cache putRowInCache(cacheRow); } else { hits++; cacheRow.referencedRecently = true; } return cacheRow; } private void putRowInCache(T cacheRow) { // Log.d(GpsTrailer.TAG,"putting "+cacheRow+" in cache"); idToCache.put(cacheRow.id, cacheRow); cacheRow.referencedRecently = true; // // Now we need to update the cache and remove an item if we've gone over the limit // //remove old one if necessary if(cache.size() == maxCacheSize) { int count = 0; while(true) { CachableRow rowToReplace = cache.getLast(); if(rowToReplace.referencedRecently) { rowToReplace.referencedRecently = false; cache.moveLastToFirst(); } else { //TODO 2.5: Should we have ViewNodes start using ids so that // we don't get a copy of the same row in the cache that is different // than the one in ViewNode? // if(rowToReplace.id == AreaPanelCache.TOP_ROW_ID) // Log.e(GTG.TAG, "What???? Why is top row being removed "+rowToReplace); // cache.replaceLast(cacheRow); //remove the item from the cache idToCache.remove(rowToReplace.id); break; } count++; } //Log.d(GTG.TAG, "set "+count+" nodes to referencedRecently=false for putRowInCache"); // Log.d(GpsTrailer.TAG,"removing "+rowToRemove.id+" from cache"); } else cache.addNoExpand(cacheRow); } /** * Gets data from database, bypassing the cache */ private T getCacheRowFromDb(int id) { T cacheRow = allocateRow(); if(!da.getRow(cacheRow, id)) return null; // Log.d(GpsTrailer.TAG,"loaded "+cacheRow+" from db"); cacheRow.isInserted = true; return cacheRow; } public int getNextRowId() { return da.getNextRowId(); } }