ScoreView.java :  » UnTagged » simplescores » org » everest » simplescores » Android Open Source

Android Open Source » UnTagged » simplescores 
simplescores » org » everest » simplescores » ScoreView.java
/**
 * 
 */
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;
        }
  }
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.