org.akop.crosswords.Storage.java Source code

Java tutorial

Introduction

Here is the source code for org.akop.crosswords.Storage.java

Source

// 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());
        }
    }
}