<?xml version="1.0" encoding="utf-8"?>
-<!--
+<!--
- FreeOTP
-
- Authors: Nathaniel McCallum <npmccallum@redhat.com>
<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
- android:minSdkVersion="14"
+ android:minSdkVersion="11"
android:targetSdkVersion="19" />
<application
--- /dev/null
+<?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
--- /dev/null
+<?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
--- /dev/null
+<?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
--- /dev/null
+<?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
+++ /dev/null
-<?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>
+++ /dev/null
-<?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>
<?xml version="1.0" encoding="utf-8"?>
-<!--
+<!--
- FreeOTP
-
- Authors: Nathaniel McCallum <npmccallum@redhat.com>
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: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
<?xml version="1.0" encoding="utf-8"?>
-<!--
+<!--
- FreeOTP
-
- Authors: Nathaniel McCallum <npmccallum@redhat.com>
- 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_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_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. 'GEZDGNBV')"
- 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: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: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
--- /dev/null
+<?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>
+++ /dev/null
-<?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>
<?xml version="1.0" encoding="utf-8"?>
-<!--
+<!--
- FreeOTP
-
- Authors: Nathaniel McCallum <npmccallum@redhat.com>
<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="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="digits">
- <item>6</item>
- <item>8</item>
- </string-array>
-
<string-array name="algorithms">
<item>MD5</item>
<item>SHA1</item>
+++ /dev/null
-/*
- * 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);
- }
-}
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.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.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.view.View;
+import android.widget.GridView;
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
@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
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();
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();
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"))
- type = TokenType.TOTP;
+ mType = TokenType.TOTP;
else if (uri.getAuthority().equals("hotp"))
- type = TokenType.HOTP;
+ mType = TokenType.HOTP;
else
throw new TokenUriInvalidException();
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 {
- Mac.getInstance("Hmac" + algo);
+ Mac.getInstance("Hmac" + mAlgorithm);
} catch (NoSuchAlgorithmException e1) {
throw new TokenUriInvalidException();
}
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();
}
- switch (type) {
+ switch (mType) {
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();
}
String p = uri.getQueryParameter("period");
if (p == null)
p = "30";
- period = Integer.parseInt(p);
+ mPeriod = Integer.parseInt(p);
} catch (NumberFormatException e) {
throw new TokenUriInvalidException();
}
try {
String s = uri.getQueryParameter("secret");
- key = Base32String.decode(s);
+ mSecret = Base32String.decode(s);
} catch (DecodingException e) {
throw new TokenUriInvalidException();
}
// 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 {
- 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());
// Zero pad
String hotp = Integer.toString(binary);
- while (hotp.length() != digits)
+ while (hotp.length() != mDigits)
hotp = "0" + hotp;
return hotp;
this(Uri.parse(uri));
}
+ public void increment() {
+ if (mType == TokenType.HOTP) {
+ mCounter++;
+ mLastCode = System.currentTimeMillis();
+ }
+ }
+
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
- id = label;
+ id = mLabel;
return id;
}
public String getIssuer() {
- return issuerExt != null ? issuerExt : "";
+ return mIssuerExt != null ? mIssuerExt : "";
}
public String getLabel() {
- return label != null ? label : "";
+ return mLabel != null ? mLabel : "";
}
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() {
- String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
+ String issuerLabel = !mIssuerExt.equals("") ? mIssuerExt + ":" + mLabel : mLabel;
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");
- builder.appendQueryParameter("counter", Long.toString(counter));
+ builder.appendQueryParameter("counter", Long.toString(mCounter + 1));
break;
case TOTP:
builder.authority("totp");
- builder.appendQueryParameter("period", Integer.toString(period));
+ builder.appendQueryParameter("period", Integer.toString(mPeriod));
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();
+++ /dev/null
-/*
- * 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();
- }
-}
+++ /dev/null
-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();
- }
-}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}