/*
* Copyright 2013 Google Inc.
*
* 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.laquysoft.droidconnl;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.text.format.Time;
import android.util.Log;
import android.widget.ImageView;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.FileNotFoundException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Random;
public class Hunt {
private static final String TAG = "Hunt";
public static final int QUESTION_STATE_NONE = 0;
public static final int QUESTION_STATE_INTRO = 1;
public static final int QUESTION_STATE_QUESTIONING = 2;
public static final String QUESTION_STATE_KEY = "QUESTION_STATE_KEY";
public static final String FINISH_TIME_KEY = "FINISH_TIME_KEY";
static Hunt theHunt;
static HuntResourceManager hrm;
public Boolean isShuffled = false;
public int questionState = 0;
public long finishTime;
public SoundManager soundManager;
public AchievementManager achievementManager;
HashMap<String, Boolean> tagsFound;
HashMap<String, AHTag> tags;
HashMap<String, Clue> clues;
ArrayList<Clue> clueList;
ArrayList<AHTag> tagList;
private boolean hasSeenIntro = false;
static final String HAS_SEEN_INTRO_KEY = "HAS_SEEN_INTRO_KEY";
static final String WRONG_CLUE = "WRONG CLUE";
static final String ACK = "ACK";
static final String CLUE_COMPLETE = "CLUE COMPLETE";
static final String ALREADY_FOUND = "ALREADY FOUND";
static final String DECOY = "DECOY";
// The actual text for the DECOY clue.
static final String DECOY_ID = "decoy";
private DownloadManager downloadManager;
private long downloadReference;
/** Returns the singleton hunt object, and initializes it if it's not ready. */
public static Hunt getHunt(Resources res, Context context) {
if (theHunt == null) {
hrm = new HuntResourceManager();
hrm.unzipFile(res);
theHunt = new Hunt(hrm.huntJSON, res, context);
String android_id = Secure.getString(context.getContentResolver(),
Secure.ANDROID_ID);
if (android_id == null) {
// Fall back on devices where ANDROID_ID is not reliable.
theHunt.shuffle(Integer.parseInt(Settings.Secure.ANDROID_ID, 0));
} else {
BigInteger bi = new BigInteger(android_id, 16);
System.out.println(bi);
theHunt.shuffle(bi.shortValue());
}
}
return theHunt;
}
/** Shuffles the clues. Note that each clue is marked with
* a difficulty group, so that, say, a hard clue can't preceed
* an easier clue.
* @param seed The random number seed.
*/
public void shuffle(int seed) {
if (isShuffled) {
return;
}
// Divide into shuffle groups
ArrayList<ArrayList<Clue>> groups = new ArrayList<ArrayList<Clue>>(10);
for (int i = 0; i < 10; i++) {
groups.add(null);
}
for (int i = 0; i < clueList.size(); i++) {
Clue c = clueList.get(i);
if (groups.get(c.shufflegroup) == null) {
groups.set(c.shufflegroup, new ArrayList<Clue>());
}
groups.get(c.shufflegroup).add(c);
}
clueList = new ArrayList<Clue>();
Random r = new Random(seed);
for (int i = 0; i < 10; i++) {
ArrayList<Clue> cl = groups.get(i);
if (cl == null)
continue;
Collections.shuffle(cl, r);
clueList.addAll(cl);
}
isShuffled = true;
}
/**
* Saves the player's progress.
*
* @param res
* @param context
*/
public void save(Resources res, Context context) {
SharedPreferences sharedPref = context.getSharedPreferences(
res.getString(R.string.preference_file_key),
Context.MODE_PRIVATE);
Editor editor = sharedPref.edit();
// XXX encrypt this?
for (String key : tagsFound.keySet()) {
editor.putBoolean(key, tagsFound.get(key));
}
editor.putBoolean(HAS_SEEN_INTRO_KEY, hasSeenIntro);
editor.putInt(QUESTION_STATE_KEY, questionState);
editor.putLong(FINISH_TIME_KEY, finishTime);
editor.commit();
}
/** Loads player progress. */
public void restore(Resources res, Context context) {
SharedPreferences sharedPref = context.getSharedPreferences(
res.getString(R.string.preference_file_key),
Context.MODE_PRIVATE);
for (String tag : tags.keySet()) {
Boolean val = sharedPref.getBoolean(tag, false);
tagsFound.put(tag, val);
}
hasSeenIntro = sharedPref.getBoolean(HAS_SEEN_INTRO_KEY, false);
questionState = sharedPref.getInt(QUESTION_STATE_KEY,
QUESTION_STATE_NONE);
finishTime = sharedPref.getLong(FINISH_TIME_KEY, 0);
}
/** Returns whether or not we're in a question so we can restore itself. */
public int getQuestionState() {
return questionState;
}
/** Generates the entire hunt structure from JSON */
Hunt(String jsonString, Resources res, Context context) {
//set filter to only when download is complete and register broadcast receiver
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
context.registerReceiver(downloadReceiver, filter);
soundManager = new SoundManager(context);
achievementManager = new AchievementManager(res);
try {
clues = new HashMap<String, Clue>();
clueList = new ArrayList<Clue>();
tags = new HashMap<String, AHTag>();
JSONObject huntObject = new JSONObject(jsonString);
JSONArray clueObjList = huntObject.getJSONArray("clues");
tagList = new ArrayList<AHTag>();
int length = clueObjList.length();
for (int i = 0; i < length; i++) {
JSONObject clueObj = clueObjList.getJSONObject(i);
Clue clue = new Clue(clueObj.getString("id"),
clueObj.getString("displayName"),
clueObj.getString("displayText"),
clueObj.getString("displayImage"));
clue.shufflegroup = clueObj.getInt("shufflegroup");
JSONArray tagObjList = clueObj.getJSONArray("tags");
int tagLength = tagObjList.length();
clueList.add(clue);
for (int j = 0; j < tagLength; j++) {
JSONObject tagObj = tagObjList.getJSONObject(j);
AHTag tag = new AHTag(tagObj.getString("id"));
tag.clueId = clue.id;
clue.addTag(tag);
tags.put(tag.id, tag);
tagList.add(tag);
}
if (clueObj.has("question")) {
clue.question = new TriviaQuestion(
clueObj.getJSONObject("question"));
}
}
} catch (Exception e) {
if (e != null)
Log.e("JSON Parser", "Error parsing Hunt data " + e.toString());
}
reset();
restore(res, context);
}
/** Deletes all player progress.*/
public void reset() {
tagsFound = new HashMap<String, Boolean>();
for (AHTag tag : tagList) {
tagsFound.put(tag.id, false);
}
// I'm not currently asking a question
questionState = QUESTION_STATE_NONE;
hasSeenIntro = false;
}
/** Gets active clue.
*
* One objection to this is that we are computing progress rather than remembering.
*
* This is deliberate; if we cache progress, we are likely
* to get be wrong, especially during state transitions. The number of
* operations here is very small and it is called infrequently, so this saves us the
* trouble of maintaining and persisting progress.
*
* If this were a performance issue (called in an inner loop, for example), of course
* we would cache this.
* @return The current clue, or else null if you are finished.
*/
Clue getCurrentClue() {
int length = clueList.size();
for (int i = 0; i < length; i++) {
Clue clue = clueList.get(i);
if (isClueFinished(clue) && questionState != QUESTION_STATE_NONE) {
// We're still asking a question
return clue;
}
if (!isClueFinished(clue)) {
return clue;
}
}
// The hunt is complete!
return null;
}
/** What clue have I *just* completed? */
public Clue getLastCompletedClue() {
int length = clueList.size();
Clue lastBestClue = null;
for (int i = 0; i < length; i++) {
Clue clue = clueList.get(i);
if (!isClueFinished(clue)) {
return lastBestClue;
}
lastBestClue = clue;
}
// The hunt is complete.
return lastBestClue;
}
public Boolean isTagFound(String id) {
if (!tagsFound.containsKey(id)) {
return false;
}
return tagsFound.get(id);
}
/**
* Called when a tag is scanned. Checks the hunt
*
* @param tagId the short string that represents the tag found.
* @return
*/
String findTag(String tagId) {
if (tagId.equals(DECOY_ID)) {
return DECOY;
}
// See if this tag is part of this clue
Clue clue = getCurrentClue();
AHTag tag = tags.get(tagId);
if (tag == null) {
return WRONG_CLUE;
}
if (clue.id.equals(tag.clueId)) {
if (isTagFound(tagId)) {
return ALREADY_FOUND;
}
tagsFound.put(tag.id, true);
if (isClueFinished(clue)) {
return CLUE_COMPLETE;
}
return ACK;
}
return WRONG_CLUE;
}
/** Have we found all the clues? Does not check question completeness. */
Boolean isClueFinished(Clue clue) {
for (AHTag tag : clue.tags) {
if (!isTagFound(tag.id)) {
return false;
}
}
return true;
}
public int getTotalClues() {
return clueList.size();
}
/** Count from 1. */
public int getClueDisplayNumber(Clue clue) {
return clueList.indexOf(clue) + 1;
}
/** Count from 0. */
public int getClueIndex(Clue clue) {
return clueList.indexOf(clue);
}
/** Return value: Whether or not it needs to load from web. */
public void setClueImage(Resources res, ImageView imgView) {
final Clue clue = getCurrentClue();
if (hrm.drawables.get(clue.displayImage) == null) {
imgView.setImageDrawable(res.getDrawable(R.drawable.ab_icon));
} else {
imgView.setImageDrawable(hrm.drawables.get(clue.displayImage));
}
}
public boolean hasSeenIntro() {
return hasSeenIntro;
}
public void setIntroSeen(Boolean val) {
hasSeenIntro = val;
}
public void setQuestionState(int state) {
questionState = state;
}
public void setStartTime() {
finishTime = SystemClock.uptimeMillis() + 15000;
}
public Time getFinishTime() {
Time t = new Time();
t.set(finishTime);
return t;
}
public int getSecondsLeft() {
long t = SystemClock.uptimeMillis();
return (int) Math.ceil((finishTime - t) / 1000.0);
}
public boolean isComplete() {
return (getCurrentClue() == null);
}
public boolean hasAnsweredQuestion(Clue c) {
int currentClueNum = getClueIndex(c);
// Find the first question in the list and see if we're past it.
int totalClues = clueList.size();
for (int i = 0; i < totalClues; i++) {
if (clueList.get(i).question != null) {
return currentClueNum > i;
}
}
return false;
}
public void reload(Context context) {
reloadFromRemote(context);
}
private BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//check if the broadcast message is for our Enqueued download
long referenceId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (downloadReference == referenceId) {
ParcelFileDescriptor file;
try {
file = downloadManager.openDownloadedFile(downloadReference);
//Log.d(TAG,"Downloaded " + downloadManager.getUriForDownloadedFile(downloadReference));
hrm = new HuntResourceManager();
hrm.unzipDownloadedFile(file);
theHunt = new Hunt(hrm.huntJSON, context.getResources(), context);
String android_id = Secure.getString(context.getContentResolver(),
Secure.ANDROID_ID);
if (android_id == null) {
// Fall back on devices where ANDROID_ID is not reliable.
theHunt.shuffle(Integer.parseInt(Settings.Secure.ANDROID_ID, 0));
} else {
BigInteger bi = new BigInteger(android_id, 16);
System.out.println(bi);
theHunt.shuffle(bi.shortValue());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
};
public void reloadFromRemote(Context context) {
downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Uri Download_Uri = Uri.parse("http://162.248.167.159:8080/zip/hunt.zip");
DownloadManager.Request request = new DownloadManager.Request(Download_Uri);
request.setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI
| DownloadManager.Request.NETWORK_MOBILE)
.setAllowedOverRoaming(false).setTitle("NFC/IO Hunt")
.setDescription("Downloading Hunt data");
//Enqueue a new download and same the referenceId
downloadReference = downloadManager.enqueue(request);
}
}