/*
* Copyright 2015 Daniel Dittmar
*
* 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 dan.dit.whatsthat.riddle;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import dan.dit.whatsthat.image.Image;
import dan.dit.whatsthat.riddle.types.PracticalRiddleType;
import dan.dit.whatsthat.riddle.types.RiddleType;
import dan.dit.whatsthat.solution.Solution;
import dan.dit.whatsthat.storage.ImagesContentProvider;
import dan.dit.whatsthat.storage.RiddleTable;
import dan.dit.whatsthat.util.general.BuildException;
import dan.dit.whatsthat.util.general.PercentProgressListener;
/**
* Basic riddle class that describes an unsolved riddle instance. Can be decorated by a RiddleGame
* to create a game that is playable by the user.<br>
* As a Riddle keeps the state of input and of any previously played RiddleGame and this is kept in
* the database, you should not keep too many instances, some RiddleGames produce an awful lot of data to
* accurately reproduce their state.
*/
public class Riddle {
/**
* The key to identify a parameter that describes the last visible unsolved riddle that was closed
* and saved when the app shut down or stopped. Useful on restart to pretent nothing happened.
*/
public static final String LAST_VISIBLE_UNSOLVED_RIDDLE_ID_KEY = "dan.dit.whatsthat.unsolved_riddle_id_key";
/**
* The key to identify a parameter that describes the last visible unsolved riddle type
* saved when the riddle fragment shut down or stopped.
*/
private static final String LAST_VISIBLE_RIDDLE_TYPE_FULL_NAME_ID_KEY = "dan.dit.whatsthat.last_visible_riddle_type_fullname_key";
/**
* A constant that is to be used when an id parameter or result value is around that is not a valid id of any riddle.
*/
public static final long NO_ID = -1L;
/**
* Prefix of the riddle's origin if the riddle was remade after already being active as a
* riddle of another type. This is making the riddle getting treatment of a custom riddle
* limiting achievements and score gains.
*/
private static final String ORIGIN_REMADE_TO_NEW_TYPE_PREFIX = "REMADE_TYPE";
private String mSolutionData;
private int mSolved; // if solved only the core needs to be saved, if not yet started solving no need to save to database
Core mCore;
private String mCurrentState; // the current state after closing
private String mAchievementData; // the achievement data after closing
public Riddle(String hash, PracticalRiddleType type, String origin) {
mCore = new Core(origin, hash, type);
mSolved = Solution.SOLVED_NOTHING;
}
public Riddle(String hash, PracticalRiddleType type, String origin, String solutionData) {
this(hash, type, origin);
mSolutionData = solutionData;
}
private Riddle(Core core, Cursor cursor) throws BuildException {
buildFromCursor(cursor, core);
}
public long getId() {
return mCore.mId;
}
public PracticalRiddleType getType() {
return mCore.mRiddleType;
}
/**
* Closes this riddle keeping (compacted) state that can be written to persistant memory.
* @param solvedValue The solved value of the SolutionInput.
* @param score The score.
* @param currentState The current state of the RiddleGame.
* @param achievementData The data that describes the AchievementData used by the RiddleGame.
* @param solutionData The current state of the SolutionInput.
*/
public void onClose(int solvedValue, int score, String currentState, String achievementData, String solutionData) {
mSolved = solvedValue;
mCore.mScore = score;
mAchievementData = achievementData;
mCurrentState = currentState;
mSolutionData = solutionData;
}
/**
* Returns a drawable to describe this riddle for easier recognition. If possible
* (that is, the riddle produced and saved a snapshot of itself and this snapshot is still in cache)
* this is an accurate snapshot of the RiddleGame, else the icon of the riddle type.
* @param res The resources used to retrieve the snapshot. If null the result might be null.
* @return A snapshot of the riddle. Might be null (especially if res is null).
*/
public Drawable getSnapshot(Resources res) {
Bitmap snapshot = RiddleManager.getFromCache(this);
if (snapshot != null && res != null) {
return new BitmapDrawable(res, snapshot);
} else {
return mCore.mRiddleType.getIcon(res);
}
}
public String getCurrentState() {
return mCurrentState;
}
public String getSolutionData() {
return mSolutionData;
}
public String getImageHash() {
return mCore.getImageHash();
}
public static void saveLastVisibleRiddleId(Context context, long id) {
context.getSharedPreferences(Image.SHAREDPREFERENCES_FILENAME, Context.MODE_PRIVATE).edit()
.putLong(LAST_VISIBLE_UNSOLVED_RIDDLE_ID_KEY, id).apply();
}
public static long getLastVisibleRiddleId(Context context) {
return context.getSharedPreferences(Image.SHAREDPREFERENCES_FILENAME, Context.MODE_PRIVATE).
getLong(LAST_VISIBLE_UNSOLVED_RIDDLE_ID_KEY, Riddle.NO_ID);
}
public static void saveLastVisibleRiddleType(Context context, PracticalRiddleType type) {
context.getSharedPreferences(Image.SHAREDPREFERENCES_FILENAME, Context.MODE_PRIVATE).edit()
.putString(LAST_VISIBLE_RIDDLE_TYPE_FULL_NAME_ID_KEY, type == null ? null : type.getFullName()).apply();
}
public static PracticalRiddleType getLastVisibleRiddleType(Context context) {
String typeFullName = context.getSharedPreferences(Image.SHAREDPREFERENCES_FILENAME, Context.MODE_PRIVATE).
getString(LAST_VISIBLE_RIDDLE_TYPE_FULL_NAME_ID_KEY, null);
if (typeFullName != null) {
return PracticalRiddleType.getInstance(typeFullName);
}
return null;
}
public int getScore() {
return mCore.mScore;
}
public static int[] loadSolvedRiddlesCountAndScore(Context context, RiddleInitializer.InitTask commandingTask) {
Cursor cursor = context.getContentResolver().query(ImagesContentProvider.CONTENT_URI_RIDDLE_SOLVED,
new String[] {RiddleTable.COLUMN_SCORE}, null, null, null);
final int PROGRESS_FOR_LOADING_CURSOR = 15;
if (commandingTask.isCancelled()) {return null;}
commandingTask.onProgressUpdate(PROGRESS_FOR_LOADING_CURSOR);
cursor.moveToFirst();
int colScore = cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_SCORE);
int score = 0;
int solvedCount = cursor.getCount();
while (!cursor.isAfterLast()) {
score += cursor.getInt(colScore);
cursor.moveToNext();
if (commandingTask.isCancelled()) {return null;}
commandingTask.onProgressUpdate(PROGRESS_FOR_LOADING_CURSOR + (PercentProgressListener.PROGRESS_COMPLETE - PROGRESS_FOR_LOADING_CURSOR) * (cursor.getPosition() + 1) / cursor.getCount());
}
Log.d("Riddle", "Loaded solved riddles count " + solvedCount + " loaded score " + score);
cursor.close();
commandingTask.onInitComplete();
return new int[]{solvedCount, score};
}
public long getTimestamp() {
return mCore.mTimestamp;
}
public String getAchievementData() {
return mAchievementData;
}
public String getOrigin() {
return mCore.mOrigin;
}
/**
* Checks if this riddle is remade. If this is not the case this simply returns 0 and this is
* a normal riddle made by the app. Else it returns the amount of times the riddle was remade
* starting from 1 for first time remade riddles. See Riddle.makeRemadeOrigin(int) for
* generating this origin information.
* @return 0 if this is a usual riddle, else the positive remade count number. If parsing
* the count of a remade riddle fails, -1 is returned.
*/
public int getRemadeCount() {
if (!mCore.mOrigin.startsWith(Riddle.ORIGIN_REMADE_TO_NEW_TYPE_PREFIX)) {
return 0;
}
try {
int start = Riddle.ORIGIN_REMADE_TO_NEW_TYPE_PREFIX.length();
int end = mCore.mOrigin.indexOf("_", start);
String numberText = mCore.mOrigin.substring(start, end == -1 ? mCore.mOrigin.length()
: end);
return TextUtils.isEmpty(numberText) ? 1 : Integer.parseInt(numberText);
} catch (NumberFormatException nfe) {
return -1;
}
}
public boolean isRemade() {
return getRemadeCount() != 0;
}
/**
* Creates a riddle origin to use for a new riddle that is being remade.
* @param remadeCount The amount of times this riddle was remade. If positive this
* information is appended to the origin.
* @return A valid origin indicating that the riddle was at least once remade.
*/
public static @NonNull String makeRemadeOrigin(int remadeCount) {
if (remadeCount <= 0) {
return ORIGIN_REMADE_TO_NEW_TYPE_PREFIX;
}
return ORIGIN_REMADE_TO_NEW_TYPE_PREFIX + Integer.toString(remadeCount);
}
/**
* Checks if this riddle is a custom riddle. That is any riddle that is not a new regular
* riddle, so a riddle with a non default origin. Can be a remade riddle or a riddle sent from
* another user.
* @return If this is a custom riddle.
*/
public boolean isCustom() {
return !Image.ORIGIN_IS_THE_APP.equalsIgnoreCase(mCore.mOrigin);
}
/****************** CORE ****************************/
public static class Core {
private long mId = NO_ID;
private long mTimestamp;
private String mOrigin;
private String mImageHash;
private PracticalRiddleType mRiddleType;
private int mScore;
/**
* Creates a new Riddle core for a new riddle.
* @param origin The origin of the riddle. If empty then origin is the app.
* @param imageHash The hash of the image used.
* @param type The type of the riddle.
*/
private Core(@Nullable String origin,@NonNull String imageHash,@NonNull PracticalRiddleType type) {
mId = RiddleInitializer.INSTANCE.nextId();
mTimestamp = System.currentTimeMillis();
mOrigin = TextUtils.isEmpty(origin) ? Image.ORIGIN_IS_THE_APP : origin;
mImageHash = imageHash;
mRiddleType = type;
}
private Core(Cursor cursor) throws BuildException {
buildFromCursor(cursor);
}
public String getImageHash() {
return mImageHash;
}
ContentValues makeContentValues() {
ContentValues cv = new ContentValues();
cv.put(RiddleTable.COLUMN_ID, mId);
cv.put(RiddleTable.COLUMN_TIMESTAMP, mTimestamp);
cv.put(RiddleTable.COLUMN_ORIGIN, mOrigin);
cv.put(RiddleTable.COLUMN_IMAGEHASH, mImageHash);
cv.put(RiddleTable.COLUMN_SCORE, mScore);
cv.put(RiddleTable.COLUMN_RIDDLETYPE, mRiddleType.getFullName());
return cv;
}
private void buildFromCursor(Cursor cursor) throws BuildException {
mId = cursor.getLong(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_ID));
mTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_TIMESTAMP));
mOrigin = cursor.getString(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_ORIGIN));
mImageHash = cursor.getString(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_IMAGEHASH));
mScore = cursor.getInt(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_SCORE));
mRiddleType = PracticalRiddleType.getInstance(cursor.getString(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_RIDDLETYPE)));
if (TextUtils.isEmpty(mOrigin)) {
mOrigin = Image.ORIGIN_IS_THE_APP;
}
if (!isId(mId) || TextUtils.isEmpty(mImageHash) || mRiddleType == null) {
throw new BuildException().setMissingData("RiddleCore", "ID " + mId + " riddle type " + mRiddleType + " hash " + mImageHash);
}
}
private static boolean isId(long id) {
return id != NO_ID;
}
@Override
public boolean equals(Object other) {
if (other instanceof Core) {
return this == other || mId == ((Core) other).mId;
} else {
return super.equals(other);
}
}
@Override
public int hashCode() {
return (int) mId;
}
@Override
public String toString() {
return "Core " + mId + " of type " + mRiddleType;
}
public long getId() {
return mId;
}
}
@Override
public String toString() {
return "Riddle, solved=" + mSolved + ", " + mCore;
}
private void buildFromCursor(Cursor cursor, Core core) throws BuildException {
if (cursor.isAfterLast()) {
throw new BuildException().setMissingData("Riddle", "No cursor data.");
}
mCore = core;
mSolved = cursor.getInt(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_SOLVED));
mCurrentState = cursor.getString(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_CURRENTSTATE));
mAchievementData = cursor.getString(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_ACHIEVEMENTDATA));
mSolutionData = cursor.getString(cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_SOLUTION));
}
private ContentValues makeContentValues() {
ContentValues cv = mCore.makeContentValues();
cv.put(RiddleTable.COLUMN_SOLVED, mSolved);
cv.put(RiddleTable.COLUMN_CURRENTSTATE, mCurrentState);
cv.put(RiddleTable.COLUMN_ACHIEVEMENTDATA, mAchievementData);
cv.put(RiddleTable.COLUMN_SOLUTION, mSolutionData);
return cv;
}
private static long saveToDatabase(Context context, ContentValues cv) {
cv.put(ImagesContentProvider.SQL_INSERT_OR_REPLACE, true);
Uri uri = context.getContentResolver().insert(ImagesContentProvider.CONTENT_URI_RIDDLE, cv);
if (uri != null) {
Log.d("Riddle", "Saved riddle " + cv.get(RiddleTable.COLUMN_IMAGEHASH) + " to database: " + uri.getLastPathSegment());
return Long.parseLong(uri.getLastPathSegment());
} else {
return NO_ID;
}
}
public boolean isSolved() {
return mSolved == Solution.SOLVED_COMPLETELY;
}
public boolean saveToDatabase(Context context) {
long id = saveToDatabase(context, makeContentValues());
if (Core.isId(id)) {
// successfully replaced or newly added
mCore.mId = id; // set id anyways to prevent accidentally saving riddle in two rows
return true;
}
return false;
}
public static boolean deleteFromDatabase(Context context, long id) {
return Core.isId(id) && context.getContentResolver().delete(ImagesContentProvider.CONTENT_URI_RIDDLE, RiddleTable.COLUMN_ID + "=?", new String[]{Long.toString(id)}) > 0;
}
static Map<RiddleType, Set<String>> loadUsedImagesForTypes(@NonNull Context context, @NonNull RiddleInitializer.InitTask commandingTask) {
Cursor cursor = context.getContentResolver().query(ImagesContentProvider.CONTENT_URI_RIDDLE,
new String[] {RiddleTable.COLUMN_RIDDLETYPE, RiddleTable.COLUMN_IMAGEHASH}, null, null, RiddleTable.COLUMN_TIMESTAMP + " DESC");
final int PROGRESS_FOR_LOADING_CURSOR = 15;
Map<RiddleType, Set<String>> used = new HashMap<>();
if (commandingTask.isCancelled()) {return used;}
commandingTask.onProgressUpdate(PROGRESS_FOR_LOADING_CURSOR);
cursor.moveToFirst();
int colRiddleType = cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_RIDDLETYPE);
int colImageHash = cursor.getColumnIndexOrThrow(RiddleTable.COLUMN_IMAGEHASH);
while (!cursor.isAfterLast()) {
RiddleType type = PracticalRiddleType.getInstance(cursor.getString(colRiddleType));
if (type != null) {
Set<String> typeSet = used.get(type);
if (typeSet == null) {
typeSet = new HashSet<>();
used.put(type, typeSet);
}
typeSet.add(cursor.getString(colImageHash));
}
cursor.moveToNext();
if (commandingTask.isCancelled()) {return used;}
commandingTask.onProgressUpdate(PROGRESS_FOR_LOADING_CURSOR + (PercentProgressListener.PROGRESS_COMPLETE - PROGRESS_FOR_LOADING_CURSOR) * (cursor.getPosition() + 1) / cursor.getCount());
}
Log.d("Riddle", "Loaded used images for types: " + used);
cursor.close();
commandingTask.onInitComplete();
return used;
}
static List<Riddle> loadUnsolvedRiddles(Context context, RiddleInitializer.InitTask commandingTask) {
Cursor cursor = context.getContentResolver().query(ImagesContentProvider.CONTENT_URI_RIDDLE_UNSOLVED,
RiddleTable.ALL_COLUMNS, null, null, RiddleTable.COLUMN_TIMESTAMP + " DESC");
final int PROGRESS_FOR_LOADING_CURSOR = 25;
List<Riddle> riddles = new ArrayList<>(cursor.getCount());
if (commandingTask.isCancelled()) {return riddles;}
commandingTask.onProgressUpdate(PROGRESS_FOR_LOADING_CURSOR);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
Core currCore = null;
try {
currCore = new Core(cursor);
} catch (BuildException exp) {
Log.e("Riddle", "Error building riddle core: " + exp.getMessage());
}
if (currCore != null) {
Riddle curr;
try {
curr = new Riddle(currCore, cursor);
} catch (BuildException exp) {
Log.e("Riddle", "Error building unsolved riddle: " + exp.getMessage());
curr = null;
}
if (curr != null) {
riddles.add(curr);
}
}
cursor.moveToNext();
if (commandingTask.isCancelled()) {return riddles;}
commandingTask.onProgressUpdate(PROGRESS_FOR_LOADING_CURSOR + (PercentProgressListener.PROGRESS_COMPLETE - PROGRESS_FOR_LOADING_CURSOR) * (cursor.getPosition() + 1)/ cursor.getCount());
}
cursor.close();
Log.d("Riddle", "Loaded unsolved riddles " + riddles);
commandingTask.onInitComplete();
return riddles;
}
}