]> Pileus Git - ~andy/freeotp/commitdiff
Tablet UI rewrite
authorNathaniel McCallum <npmccallum@redhat.com>
Sat, 30 Nov 2013 16:59:46 +0000 (11:59 -0500)
committerNathaniel McCallum <npmccallum@redhat.com>
Sat, 30 Nov 2013 16:59:46 +0000 (11:59 -0500)
30 files changed:
AndroidManifest.xml
res/drawable-hdpi/delete.png [deleted file]
res/drawable-hdpi/generate.png [deleted file]
res/drawable-mdpi/delete.png [deleted file]
res/drawable-mdpi/generate.png [deleted file]
res/drawable-xhdpi/delete.png [deleted file]
res/drawable-xhdpi/generate.png [deleted file]
res/drawable/token.xml [new file with mode: 0644]
res/drawable/token_disabled.xml [new file with mode: 0644]
res/drawable/token_normal.xml [new file with mode: 0644]
res/drawable/token_pressed.xml [new file with mode: 0644]
res/layout/common.xml [deleted file]
res/layout/hotp.xml [deleted file]
res/layout/main.xml
res/layout/manual.xml
res/layout/token.xml [new file with mode: 0644]
res/layout/totp.xml [deleted file]
res/menu/main.xml
res/values/strings.xml
src/org/fedorahosted/freeotp/CircleProgressBar.java [deleted file]
src/org/fedorahosted/freeotp/MainActivity.java
src/org/fedorahosted/freeotp/Token.java
src/org/fedorahosted/freeotp/TokenAdapter.java [deleted file]
src/org/fedorahosted/freeotp/TokenStore.java [deleted file]
src/org/fedorahosted/freeotp/UrgencyProgressBar.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/adapters/BaseAdapter.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/adapters/DeleteActionBarBaseAdapter.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/adapters/ReorderableBaseAdapter.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/adapters/TokenAdapter.java [new file with mode: 0644]
src/org/fedorahosted/freeotp/adapters/TokenPersistenceBaseAdapter.java [new file with mode: 0644]

index 73ed9c4c4479be5a28051bc926497a8f0953cf55..af4a8bef298b8c736c6ab0ae5c89ae580c61be4d 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
-<!-- 
+<!--
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="org.fedorahosted.freeotp"
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="org.fedorahosted.freeotp"
-    android:versionCode="2"
-    android:versionName="1.0" >
+    android:versionCode="3"
+    android:versionName="1.1" >
 
     <uses-sdk
 
     <uses-sdk
-        android:minSdkVersion="14"
+        android:minSdkVersion="11"
         android:targetSdkVersion="19" />
 
     <application
         android:targetSdkVersion="19" />
 
     <application
diff --git a/res/drawable-hdpi/delete.png b/res/drawable-hdpi/delete.png
deleted file mode 100755 (executable)
index e9ce89e..0000000
Binary files a/res/drawable-hdpi/delete.png and /dev/null differ
diff --git a/res/drawable-hdpi/generate.png b/res/drawable-hdpi/generate.png
deleted file mode 100755 (executable)
index 0f3ceec..0000000
Binary files a/res/drawable-hdpi/generate.png and /dev/null differ
diff --git a/res/drawable-mdpi/delete.png b/res/drawable-mdpi/delete.png
deleted file mode 100755 (executable)
index cedb108..0000000
Binary files a/res/drawable-mdpi/delete.png and /dev/null differ
diff --git a/res/drawable-mdpi/generate.png b/res/drawable-mdpi/generate.png
deleted file mode 100755 (executable)
index ca28ba3..0000000
Binary files a/res/drawable-mdpi/generate.png and /dev/null differ
diff --git a/res/drawable-xhdpi/delete.png b/res/drawable-xhdpi/delete.png
deleted file mode 100755 (executable)
index 98c73da..0000000
Binary files a/res/drawable-xhdpi/delete.png and /dev/null differ
diff --git a/res/drawable-xhdpi/generate.png b/res/drawable-xhdpi/generate.png
deleted file mode 100755 (executable)
index a80b9ab..0000000
Binary files a/res/drawable-xhdpi/generate.png and /dev/null differ
diff --git a/res/drawable/token.xml b/res/drawable/token.xml
new file mode 100644 (file)
index 0000000..6b21b2f
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   - FreeOTP
+   -
+   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
+   -
+   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
+   -
+   - 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.
+   -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+          android:drawable="@drawable/token_disabled" />
+    <item android:state_pressed="true"
+          android:drawable="@drawable/token_pressed" />
+    <item android:drawable="@drawable/token_normal" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable/token_disabled.xml b/res/drawable/token_disabled.xml
new file mode 100644 (file)
index 0000000..31091d1
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   - FreeOTP
+   -
+   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
+   -
+   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
+   -
+   - 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.
+   -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:dither="true" android:shape="rectangle">
+            <solid android:color="#B2CCCCCC" />
+            <corners android:radius="2dp" />
+        </shape>
+    </item>
+    <item android:bottom="2dp">
+        <shape android:dither="true" android:shape="rectangle">
+            <solid android:color="#B2FFFFFF" />
+            <corners android:radius="2dp" />
+            <padding
+                android:top="8dp"
+                android:bottom="8dp"
+                android:left="8dp"
+                android:right="8dp"
+                />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/token_normal.xml b/res/drawable/token_normal.xml
new file mode 100644 (file)
index 0000000..d015b96
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   - FreeOTP
+   -
+   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
+   -
+   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
+   -
+   - 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.
+   -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:dither="true" android:shape="rectangle">
+            <solid android:color="#CCCCCC" />
+            <corners android:radius="2dp" />
+        </shape>
+    </item>
+    <item android:bottom="2dp">
+        <shape android:dither="true" android:shape="rectangle">
+            <solid android:color="#FFFFFF" />
+            <corners android:radius="2dp" />
+            <padding
+                android:top="8dp"
+                android:bottom="8dp"
+                android:left="8dp"
+                android:right="8dp"
+                />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/token_pressed.xml b/res/drawable/token_pressed.xml
new file mode 100644 (file)
index 0000000..3439865
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   - FreeOTP
+   -
+   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
+   -
+   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
+   -
+   - 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.
+   -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:dither="true" android:shape="rectangle">
+            <solid android:color="#CCCCCC" />
+            <corners android:radius="2dp" />
+        </shape>
+    </item>
+    <item android:bottom="2dp" android:drawable="?android:attr/selectableItemBackground">
+        <shape android:dither="true" android:shape="rectangle">
+            <solid android:color="@android:color/transparent" />
+            <corners android:radius="2dp" />
+            <padding
+                android:top="8dp"
+                android:bottom="8dp"
+                android:left="8dp"
+                android:right="8dp"
+                />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/layout/common.xml b/res/layout/common.xml
deleted file mode 100644 (file)
index 12f5466..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 
-   - FreeOTP
-   -
-   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
-   -
-   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
-   -
-   - 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.
-   -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="0dp"
-    android:layout_height="72dp"
-    android:layout_weight="1"
-    android:orientation="vertical" >
-
-    <TextView
-        android:id="@+id/title"
-        android:layout_width="match_parent"
-        android:layout_height="26dp"
-        android:gravity="center_vertical|left"
-        android:paddingTop="4dp"
-        android:paddingLeft="8dp"
-        android:text="Example: example@example.com"
-        android:textSize="14sp" />
-    
-    <LinearLayout
-           android:layout_width="match_parent"
-           android:layout_height="46dp"
-           android:orientation="horizontal" >
-           <ImageButton
-               android:id="@+id/button"
-            android:layout_width="46dp"
-            android:layout_height="46dp"
-            android:layout_marginLeft="8dp"
-            android:src="@drawable/delete"
-            style="?android:attr/borderlessButtonStyle"
-            android:layout_gravity="center" />
-           
-           <TextView
-               android:id="@+id/code"
-               android:layout_width="match_parent"
-               android:layout_height="46dp"
-               android:gravity="center_vertical|left"
-               android:padding="0dp"
-               android:text="00000000"
-               android:textSize="34sp"
-               android:typeface="monospace" />
-       </LinearLayout>
-    
-</LinearLayout>
diff --git a/res/layout/hotp.xml b/res/layout/hotp.xml
deleted file mode 100644 (file)
index c228a00..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 
-   - FreeOTP
-   -
-   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
-   -
-   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
-   -
-   - 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.
-   -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:paddingLeft="8dp"
-    android:orientation="horizontal" >
-
-    <include layout="@layout/common" />
-
-    <View
-        android:layout_width="1dp"
-        android:layout_height="72dp"
-        android:paddingTop="8dp"
-        android:paddingBottom="4dp"
-        android:background="?android:attr/dividerVertical"
-        />
-    
-    <ImageButton
-        android:id="@+id/hotpButton"
-        android:layout_width="72dp"
-        android:layout_height="72dp"
-        android:padding="8dp"
-        android:src="@drawable/generate"
-        style="?android:attr/borderlessButtonStyle"
-        />
-</LinearLayout>
index 5f20ec399a6bc6ecdaf692990d1cde67d199f1dc..a1770ae309df4af2be0fcd0e7299e4345b3169eb 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
-<!-- 
+<!--
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
         android:text="@string/no_keys"
         />
 
         android:text="@string/no_keys"
         />
 
-    <ListView
-        android:id="@android:id/list"
-        android:layout_width="match_parent"
+       <GridView
+           android:id="@+id/grid"
+           android:layout_width="match_parent"
         android:layout_height="0dp"
         android:layout_weight="1"
         android:layout_height="0dp"
         android:layout_weight="1"
-        android:drawSelectorOnTop="false"
-        />
+           android:columnWidth="250dp"
+           android:gravity="center"
+           android:horizontalSpacing="8dp"
+           android:numColumns="auto_fit"
+           android:padding="8dp"
+           android:stretchMode="columnWidth"
+           android:verticalSpacing="8dp"
+           android:background="#E5E5E5"
+           />
 </LinearLayout>
\ No newline at end of file
 </LinearLayout>
\ No newline at end of file
index fd11414ad75ef859115e9af8ee24735a3c99ea89..145d32bc32b41be91cf427db29bfc1c52094bfb4 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
-<!-- 
+<!--
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
    - See the License for the specific language governing permissions and
    - limitations under the License.
    -->
    - See the License for the specific language governing permissions and
    - limitations under the License.
    -->
-
-<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:padding="16dp" >
-
-    <TableRow
-        android:layout_width="match_parent"
-        android:layout_height="48dp" >
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center_vertical|right"
-            android:text="@string/issuer"
-            android:paddingRight="8dp" />
-
-        <EditText
-            android:id="@+id/issuer"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:hint="jdoe@example.com"
-            android:textAppearance="?android:attr/textAppearanceSmallInverse" />
-    </TableRow>
-
-    <TableRow
+    android:layout_height="match_parent" >
+    <TableLayout
         android:layout_width="match_parent"
         android:layout_width="match_parent"
-        android:layout_height="48dp" >
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center_vertical|right"
-            android:text="@string/id"
-            android:paddingRight="8dp" />
-
-        <EditText
-            android:id="@+id/id"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:hint="18c5d06cfcbd4927"
-            android:textAppearance="?android:attr/textAppearanceSmallInverse" />
-    </TableRow>
-
-    <TableRow
-        android:layout_width="match_parent"
-        android:layout_height="48dp" >
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center_vertical|right"
-            android:text="@string/secret"
-            android:paddingRight="8dp" />
-
-        <EditText
-            android:id="@+id/secret"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:digits="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ234567="
-            android:hint="Base32 (ex. &apos;GEZDGNBV&apos;)"
-            android:textAppearance="?android:attr/textAppearanceSmall" />
-    </TableRow>
-
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:layout_marginTop="12dp"
-        android:layout_marginBottom="4dp"
-        android:background="?android:attr/dividerHorizontal"
-        />
+        android:layout_height="wrap_content"
+        android:padding="16dp" >
 
 
-    <TableRow
-        android:layout_width="match_parent"
-        android:layout_height="48dp" >
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center_vertical|right"
-            android:text="@string/type"
-            android:paddingRight="8dp" />
-
-        <Spinner
-            android:id="@+id/type"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:entries="@array/token_types" />
-    </TableRow>
-
-    <TableRow
-        android:layout_width="match_parent"
-        android:layout_height="48dp" >
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center_vertical|right"
-            android:text="@string/algorithm"
-            android:paddingRight="8dp" />
-
-        <Spinner
-            android:id="@+id/algorithm"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:entries="@array/algorithms" />
-    </TableRow>
-
-    <TableRow
-        android:layout_width="match_parent"
-        android:layout_height="48dp" >
-        <TextView
-            android:id="@+id/interval_label"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center_vertical|right"
-            android:text="@string/interval"
-            android:paddingRight="8dp" />
-
-        <EditText
-            android:id="@+id/interval"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:inputType="number"
-            android:text="30" />
-    </TableRow>
-
-    <TableRow
-        android:layout_width="match_parent"
-        android:layout_height="48dp" >
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center_vertical|right"
-            android:text="@string/digits"
-            android:paddingRight="8dp" />
-
-        <RadioGroup
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:orientation="horizontal" >
-            <RadioButton
-                android:id="@+id/digits6"
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="48dp" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical|right"
+                android:paddingRight="8dp"
+                android:text="@string/issuer" />
+
+            <EditText
+                android:id="@+id/issuer"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:hint="jdoe@example.com"
+                android:textAppearance="?android:attr/textAppearanceSmallInverse" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="48dp" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical|right"
+                android:paddingRight="8dp"
+                android:text="@string/id" />
+
+            <EditText
+                android:id="@+id/id"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:hint="18c5d06cfcbd4927"
+                android:textAppearance="?android:attr/textAppearanceSmallInverse" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="48dp" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical|right"
+                android:paddingRight="8dp"
+                android:text="@string/secret" />
+
+            <EditText
+                android:id="@+id/secret"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:digits="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ234567="
+                android:hint="(Base32 encoded)"
+                android:textAppearance="?android:attr/textAppearanceSmall" />
+        </TableRow>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginBottom="4dp"
+            android:layout_marginTop="12dp"
+            android:background="?android:attr/dividerHorizontal" />
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="48dp" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical|right"
+                android:paddingRight="8dp"
+                android:text="@string/type" />
+
+            <Spinner
+                android:id="@+id/type"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:entries="@array/token_types" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="48dp" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical|right"
+                android:paddingRight="8dp"
+                android:text="@string/algorithm" />
+
+            <Spinner
+                android:id="@+id/algorithm"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:entries="@array/algorithms" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="48dp" >
+
+            <TextView
+                android:id="@+id/interval_label"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical|right"
+                android:paddingRight="8dp"
+                android:text="@string/interval" />
+
+            <EditText
+                android:id="@+id/interval"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1"
-                android:text="6"
-                android:checked="true" />
+                android:inputType="number"
+                android:text="30" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="48dp" >
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical|right"
+                android:paddingRight="8dp"
+                android:text="@string/digits" />
 
 
-            <RadioButton
-                android:id="@+id/digits8"
+            <RadioGroup
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1"
-                android:text="8" />
-        </RadioGroup>
-    </TableRow>
-</TableLayout>
\ No newline at end of file
+                android:orientation="horizontal" >
+
+                <RadioButton
+                    android:id="@+id/digits6"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:checked="true"
+                    android:text="6" />
+
+                <RadioButton
+                    android:id="@+id/digits8"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:text="8" />
+            </RadioGroup>
+        </TableRow>
+    </TableLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout/token.xml b/res/layout/token.xml
new file mode 100644 (file)
index 0000000..9f5cd3d
--- /dev/null
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   - FreeOTP
+   -
+   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
+   -
+   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
+   -
+   - 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.
+   -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="4dp"
+    android:layout_marginBottom="4dp"
+    android:layout_marginLeft="8dp"
+    android:layout_marginRight="8dp"
+    android:background="@drawable/token"
+    android:orientation="vertical" >
+    <TextView
+        android:id="@+id/code"
+        android:layout_width="match_parent"
+        android:layout_height="60dp"
+        android:gravity="center"
+        android:paddingTop="4dp"
+        android:text="01234567"
+        android:textSize="48sp"
+        android:textStyle="bold"
+        android:typeface="monospace" />
+
+    <org.fedorahosted.freeotp.UrgencyProgressBar
+        android:id="@+id/progress"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:max="1000"
+        android:progress="500" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="4dp"
+        android:orientation="horizontal" >
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical" >
+
+            <TextView
+                android:id="@+id/issuer"
+                android:layout_width="match_parent"
+                android:layout_height="24dp"
+                android:ellipsize="end"
+                android:gravity="center_vertical"
+                android:singleLine="true"
+                android:text="example@example.com"
+                android:textColor="@android:color/primary_text_light"
+                android:textSize="16sp"
+                android:textStyle="bold" />
+
+            <TextView
+                android:id="@+id/label"
+                android:layout_width="match_parent"
+                android:layout_height="16dp"
+                android:ellipsize="end"
+                android:gravity="center_vertical"
+                android:singleLine="true"
+                android:text="1FMAS0M43MF98ASMDMF10MF0M"
+                android:textColor="@android:color/secondary_text_light"
+                android:textSize="12sp"
+                android:textStyle="normal" />
+        </LinearLayout>
+
+        <CheckBox
+            android:id="@+id/checkBox"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:background="?android:attr/selectableItemBackground" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/totp.xml b/res/layout/totp.xml
deleted file mode 100644 (file)
index 9c38a4f..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 
-   - FreeOTP
-   -
-   - Authors: Nathaniel McCallum <npmccallum@redhat.com>
-   -
-   - Copyright (C) 2013  Nathaniel McCallum, Red Hat
-   -
-   - 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.
-   -->
-  
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:paddingLeft="8dp"
-    android:orientation="horizontal" >
-
-    <include layout="@layout/common" />
-    
-    <View
-        android:layout_width="1dp"
-        android:layout_height="72dp"
-        android:paddingTop="8dp"
-        android:paddingBottom="4dp"
-        android:background="?android:attr/dividerVertical"
-        />
-
-    <org.fedorahosted.freeotp.CircleProgressBar
-        android:id="@+id/totpProgressBar"
-        android:layout_width="72dp"
-        android:layout_height="72dp"
-        android:padding="24dp"
-        android:max="1000"
-        style="@android:style/Widget.ProgressBar.Horizontal"
-        />
-</LinearLayout>
index 4d8fe7b9e0690d8dcba3481454202f6ee62ff11c..827c7d9a7977853e739f54a37c9907eb0ddc419c 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
-<!-- 
+<!--
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
    - FreeOTP
    -
    - Authors: Nathaniel McCallum <npmccallum@redhat.com>
index 072dec948b050dac3facf2911ec5eb3dccc929a7..fec49c5b236fc97da6be04680f7ac85a350e66b5 100644 (file)
@@ -3,7 +3,6 @@
     <string name="app_name">FreeOTP</string>
     <string name="add">Add</string>
     <string name="invalid_token">The token specified was invalid!</string>
     <string name="app_name">FreeOTP</string>
     <string name="add">Add</string>
     <string name="invalid_token">The token specified was invalid!</string>
-    <string name="delete_message">Are you sure you want to remove this token?\n\nNOTE: This will NOT disable two-factor authentication on the server.\n\n</string>
     <string name="delete">Delete</string>
     <string name="yes">Yes</string>
     <string name="no">No</string>
     <string name="delete">Delete</string>
     <string name="yes">Yes</string>
     <string name="no">No</string>
     <string name="algorithm">Algorithm</string>
     <string name="digits">Digits</string>
 
     <string name="algorithm">Algorithm</string>
     <string name="digits">Digits</string>
 
+    <plurals name="tokens_selected">
+        <item quantity="one">%d token selected</item>
+        <item quantity="other">%d tokens selected</item>
+    </plurals>
+
     <string-array name="token_types">
         <item>Time-based (TOTP)</item>
         <item>Counter-based (HOTP)</item>
     </string-array>
 
     <string-array name="token_types">
         <item>Time-based (TOTP)</item>
         <item>Counter-based (HOTP)</item>
     </string-array>
 
-    <string-array name="digits">
-        <item>6</item>
-        <item>8</item>
-    </string-array>
-
     <string-array name="algorithms">
         <item>MD5</item>
         <item>SHA1</item>
     <string-array name="algorithms">
         <item>MD5</item>
         <item>SHA1</item>
diff --git a/src/org/fedorahosted/freeotp/CircleProgressBar.java b/src/org/fedorahosted/freeotp/CircleProgressBar.java
deleted file mode 100644 (file)
index e9772e6..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum <npmccallum@redhat.com>
- *
- * Copyright (C) 2013  Nathaniel McCallum, Red Hat
- *
- * 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 org.fedorahosted.freeotp;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Paint.Style;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.widget.ProgressBar;
-
-public class CircleProgressBar extends ProgressBar {
-       private Paint paint;
-       private RectF rectf;
-       private Rect rect;
-
-       public CircleProgressBar(Context context, AttributeSet attrs, int defStyle) {
-               super(context, attrs, defStyle);
-               setup();
-       }
-
-       public CircleProgressBar(Context context, AttributeSet attrs) {
-               super(context, attrs);
-               setup();
-       }
-
-       public CircleProgressBar(Context context) {
-               super(context);
-               setup();
-       }
-
-       private void setup() {
-               paint = new Paint();
-        rectf = new RectF();
-        rect = new Rect();
-
-        paint.setColor(0x33333300);
-        paint.setAlpha(0x99);
-        paint.setAntiAlias(true);
-        paint.setStyle(Style.FILL_AND_STROKE);
-       }
-
-       @Override
-       protected synchronized void onDraw(Canvas canvas) {
-               getDrawingRect(rect);
-               rect.left += getPaddingLeft() + 2;
-               rect.top += getPaddingTop() + 2;
-               rect.right -= getPaddingRight() + 2;
-               rect.bottom -= getPaddingBottom() + 2;
-
-               rectf.set(rect);
-               canvas.drawArc(rectf, -90, getProgress() * 360 / getMax(), true, paint);
-       }
-}
index ad62761fa7f1655e4cdb48dc1062f4cc0cb40e81..d8278c2430079f9de6a4031be5d4147b95535ef1 100644 (file)
@@ -40,23 +40,27 @@ import java.util.Arrays;
 import java.util.List;
 
 import org.fedorahosted.freeotp.Token.TokenUriInvalidException;
 import java.util.List;
 
 import org.fedorahosted.freeotp.Token.TokenUriInvalidException;
+import org.fedorahosted.freeotp.adapters.TokenAdapter;
 
 
+import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.AlertDialog;
-import android.app.ListActivity;
 import android.content.ActivityNotFoundException;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.ActivityNotFoundException;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.database.DataSetObserver;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MenuItem.OnMenuItemClickListener;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.widget.GridView;
 import android.widget.Toast;
 
 import android.widget.Toast;
 
-public class MainActivity extends ListActivity {
+public class MainActivity extends Activity {
        private static final String ACTION_SCAN = "com.google.zxing.client.android.SCAN";
        private static final List<String> PROVIDERS = Arrays.asList(new String[] {
                "com.google.zxing.client.android", // Barcode Scanner
        private static final String ACTION_SCAN = "com.google.zxing.client.android.SCAN";
        private static final List<String> PROVIDERS = Arrays.asList(new String[] {
                "com.google.zxing.client.android", // Barcode Scanner
@@ -82,10 +86,24 @@ public class MainActivity extends ListActivity {
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.main);
-        ta = new TokenAdapter(this);
-        setListAdapter(ta);
+               super.onCreate(savedInstanceState);
+               setContentView(R.layout.main);
+
+               ta = new TokenAdapter(this);
+               ((GridView) findViewById(R.id.grid)).setAdapter(ta);
+
+               DataSetObserver dso = new DataSetObserver() {
+                       @Override
+                       public void onChanged() {
+                               super.onChanged();
+                               if (ta.getCount() == 0)
+                                       findViewById(android.R.id.empty).setVisibility(View.VISIBLE);
+                               else
+                                       findViewById(android.R.id.empty).setVisibility(View.GONE);
+                       }
+               };
+               ta.registerDataSetObserver(dso);
+               dso.onChanged();
     }
 
     @Override
     }
 
     @Override
@@ -99,7 +117,7 @@ public class MainActivity extends ListActivity {
                                        @Override
                                        public void addToken(String uri) {
                                                try {
                                        @Override
                                        public void addToken(String uri) {
                                                try {
-                                                       ta.add(MainActivity.this, uri);
+                                                       ta.add(uri);
                                                } catch (TokenUriInvalidException e) {
                                                        Toast.makeText(MainActivity.this, R.string.invalid_token, Toast.LENGTH_SHORT).show();
                                                        e.printStackTrace();
                                                } catch (TokenUriInvalidException e) {
                                                        Toast.makeText(MainActivity.this, R.string.invalid_token, Toast.LENGTH_SHORT).show();
                                                        e.printStackTrace();
@@ -162,7 +180,7 @@ public class MainActivity extends ListActivity {
        public void onActivityResult(int requestCode, int resultCode, Intent intent) {
                if (resultCode == RESULT_OK) {
                        try {
        public void onActivityResult(int requestCode, int resultCode, Intent intent) {
                if (resultCode == RESULT_OK) {
                        try {
-                               ta.add(this, intent.getStringExtra("SCAN_RESULT"));
+                               ta.add(intent.getStringExtra("SCAN_RESULT"));
                        } catch (TokenUriInvalidException e) {
                                Toast.makeText(this, R.string.invalid_token, Toast.LENGTH_SHORT).show();
                                e.printStackTrace();
                        } catch (TokenUriInvalidException e) {
                                Toast.makeText(this, R.string.invalid_token, Toast.LENGTH_SHORT).show();
                                e.printStackTrace();
index 35a9576a1539fc1c88d8caa081b4e73bcba04d93..f087297a8971dc17a9ce3fc202bb666e219a3c9b 100644 (file)
@@ -42,24 +42,25 @@ public class Token {
                HOTP, TOTP
        }
 
                HOTP, TOTP
        }
 
-       private final String issuerInt;
-       private final String issuerExt;
-       private final String label;
-       private TokenType type;
-       private String algo;
-       private byte[] key;
-       private int digits;
-       private long counter;
-       private int period;
+       private final String mIssuerInt;
+       private final String mIssuerExt;
+       private final String mLabel;
+       private TokenType mType;
+       private String mAlgorithm;
+       private byte[] mSecret;
+       private int mDigits;
+       private long mCounter;
+       private int mPeriod;
+       private long mLastCode;
 
        private Token(Uri uri) throws TokenUriInvalidException {
                if (!uri.getScheme().equals("otpauth"))
                        throw new TokenUriInvalidException();
 
                if (uri.getAuthority().equals("totp"))
 
        private Token(Uri uri) throws TokenUriInvalidException {
                if (!uri.getScheme().equals("otpauth"))
                        throw new TokenUriInvalidException();
 
                if (uri.getAuthority().equals("totp"))
-                       type = TokenType.TOTP;
+                       mType = TokenType.TOTP;
                else if (uri.getAuthority().equals("hotp"))
                else if (uri.getAuthority().equals("hotp"))
-                       type = TokenType.HOTP;
+                       mType = TokenType.HOTP;
                else
                        throw new TokenUriInvalidException();
 
                else
                        throw new TokenUriInvalidException();
 
@@ -74,16 +75,16 @@ public class Token {
                        throw new TokenUriInvalidException();
 
                int i = path.indexOf(':');
                        throw new TokenUriInvalidException();
 
                int i = path.indexOf(':');
-               issuerExt = i < 0 ? "" : path.substring(0, i);
-               issuerInt = uri.getQueryParameter("issuer");
-               label = path.substring(i >= 0 ? i + 1 : 0);
-
-               algo = uri.getQueryParameter("algorithm");
-               if (algo == null)
-                       algo = "sha1";
-               algo = algo.toUpperCase(Locale.US);
+               mIssuerExt = i < 0 ? "" : path.substring(0, i);
+               mIssuerInt = uri.getQueryParameter("issuer");
+               mLabel = path.substring(i >= 0 ? i + 1 : 0);
+
+               mAlgorithm = uri.getQueryParameter("algorithm");
+               if (mAlgorithm == null)
+                       mAlgorithm = "sha1";
+               mAlgorithm = mAlgorithm.toUpperCase(Locale.US);
                try {
                try {
-                       Mac.getInstance("Hmac" + algo);
+                       Mac.getInstance("Hmac" + mAlgorithm);
                } catch (NoSuchAlgorithmException e1) {
                        throw new TokenUriInvalidException();
                }
                } catch (NoSuchAlgorithmException e1) {
                        throw new TokenUriInvalidException();
                }
@@ -92,20 +93,20 @@ public class Token {
                        String d = uri.getQueryParameter("digits");
                        if (d == null)
                                d = "6";
                        String d = uri.getQueryParameter("digits");
                        if (d == null)
                                d = "6";
-                       digits = Integer.parseInt(d);
-                       if (digits != 6 && digits != 8)
+                       mDigits = Integer.parseInt(d);
+                       if (mDigits != 6 && mDigits != 8)
                                throw new TokenUriInvalidException();
                } catch (NumberFormatException e) {
                        throw new TokenUriInvalidException();
                }
 
                                throw new TokenUriInvalidException();
                } catch (NumberFormatException e) {
                        throw new TokenUriInvalidException();
                }
 
-               switch (type) {
+               switch (mType) {
                case HOTP:
                        try {
                                String c = uri.getQueryParameter("counter");
                                if (c == null)
                                        c = "0";
                case HOTP:
                        try {
                                String c = uri.getQueryParameter("counter");
                                if (c == null)
                                        c = "0";
-                               counter = Long.parseLong(c);
+                               mCounter = Long.parseLong(c) - 1;
                        } catch (NumberFormatException e) {
                                throw new TokenUriInvalidException();
                        }
                        } catch (NumberFormatException e) {
                                throw new TokenUriInvalidException();
                        }
@@ -115,7 +116,7 @@ public class Token {
                                String p = uri.getQueryParameter("period");
                                if (p == null)
                                        p = "30";
                                String p = uri.getQueryParameter("period");
                                if (p == null)
                                        p = "30";
-                               period = Integer.parseInt(p);
+                               mPeriod = Integer.parseInt(p);
                        } catch (NumberFormatException e) {
                                throw new TokenUriInvalidException();
                        }
                        } catch (NumberFormatException e) {
                                throw new TokenUriInvalidException();
                        }
@@ -124,7 +125,7 @@ public class Token {
 
                try {
                        String s = uri.getQueryParameter("secret");
 
                try {
                        String s = uri.getQueryParameter("secret");
-                       key = Base32String.decode(s);
+                       mSecret = Base32String.decode(s);
                } catch (DecodingException e) {
                        throw new TokenUriInvalidException();
                }
                } catch (DecodingException e) {
                        throw new TokenUriInvalidException();
                }
@@ -137,13 +138,13 @@ public class Token {
 
                // Create digits divisor
                int div = 1;
 
                // Create digits divisor
                int div = 1;
-               for (int i = digits; i > 0; i--)
+               for (int i = mDigits; i > 0; i--)
                        div *= 10;
 
                // Create the HMAC
                try {
                        div *= 10;
 
                // Create the HMAC
                try {
-                       Mac mac = Mac.getInstance("Hmac" + algo);
-                       mac.init(new SecretKeySpec(key, "Hmac" + algo));
+                       Mac mac = Mac.getInstance("Hmac" + mAlgorithm);
+                       mac.init(new SecretKeySpec(mSecret, "Hmac" + mAlgorithm));
 
                        // Do the hashing
                        byte[] digest = mac.doFinal(bb.array());
 
                        // Do the hashing
                        byte[] digest = mac.doFinal(bb.array());
@@ -159,7 +160,7 @@ public class Token {
 
                        // Zero pad
                        String hotp = Integer.toString(binary);
 
                        // Zero pad
                        String hotp = Integer.toString(binary);
-                       while (hotp.length() != digits)
+                       while (hotp.length() != mDigits)
                                hotp = "0" + hotp;
 
                        return hotp;
                                hotp = "0" + hotp;
 
                        return hotp;
@@ -176,86 +177,88 @@ public class Token {
                this(Uri.parse(uri));
        }
 
                this(Uri.parse(uri));
        }
 
+       public void increment() {
+               if (mType == TokenType.HOTP) {
+                       mCounter++;
+                       mLastCode = System.currentTimeMillis();
+               }
+       }
+
        public String getID() {
                String id;
        public String getID() {
                String id;
-               if (issuerInt != null && !issuerInt.equals(""))
-                       id = issuerInt + ":" + label;
-               else if (issuerExt != null && !issuerExt.equals(""))
-                       id = issuerExt + ":" + label;
+               if (mIssuerInt != null && !mIssuerInt.equals(""))
+                       id = mIssuerInt + ":" + mLabel;
+               else if (mIssuerExt != null && !mIssuerExt.equals(""))
+                       id = mIssuerExt + ":" + mLabel;
                else
                else
-                       id = label;
+                       id = mLabel;
 
                return id;
        }
 
        public String getIssuer() {
 
                return id;
        }
 
        public String getIssuer() {
-               return issuerExt != null ? issuerExt : "";
+               return mIssuerExt != null ? mIssuerExt : "";
        }
 
        public String getLabel() {
        }
 
        public String getLabel() {
-               return label != null ? label : "";
+               return mLabel != null ? mLabel : "";
        }
 
        public String getCode() {
        }
 
        public String getCode() {
-               switch (type) {
-               case HOTP:
-                       return getHOTP(counter);
-               case TOTP:
-                       return getHOTP(System.currentTimeMillis() / 1000 / period);
+               if (mType == TokenType.TOTP)
+                       return getHOTP(System.currentTimeMillis() / 1000 / mPeriod);
+
+               long time = System.currentTimeMillis();
+               if (time - mLastCode > 60000) {
+                       StringBuilder sb = new StringBuilder(mDigits);
+                       for (int i = 0; i < mDigits; i++)
+                               sb.append('-');
+                       return sb.toString();
                }
 
                }
 
-               return null;
+               return getHOTP(mCounter);
        }
 
        }
 
-       public String getPlaceholder() {
-               StringBuilder sb = new StringBuilder(digits);
-               for (int i = 0; i < digits; i++)
-                       sb.append('-');
-               return sb.toString();
+       public TokenType getType() {
+               return mType;
        }
 
        }
 
-       public void increment() {
-               if (type == TokenType.HOTP)
-                       counter++;
+       // Progress is on a scale from 0 - 1000.
+       public int getProgress() {
+               long time = System.currentTimeMillis();
+
+               if (mType == TokenType.TOTP)
+                       return (int) (time % (mPeriod * 1000) / mPeriod);
+
+               long state = (time - mLastCode) / 60;
+               return (int) (state > 1000 ? 1000 : state);
        }
 
        public Uri toUri() {
        }
 
        public Uri toUri() {
-               String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
+               String issuerLabel = !mIssuerExt.equals("") ? mIssuerExt + ":" + mLabel : mLabel;
 
                Uri.Builder builder = new Uri.Builder()
                        .scheme("otpauth")
                        .path(issuerLabel)
 
                Uri.Builder builder = new Uri.Builder()
                        .scheme("otpauth")
                        .path(issuerLabel)
-                       .appendQueryParameter("secret", Base32String.encode(key))
-                       .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
-                       .appendQueryParameter("algorithm", algo)
-                       .appendQueryParameter("digits", Integer.toString(digits));
+                       .appendQueryParameter("secret", Base32String.encode(mSecret))
+                       .appendQueryParameter("issuer", mIssuerInt == null ? mIssuerExt : mIssuerInt)
+                       .appendQueryParameter("algorithm", mAlgorithm)
+                       .appendQueryParameter("digits", Integer.toString(mDigits));
 
 
-               switch (type) {
+               switch (mType) {
                case HOTP:
                        builder.authority("hotp");
                case HOTP:
                        builder.authority("hotp");
-                       builder.appendQueryParameter("counter", Long.toString(counter));
+                       builder.appendQueryParameter("counter", Long.toString(mCounter + 1));
                        break;
                case TOTP:
                        builder.authority("totp");
                        break;
                case TOTP:
                        builder.authority("totp");
-                       builder.appendQueryParameter("period", Integer.toString(period));
+                       builder.appendQueryParameter("period", Integer.toString(mPeriod));
                        break;
                }
 
                return builder.build();
        }
 
                        break;
                }
 
                return builder.build();
        }
 
-       public TokenType getType() {
-               return type;
-       }
-
-       // Progress is on a scale from 0 - 1000.
-       public int getProgress() {
-               int p = period * 10;
-
-               long time = System.currentTimeMillis() / 100;
-               return (int) ((time % p) * 1000 / p);
-       }
-
        @Override
        public String toString() {
                return toUri().toString();
        @Override
        public String toString() {
                return toUri().toString();
diff --git a/src/org/fedorahosted/freeotp/TokenAdapter.java b/src/org/fedorahosted/freeotp/TokenAdapter.java
deleted file mode 100644 (file)
index b0035e5..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum <npmccallum@redhat.com>
- *
- * Copyright (C) 2013  Nathaniel McCallum, Red Hat
- *
- * 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 org.fedorahosted.freeotp;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.fedorahosted.freeotp.Token.TokenUriInvalidException;
-
-import android.app.AlertDialog;
-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;
-import android.widget.BaseAdapter;
-import android.widget.ImageButton;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-public class TokenAdapter extends BaseAdapter {
-       private static class Ticker extends Handler {
-               private static interface OnTickListener {
-                       public void tick(ProgressBar pb);
-               }
-
-               private final Map<ProgressBar, OnTickListener> map = new HashMap<ProgressBar, OnTickListener>();
-
-               @Override
-               public void handleMessage(Message msg) {
-                       for (ProgressBar pb : map.keySet())
-                               map.get(pb).tick(pb);
-
-                       sendEmptyMessageDelayed(0, 200);
-               }
-
-               public void set(ProgressBar pb, OnTickListener otl) {
-                       map.put(pb, otl);
-               }
-       }
-
-       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) {
-               ts = new TokenStore(ctx);
-               ticker.sendEmptyMessageDelayed(0, 200);
-       }
-
-       @Override
-       public int getCount() {
-               return ts.getTokenCount();
-       }
-
-       @Override
-       public Token getItem(int position) {
-               return ts.get(position);
-       }
-
-       @Override
-       public long getItemId(int position) {
-               return position;
-       }
-
-       @Override
-       public View getView(int position, View convertView, ViewGroup parent) {
-               Token token = ts.get(position);
-
-        if (convertView == null)
-            convertView = newView(parent.getContext(), token.getType(), parent);
-
-        bindView(parent.getContext(), convertView, token, position);
-        return convertView;
-       }
-
-       public void bindView(Context ctx, View view, Token token, int position) {
-               ViewHolder holder = (ViewHolder) view.getTag();
-               holder.index = position;
-               holder.token = token;
-
-               holder.code.setText(token.getPlaceholder());
-               holder.title.setText(token.getIssuer() + ": " + token.getLabel());
-       }
-
-       public View newView(Context ctx, Token.TokenType type, ViewGroup parent) {
-               LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-               ViewHolder holder = new ViewHolder();
-
-               View view = null;
-               switch (type) {
-               case HOTP:
-                       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) {
-                                       ViewHolder holder = (ViewHolder) v.getTag();
-                                       holder.code.setText(holder.token.getCode());
-                                       holder.token.increment();
-                                       ts.save(holder.token);
-                               }
-                       });
-                       break;
-
-               case TOTP:
-                       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) {
-                                       ViewHolder holder = (ViewHolder) pb.getTag();
-                                       pb.setProgress(pb.getMax() - holder.token.getProgress());
-                                       holder.code.setText(holder.token.getCode());
-                               }
-                       });
-                       break;
-               }
-
-               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
-       public int getViewTypeCount() {
-               return 2;
-       }
-
-       @Override
-       public int getItemViewType(int position) {
-               switch (getItem(position).getType()) {
-               case HOTP:
-                       return 0;
-               case TOTP:
-                       return 1;
-               default:
-                       return IGNORE_ITEM_VIEW_TYPE;
-               }
-       }
-
-       public void add(Context ctx, String uri) throws TokenUriInvalidException {
-               ts.add(new Token(uri));
-               notifyDataSetChanged();
-       }
-}
diff --git a/src/org/fedorahosted/freeotp/TokenStore.java b/src/org/fedorahosted/freeotp/TokenStore.java
deleted file mode 100644 (file)
index a9aa560..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-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<String> getTokenOrder() {
-               try {
-                       JSONArray array = new JSONArray(prefs.getString(ORDER, null));
-                       List<String> out = new LinkedList<String>();
-                       for (int i = 0; i < array.length(); i++)
-                               out.add(array.getString(i));
-                       return out;
-               } catch (JSONException e) {
-               } catch (NullPointerException e) {
-               }
-               
-               return new LinkedList<String>();
-       }
-       
-       private SharedPreferences.Editor setTokenOrder(List<String> 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<String> order = getTokenOrder();
-               order.add(0, key);
-               setTokenOrder(order).putString(key, token.toString()).apply();
-       }
-       
-       public void del(int index) {            
-               List<String> 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<String> 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();
-       }
-}
diff --git a/src/org/fedorahosted/freeotp/UrgencyProgressBar.java b/src/org/fedorahosted/freeotp/UrgencyProgressBar.java
new file mode 100644 (file)
index 0000000..d9fe2f0
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum <npmccallum@redhat.com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ *
+ * 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 org.fedorahosted.freeotp;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PorterDuff.Mode;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+
+public class UrgencyProgressBar extends ProgressBar {
+       public UrgencyProgressBar(Context context, AttributeSet attrs, int defStyle) {
+               super(context, attrs, defStyle);
+       }
+
+       public UrgencyProgressBar(Context context, AttributeSet attrs) {
+               super(context, attrs);
+       }
+
+       public UrgencyProgressBar(Context context) {
+               super(context);
+       }
+
+       @Override
+       public synchronized void setProgress(int progress) {
+               super.setProgress(progress);
+
+               int percent = progress * 100 / getMax();
+               if (percent > 33 || progress == 0)
+                       getProgressDrawable().clearColorFilter();
+               else {
+                       int green = 0xe0 * percent / 33;
+                       getProgressDrawable().setColorFilter(Color.RED | (green << 8), Mode.SRC_IN);
+               }
+       }
+
+
+}
diff --git a/src/org/fedorahosted/freeotp/adapters/BaseAdapter.java b/src/org/fedorahosted/freeotp/adapters/BaseAdapter.java
new file mode 100644 (file)
index 0000000..96442de
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum <npmccallum@redhat.com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ *
+ * 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 org.fedorahosted.freeotp.adapters;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+public abstract class BaseAdapter extends android.widget.BaseAdapter {
+       @Override
+       public long getItemId(int position) {
+               return position;
+       }
+
+       @Override
+       public View getView(int position, View convertView, ViewGroup parent) {
+               if (convertView == null) {
+                       int type = getItemViewType(position);
+                       convertView = createView(parent, type);
+                       processView(convertView, type);
+               }
+
+               bindView(convertView, position);
+               return convertView;
+       }
+
+       protected abstract void bindView(View view, int position);
+       protected abstract void processView(View view, int type);
+       protected abstract View createView(ViewGroup parent, int type);
+}
diff --git a/src/org/fedorahosted/freeotp/adapters/DeleteActionBarBaseAdapter.java b/src/org/fedorahosted/freeotp/adapters/DeleteActionBarBaseAdapter.java
new file mode 100644 (file)
index 0000000..3ccc309
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum <npmccallum@redhat.com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ *
+ * 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 org.fedorahosted.freeotp.adapters;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import org.fedorahosted.freeotp.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CompoundButton;
+
+public abstract class DeleteActionBarBaseAdapter extends ReorderableBaseAdapter {
+       private final Map<View, CompoundButton> mButtons = new WeakHashMap<View, CompoundButton>();
+       private final Set<Integer> mChecked = new HashSet<Integer>();
+       private ActionMode mActionMode;
+
+       @Override
+       public void notifyDataSetChanged() {
+               if (mActionMode != null)
+                       mActionMode.finish();
+               mChecked.clear();
+               super.notifyDataSetChanged();
+       }
+
+       @Override
+       public void notifyDataSetInvalidated() {
+               if (mActionMode != null)
+                       mActionMode.finish();
+               mChecked.clear();
+               super.notifyDataSetInvalidated();
+       }
+
+       @Override
+       protected void bindView(View view, int position) {
+               super.bindView(view, position);
+               mButtons.get(view).setChecked(mChecked.contains(position));
+       }
+
+       @Override
+       protected void processView(View view, int type) {
+               super.processView(view, type);
+
+               CompoundButton cb = getCompoundButton(view);
+               mButtons.put(view, cb);
+               cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+                       @Override
+                       public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                               int position = getPositionFromView(buttonView);
+
+                               if (!isChecked) {
+                                       mChecked.remove(position);
+                                       if (mChecked.size() == 0 && mActionMode != null)
+                                               mActionMode.finish();
+
+                                       setTitle(buttonView.getContext());
+                                       return;
+                               }
+
+                               if (mChecked.size() == 0) {
+                                       mActionMode = buttonView.startActionMode(new ActionMode.Callback() {
+                                               @Override
+                                               public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+                                                       return false;
+                                               }
+
+                                               @Override
+                                               public void onDestroyActionMode(ActionMode mode) {
+                                                       mActionMode = null;
+                                                       for (final CompoundButton cb : mButtons.values()) {
+                                                               cb.post(new Runnable() {
+                                                                       @Override
+                                                                       public void run() {
+                                                                               cb.setChecked(false);
+                                                                       }
+                                                               });
+                                                       }
+                                               }
+
+                                               @Override
+                                               public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+                                                       menu.add(R.string.delete).setIcon(android.R.drawable.ic_menu_delete);
+                                                       return true;
+                                               }
+
+                                               @Override
+                                               public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+                                                       // Get the list of all the checked positions
+                                                       // and reverse sort the list.
+                                                       List<Integer> list = new ArrayList<Integer>(mChecked);
+                                                       Collections.sort(list, new Comparator<Integer>() {
+                                                               @Override
+                                                               public int compare(Integer lhs, Integer rhs) {
+                                                                       return rhs.intValue() - lhs.intValue();
+                                                               }
+                                                       });
+
+                                                       // Delete all the selected tokens (in reverse order!)
+                                                       for (Integer i : list)
+                                                               delete(i);
+
+                                                       mode.finish();
+                                                       return true;
+                                               }
+                                       });
+                               }
+
+                               mChecked.add(position);
+                               setTitle(buttonView.getContext());
+                       }
+               });
+       }
+
+       private void setTitle(Context ctx) {
+               if (mActionMode == null || mChecked.size() == 0)
+                       return;
+
+               Resources res = ctx.getResources();
+               mActionMode.setTitle(res.getQuantityString(R.plurals.tokens_selected,
+                                                   mChecked.size(),
+                                                   mChecked.size()));
+       }
+
+       protected abstract CompoundButton getCompoundButton(View view);
+       public abstract void delete(int position);
+}
diff --git a/src/org/fedorahosted/freeotp/adapters/ReorderableBaseAdapter.java b/src/org/fedorahosted/freeotp/adapters/ReorderableBaseAdapter.java
new file mode 100644 (file)
index 0000000..d9fba06
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum <npmccallum@redhat.com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ *
+ * 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 org.fedorahosted.freeotp.adapters;
+
+import java.util.WeakHashMap;
+
+import android.content.ClipData;
+import android.view.DragEvent;
+import android.view.View;
+import android.view.View.DragShadowBuilder;
+import android.view.ViewParent;
+
+public abstract class ReorderableBaseAdapter extends BaseAdapter {
+       private final WeakHashMap<View, Integer> mPositions = new WeakHashMap<View, Integer>();
+       private class Reference<T> {
+               public Reference(T t) { reference = t; }
+               T reference;
+       }
+
+       @Override
+       public void notifyDataSetChanged() {
+               mPositions.clear();
+               super.notifyDataSetChanged();
+       }
+
+       @Override
+       public void notifyDataSetInvalidated() {
+               mPositions.clear();
+               super.notifyDataSetInvalidated();
+       }
+
+       @Override
+       protected void bindView(View view, int position) {
+               mPositions.put(view, position);
+       }
+
+       @Override
+       protected void processView(View view, int type) {
+               view.setOnDragListener(new View.OnDragListener() {
+                       @Override
+                       public boolean onDrag(View dstView, DragEvent event) {
+                               Reference<View> ref = (Reference<View>) event.getLocalState();
+                               final View srcView = ref.reference;
+
+                               switch (event.getAction()) {
+                               case DragEvent.ACTION_DRAG_ENTERED:
+                                       srcView.setVisibility(View.VISIBLE);
+                                       dstView.setVisibility(View.INVISIBLE);
+
+                                       Integer src = mPositions.get(srcView);
+                                       Integer dst = mPositions.get(dstView);
+                                       if (src != null && dst != null)
+                                               move(src, dst);
+
+                                       ref.reference = dstView;
+                                       break;
+
+                               case DragEvent.ACTION_DRAG_ENDED:
+                                   srcView.post(new Runnable() {
+                                               @Override
+                                               public void run() {
+                                                       srcView.setVisibility(View.VISIBLE);
+                                               }
+                                       });
+                                       break;
+                               }
+
+                               return true;
+                       }
+               });
+
+               view.setOnLongClickListener(new View.OnLongClickListener() {
+                       @Override
+                       public boolean onLongClick(final View view) {
+                               view.setVisibility(View.INVISIBLE);
+
+                               // Force a reset of any states
+                               notifyDataSetChanged();
+
+                               // Start the drag on the main loop to allow
+                               // the above state reset to settle.
+                               view.post(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               ClipData data = ClipData.newPlainText("", "");
+                                               DragShadowBuilder sb = new View.DragShadowBuilder(view);
+                                               view.startDrag(data, sb, new Reference<View>(view), 0);
+                                       }
+                               });
+
+                               return true;
+                       }
+               });
+       }
+
+       protected int getPositionFromView(View view) {
+               while(true) {
+                       Integer position = mPositions.get(view);
+                       if (position != null)
+                               return position;
+
+                       ViewParent vp = view.getParent();
+                       if (vp == null || !(vp instanceof View))
+                               break;
+
+                       view = (View) vp;
+               }
+
+               return -1;
+       }
+
+       public abstract void move(int fromPosition, int toPosition);
+}
diff --git a/src/org/fedorahosted/freeotp/adapters/TokenAdapter.java b/src/org/fedorahosted/freeotp/adapters/TokenAdapter.java
new file mode 100644 (file)
index 0000000..4824e20
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum <npmccallum@redhat.com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ *
+ * 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 org.fedorahosted.freeotp.adapters;
+
+import java.lang.ref.WeakReference;
+
+import org.fedorahosted.freeotp.R;
+import org.fedorahosted.freeotp.Token;
+import org.fedorahosted.freeotp.Token.TokenType;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+
+public class TokenAdapter extends TokenPersistenceBaseAdapter {
+       private class ViewHolder {
+               Token token;
+               TextView code;
+               TextView label;
+               TextView issuer;
+               ProgressBar progress;
+       }
+
+       private static class Ticker extends Handler {
+               WeakReference<View> wr;
+
+               public Ticker(View view) {
+                       wr = new WeakReference<View>(view);
+               }
+
+               @Override
+               public void handleMessage(Message msg) {
+                       View view = wr.get();
+                       if (view == null)
+                               return;
+
+                       ViewHolder holder = (ViewHolder) view.getTag();
+                       int progress = 1000 - holder.token.getProgress();
+                       holder.code.setText(holder.token.getCode());
+                       holder.progress.setProgress(progress);
+                       if (progress > 0 && progress < 950)
+                               view.setEnabled(true);
+                       start();
+               }
+
+               public void start() {
+                       stop();
+                       sendEmptyMessageDelayed(0, 100);
+               }
+
+               public void stop() {
+                       removeMessages(0);
+               }
+       }
+
+       private final LayoutInflater mLayoutInflater;
+
+       public TokenAdapter(Context ctx) {
+               super(ctx);
+               mLayoutInflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+       }
+
+       @Override
+       protected void bindView(View view, int position) {
+               super.bindView(view, position);
+
+               ViewHolder holder = (ViewHolder) view.getTag();
+               holder.token = getItem(position);
+
+               // Update views
+               holder.code.setText(holder.token.getCode());
+               holder.label.setText(holder.token.getLabel());
+               holder.issuer.setText(holder.token.getIssuer());
+               holder.progress.setProgress(holder.token.getProgress());
+
+               // Update click listener
+               View.OnClickListener ocl = null;
+               if (holder.token.getType() == TokenType.HOTP) {
+                       ocl = new View.OnClickListener() {
+                               @Override
+                               public void onClick(View v) {
+                                       ViewHolder holder = (ViewHolder) v.getTag();
+                                       holder.token.increment();
+                                       holder.code.setText(holder.token.getCode());
+                                       save(holder.token);
+                                       v.setEnabled(false);
+                               }
+                       };
+               }
+               view.setOnClickListener(ocl);
+
+               if (holder.token.getType() == TokenType.TOTP)
+                       view.setBackgroundResource(R.drawable.token_normal);
+               else
+                       view.setBackgroundResource(R.drawable.token);
+       }
+
+       @Override
+       protected View createView(ViewGroup parent, int type) {
+               View view = mLayoutInflater.inflate(R.layout.token, parent, false);
+
+               ViewHolder holder = new ViewHolder();
+               holder.code = (TextView) view.findViewById(R.id.code);
+               holder.label = (TextView) view.findViewById(R.id.label);
+               holder.issuer = (TextView) view.findViewById(R.id.issuer);
+               holder.progress = (ProgressBar) view.findViewById(R.id.progress);
+               view.setTag(holder);
+
+               new Ticker(view).start();
+
+               return view;
+       }
+
+       @Override
+       protected CompoundButton getCompoundButton(View view) {
+               return (CompoundButton) view.findViewById(R.id.checkBox);
+       }
+}
diff --git a/src/org/fedorahosted/freeotp/adapters/TokenPersistenceBaseAdapter.java b/src/org/fedorahosted/freeotp/adapters/TokenPersistenceBaseAdapter.java
new file mode 100644 (file)
index 0000000..06b4b3c
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum <npmccallum@redhat.com>
+ *
+ * Copyright (C) 2013  Nathaniel McCallum, Red Hat
+ *
+ * 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 org.fedorahosted.freeotp.adapters;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.fedorahosted.freeotp.Token;
+import org.fedorahosted.freeotp.Token.TokenUriInvalidException;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public abstract class TokenPersistenceBaseAdapter extends DeleteActionBarBaseAdapter {
+       private static final String NAME = "tokens";
+       private static final String ORDER = "tokenOrder";
+       private final SharedPreferences prefs;
+
+       private List<String> getTokenOrder() {
+               try {
+                       JSONArray array = new JSONArray(prefs.getString(ORDER, null));
+                       List<String> out = new LinkedList<String>();
+                       for (int i = 0; i < array.length(); i++)
+                               out.add(array.getString(i));
+                       return out;
+               } catch (JSONException e) {
+               } catch (NullPointerException e) {
+               }
+
+               return new LinkedList<String>();
+       }
+
+       private SharedPreferences.Editor setTokenOrder(List<String> order) {
+               JSONArray array = new JSONArray();
+               for (String key : order)
+                       array.put(key);
+
+               return prefs.edit().putString(ORDER, array.toString());
+       }
+
+       public TokenPersistenceBaseAdapter(Context ctx) {
+               prefs = ctx.getApplicationContext()
+                                  .getSharedPreferences(NAME, Context.MODE_PRIVATE);
+       }
+
+       @Override
+       public int getCount() {
+               return getTokenOrder().size();
+       }
+
+       @Override
+       public Token getItem(int position) {
+               try {
+                       return new Token(prefs.getString(getTokenOrder().get(position), null));
+               } catch (TokenUriInvalidException e) {
+                       e.printStackTrace();
+               } catch (NullPointerException e) {
+                       e.printStackTrace();
+               }
+
+               return null;
+       }
+
+       @Override
+       public long getItemId(int position) {
+               return position;
+       }
+
+       @Override
+       public void move(int fromPosition, int toPosition) {
+               if (fromPosition == toPosition)
+                       return;
+
+               List<String> order = getTokenOrder();
+               if (fromPosition < 0 || fromPosition > order.size())
+                       return;
+               if (toPosition < 0 || toPosition > order.size())
+                       return;
+
+               order.add(toPosition, order.remove(fromPosition));
+               setTokenOrder(order).apply();
+               notifyDataSetChanged();
+       }
+
+       public void add(String uri) throws TokenUriInvalidException {
+               Token token = new Token(uri);
+               String key = token.getID();
+
+               if (prefs.contains(key))
+                       return;
+
+               List<String> order = getTokenOrder();
+               order.add(0, key);
+               setTokenOrder(order).putString(key, token.toString()).apply();
+               notifyDataSetChanged();
+       }
+
+       @Override
+       public void delete(int position) {
+               List<String> order = getTokenOrder();
+               String key = order.remove(position);
+               setTokenOrder(order).remove(key).apply();
+               notifyDataSetChanged();
+       }
+
+       protected void save(Token token) {
+               prefs.edit().putString(token.getID(), token.toString()).apply();
+       }
+}