/*
* 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.tilt;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.google.android.apps.santatracker.doodles.shared.Actor;
import com.google.android.apps.santatracker.doodles.shared.ExternalStoragePermissions;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.List;
/**
* A helper class which handles the saving and loading of levels for the doodle games.
*
* The {@code LevelManager} stores levels in a simple JSON format. The {@code Actor} classes wishing
* to be managed by a {@code LevelManager} should handle their own serialization and
* deserialization.
*/
public abstract class LevelManager<T extends TiltModel> {
public static final String TAG = LevelManager.class.getSimpleName();
private static final String ACTORS_KEY = "actors";
protected final Context context;
private final ExternalStoragePermissions storagePermissions;
public LevelManager(Context context) {
this(context, new ExternalStoragePermissions());
}
@VisibleForTesting
LevelManager(Context context, ExternalStoragePermissions storagePermissions) {
this.context = context;
this.storagePermissions = storagePermissions;
}
/**
* Saves a level to persistent storage.
*
* @param level The level to save.
* @param filename The name of the level's file on disk. This file will be stored inside of a
* preset directory, defined by the LevelManager implementation.
*/
public void saveLevel(T level, String filename) {
File levelsDir = getLevelsDir();
if (!levelsDir.exists() && !levelsDir.mkdirs()) {
// If we are unable to find or make the desired output directory, log a warning and fail.
Log.w(TAG, "Unable to reach dir: " + levelsDir.getAbsolutePath());
return;
}
try {
FileOutputStream outputStream = new FileOutputStream(new File(levelsDir, filename));
saveLevel(level, outputStream);
} catch (FileNotFoundException e) {
Log.w(TAG, "Unable to save file: " + filename);
}
}
/**
* Writes a level to the provided OutputStream.
*
* @param level The level to save.
* @param outputStream The stream to which the level should be written.
*/
@VisibleForTesting
void saveLevel(T level, OutputStream outputStream) {
try {
saveActors(level.getActors(), outputStream);
} catch (JSONException e) {
Log.w(TAG, "Unable to create level JSON.");
} catch (IOException e) {
Log.w(TAG, "Unable to write actors to output stream.");
}
}
/**
* Loads a level from persistent storage.
*
* <p>This loads first tries to load a level from assets, falling back to external storage if
* necessary. If it still cannot load the level, the default level will be loaded.</p>
*
* @param filename The name of the file to load. This file should be stored in a preset directory,
* specified by the LevelManager implementation.
* @return The loaded level.
*/
public T loadLevel(String filename) {
if (filename == null) {
Log.w(TAG, "Couldn't load level with null filename, using default level instead.");
return loadDefaultLevel();
}
BufferedReader externalStorageReader = null;
try {
externalStorageReader =
new BufferedReader(
new InputStreamReader(
new FileInputStream(new File(getLevelsDir(), filename)), "UTF-8"));
} catch (IOException e) {
Log.d(TAG, "Unable to load file from external storage: " + filename);
}
BufferedReader assetsReader = null;
try {
assetsReader =
new BufferedReader(new InputStreamReader(context.getAssets().open(filename), "UTF-8"));
} catch (IOException e) {
Log.d(TAG, "Unable to load file from assets: " + filename);
}
T model = loadLevel(externalStorageReader, assetsReader);
model.setLevelName(filename);
return model;
}
/**
* Loads a level from either assets, or from external storage.
*
* <p>This first tries to load a level from assets, falling back to external storage if
* necessary. If it still cannot load the level, the default level will be loaded.</p>
*
* <p>This method should only be used for testing LevelManager. Real use cases should generally
* use {@code loadLevel(String filename)}.</p>
*
* @param externalStorageReader
* @param assetsInputReader
* @return The loaded level.
*/
@VisibleForTesting
T loadLevel(BufferedReader externalStorageReader, BufferedReader assetsInputReader) {
JSONObject json = null;
if (assetsInputReader != null) {
json = readLevelJson(assetsInputReader);
Log.d(TAG, "Loaded level from assets.");
}
if (json == null
&& externalStorageReader != null
&& storagePermissions.isExternalStorageReadable()) {
json = readLevelJson(externalStorageReader);
Log.d(TAG, "Loaded level from external storage.");
}
if (json == null) {
Log.w(TAG, "Couldn't load level data, using default level instead.");
return loadDefaultLevel();
}
T model = getEmptyModel();
try {
JSONArray actors = json.getJSONArray(ACTORS_KEY);
for (int i = 0; i < actors.length(); i++) {
Actor actor = loadActorFromJSON(actors.getJSONObject(i));
if (actor != null) {
model.addActor(actor);
}
}
} catch (JSONException e) {
Log.w(TAG, "Couldn't load actors, using default level instead.");
return loadDefaultLevel();
}
return model;
}
/**
* Initializes the default level and returns it.
*
* <p>In general, this should be an empty or minimal level.</p>
*
* @return the initialized default level.
*/
public abstract T loadDefaultLevel();
/**
* Returns the external storage directory within which levels of this type should be saved.
*
* @return The base directory which should be used to save levels.
*/
protected abstract File getLevelsDir();
/**
* Loads a single actor from JSON.
*
* @param json The JSON representation of the actor to be loaded
* @return The loaded actor, or null if the actor could not be loaded.
* @throws JSONException if the JSON is malformed, or fails to be parsed.
*/
@VisibleForTesting
abstract Actor loadActorFromJSON(JSONObject json) throws JSONException;
/**
* Returns an empty, or minimal model of the appropriate type. Generally, this should be the same
* as asking for a {@code new T()}.
*
* @return The empty model.
*/
protected abstract T getEmptyModel();
/**
* Returns a JSONArray containing the JSON representation of the passed-in list of actors.
*
* <p>If a given actor does not provide a JSON representation, it will not appear in the returned
* JSONArray.</p>
*
* @param actors The actors to convert into JSON.
* @return The JSON representation of the list of actors.
* @throws JSONException If the parsing of an actor into JSON fails.
*/
@VisibleForTesting
JSONArray getActorsJson(List<Actor> actors) throws JSONException {
JSONArray actorsJson = new JSONArray();
for (Actor actor : actors) {
JSONObject json = actor.toJSON();
if (json != null) {
actorsJson.put(json);
}
}
return actorsJson;
}
/**
* Writes a list of actors to an OutputStream.
*
* @param actors The actors to be written.
* @param outputStream The OuputStream which should be used to write the list of actors.
* @throws JSONException If the conversion of actors into JSON fails.
* @throws IOException If we fail to write to the OutputStream.
*/
private void saveActors(List<Actor> actors, OutputStream outputStream)
throws JSONException, IOException {
JSONObject json = new JSONObject();
json.put(ACTORS_KEY, getActorsJson(actors));
Log.d(TAG, json.toString(2));
if (storagePermissions.isExternalStorageWritable()) {
writeLevelJson(json, outputStream);
} else {
Log.w(TAG, "External storage is not writable");
}
}
/**
* Writes a JSONObject to an OutputStream.
*
* @param json The object to be written.
* @param outputStream The stream to be written to.
* @throws IOException If we fail to write to the OutputStream.
*/
private void writeLevelJson(JSONObject json, OutputStream outputStream) throws IOException {
outputStream.write(json.toString().getBytes());
outputStream.close();
}
/**
* Read a JSONObject from a BufferedReader.
*
* @param reader The BufferedReader which contains the JSON to be read.
* @return A JSONObject parsed from the contents of the BufferedReader.
*/
private JSONObject readLevelJson(BufferedReader reader) {
try {
String levelData = "";
String line = reader.readLine();
while (line != null) {
levelData += line;
line = reader.readLine();
}
reader.close();
return new JSONObject(levelData);
} catch (IOException e) {
Log.w(TAG, "readLevelJson: Couldn't read JSON.");
} catch (JSONException e) {
Log.w(TAG, "readLevelJson: Couldn't create JSON.");
}
return null;
}
}