/*
* Copyright (C) 2016 Google Inc. All Rights Reserved.
*
* 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 com.google.android.apps.santatracker.doodles.shared;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Maintains the history and stats of what the user has accomplished.
*
* <p>Note that this class handles the serializing into JSON instead of each game. This was done
* to make it easier to make a game picker that showed your status on each game. Since there are
* canonical types it would then know how to read them. We add a setArbitraryData and
* getArbitaryData for any game that wants to put other kind of information in.
*/
public class HistoryManager {
private static final String TAG = HistoryManager.class.getSimpleName();
public static final String BEST_PLACE_KEY = "place";
public static final String BEST_STAR_COUNT_KEY = "stars";
public static final String BEST_TIME_MILLISECONDS_KEY = "time";
public static final String BEST_SCORE_KEY = "score";
public static final String BEST_DISTANCE_METERS_KEY = "distance";
public static final String ARBITRARY_DATA_KEY = "arb";
private static final String FILENAME = "history.json";
private final Context context;
private volatile JSONObject history;
/**
* Listener for when the history is loaded.
*/
public static interface HistoryListener {
public void onFinishedLoading();
public void onFinishedSaving();
}
private HistoryListener listener;
/**
* Creates a history manager.
* HistoryListener can be null.
*/
public HistoryManager(Context context, HistoryListener listener) {
this.context = context;
this.listener = listener;
// While history is loading from disk, we ignore any changes clients might ask for.
history = null;
load();
}
public void setListener(HistoryListener listener) {
this.listener = listener;
}
/**
* Gets the json object for a particular game type.
*/
private JSONObject getGameObject(GameType gameType) throws JSONException {
if (history == null) {
throw new JSONException("null history");
}
JSONObject gameObject = history.optJSONObject(gameType.toString());
if (gameObject == null) {
gameObject = new JSONObject();
}
return gameObject;
}
/************************ Setters *****************************/
/**
* Set the best place (1st, 2nd, 3rd) for a game type.
* NOTE: It's expected for the client to figure out if it is the best place.
*/
public void setBestPlace(GameType gameType, int place) {
try {
JSONObject gameObject = getGameObject(gameType);
gameObject.put(BEST_PLACE_KEY, place);
history.put(gameType.toString(), gameObject);
} catch (JSONException e) {
Log.e(TAG, "error setting place", e);
}
}
/**
* Set the best star count for a game type.
* NOTE: It's expected for the client to figure out if it is the best star count.
*/
public void setBestStarCount(GameType gameType, int count) {
try {
JSONObject gameObject = getGameObject(gameType);
gameObject.put(BEST_STAR_COUNT_KEY, count);
history.put(gameType.toString(), gameObject);
} catch (JSONException e) {
Log.e(TAG, "error setting place", e);
}
}
/**
* Set the best time for a game type.
* NOTE: it's expected for the client to figure out if it is the best time since some will want
* bigger and some will want smaller numbers.
*/
public void setBestTime(GameType gameType, long timeInMilliseconds) {
try {
JSONObject gameObject = getGameObject(gameType);
gameObject.put(BEST_TIME_MILLISECONDS_KEY, timeInMilliseconds);
history.put(gameType.toString(), gameObject);
} catch (JSONException e) {
Log.e(TAG, "error setting time", e);
}
}
/**
* Set the best score for a game type.
* NOTE: it's expected for the client to figure out if it is the best score since some will want
* bigger and some will want smaller numbers.
*/
public void setBestScore(GameType gameType, double score) {
try {
JSONObject gameObject = getGameObject(gameType);
gameObject.put(BEST_SCORE_KEY, score);
history.put(gameType.toString(), gameObject);
} catch (JSONException e) {
Log.e(TAG, "error setting score", e);
}
}
/**
* Set the best distance for a game type.
* NOTE: it's expected for the client to figure out if it is the best distance since some will
* want bigger and some will want smaller numbers.
*/
public void setBestDistance(GameType gameType, double distanceInMeters) {
try {
JSONObject gameObject = getGameObject(gameType);
gameObject.put(BEST_DISTANCE_METERS_KEY, distanceInMeters);
history.put(gameType.toString(), gameObject);
} catch (JSONException e) {
Log.e(TAG, "error setting distance", e);
}
}
/**
* Sets an arbitrary jsonObject a game might want.
*/
public void setArbitraryData(GameType gameType, JSONObject data) {
try {
JSONObject gameObject = getGameObject(gameType);
gameObject.put(ARBITRARY_DATA_KEY, data);
history.put(gameType.toString(), gameObject);
} catch (JSONException e) {
Log.e(TAG, "error setting distance", e);
}
}
/************************ Getters *****************************/
/**
* Returns the best place so far. Null if no value has been given yet.
*/
public Integer getBestPlace(GameType gameType) {
try {
JSONObject gameObject = getGameObject(gameType);
return gameObject.getInt(BEST_PLACE_KEY);
} catch (JSONException e) {
return null;
}
}
/**
* Returns the best star count so far. Null if no value has been given yet.
*/
public Integer getBestStarCount(GameType gameType) {
try {
JSONObject gameObject = getGameObject(gameType);
return gameObject.getInt(BEST_STAR_COUNT_KEY);
} catch (JSONException e) {
return null;
}
}
/**
* Returns the best time so far. Null if no value has been given yet.
*/
public Long getBestTime(GameType gameType) {
try {
JSONObject gameObject = getGameObject(gameType);
return gameObject.getLong(BEST_TIME_MILLISECONDS_KEY);
} catch (JSONException e) {
return null;
}
}
/**
* Returns the best score so far. Null if no value has been given yet.
*/
public Double getBestScore(GameType gameType) {
try {
JSONObject gameObject = getGameObject(gameType);
return gameObject.getDouble(BEST_SCORE_KEY);
} catch (JSONException e) {
return null;
}
}
/**
* Returns the best distance so far. Null if no value has been given yet.
*/
public Double getBestDistance(GameType gameType) {
try {
JSONObject gameObject = getGameObject(gameType);
return gameObject.getDouble(BEST_DISTANCE_METERS_KEY);
} catch (JSONException e) {
return null;
}
}
/**
* Returns arbitrary JSONObject a game might want. Null if no value has been given yet.
*/
public JSONObject getArbitraryData(GameType gameType) {
try {
JSONObject gameObject = getGameObject(gameType);
return gameObject.getJSONObject(ARBITRARY_DATA_KEY);
} catch (JSONException e) {
return null;
}
}
/********************** File Management **************************/
/**
* Saves the file in the background.
*/
public void save() {
new AsyncTask<Void, Void, Void> () {
@Override
protected Void doInBackground(Void... params) {
try {
FileOutputStream outputStream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE);
byte[] bytes = history.toString().getBytes();
outputStream.write(bytes);
outputStream.close();
Log.i(TAG, "Saved: " + history);
} catch (IOException e) {
Log.w(TAG, "Couldn't save JSON at: " + FILENAME);
} catch (Exception e) {
Log.w(TAG, "Crazy exception happened", e);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (listener != null) {
listener.onFinishedSaving();
}
}
}.execute();
}
/**
* Loads the history object from file. Then merges with any changes that might have occured while
* we waited for it to load.
*/
private void load() {
new AsyncTask<Void, Void, Void> () {
@Override
protected Void doInBackground(Void... params) {
try {
File file = new File(context.getFilesDir(), FILENAME);
int length = (int) file.length();
if (length <= 0) {
history = new JSONObject();
return null;
}
byte[] bytes = new byte[length];
FileInputStream inputStream = new FileInputStream(file);
inputStream.read(bytes);
inputStream.close();
history = new JSONObject(new String(bytes, "UTF-8"));
Log.i(TAG, "Loaded: " + history);
} catch (JSONException e) {
Log.w(TAG, "Couldn't create JSON for: " + FILENAME);
} catch (UnsupportedEncodingException e) {
Log.d(TAG, "Couldn't decode: " + FILENAME);
} catch (IOException e) {
Log.w(TAG, "Couldn't read history: " + FILENAME);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (listener != null) {
listener.onFinishedLoading();
}
}
}.execute();
}
}