Java tutorial
/** * Copyright 2011-2012 Will Harris will@phase.net * * 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 net.phase.wallet; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileFilter; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringReader; import java.net.URI; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.ParseException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.text.ClipboardManager; import android.text.format.DateFormat; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.View.OnClickListener; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; class Currency implements Runnable { public final static int STATUS_SUCCESS = 0; public final static int STATUS_ERROR = 1; private Handler handler; private static long lastChecked = 0; private static JSONObject lastResult = null; private static long cacheLength = 1000 * 60 * 30; // 30 minutes private final static String url = "http://bitcoincharts.com/t/weighted_prices.json"; public Currency(Handler h) { this.handler = h; } public static void updateWeightedCurrencies() throws IOException, JSONException { Date now = new Date(); if ((now.getTime() - lastChecked) > cacheLength) { HttpClient client = new DefaultHttpClient(); HttpGet hg = new HttpGet(url); HttpResponse response = client.execute(hg); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { lastResult = new JSONObject(EntityUtils.toString(response.getEntity())); lastChecked = now.getTime(); } else { WalletActivity.toastMessage("Warning: unable to get currency information"); } } } public static String[] getAvailableCurrencies() throws IOException, JSONException { String[] result = null; ArrayList<String> resAL = new ArrayList<String>(); updateWeightedCurrencies(); if (lastResult != null) { // JSONObject.keys() returns Iterator<String> but for some reason // isn't typed that way @SuppressWarnings("unchecked") Iterator<String> itr = lastResult.keys(); // look through every transaction while (itr.hasNext()) { resAL.add(itr.next()); } } if (resAL.size() > 0) { result = new String[resAL.size()]; resAL.toArray(result); Arrays.sort(result); } return result; } public static double getRate(String currency) { double result = 0; if (lastResult != null) { try { // JSONObject.keys() returns Iterator<String> but for some // reason // isn't typed that way @SuppressWarnings("unchecked") Iterator<String> itr = lastResult.keys(); // look through every transaction while (itr.hasNext()) { String cur = itr.next(); if (cur.equals(currency)) { JSONObject currencyObject = lastResult.getJSONObject(cur); try { result = currencyObject.getDouble("30d"); result = currencyObject.getDouble("7d"); result = currencyObject.getDouble("24h"); } catch (JSONException e) { // caught when the first failure occurs above } } } } catch (JSONException e) { } } return result; } @Override public void run() { try { updateWeightedCurrencies(); handler.sendEmptyMessage(STATUS_SUCCESS); } catch (IOException e) { handler.sendEmptyMessage(STATUS_ERROR); } catch (JSONException e) { handler.sendEmptyMessage(STATUS_ERROR); } } } class Transaction implements Parcelable, Comparable<Transaction> { public Date date; public long amount; public long amountin; public long amountout; public String from; public String to; public long balance; // used for rolling balance protected static Transaction[] compressTransactions(Transaction[] transactions) { ArrayList<Transaction> txs = new ArrayList<Transaction>(); Date currentDate = new Date(); Transaction currentTransaction = null; for (Transaction transaction : transactions) { if (!currentDate.equals(transaction.date)) { currentTransaction = new Transaction(transaction.date, 0, transaction.from, transaction.to); currentTransaction.addTransaction(transaction); txs.add(currentTransaction); currentDate = transaction.date; } else { // same date, just add the data if (currentTransaction != null) { currentTransaction.addTransaction(transaction); } } } Transaction[] result = new Transaction[txs.size()]; txs.toArray(result); return result; } protected static Date latest(Transaction[] transactions) { Date result = null; if (transactions != null && transactions.length > 0) { for (Transaction t : transactions) { if (result == null || result.before(t.date)) { result = t.date; } } } return result; } private void addTransaction(Transaction tx) { this.amount += tx.amount; if (amount >= 0) { this.amountin += tx.amount; } else { this.to = tx.to; this.amountout -= tx.amount; } } protected void saveTransaction(PrintWriter out) throws IOException { out.println(date.getTime()); out.println(amount); out.println(from); out.println(to); } protected Transaction(BufferedReader in) throws NumberFormatException, IOException { this.date = WalletActivity.myParseDate(in.readLine()); this.amount = Long.parseLong(in.readLine()); this.from = in.readLine(); this.to = in.readLine(); } public Transaction(Date date, long amount, String from, String to) { this.date = date; this.amount = amount; this.from = from; this.to = to; } public Transaction(Parcel in) { this.date = (Date) in.readSerializable(); this.amount = in.readLong(); this.from = in.readString(); this.to = in.readString(); this.amountin = in.readLong(); this.amountout = in.readLong(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeSerializable(this.date); dest.writeLong(amount); dest.writeString(from); dest.writeString(to); dest.writeLong(amountin); dest.writeLong(amountout); } public static final Parcelable.Creator<Transaction> CREATOR = new Parcelable.Creator<Transaction>() { public Transaction createFromParcel(Parcel in) { return new Transaction(in); } public Transaction[] newArray(int size) { return new Transaction[size]; } }; @Override public int compareTo(Transaction another) { return another.date.compareTo(date); } } class Key { public String hash; public int hit; public Key(String hash) { this.hash = hash; this.hit = 0; } public Key(String hash, int hit) { this.hash = hash; this.hit = hit; } public static boolean arrayContains(Key[] keys, String hash) { for (Key key : keys) { if (key.hash.equals(hash)) { key.hit = 1; return true; } } return false; } public boolean equals(Key k) { return (k.hash.equals(this.hash)); } } class Wallet { public String name; public long balance; public Key[] keys; public Date lastUpdated; public Transaction[] transactions; private int version; private WalletActivity activity; private final static int LEGACY_VERSION = 1; private final static int TRANSACTIONS_ADDED_VERSION = 2; private final static int CURRENT_VERSION = 2; public void notifyUser() { /** * code not ready yet if ( activity != null ) { long when = * System.currentTimeMillis(); Notification n = new Notification( * android.R.drawable.stat_notify_more, "Balance", when); * * NotificationManager nm = (NotificationManager) * activity.getSystemService( Context.NOTIFICATION_SERVICE ); Context * globalContext = activity.getApplicationContext(); * * Intent notificationIntent = new Intent( activity, activity.getClass() * ); PendingIntent contentIntent = PendingIntent.getActivity( activity, * 0, notificationIntent, 0 ); * * n.setLatestEventInfo( globalContext, "New Balance", "New Balance", * contentIntent ); nm.notify(1, n); } */ } protected void SaveWallet(PrintWriter out) throws IOException { version = CURRENT_VERSION; if (out != null) { out.println("walletversion"); out.println(version); out.println(name); out.println(balance); out.println(lastUpdated.getTime()); if (transactions == null) { out.println(0); } else { out.println(transactions.length); for (Transaction t : transactions) { t.saveTransaction(out); } } for (Key key : keys) { out.println(key.hash); out.println(key.hit); } } } private static ArrayList<Key> parseFromReader(BufferedReader bin) throws IOException, ParseException { ArrayList<Key> keysArray = new ArrayList<Key>(); String keyHash; boolean foundKey = false; while ((keyHash = bin.readLine()) != null) { // add a space at the start to make the regex work more easily keyHash = " " + keyHash + " "; Pattern p = Pattern.compile("\\W(1[1-9A-HJ-NP-Za-km-z]{27,34})\\W"); Matcher m = p.matcher(keyHash); if (m.find()) { Log.d("balance", "Key " + m.group(1) + " found"); Key newKey = new Key(m.group(1)); if (!keysArray.contains(newKey)) { keysArray.add(newKey); foundKey = true; } } } if (foundKey) { return keysArray; } else { return null; } } private boolean fillFromReader(BufferedReader bin) throws IOException, ParseException { ArrayList<Key> keysArray = parseFromReader(bin); if (keysArray != null) { keys = new Key[keysArray.size()]; keysArray.toArray(keys); lastUpdated = new Date(); balance = 0; } return (keysArray != null); } public int addFromReader(BufferedReader bin) throws IOException, ParseException { ArrayList<Key> keysArray = parseFromReader(bin); int added = 0; if (keys != null && keysArray != null) { ArrayList<Key> newKeys = new ArrayList<Key>(); // add all existing keys for (int i = 0; i < keys.length; i++) { newKeys.add(keys[i]); } // add new keys for (int i = 0; i < keysArray.size(); i++) { Key newKey = keysArray.get(i); // only if not already in there if (!Key.arrayContains(keys, newKey.hash)) { newKey.hit = 1; newKeys.add(newKey); added++; } } // recreate array keys = new Key[newKeys.size()]; newKeys.toArray(keys); } return added; } public Wallet(String name, File file, WalletActivity activity) throws IOException { this.activity = activity; this.name = name; boolean foundKey = false; BufferedReader in = new BufferedReader(new FileReader(file)); foundKey = fillFromReader(in); in.close(); if (!foundKey) { throw new ParseException("Did not find any keys"); } } public Wallet(String name, URI url, WalletActivity activity) throws IOException, ParseException { this.activity = activity; this.name = name; HttpClient client = new DefaultHttpClient(); HttpGet hg = new HttpGet(url); boolean foundKey = false; HttpResponse resp; resp = client.execute(hg); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { HttpEntity entity = resp.getEntity(); if (entity != null) { foundKey = fillFromReader(new BufferedReader(new InputStreamReader(entity.getContent()), (int) entity.getContentLength())); } else { throw new ParseException("No response body"); } } else { throw new ParseException("Did not get 200 OK"); } if (!foundKey) { throw new ParseException("Did not find any keys"); } } protected Wallet(BufferedReader in, WalletActivity activity) throws IOException, NumberFormatException { this.activity = activity; name = in.readLine(); if (name.equals("walletversion")) { version = Integer.parseInt(in.readLine()); name = in.readLine(); } else { version = LEGACY_VERSION; } balance = Long.parseLong(in.readLine()); lastUpdated = WalletActivity.myParseDate(in.readLine()); if (version >= TRANSACTIONS_ADDED_VERSION) { int numTx = Integer.parseInt(in.readLine()); transactions = new Transaction[numTx]; for (int i = 0; i < numTx; i++) { transactions[i] = new Transaction(in); } } ArrayList<Key> keysArray = new ArrayList<Key>(); String keyHash = null; while ((keyHash = in.readLine()) != null) { int hit = Integer.parseInt(in.readLine()); keysArray.add(new Key(keyHash, hit)); } ; keys = new Key[keysArray.size()]; keysArray.toArray(keys); } public Wallet(String name, String inputkeys, WalletActivity parentActivity) throws IOException, ParseException { this.activity = parentActivity; this.name = name; boolean foundKey = false; ArrayList<Key> keysArray = new ArrayList<Key>(); Pattern p = Pattern.compile("\\W(1[1-9A-HJ-NP-Za-km-z]{27,34})"); for (String keyHash : inputkeys.split(" ")) { // add a space at the start to make the regex work more easily keyHash = " " + keyHash; Matcher m = p.matcher(keyHash); if (m.find()) { Log.d("balance", "Key " + m.group(1) + " found"); keysArray.add(new Key(m.group(1))); foundKey = true; } } if (foundKey) { keys = new Key[keysArray.size()]; keysArray.toArray(keys); lastUpdated = new Date(); balance = 0; } else { throw new ParseException("Did not find any keys"); } } public static Wallet[] getStoredWallets(Context context, WalletActivity activity) throws IOException { ArrayList<Wallet> walletsArray = new ArrayList<Wallet>(); String[] files = context.fileList(); if (files.length == 0) return null; for (String filename : files) { BufferedReader in = new BufferedReader(new InputStreamReader(context.openFileInput(filename))); try { walletsArray.add(new Wallet(in, activity)); } catch (NumberFormatException e) { } in.close(); } Wallet[] wallets = new Wallet[walletsArray.size()]; walletsArray.toArray(wallets); return wallets; } public static void saveWallets(Wallet[] wallets, Context context) throws IOException { for (Wallet w : wallets) { PrintWriter out = new PrintWriter( new OutputStreamWriter(context.openFileOutput(w.name, Context.MODE_PRIVATE))); w.SaveWallet(out); out.close(); } } public int getActiveKeyCount() { int count = 0; for (Key k : keys) { if (k.hit > 0) count++; } if (count == 0) count = keys.length; return count; } } class WalletAdapter extends BaseAdapter { private Wallet[] wallets; private WalletActivity context; private int decimalpoints = 2; public WalletAdapter(WalletActivity c, Wallet[] w, int decimalpoints) { this.wallets = w; this.context = c; this.decimalpoints = decimalpoints; } @Override public int getCount() { return wallets.length; } @Override public Object getItem(int position) { return wallets[position]; } @Override public long getItemId(int position) { return 0; } private String getTimeStampString(Date d) { String result = DateFormat.getDateFormat(context).format(d); result += " " + DateFormat.format("h:mmaa", d).toString(); return result; } @Override public View getView(int position, View convertView, ViewGroup parent) { double exchrate = Currency.getRate(context.getActiveCurrency()); LayoutInflater inflater = LayoutInflater.from(context); View v = inflater.inflate(R.layout.walletlayout, null); v.setLongClickable(true); v.setOnClickListener(context); v.setTag(position); DecimalFormat df = new DecimalFormat(WalletActivity.decimalString(decimalpoints)); TextView balanceTextView = (TextView) v.findViewById(R.id.walletBalanceText); balanceTextView.setText(df.format(wallets[position].balance / BalanceRetriever.SATOSHIS_PER_BITCOIN)); TextView curTextView = (TextView) v.findViewById(R.id.walletCurText); curTextView.setTextSize(10); if (exchrate != 0) { curTextView.setText( "(" + df.format(wallets[position].balance * exchrate / BalanceRetriever.SATOSHIS_PER_BITCOIN) + context.getActiveCurrency() + ")"); } else { curTextView.setText(""); } balanceTextView.setTextSize(20); balanceTextView.setTextColor(Color.GREEN); TextView nameTextView = (TextView) v.findViewById(R.id.walletNameText); nameTextView.setText(wallets[position].name); nameTextView.setTextColor(Color.BLACK); nameTextView.setTextSize(16); TextView lastUpdatedTextView = (TextView) v.findViewById(R.id.lastUpdatedText); lastUpdatedTextView.setTextColor(Color.GRAY); lastUpdatedTextView.setTextSize(8); lastUpdatedTextView.setText("Last Updated: " + getTimeStampString(wallets[position].lastUpdated)); TextView infoTextView = (TextView) v.findViewById(R.id.infoText); infoTextView.setTextColor(Color.GRAY); infoTextView.setTextSize(8); infoTextView.setText( wallets[position].keys.length + " keys (" + wallets[position].getActiveKeyCount() + " in use)"); TextView txLastUpdatedTextView = (TextView) v.findViewById(R.id.txLastUpdatedText); txLastUpdatedTextView.setTextColor(Color.GRAY); txLastUpdatedTextView.setTextSize(8); TextView txInfoTextView = (TextView) v.findViewById(R.id.txInfoText); txInfoTextView.setTextColor(Color.GRAY); txInfoTextView.setTextSize(8); if (wallets[position].transactions != null && wallets[position].transactions.length > 0) { txLastUpdatedTextView.setText( "Last Transaction: " + getTimeStampString(Transaction.latest(wallets[position].transactions))); txInfoTextView.setText(wallets[position].transactions.length + " transactions (" + Transaction.compressTransactions(wallets[position].transactions).length + " unique)"); } else { txLastUpdatedTextView.setText(""); txInfoTextView.setText(""); } Button button = (Button) v.findViewById(R.id.updateButton); button.setTag(position); button.setOnClickListener(context); return v; } } public class WalletActivity extends Activity implements OnClickListener { public final static String TRANSACTIONS = "net.phase.wallet.transactions"; public final static String WALLETNAME = "net.phase.wallet.name"; public final static String TRANSACTIONSTYLE = "net.phase.wallet.transactionsstyle"; public final static String DECIMALPOINTS = "net.phase.wallet.decimal"; private ProgressDialog dialog; private Wallet[] wallets; private int nextWallet = -1; private static final int DIALOG_URL = 1; private static final int DIALOG_FILE = 2; private static final int DIALOG_OPTIONS = 3; private static final int DIALOG_PASTE = 4; private String activeCurrency = "USD"; private static Context context; private int maxlength = 40; private int decimalpoints = 2; private static ProgressDialog progress = null; public static void dismissProgress() { if (progress != null) { progress.dismiss(); progress = null; } } public static String decimalString(int decimalpoints) { if (decimalpoints == 0) return "0"; StringBuilder result = new StringBuilder("0."); for (int i = 0; i < decimalpoints; i++) { result.append('0'); } return result.toString(); } public static Date myParseDate(String line) { Date d = null; // make guesses - if it contains a space, it's probably the output of // date.toString() if (line.indexOf(' ') == -1) { try { d = new Date(Long.parseLong(line)); } catch (NumberFormatException e) { } } else { try { d = new Date(line); } catch (IllegalArgumentException e) { } } if (d == null) { // set it to 1970... not much we can do here d = new Date(0); } return d; } private boolean savePreferences() { SharedPreferences pref = getPreferences(MODE_PRIVATE); SharedPreferences.Editor editor = pref.edit(); editor.putString("currency", activeCurrency); editor.putInt("maxlength", maxlength); editor.putInt("decimal", decimalpoints); return editor.commit(); } private void loadPreferences() { SharedPreferences pref = getPreferences(MODE_PRIVATE); setActiveCurrency(pref.getString("currency", "USD")); maxlength = pref.getInt("maxlength", 40); decimalpoints = pref.getInt("decimal", 2); } public static void toastMessage(String message) { CharSequence text = message; int duration = Toast.LENGTH_SHORT; if (context != null) { Toast toast = Toast.makeText(context, text, duration); toast.show(); } } /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); WalletActivity.context = getApplicationContext(); loadPreferences(); try { wallets = Wallet.getStoredWallets(this, this); } catch (IOException e) { toastMessage(e.getMessage()); } Currency c = new Currency(currencyHandler); Thread t = new Thread(c); t.start(); updateWalletList(); } public void addWallet(Wallet wallet) { if (wallets == null) { wallets = new Wallet[1]; wallets[0] = wallet; } else { Wallet[] newWallets = new Wallet[wallets.length + 1]; int i = 0; for (i = 0; i < wallets.length; i++) { newWallets[i] = wallets[i]; if (newWallets[i].name.equals(wallet.name)) { // name clash! wallet.name = wallet.name.concat("2"); } } newWallets[i] = wallet; wallets = newWallets; } updateWalletList(); } public void updateWalletList() { if (wallets != null) { setContentView(R.layout.main); ListView view = (ListView) findViewById(R.id.walletListView); WalletAdapter adapter = new WalletAdapter(this, wallets, decimalpoints); view.setAdapter(adapter); double exchrate = Currency.getRate(getActiveCurrency()); long balance = 0; for (int i = 0; i < wallets.length; i++) { balance += wallets[i].balance; } DecimalFormat df = new DecimalFormat(decimalString(decimalpoints)); TextView btcBalance = (TextView) findViewById(R.id.btcBalance); btcBalance.setText(df.format(balance / BalanceRetriever.SATOSHIS_PER_BITCOIN) + " BTC"); TextView curBalance = (TextView) findViewById(R.id.curBalance); if (exchrate != 0) { curBalance.setText(df.format(balance * exchrate / BalanceRetriever.SATOSHIS_PER_BITCOIN) + " " + getActiveCurrency()); } else { curBalance.setText(""); } registerForContextMenu(view); try { Wallet.saveWallets(wallets, this); } catch (IOException e) { toastMessage("Unable to save wallet data " + e.getMessage()); } } else { setContentView(R.layout.mainnowallets); } } private void updateAll() { if (wallets != null) { if (wallets.length > 0) { nextWallet = 1; } updateWalletBalance(wallets[0], true, maxlength); } else { toastMessage("No wallets to update!"); } } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.mainmenu, menu); return true; } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.walletmenu, menu); } FileFilter fileFilter = new FileFilter() { @Override public boolean accept(File pathname) { if (pathname.getName().contains("key") && pathname.getName().endsWith(".txt")) { return true; } return false; } }; protected void onPrepareDialog(int id, Dialog dialog) { switch (id) { case DIALOG_URL: TextView tv = (TextView) dialog.findViewById(R.id.pasteBinHelpText); tv.setMovementMethod(LinkMovementMethod.getInstance()); break; case DIALOG_FILE: TextView tvhelp = (TextView) dialog.findViewById(R.id.helpText); tvhelp.setMovementMethod(LinkMovementMethod.getInstance()); Spinner fileSpinner = (Spinner) dialog.findViewById(R.id.fileInput); File[] files = Environment.getExternalStorageDirectory().listFiles(fileFilter); if (files == null || files.length == 0) { toastMessage("No files found on sdcard"); } else { ArrayAdapter<File> adapter = new ArrayAdapter<File>(this, android.R.layout.simple_spinner_item, files); fileSpinner.setAdapter(adapter); } break; case DIALOG_PASTE: EditText keyText = (EditText) dialog.findViewById(R.id.keysText); keyText.setText(""); break; case DIALOG_OPTIONS: EditText reqText = (EditText) dialog.findViewById(R.id.reqText); reqText.setText(Integer.toString(maxlength)); Spinner currencySpinner = (Spinner) dialog.findViewById(R.id.currencySpinner); Spinner decimalSpinner = (Spinner) dialog.findViewById(R.id.decpointSpinner); String current = getActiveCurrency(); String[] currencies = { current }; try { currencies = Currency.getAvailableCurrencies(); } catch (IOException e) { WalletActivity.toastMessage("Warning: unable to obtain currency information"); } catch (JSONException e) { } ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, currencies); currencySpinner.setAdapter(adapter); for (int i = 0; i < currencies.length; i++) { if (currencies[i].equals(current)) { currencySpinner.setSelection(i); break; } } Integer[] decimaloptions = { 0, 1, 2, 3, 4, 5 }; ArrayAdapter<Integer> adapter2 = new ArrayAdapter<Integer>(this, android.R.layout.simple_spinner_item, decimaloptions); decimalSpinner.setAdapter(adapter2); for (int i = 0; i < decimaloptions.length; i++) { if (decimaloptions[i].intValue() == decimalpoints) { decimalSpinner.setSelection(i); break; } } break; } } private void showTransactions(Wallet wallet) { showTransactions(wallet, TransactionAdapter.STYLE_NORMAL); } private void showTransactions(Wallet wallet, int style) { if (progress == null && wallet.transactions.length > 10) { progress = ProgressDialog.show(this, "Building Transaction List", "Please wait...", true); } Intent intent = new Intent(this, TransactionsActivity.class); intent.putExtra(WALLETNAME, wallet.name); intent.putExtra(TRANSACTIONSTYLE, style); intent.putExtra(DECIMALPOINTS, decimalpoints); Arrays.sort(wallet.transactions); if (style == TransactionAdapter.STYLE_NORMAL) { Transaction[] compressedTransactions = Transaction.compressTransactions(wallet.transactions); intent.putExtra(TRANSACTIONS, compressedTransactions); } else { intent.putExtra(TRANSACTIONS, wallet.transactions); } startActivity(intent); } protected Dialog onCreateDialog(int id) { AlertDialog.Builder builder; AlertDialog alertDialog; builder = new AlertDialog.Builder(this); LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); View layout = null; final WalletActivity parentActivity = this; switch (id) { case DIALOG_URL: layout = inflater.inflate(R.layout.urlfetch_dialog, null); TextView tv = (TextView) layout.findViewById(R.id.pasteBinHelpText); tv.setMovementMethod(LinkMovementMethod.getInstance()); final EditText hashEditText = (EditText) layout.findViewById(R.id.hashEditText); final EditText nameEditText = (EditText) layout.findViewById(R.id.nameEditText); builder.setView(layout); builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { String hash = hashEditText.getText().toString(); String name = nameEditText.getText().toString(); if (!hash.startsWith("http")) { hash = "http://pastebin.com/raw.php?i=" + hash; } try { Wallet w = new Wallet(name, new URI(hash), parentActivity); addWallet(w); } catch (Exception e) { toastMessage(e.getMessage()); } } }); builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); break; case DIALOG_FILE: layout = inflater.inflate(R.layout.file_dialog, null); TextView tvhelp = (TextView) layout.findViewById(R.id.helpText); tvhelp.setMovementMethod(LinkMovementMethod.getInstance()); final Spinner fileSpinner = (Spinner) layout.findViewById(R.id.fileInput); final EditText nameEditText2 = (EditText) layout.findViewById(R.id.nameEditText); builder.setView(layout); builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { File filename = (File) fileSpinner.getSelectedItem(); String name = nameEditText2.getText().toString(); boolean alreadyexists = false; if (name.length() > 0) { if (wallets != null && wallets.length > 0) { for (Wallet w : wallets) { if (w.name.equals(name)) { alreadyexists = true; } } } if (!alreadyexists) { try { Wallet w = new Wallet(name, filename, parentActivity); addWallet(w); } catch (Exception e) { toastMessage(e.getMessage()); } } else { toastMessage("Wallet already exists"); } } else { toastMessage("Invalid Name"); } } }); builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); break; case DIALOG_PASTE: layout = inflater.inflate(R.layout.paste_dialog, null); final EditText keysText = (EditText) layout.findViewById(R.id.keysText); final EditText nameEditText3 = (EditText) layout.findViewById(R.id.walletNameText); builder.setView(layout); builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { String keys = keysText.getText().toString(); String name = nameEditText3.getText().toString(); boolean alreadyexists = false; if (name.length() > 0) { if (wallets != null && wallets.length > 0) { for (Wallet w : wallets) { if (w.name.equals(name)) { alreadyexists = true; } } } if (!alreadyexists) { try { Wallet w = new Wallet(name, keys, parentActivity); addWallet(w); } catch (Exception e) { toastMessage(e.getMessage()); } } else { toastMessage("Wallet already exists"); } } else { toastMessage("Invalid Name"); } } }); builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); break; case DIALOG_OPTIONS: layout = inflater.inflate(R.layout.options_dialog, null); final Spinner currencySpinner = (Spinner) layout.findViewById(R.id.currencySpinner); final EditText reqText = (EditText) layout.findViewById(R.id.reqText); final Spinner decpointSpinner = (Spinner) layout.findViewById(R.id.decpointSpinner); builder.setView(layout); builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { String currency = (String) currencySpinner.getSelectedItem(); decimalpoints = ((Integer) decpointSpinner.getSelectedItem()).intValue(); setActiveCurrency(currency); try { maxlength = Integer.parseInt(reqText.getText().toString()); } catch (RuntimeException e) { toastMessage("Invalid number"); } savePreferences(); } }); builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); break; } alertDialog = builder.create(); return alertDialog; } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); switch (item.getItemId()) { case R.id.updateItem: updateWalletBalance(wallets[info.position], false, maxlength); return true; case R.id.removeItem: removeWallet(wallets[info.position].name); updateWalletList(); return true; case R.id.pasteClipKeys: ClipboardManager clip = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clip != null) { if (clip.getText() != null) { int added = 0; try { InputStream is = new ByteArrayInputStream(clip.getText().toString().getBytes()); BufferedReader br = new BufferedReader(new InputStreamReader(is)); added = wallets[info.position].addFromReader(br); } catch (IOException e) { } if (added > 0) { toastMessage("Added " + added + " key(s)"); updateWalletList(); } else { toastMessage("Found no new keys "); } } else { toastMessage("Nothing on clipboard"); } } else { toastMessage("Could not obtain clipboard"); } return true; case R.id.viewItem: if (wallets[info.position].transactions == null) { toastMessage("Please update wallet first"); } else { showTransactions(wallets[info.position]); } return true; default: return super.onOptionsItemSelected(item); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.importURL: showDialog(DIALOG_URL); return true; case R.id.importFile: showDialog(DIALOG_FILE); return true; case R.id.importPaste: showDialog(DIALOG_PASTE); return true; case R.id.updateAllItem: updateAll(); return true; case R.id.optionsItem: showDialog(DIALOG_OPTIONS); return true; case R.id.helpItem: Uri uri = Uri.parse("http://code.google.com/p/bitcoinwallet/wiki/Using"); startActivity(new Intent(Intent.ACTION_VIEW, uri)); return true; default: return super.onOptionsItemSelected(item); } } public Thread updateWalletBalance(Wallet w, boolean fast, int maxlength) { BalanceRetriever br = new BalanceRetriever(progressHandler, w, fast, maxlength); dialog = new ProgressDialog(this); dialog.setMessage("Obtaining balance (" + w.name + ")..."); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); if (fast) dialog.setMax(w.getActiveKeyCount()); else dialog.setMax(w.keys.length); dialog.setProgress(0); dialog.show(); Thread t = new Thread(br); t.start(); return t; } public void removeWallet(String name) { Wallet[] newWallets = null; if (wallets.length == 1) { wallets = null; updateWalletList(); } else { newWallets = new Wallet[wallets.length - 1]; int i = 0; for (Wallet wallet : wallets) { if (!wallet.name.equals(name)) newWallets[i++] = wallet; } } // delete the wallet file deleteFile(name); wallets = newWallets; } Handler currencyHandler = new Handler() { public void handleMessage(Message msg) { updateWalletList(); } }; Handler progressHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case BalanceRetriever.MESSAGE_UPDATE: dialog.incrementProgressBy(1); break; case BalanceRetriever.MESSAGE_FINISHED: dialog.dismiss(); switch (msg.arg1) { case BalanceRetriever.MESSAGE_STATUS_SUCCESS: // code to refresh all wallets if (nextWallet > 0 && nextWallet < wallets.length) { updateWalletBalance(wallets[nextWallet], false, maxlength); nextWallet++; } else { updateWalletList(); // send an update to the widget Intent intent = new Intent(WalletWidgetProvider.UPDATE_WIDGET); intent.setClassName("net.phase.wallet", "net.phase.wallet.WalletWidgetProvider"); sendBroadcast(intent); nextWallet = -1; } break; case BalanceRetriever.MESSAGE_STATUS_NETWORK: toastMessage("Network error"); break; case BalanceRetriever.MESSAGE_STATUS_NOKEYS: toastMessage("No keys at that location"); break; case BalanceRetriever.MESSAGE_STATUS_JSON: toastMessage("JSON Parse Error"); break; case BalanceRetriever.MESSAGE_STATUS_PARSE: toastMessage("Parse Error"); break; } break; case BalanceRetriever.MESSAGE_SETLENGTH: dialog.setMax(msg.arg1); break; } } }; @Override public void onClick(View v) { int i = (Integer) v.getTag(); if (v.getId() == R.id.updateButton) { updateWalletBalance(wallets[i], true, maxlength); } else { if (wallets[i].transactions == null) { toastMessage("Please update wallet"); } else { showTransactions(wallets[i]); } } } public String getActiveCurrency() { return activeCurrency; } public void setActiveCurrency(String currency) { activeCurrency = currency; updateWalletList(); } } class tx { public String txhash; public int rec; public long value; public String inKeyHash; public String outKeyHash; public tx(String txhash, int rec, long value, String outKeyHash, String inKeyHash) { // value is number of satoshis this.txhash = txhash; this.rec = rec; this.value = value; this.outKeyHash = outKeyHash; this.inKeyHash = inKeyHash; } } class prevout { public String prevTxHash; public String txhash; public int rec; public Date date; public String addr; public prevout(String txhash, String prevTxHash, int rec, Date date, String addr) { this.txhash = txhash; this.prevTxHash = prevTxHash; this.rec = rec; this.date = date; this.addr = addr; } } class BalanceRetriever implements Runnable { public static final int MESSAGE_UPDATE = 1; public static final int MESSAGE_FINISHED = 2; public static final int MESSAGE_SETLENGTH = 3; public static final int MESSAGE_STATUS_SUCCESS = 0; public static final int MESSAGE_STATUS_NOKEYS = 1; public static final int MESSAGE_STATUS_NETWORK = 2; public static final int MESSAGE_STATUS_JSON = 3; public static final int MESSAGE_STATUS_PARSE = 4; public static final int MESSAGE_STATUS_MISSING_TX = 5; public static final double SATOSHIS_PER_BITCOIN = 100000000.0; private static final String baseUrl = "http://blockexplorer.com/q/mytransactions/"; private static final String emptyBaseUrl = "http://blockexplorer.com/q/mytransactions"; // number of transactions that can be queried from blockexplorer in each GET // request // this is limited by the maximum length of a GET request private int maxlength = 40; // balance is number of microbitcoins (a millionth of a bitcoin) private long balance; private Handler updateHandler; private Wallet wallet; private boolean fast; private ArrayList<String> transactionCache; private ArrayList<Transaction> transactions; private ArrayList<tx> txs; private ArrayList<prevout> pendingDebits; private static final java.text.DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public long getFinalBalance() { return this.balance; } public int getNumberOfKeys() { return wallet.keys.length; } public BalanceRetriever(Handler updateHandler, Wallet wallet, boolean fast, int maxlength) { this.balance = 0; this.updateHandler = updateHandler; this.wallet = wallet; this.fast = fast; this.maxlength = maxlength; transactions = new ArrayList<Transaction>(); transactionCache = new ArrayList<String>(); txs = new ArrayList<tx>(); pendingDebits = new ArrayList<prevout>(); } private tx getMatchingTx(String txhash, int rec) { for (tx theTx : txs) { if (theTx.txhash.equals(txhash) && theTx.rec == rec) { return theTx; } } // Log.e("balance", "Could not find hash " + hash + ":" + rec ); return null; } private tx getFirstNonMatchingTx(String txhash, Key[] keys) { for (tx theTx : txs) { if (theTx.txhash.equals(txhash) && !Key.arrayContains(keys, theTx.outKeyHash)) { return theTx; } } return null; } public void run() { balance = 0; int i = 0; StringBuffer url = new StringBuffer(baseUrl); int status = MESSAGE_STATUS_SUCCESS; boolean fastKeyFound = false; for (Key k : wallet.keys) { if (!fast) { k.hit = 0; } else { if (k.hit > 0) { fastKeyFound = true; } } } // if we asked for fast, but no fast keys found, just override this if (fast && !fastKeyFound) { fast = false; } try { int numberOfKeys = wallet.keys.length; if (fast) { numberOfKeys = wallet.getActiveKeyCount(); } updateHandler.sendMessage(updateHandler.obtainMessage(MESSAGE_SETLENGTH, numberOfKeys, 0)); updateHandler.sendMessage(updateHandler.obtainMessage(MESSAGE_UPDATE)); for (Key key : wallet.keys) { if ((i % maxlength) == (maxlength - 1)) { updateBalanceFromUrl(url.substring(0, url.length() - 1)); url = new StringBuffer(baseUrl); } if (!fast || key.hit > 0) { url.append(key.hash); url.append('.'); i++; } updateHandler.sendMessage(updateHandler.obtainMessage(MESSAGE_UPDATE)); } updateBalanceFromUrl(url.substring(0, url.length() - 1)); updateHandler.sendMessage(updateHandler.obtainMessage(MESSAGE_UPDATE)); // look through previous transactions and debit payments for (prevout previousOut : pendingDebits) { tx matchingTx = getMatchingTx(previousOut.prevTxHash, previousOut.rec); if (matchingTx != null) { balance -= matchingTx.value; tx outputTx = getFirstNonMatchingTx(previousOut.txhash, wallet.keys); if (outputTx != null) { transactions.add(new Transaction(previousOut.date, -matchingTx.value, previousOut.addr, outputTx.outKeyHash)); } else { transactions.add( new Transaction(previousOut.date, -matchingTx.value, previousOut.addr, "unknown")); } } else { status = MESSAGE_STATUS_MISSING_TX; // not sure how this can happen, but it happened once for a // user, so handle it here Log.w("wallet", "could not retrieve previous tx for " + previousOut.prevTxHash + ":" + previousOut.rec); } } } catch (IOException e) { status = MESSAGE_STATUS_NETWORK; } catch (JSONException e) { status = MESSAGE_STATUS_JSON; } catch (java.text.ParseException e) { status = MESSAGE_STATUS_PARSE; } updateHandler.sendMessage(updateHandler.obtainMessage(MESSAGE_FINISHED, status, 0)); if (status == MESSAGE_STATUS_SUCCESS) { if (wallet.balance != 0 && wallet.balance != balance) { wallet.notifyUser(); } wallet.balance = balance; wallet.lastUpdated = new Date(); wallet.transactions = new Transaction[transactions.size()]; transactions.toArray(wallet.transactions); } } private void updateBalanceFromUrl(String url) throws IOException, JSONException, java.text.ParseException { if (url.equals(emptyBaseUrl)) { Log.i("balance", "skipping URL " + url); return; } Log.i("balance", "fetching URL " + url); HttpClient client = new DefaultHttpClient(); HttpGet hg = new HttpGet(url); HttpResponse response = client.execute(hg); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { JSONObject resp = new JSONObject(EntityUtils.toString(response.getEntity())); // JSONObject.keys() returns Iterator<String> but for some reason // isn't typed that way @SuppressWarnings("unchecked") Iterator<String> itr = resp.keys(); // look through every transaction while (itr.hasNext()) { JSONObject txObject = resp.getJSONObject(itr.next()); String txHash = txObject.getString("hash"); String inKeyHash = "unknown"; // only process transaction if we haven't seen it before if (!transactionCache.contains(txHash)) { Log.i("balance", "Parsing txObject " + txHash); transactionCache.add(txHash); // find the in transaction JSONArray txsIn = txObject.getJSONArray("in"); Date date = formatter.parse(txObject.getString("time")); for (int i = 0; i < txsIn.length(); i++) { JSONObject inRecord = txsIn.getJSONObject(i); try { inKeyHash = inRecord.optString("address"); // if one of our keys is there, we are paying :( if (Key.arrayContains(wallet.keys, inKeyHash)) { JSONObject prevRecord = inRecord.getJSONObject("prev_out"); // if we paid for part of this transaction, // record this. pendingDebits.add(new prevout(txHash, prevRecord.getString("hash"), prevRecord.getInt("n"), date, inKeyHash)); } } catch (JSONException e) { // no address. Probably a generation transaction } } // find the out transaction JSONArray txsOut = txObject.getJSONArray("out"); for (int i = 0; i < txsOut.length(); i++) { JSONObject outRecord = txsOut.getJSONObject(i); String outKeyHash = outRecord.optString("address"); // convert to microbitcoins for accuracy long value = (long) (outRecord.getDouble("value") * SATOSHIS_PER_BITCOIN); // store the out transaction, this is used later on txs.add(new tx(txHash, i, value, outKeyHash, inKeyHash)); // if one of our keys is there, add the balance if (Key.arrayContains(wallet.keys, outKeyHash)) { transactions.add(new Transaction(date, value, inKeyHash, outKeyHash)); balance += value; } } } } } else { Log.e("wallet", "Got " + response.getStatusLine().getStatusCode() + " back from HTTP GET"); } } }