/**
*
*/
package org.everest.simplescores;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
/**
* @author Rob Everest <rob.everest@gmail.com>
* A View that shows the high scores
*/
public class ScoreView extends ListView {
private static final String DEFAULT_SCORE_URL = "http://simplescores.appspot.com";
private static final String SCORE_TABLE = "/get";
private static final String SUBMIT_SCORE = "/submit";
private static final String ENCODING = "UTF-8";
private static final String HASH_ALGORITHM = "HmacSHA1";
private static final String KEY_BOARD_ID = "board_id";
private static final String KEY_SCORE_TABLE = "scores";
private static final String KEY_SCORE_TABLE_SIZE = "size";
private static final String KEY_NAME = "name";
private static final String KEY_COMMENT = "comment";
private static final String KEY_DISPLAY_SCORE = "display_score";
private static final String KEY_COMPARE_SCORE = "compare_score";
private static final String KEY_SIGNATURE = "sig";
public static final int ERROR_NONE = 0;
public static final int ERROR_NO_SCORES = 1;
public static final int ERROR_DOWNLOADING = 2;
public static final int ERROR_SUBMITTING = 3;
private ArrayList<Score> scores = null;
private ScoreAdapter adapter;
private String boardId;
private String privateKey;
private Score lowestScore = null;
private Score scoreToSubmit;
private long submittedScore;
private String submittedDisplayScore;
private boolean scoreSubmittable = false;
private Runnable errorRunnable;
private Runnable successRunnable;
private Runnable successfulSubmissionRunnable;
private Runnable startSubmissionRunnable;
private String scoreUrl;
private int evenItemsBackground;
private int oddItemsBackground;
private int inputPosition;
private Handler handler;
private int lastError = ERROR_NONE;
public ScoreView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public ScoreView(Context context) {
super(context);
init(context, null);
}
public ScoreView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
scores = new ArrayList<Score>();
this.adapter = new ScoreAdapter(context, R.layout.simple_scores_prompt_row, scores);
setAdapter(this.adapter);
scoreUrl = DEFAULT_SCORE_URL;
handler = new Handler(context.getMainLooper());
setClickable(false);
setFocusable(false);
setSelector(new ColorDrawable(0));
setDividerHeight(0);
setCacheColorHint(0);
if (attrs != null) {
evenItemsBackground = attrs.getAttributeResourceValue(null, "evenItemsBackground", R.drawable.evenItemsBackground);
oddItemsBackground = attrs.getAttributeResourceValue(null, "oddItemsBackground", R.drawable.oddItemsBackground);
}
}
/**
* Loads the list of scores in separate thread then tells the UI thread to update the list.
*/
public void loadScores() {
lastError = ERROR_NONE; //No error yet
adapter.clear();
scores.clear();
adapter.notifyDataSetInvalidated();
Thread thread = new Thread(null, getScores, "score_downloader");
thread.start();
}
public void submit(long scoreToSubmit, String scoreToDisplay) {
scoreSubmittable = true; //Assume true before scores have been loaded
submittedScore = scoreToSubmit;
submittedDisplayScore = scoreToDisplay;
loadScores();
}
private void startScoreSubmitter() {
lastError = ERROR_NONE;
Thread thread = new Thread(null, submitScore, "score_submitter");
thread.start();
if (startSubmissionRunnable != null)
startSubmissionRunnable.run();
}
/**
* Gets the last error that occurred while retrieving scores. Including no scores submitted yet.
* @return Either ERROR_NONE, ERROR_NO_SCORES, ERROR_DOWNLOADING
*/
public int getLastError() {
return lastError;
}
/**
* @return The Runnable that will be executed by the UI thread in the event there is an error retrieving the scores
*/
public Runnable getErrorRunnable() {
return errorRunnable;
}
/**
* @param The Runnable to be executed in the event of an error
*/
public void setError(Runnable errorRunnable) {
this.errorRunnable = errorRunnable;
}
/**
* @return The Runnable executed by the UI thread if scores are successfully loaded
*/
public Runnable getSuccessRunnable() {
return successRunnable;
}
/**
* @param The Runnable to be executed by the UI thread if scores are successfully loaded
*/
public void setSuccess(Runnable successRunnable) {
this.successRunnable = successRunnable;
}
/**
* @return The Runnable executed on the UI thread when a score is successfully submitted
*/
public Runnable getSuccessfulSubmissionRunnable() {
return successfulSubmissionRunnable;
}
/**
* @param The Runnable to be executed on the UI thread when a score is successfully submitted
*/
public void setSuccessfulSubmissionRunnable(
Runnable successfulSubmissionRunnable) {
this.successfulSubmissionRunnable = successfulSubmissionRunnable;
}
public Runnable getStartSubmissionRunnable() {
return startSubmissionRunnable;
}
public void setStartSubmissionRunnable(Runnable startSubmissionRunnable) {
this.startSubmissionRunnable = startSubmissionRunnable;
}
/**
* @return The url from which scores will be retrieved and new scores sent
*/
public String getScoreUrl() {
return scoreUrl;
}
/**
* Use this to set the url of the Simple Scores server.
* If not set the default server will be used.
* @param The url of the SimpleScores server.
*/
public void setScoreUrl(String scoreUrl) {
if (scoreUrl != null)
this.scoreUrl = scoreUrl;
}
/**
* @param The handler to be used when the View needs updating. By default it is the handler of the parent context.
*/
public void setHandler(Handler handler) {
this.handler = handler;
}
/**
* Sets the private key to use
* @param privateKey
*/
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
/**
* Sets the id of the board from which to load the scores
* @param boardId
*/
public void setBoardId(String boardId) {
this.boardId = boardId;
}
/**
* Converts a string of hex characters into a byte array
* @param s
* @return
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
private void submitScore(Score score) {
String reqString = "";
try {
reqString = scoreUrl + SUBMIT_SCORE + "?" +
KEY_BOARD_ID + "=" + boardId + "&" +
KEY_NAME + "=" + URLEncoder.encode(score.name, ENCODING) + "&" +
KEY_COMMENT + "=" + URLEncoder.encode(score.comment, ENCODING) + "&" +
KEY_DISPLAY_SCORE + "=" + score.displayScore + "&" +
KEY_COMPARE_SCORE + "=" + score.compareScore + "&" +
KEY_SIGNATURE + "=" + getSignature(score);
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
try {
getRemoteData(reqString);
if (lastError == ERROR_DOWNLOADING)
lastError = ERROR_SUBMITTING;
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Gets the signature for the corresponding score details
* @param name
* @param comment
* @param displayScore
* @param compareScore
* @return
*/
private String getSignature (Score score) {
return getSignature(score.name + score.comment + score.displayScore + score.compareScore);
}
/**
* Gets the signature for the given string
* @param input
* @return
*/
private String getSignature(String input) {
SecretKey secretKey = new SecretKeySpec(hexStringToByteArray(privateKey), HASH_ALGORITHM);
Mac mac;
try {
mac = Mac.getInstance(HASH_ALGORITHM);
mac.init(secretKey);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
} catch (InvalidKeyException e) {
e.printStackTrace();
return null;
}
mac.update(input.getBytes());
byte[] bytes = mac.doFinal();
String hex = "";
for (int i = 0; i < bytes.length; i++)
hex += String.format("%02X", bytes[i] & 0xff);
return hex;
}
private void parseJSON(String jsonString) throws JSONException {
JSONObject jsonObj;
if (jsonString != null) {
jsonObj = new JSONObject(jsonString);
JSONArray jsonScores = jsonObj.getJSONArray(KEY_SCORE_TABLE);
int numberOfScores = jsonScores.length();
int maxNumberOfScores = jsonObj.getInt(KEY_SCORE_TABLE_SIZE);
int i = 0;
Score emptyScore = new Score();
if (scoreSubmittable) {
emptyScore.compareScore = submittedScore;
emptyScore.displayScore = submittedDisplayScore;
if (numberOfScores >= maxNumberOfScores) {
lowestScore = getScoreFromJSON(jsonScores.getJSONObject(0));
if (submittedScore > lowestScore.compareScore) {
i++;
emptyScore.needsUserInput = true;
} else {
Log.d("simplescores", "Score not good enough");
emptyScore.name = getContext().getString(R.string.you);
emptyScore.comment = getContext().getString(R.string.keep_trying);
emptyScore.highlight = true;
emptyScore.noRanking = true;
inputPosition = numberOfScores;
scores.add(emptyScore);
scoreSubmittable = false;
}
} else {
emptyScore.needsUserInput = true;
}
//If there are no scores yet
if (numberOfScores == 0) {
scores.add(emptyScore);
scoreSubmittable = false;
}
}
for (; i < numberOfScores; i++) {
JSONObject jsonScore = jsonScores.getJSONObject(i);
Score score = getScoreFromJSON(jsonScore);
if (scoreSubmittable && submittedScore <= score.compareScore) {
scores.add(emptyScore);
inputPosition = numberOfScores - i;
scoreSubmittable = false; //Score now prepared to be submitted
}
scores.add(score);
}
if (scoreSubmittable) {
scores.add(emptyScore);
scoreSubmittable = false;
}
}
}
private Score getScoreFromJSON(JSONObject jsonScore) throws JSONException {
Score score = new Score();
score.name = jsonScore.getString(KEY_NAME);
score.comment = jsonScore.getString(KEY_COMMENT);
score.displayScore = jsonScore.getString(KEY_DISPLAY_SCORE);
score.compareScore = jsonScore.getLong(KEY_COMPARE_SCORE);
return score;
}
/**
* Creates a string from an input stream.
* @param inputStream
* @return
* @throws IOException
*/
private String toString(InputStream inputStream) throws IOException {
StringBuilder outputBuilder = new StringBuilder();
String string;
if (inputStream != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(
inputStream, ENCODING));
while (null != (string = reader.readLine())) {
outputBuilder.append(string).append('\n');
}
}
return outputBuilder.toString();
}
private String getRemoteData(String u) {
try {
URL url = new URL(u);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.connect();
String outputString = toString(conn.getInputStream());
return outputString;
} catch (IOException e) {
lastError = ERROR_DOWNLOADING;
Log.e("SimpleScores", "Unable to retrieve scores");
e.printStackTrace();
}
return null;
}
private Runnable submitScore = new Runnable() {
@Override
public void run() {
submitScore(scoreToSubmit);
if (lastError == ERROR_NONE) {
handler.post(successfulSubmissionRunnable);
} else {
handler.post(errorRunnable);
}
}
};
private Runnable showScores = new Runnable() {
@Override
public void run() {
if (scores.size() == 0 && lastError != ERROR_DOWNLOADING && !scoreSubmittable) {
lastError = ERROR_NO_SCORES;
}
if (lastError != ERROR_NONE) {
onError(lastError);
if (errorRunnable != null)
errorRunnable.run();
return;
}
adapter.notifyDataSetChanged();
if (inputPosition != -1) {
setSelection(inputPosition);
}
onSuccess();
if (successRunnable != null)
successRunnable.run();
}
};
/**
* Fills the scores array with values from the JSON data returned from the server.
*/
private Runnable getScores = new Runnable() {
@Override
public void run() {
try {
parseJSON(getRemoteData(scoreUrl + SCORE_TABLE + "?" + KEY_BOARD_ID + "=" + boardId));
} catch (JSONException e) {
e.printStackTrace();
}
handler.post(showScores);
}
};
private class Score {
public String name;
public String comment;
public String displayScore;
public Long compareScore;
public boolean needsUserInput = false;
public boolean highlight = false;
public boolean noRanking = false;
}
/**
* To be used by subclasses in the event of an error loading the scores.
* @param The error that occured
*/
protected void onError(int error) {}
/**
* To be used by subclasses in the event the scores are loaded successfully.
*/
protected void onSuccess() {}
private class ScoreAdapter extends ArrayAdapter<Score> {
private ArrayList<Score> items;
private View promptView;
LayoutInflater vi;
public ScoreAdapter(Context context, int textViewResourceId, ArrayList<Score> items) {
super(context, textViewResourceId, items);
this.items = items;
vi = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
init();
setNotifyOnChange(false);
}
public void init() {
promptView = vi.inflate(R.layout.simple_scores_prompt_row, null);
promptView.findViewById(R.id.submit).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Score s = items.get(items.size() - inputPosition - 1);
EditText name = (EditText)promptView.findViewById(R.id.name_prompt);
EditText comment = (EditText)promptView.findViewById(R.id.comment_prompt);
s.name = name.getText().toString();
s.comment = comment.getText().toString();
s.needsUserInput = false;
s.highlight = true;
scoreToSubmit = s;
ScoreView.this.removeViewInLayout(promptView); //Hack to get around issue with notifyDataSetChanged
startScoreSubmitter();
notifyDataSetChanged();
}
});
promptView.findViewById(R.id.cancel).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
items.remove(items.size() - inputPosition - 1);
//Replace the lowest score
if (lowestScore != null) {
items.add(0, lowestScore);
}
notifyDataSetChanged();
}
});
}
@Override
public int getItemViewType(int position) {
return IGNORE_ITEM_VIEW_TYPE;
}
@Override
public Score getItem(int position) {
return items.get(items.size() - position - 1);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
Score o = items.get(items.size() - position - 1);
if (o.needsUserInput) {
v = promptView;
} else {
if (v == null || v.getId() != R.layout.simple_scores_row)
v = vi.inflate(R.layout.simple_scores_row, null);
}
TextView ranking = (TextView) v.findViewById(R.id.ranking);
TextView score = (TextView) v.findViewById(R.id.score);
if (o.noRanking) {
ranking.setVisibility(View.GONE);
} else {
ranking.setText((position + 1) + "");
}
score.setText(o.displayScore);
if (!o.needsUserInput) {
TextView name = (TextView) v.findViewById(R.id.name);
TextView comment = (TextView) v.findViewById(R.id.comment);
if (name != null) {
name.setText(o.name);
}
if(comment != null){
comment.setText(o.comment);
}
}
if (o.highlight) {
v.setBackgroundResource(R.drawable.highlightedItemBackground);
} else {
v.setBackgroundResource((position % 2 == 0) ? evenItemsBackground:oddItemsBackground);
}
return v;
}
}
}
|