Java tutorial
// Copyright (c) 2014-2015 Akop Karapetyan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package org.akop.crosswords; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.SystemClock; import android.support.v4.content.LocalBroadcastManager; import android.util.SparseArray; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import org.akop.xross.object.Crossword; import org.joda.time.DateTime; import java.lang.reflect.Type; public final class Storage { private static final String DATABASE_NAME = "crosswords.sqlite"; private static final int DATABASE_VERSION = 1; public static final String ACTION_PUZZLE_CHANGE = "org.akop.crosswords.action.Write"; public static final String ACTION_PUZZLE_STATE_CHANGE = "org.akop.crosswords.PuzzleStateChanged"; public static final String INTENT_PUZZLE_ID = "puzzleId"; public static final String INTENT_PUZZLE_URL = "puzzleUrls"; public static final long FOLDER_INBOX = 1; public static final long FOLDER_ARCHIVES = 2; public static final long FOLDER_TRASH = 3; public static class Puzzle { public static final String TABLE = "Puzzles"; public static final String _ID = "_id"; public static final String SOURCE_ID = "sourceId"; public static final String CLASS = "class"; public static final String SOURCE_URL = "sourceUrl"; public static final String TITLE = "title"; public static final String AUTHOR = "author"; public static final String COPYRIGHT = "copyright"; public static final String HASH = "hash"; public static final String DATE = "date"; public static final String FOLDER_ID = "folderId"; public static final String OBJECT = "object"; public static final String OBJECT_VERSION = "objectVersion"; public static final String LAST_UPDATED = "lastUpdated"; } public static class PuzzleState { public static final String TABLE = "PuzzleStates"; public static final String _ID = "_id"; public static final String PUZZLE_ID = "puzzleId"; public static final String CLASS = "class"; public static final String PERCENT_SOLVED = "percentSolved"; public static final String PERCENT_CHEATED = "percentCheated"; public static final String PERCENT_WRONG = "percentWrong"; public static final String PLAY_TIME_MILLIS = "playTimeMillis"; public static final String OBJECT = "object"; public static final String OBJECT_VERSION = "objectVersion"; public static final String LAST_PLAYED = "lastPlayed"; public static final String LAST_UPDATED = "lastUpdated"; } public static class Folder { public static final String TABLE = "Folders"; public static final String _ID = "_id"; public static final String TITLE = "title"; } public static final long ID_NOT_FOUND = -1; private static Storage sInstance = null; private SparseArray<Crossword> mPuzzleCache; private SparseArray<Crossword.State> mStateCache; private Gson mGson; public static Storage getInstance() { if (sInstance == null) { sInstance = new Storage(); } return sInstance; } private Storage() { mPuzzleCache = new SparseArray<>(); mStateCache = new SparseArray<>(); mGson = new GsonBuilder().registerTypeAdapter(DateTime.class, new DateTimeAdapter()) .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY).create(); } private StorageHelper getHelper() { return new StorageHelper(Crosswords.getInstance()); } private void sendLocalBroadcast(Intent intent) { Crosswords app = Crosswords.getInstance(); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(app); lbm.sendBroadcast(intent); } public SQLiteDatabase getDatabase(boolean writable) { if (writable) { return getHelper().getWritableDatabase(); } else { return getHelper().getReadableDatabase(); } } public long findByHash(String hash) { StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getReadableDatabase(); long id = ID_NOT_FOUND; try { Cursor cursor = db.query(Puzzle.TABLE, new String[] { Puzzle._ID }, Puzzle.HASH + "=?", new String[] { hash }, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { id = cursor.getLong(0); } } finally { cursor.close(); } } } finally { db.close(); } return id; } public long findBySourceUrl(String sourceUrl) { StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getReadableDatabase(); long id = ID_NOT_FOUND; try { Cursor cursor = db.query(Puzzle.TABLE, new String[] { Puzzle._ID }, Puzzle.SOURCE_URL + " = ?", new String[] { sourceUrl }, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { id = cursor.getLong(0); } } finally { cursor.close(); } } } finally { db.close(); } return id; } public Crossword getPuzzle(long puzzleId) { long started = SystemClock.uptimeMillis(); Crossword crossword = mPuzzleCache.get((int) puzzleId); if (crossword == null) { StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getReadableDatabase(); try { Cursor cursor = db.query(Puzzle.TABLE, new String[] { Puzzle.CLASS, Puzzle.OBJECT, }, Puzzle._ID + " = " + puzzleId, null, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { crossword = mGson.fromJson(cursor.getString(1), Crossword.class); mPuzzleCache.put((int) puzzleId, crossword); } } finally { cursor.close(); } } } finally { db.close(); } } if (crossword != null) { Crosswords.logv("Loaded crossword %s (%dms)", crossword.getHash(), SystemClock.uptimeMillis() - started); } return crossword; } public Crossword.State getPuzzleState(long puzzleId) { long started = SystemClock.uptimeMillis(); Crossword.State state = mStateCache.get((int) puzzleId); if (state == null) { StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getReadableDatabase(); try { Cursor cursor = db.query(PuzzleState.TABLE, new String[] { PuzzleState.CLASS, PuzzleState.OBJECT, }, PuzzleState.PUZZLE_ID + "=" + puzzleId, null, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { state = mGson.fromJson(cursor.getString(1), Crossword.State.class); mStateCache.put((int) puzzleId, new Crossword.State(state)); } } finally { cursor.close(); } } } finally { db.close(); } } if (state != null) { Crosswords.logv("Loaded state for %d (%dms)", puzzleId, SystemClock.uptimeMillis() - started); } return state; } public boolean emptyFolder(long folderId) { StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getWritableDatabase(); int puzzlesDeleted; int statesDeleted; try { statesDeleted = db.delete(PuzzleState.TABLE, PuzzleState.PUZZLE_ID + " IN (" + "SELECT " + Puzzle._ID + " " + "FROM " + Puzzle.TABLE + " " + "WHERE " + Puzzle.FOLDER_ID + "=" + folderId + ")", null); puzzlesDeleted = db.delete(Puzzle.TABLE, Puzzle.FOLDER_ID + "=" + folderId, null); } finally { db.close(); } if (puzzlesDeleted > 0) { Intent outgoing = new Intent(ACTION_PUZZLE_CHANGE); sendLocalBroadcast(outgoing); } Crosswords.logv("Deleted %d puzzles and %d states", puzzlesDeleted, statesDeleted); return puzzlesDeleted > 0; } public boolean moveTo(long folderId, long... puzzleIds) { if (puzzleIds.length < 1) { return false; } StringBuilder whereBuilder = new StringBuilder(); whereBuilder.append(Puzzle._ID); whereBuilder.append(" IN ("); for (int i = 0, n = puzzleIds.length - 1; i <= n; i++) { whereBuilder.append(puzzleIds[i]); if (i < n) { whereBuilder.append(","); } } whereBuilder.append(")"); StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getWritableDatabase(); int rowsUpdated = 0; ContentValues cv = new ContentValues(); cv.put(Puzzle.FOLDER_ID, folderId); try { rowsUpdated += db.update(Puzzle.TABLE, cv, whereBuilder.toString(), null); } finally { db.close(); } if (rowsUpdated > 0) { Intent outgoing = new Intent(ACTION_PUZZLE_CHANGE); sendLocalBroadcast(outgoing); } Crosswords.logv("Moved %d puzzles to folder id %d", rowsUpdated, folderId); return rowsUpdated > 0; } public long write(long folderId, Crossword crossword) { return write(folderId, ID_NOT_FOUND, crossword); } public long write(long folderId, long puzzleId, Crossword crossword) { long started = SystemClock.uptimeMillis(); StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getWritableDatabase(); ContentValues cv; try { cv = new ContentValues(); cv.put(Puzzle.CLASS, crossword.getClass().getName()); cv.put(Puzzle.SOURCE_URL, crossword.getSourceUrl()); cv.put(Puzzle.TITLE, crossword.getTitle()); cv.put(Puzzle.AUTHOR, crossword.getAuthor()); cv.put(Puzzle.COPYRIGHT, crossword.getCopyright()); cv.put(Puzzle.HASH, crossword.getHash()); cv.put(Puzzle.SOURCE_ID, crossword.getSourceId()); cv.put(Puzzle.FOLDER_ID, folderId); cv.put(Puzzle.OBJECT, mGson.toJson(crossword)); cv.put(Puzzle.OBJECT_VERSION, 1); cv.put(Puzzle.LAST_UPDATED, System.currentTimeMillis()); Long millis = null; if (crossword.getDate() != null) { millis = crossword.getDate().getMillis(); } cv.put(Puzzle.DATE, millis); if (puzzleId != ID_NOT_FOUND) { db.update(Puzzle.TABLE, cv, Puzzle._ID + " = " + puzzleId, null); } else { puzzleId = db.insert(Puzzle.TABLE, null, cv); } } finally { db.close(); } Crosswords.logv("Wrote crossword %s (%dms)", crossword.getHash(), SystemClock.uptimeMillis() - started); Intent outgoing = new Intent(ACTION_PUZZLE_CHANGE); outgoing.putExtra(INTENT_PUZZLE_ID, puzzleId); outgoing.putExtra(INTENT_PUZZLE_URL, crossword.getSourceUrl()); sendLocalBroadcast(outgoing); return puzzleId; } public void write(long puzzleId, Crossword.State state) { long started = SystemClock.uptimeMillis(); ContentValues cv = new ContentValues(); cv.put(PuzzleState.PUZZLE_ID, puzzleId); cv.put(PuzzleState.CLASS, state.getClass().getName()); cv.put(PuzzleState.OBJECT, mGson.toJson(state)); cv.put(PuzzleState.OBJECT_VERSION, 1); cv.put(PuzzleState.PERCENT_SOLVED, state.getPercentSolved()); cv.put(PuzzleState.PERCENT_CHEATED, state.getPercentCheated()); cv.put(PuzzleState.PERCENT_WRONG, state.getPercentWrong()); cv.put(PuzzleState.PLAY_TIME_MILLIS, state.getPlayTimeMillis()); long lastPlayed = 0; if (state.getLastPlayed() != null) { lastPlayed = state.getLastPlayed().getMillis(); } cv.put(PuzzleState.LAST_PLAYED, lastPlayed); cv.put(PuzzleState.LAST_UPDATED, System.currentTimeMillis()); StorageHelper helper = getHelper(); SQLiteDatabase db = helper.getWritableDatabase(); try { // Delete any existing rows db.delete(PuzzleState.TABLE, PuzzleState.PUZZLE_ID + "=" + puzzleId, null); // Insert the new one db.insert(PuzzleState.TABLE, null, cv); } finally { db.close(); } Crosswords.logv("Wrote state for %d (%dms)", puzzleId, SystemClock.uptimeMillis() - started); // Add a copy to the cache mStateCache.put((int) puzzleId, new Crossword.State(state)); // Broadcast the change Intent intent = new Intent(ACTION_PUZZLE_STATE_CHANGE); intent.putExtra(INTENT_PUZZLE_ID, puzzleId); Context context = Crosswords.getInstance(); LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } private static class StorageHelper extends SQLiteOpenHelper { private Context mContext; public StorageHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + Puzzle.TABLE + " (" + Puzzle._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + Puzzle.SOURCE_ID + " TEXT NOT NULL, " + Puzzle.CLASS + " TEXT NOT NULL, " + Puzzle.OBJECT + " TEXT NOT NULL, " + Puzzle.OBJECT_VERSION + " INTEGER NOT NULL, " + Puzzle.SOURCE_URL + " TEXT, " + Puzzle.TITLE + " TEXT NOT NULL, " + Puzzle.AUTHOR + " TEXT, " + Puzzle.COPYRIGHT + " TEXT, " + Puzzle.HASH + " TEXT, " + Puzzle.DATE + " INTEGER, " + Puzzle.FOLDER_ID + " INTEGER NOT NULL, " + Puzzle.LAST_UPDATED + " INTEGER NOT NULL)"); db.execSQL("CREATE INDEX ix_source_url ON " + Puzzle.TABLE + " (" + Puzzle.SOURCE_URL + " ASC)"); db.execSQL("CREATE INDEX ix_hash ON " + Puzzle.TABLE + " (" + Puzzle.HASH + " ASC)"); db.execSQL("CREATE INDEX ix_source_date ON " + Puzzle.TABLE + " (" + Puzzle.SOURCE_ID + " ASC, " + Puzzle.DATE + " ASC)"); db.execSQL("CREATE TABLE " + PuzzleState.TABLE + " (" + PuzzleState._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + PuzzleState.PUZZLE_ID + " INTEGER NOT NULL, " + PuzzleState.CLASS + " TEXT NOT NULL, " + PuzzleState.OBJECT + " TEXT NOT NULL, " + PuzzleState.OBJECT_VERSION + " INTEGER NOT NULL, " + PuzzleState.PERCENT_SOLVED + " INTEGER NOT NULL, " + PuzzleState.PERCENT_CHEATED + " INTEGER NOT NULL, " + PuzzleState.PERCENT_WRONG + " INTEGER NOT NULL, " + PuzzleState.PLAY_TIME_MILLIS + " INTEGER NOT NULL, " + PuzzleState.LAST_PLAYED + " INTEGER NOT NULL, " + PuzzleState.LAST_UPDATED + " INTEGER NOT NULL)"); db.execSQL( "CREATE INDEX ix_puzzle_id ON " + PuzzleState.TABLE + " (" + PuzzleState.PUZZLE_ID + " ASC)"); db.execSQL("CREATE TABLE " + Folder.TABLE + " (" + Folder._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + Folder.TITLE + " TEXT NOT NULL)"); // Insert the core folders db.execSQL("INSERT INTO " + Folder.TABLE + " (" + Folder._ID + "," + Folder.TITLE + ") " + "VALUES (" + FOLDER_INBOX + ",?)", new Object[] { mContext.getString(R.string.inbox), }); db.execSQL("INSERT INTO " + Folder.TABLE + " (" + Folder._ID + "," + Folder.TITLE + ") " + "VALUES (" + FOLDER_ARCHIVES + ",?)", new Object[] { mContext.getString(R.string.archives), }); db.execSQL("INSERT INTO " + Folder.TABLE + " (" + Folder._ID + "," + Folder.TITLE + ") " + "VALUES (" + FOLDER_TRASH + ",?)", new Object[] { mContext.getString(R.string.trash), }); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } } public static class DateTimeAdapter implements JsonSerializer<DateTime>, JsonDeserializer<DateTime> { public JsonElement serialize(DateTime dateTime, Type type, JsonSerializationContext context) { return new JsonPrimitive(dateTime.toString()); } @Override public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { return new DateTime(json.getAsString()); } } }