From 6e4f358bad89d7cfa30a33aaa0936761789e662a Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Tue, 26 Nov 2013 16:24:18 -0500 Subject: [PATCH] Update storage model to match iOS and permit reordering --- src/org/fedorahosted/freeotp/Token.java | 72 +++----- .../fedorahosted/freeotp/TokenAdapter.java | 169 +++++++++--------- src/org/fedorahosted/freeotp/TokenStore.java | 92 ++++++++++ 3 files changed, 204 insertions(+), 129 deletions(-) create mode 100644 src/org/fedorahosted/freeotp/TokenStore.java diff --git a/src/org/fedorahosted/freeotp/Token.java b/src/org/fedorahosted/freeotp/Token.java index b7416c8..35a9576 100644 --- a/src/org/fedorahosted/freeotp/Token.java +++ b/src/org/fedorahosted/freeotp/Token.java @@ -23,15 +23,11 @@ package org.fedorahosted.freeotp; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import android.content.Context; -import android.content.SharedPreferences; import android.net.Uri; import com.google.android.apps.authenticator.Base32String; @@ -56,21 +52,6 @@ public class Token { private long counter; private int period; - public static List getTokens(Context ctx) { - SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE); - - List tokens = new ArrayList(); - for (String key : prefs.getAll().keySet()) { - try { - tokens.add(new Token(prefs.getString(key, null))); - } catch (TokenUriInvalidException e) { - e.printStackTrace(); - } - } - - return tokens; - } - private Token(Uri uri) throws TokenUriInvalidException { if (!uri.getScheme().equals("otpauth")) throw new TokenUriInvalidException(); @@ -195,7 +176,7 @@ public class Token { this(Uri.parse(uri)); } - private String getId() { + public String getID() { String id; if (issuerInt != null && !issuerInt.equals("")) id = issuerInt + ":" + label; @@ -207,42 +188,35 @@ public class Token { return id; } - public void remove(Context ctx) { - SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE); - prefs.edit().remove(getId()).apply(); + public String getIssuer() { + return issuerExt != null ? issuerExt : ""; } - public void save(Context ctx) { - SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE); - prefs.edit().putString(getId(), toString()).apply(); + public String getLabel() { + return label != null ? label : ""; } - public String getTitle() { - String title = ""; - if (issuerExt != null && !issuerExt.equals("")) - title += issuerExt + ": "; - title += label; - return title; + public String getCode() { + switch (type) { + case HOTP: + return getHOTP(counter); + case TOTP: + return getHOTP(System.currentTimeMillis() / 1000 / period); + } + + return null; } - public String getCurrentTokenValue(Context ctx, boolean increment) { - if (type == TokenType.HOTP) { - if (increment) { - try { - return getHOTP(counter++); - } finally { - save(ctx); - } - } else { - String placeholder = ""; - for (int i = 0; i < digits; i++) - placeholder += "-"; - - return placeholder; - } - } + public String getPlaceholder() { + StringBuilder sb = new StringBuilder(digits); + for (int i = 0; i < digits; i++) + sb.append('-'); + return sb.toString(); + } - return getHOTP(System.currentTimeMillis() / 1000 / period); + public void increment() { + if (type == TokenType.HOTP) + counter++; } public Uri toUri() { diff --git a/src/org/fedorahosted/freeotp/TokenAdapter.java b/src/org/fedorahosted/freeotp/TokenAdapter.java index 1915548..b0035e5 100644 --- a/src/org/fedorahosted/freeotp/TokenAdapter.java +++ b/src/org/fedorahosted/freeotp/TokenAdapter.java @@ -20,11 +20,7 @@ package org.fedorahosted.freeotp; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.fedorahosted.freeotp.Token.TokenUriInvalidException; @@ -34,6 +30,7 @@ import android.content.Context; import android.content.DialogInterface; import android.os.Handler; import android.os.Message; +import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -63,32 +60,29 @@ public class TokenAdapter extends BaseAdapter { } } - private final List tokens = new ArrayList(); - private final Ticker ticker = new Ticker(); - - private void sort() { - Collections.sort(tokens, new Comparator() { - @Override - public int compare(Token lhs, Token rhs) { - return lhs.getTitle().compareTo(rhs.getTitle()); - } - }); + private static class ViewHolder { + int index; + Token token; + TextView code; + TextView title; } + private final TokenStore ts; + private final Ticker ticker = new Ticker(); + public TokenAdapter(Context ctx) { - tokens.addAll(Token.getTokens(ctx)); + ts = new TokenStore(ctx); ticker.sendEmptyMessageDelayed(0, 200); - sort(); } @Override public int getCount() { - return tokens.size(); + return ts.getTokenCount(); } @Override public Token getItem(int position) { - return tokens.get(position); + return ts.get(position); } @Override @@ -98,86 +92,104 @@ public class TokenAdapter extends BaseAdapter { @Override public View getView(int position, View convertView, ViewGroup parent) { - final Context ctx = parent.getContext(); + Token token = ts.get(position); - if (convertView == null) { - switch (getItem(position).getType()) { - case HOTP: - convertView = View.inflate(ctx, R.layout.hotp, null); - break; + if (convertView == null) + convertView = newView(parent.getContext(), token.getType(), parent); - case TOTP: - convertView = View.inflate(ctx, R.layout.totp, null); - break; - } - } + bindView(parent.getContext(), convertView, token, position); + return convertView; + } - final Token item = getItem(position); - final TextView code = (TextView) convertView.findViewById(R.id.code); - final TextView title = (TextView) convertView.findViewById(R.id.title); - final ImageButton ib = (ImageButton) convertView.findViewById(R.id.button); + public void bindView(Context ctx, View view, Token token, int position) { + ViewHolder holder = (ViewHolder) view.getTag(); + holder.index = position; + holder.token = token; - code.setText(item.getCurrentTokenValue(ctx, false)); - title.setText(item.getTitle()); + holder.code.setText(token.getPlaceholder()); + holder.title.setText(token.getIssuer() + ": " + token.getLabel()); + } - ib.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - String delmsg = ctx.getString(R.string.delete_message); - - AlertDialog ad = new AlertDialog.Builder(ctx) - .setTitle("Delete") - .setMessage(delmsg + item.getTitle()) - .setIcon(android.R.drawable.ic_delete) - .setPositiveButton(R.string.delete, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - tokens.remove(tokens.indexOf(item)); - item.remove(ctx); - notifyDataSetChanged(); - dialog.dismiss(); - } - - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }).create(); - ad.show(); - } - }); + public View newView(Context ctx, Token.TokenType type, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ViewHolder holder = new ViewHolder(); - switch (getItem(position).getType()) { + View view = null; + switch (type) { case HOTP: - ImageButton hotp = (ImageButton) convertView.findViewById(R.id.hotpButton); + view = inflater.inflate(R.layout.hotp, parent, false); + ImageButton hotp = (ImageButton) view.findViewById(R.id.hotpButton); + hotp.setTag(holder); hotp.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - code.setText(item.getCurrentTokenValue(ctx, true)); + ViewHolder holder = (ViewHolder) v.getTag(); + holder.code.setText(holder.token.getCode()); + holder.token.increment(); + ts.save(holder.token); } }); break; case TOTP: - ProgressBar pb = (ProgressBar) convertView.findViewById(R.id.totpProgressBar); + view = inflater.inflate(R.layout.totp, parent, false); + ProgressBar pb = (ProgressBar) view.findViewById(R.id.totpProgressBar); + pb.setTag(holder); ticker.set(pb, new Ticker.OnTickListener() { @Override public void tick(ProgressBar pb) { - int max = pb.getMax(); - int pro = item.getProgress(); - pb.setProgress(max - pro); - if (pro < max / 20 || pro > max / 20 * 19) - code.setText(item.getCurrentTokenValue(ctx, false)); + ViewHolder holder = (ViewHolder) pb.getTag(); + pb.setProgress(pb.getMax() - holder.token.getProgress()); + holder.code.setText(holder.token.getCode()); } }); break; } - return convertView; + ImageButton ib = (ImageButton) view.findViewById(R.id.button); + ib.setTag(holder); + ib.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + final ViewHolder holder = (ViewHolder) v.getTag(); + + StringBuilder sb = new StringBuilder(); + sb.append(v.getContext().getString(R.string.delete_message)); + sb.append(holder.token.getIssuer()); + sb.append("\n"); + sb.append(holder.token.getLabel()); + + AlertDialog ad = new AlertDialog.Builder(v.getContext()) + .setTitle("Delete") + .setMessage(sb.toString()) + .setIcon(android.R.drawable.ic_delete) + .setPositiveButton(R.string.delete, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ts.del(holder.index); + notifyDataSetChanged(); + dialog.dismiss(); + } + + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }) + .create(); + ad.show(); + } + }); + + holder.code = (TextView) view.findViewById(R.id.code); + holder.title = (TextView) view.findViewById(R.id.title); + view.setTag(holder); + + return view; } @Override @@ -193,15 +205,12 @@ public class TokenAdapter extends BaseAdapter { case TOTP: return 1; default: - return -1; + return IGNORE_ITEM_VIEW_TYPE; } } public void add(Context ctx, String uri) throws TokenUriInvalidException { - Token t = new Token(uri); - t.save(ctx); - tokens.add(t); - sort(); + ts.add(new Token(uri)); notifyDataSetChanged(); } } diff --git a/src/org/fedorahosted/freeotp/TokenStore.java b/src/org/fedorahosted/freeotp/TokenStore.java new file mode 100644 index 0000000..a9aa560 --- /dev/null +++ b/src/org/fedorahosted/freeotp/TokenStore.java @@ -0,0 +1,92 @@ +package org.fedorahosted.freeotp; + +import java.util.LinkedList; +import java.util.List; + +import org.fedorahosted.freeotp.Token.TokenUriInvalidException; +import org.json.JSONArray; +import org.json.JSONException; + +import android.content.Context; +import android.content.SharedPreferences; + +public class TokenStore { + private static final String NAME = "tokens"; + private static final String ORDER = "tokenOrder"; + private SharedPreferences prefs; + + private List getTokenOrder() { + try { + JSONArray array = new JSONArray(prefs.getString(ORDER, null)); + List out = new LinkedList(); + for (int i = 0; i < array.length(); i++) + out.add(array.getString(i)); + return out; + } catch (JSONException e) { + } catch (NullPointerException e) { + } + + return new LinkedList(); + } + + private SharedPreferences.Editor setTokenOrder(List order) { + JSONArray array = new JSONArray(); + for (String key : order) + array.put(key); + + return prefs.edit().putString(ORDER, array.toString()); + } + + public TokenStore(Context ctx) { + prefs = ctx.getApplicationContext() + .getSharedPreferences(NAME, Context.MODE_PRIVATE); + } + + public void add(Token token) { + String key = token.getID(); + + if (prefs.contains(key)) + return; + + List order = getTokenOrder(); + order.add(0, key); + setTokenOrder(order).putString(key, token.toString()).apply(); + } + + public void del(int index) { + List order = getTokenOrder(); + String key = order.remove(index); + setTokenOrder(order).remove(key).apply(); + } + + public void save(Token token) { + prefs.edit().putString(token.getID(), token.toString()).apply(); + } + + public Token get(int index) { + try { + return new Token(prefs.getString(getTokenOrder().get(index), null)); + } catch (TokenUriInvalidException e) { + e.printStackTrace(); + } catch (NullPointerException e) { + e.printStackTrace(); + } + + return null; + } + + public int getTokenCount() { + return getTokenOrder().size(); + } + + public void move(int fromIndex, int toIndex) { + List order = getTokenOrder(); + if (fromIndex < 0 || fromIndex > order.size()) + return; + if (toIndex < 0 || toIndex > order.size()) + return; + + order.add(toIndex, order.remove(fromIndex)); + setTokenOrder(order).apply(); + } +} -- 2.43.2