diff --git a/COPYING.icons b/COPYING.icons
deleted file mode 100644
index dfdcaf3a..00000000
--- a/COPYING.icons
+++ /dev/null
@@ -1,10 +0,0 @@
-Unless otherwise noted, icons used by this application are reproduced from
-the Android Action Bar Icon Pack which is work created and shared by the
-Android Open Source Project and used according to terms described in the
-Creative Commons 2.5 Attribution License.
-
- * http://creativecommons.org/licenses/by/2.5/legalcode
- * https://developer.android.com/license.html
-
-Original Icons:
- * ic_launcher.png - Copyright 2013: Red Hat, Inc.
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 82370767..00000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,36 +0,0 @@
-apply plugin: 'com.android.application'
-
-dependencies {
- implementation 'com.google.zxing:core:3.3.0'
- implementation 'com.google.code.gson:gson:2.8.5'
- implementation 'com.squareup.picasso:picasso:2.5.2'
- implementation 'io.fotoapparat.fotoapparat:library:1.4.1'
- testImplementation 'junit:junit:4.12'
- testImplementation 'org.mockito:mockito-core:1.10.19'
-}
-
-allprojects {
- gradle.projectsEvaluated {
- tasks.withType(JavaCompile) {
- options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
- }
- }
-}
-
-android {
- lintOptions {
- abortOnError false
- }
-
- testOptions {
- unitTests.returnDefaultValues = true
- }
-
- defaultConfig {
- minSdkVersion 23
- targetSdkVersion 29
- }
-
- compileSdkVersion 29 // NOTE: update build-tools-* in .travis.yml
- buildToolsVersion '29.0.2' // NOTE: update build-tools-* in .travis.yml
-}
diff --git a/app/src/main/ic_freeotp_logo-web.png b/app/src/main/ic_freeotp_logo-web.png
deleted file mode 100644
index 540fb101..00000000
Binary files a/app/src/main/ic_freeotp_logo-web.png and /dev/null differ
diff --git a/app/src/main/java/com/google/android/apps/authenticator/Base32String.java b/app/src/main/java/com/google/android/apps/authenticator/Base32String.java
deleted file mode 100644
index d5d55313..00000000
--- a/app/src/main/java/com/google/android/apps/authenticator/Base32String.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright 2009 Google Inc. All Rights Reserved.
- *
- * 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 com.google.android.apps.authenticator;
-
-import java.util.HashMap;
-import java.util.Locale;
-
-/**
- * Encodes arbitrary byte arrays as case-insensitive base-32 strings.
- *
- * The implementation is slightly different than in RFC 4648. During encoding,
- * padding is not added, and during decoding the last incomplete chunk is not
- * taken into account. The result is that multiple strings decode to the same
- * byte array, for example, string of sixteen 7s ("7...7") and seventeen 7s both
- * decode to the same byte array.
- * TODO(sarvar): Revisit this encoding and whether this ambiguity needs fixing.
- *
- * @author sweis@google.com (Steve Weis)
- * @author Neal Gafter
- */
-public class Base32String {
- // singleton
-
- private static final Base32String INSTANCE =
- new Base32String("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"); // RFC 4648/3548
-
- static Base32String getInstance() {
- return INSTANCE;
- }
-
- // 32 alpha-numeric characters.
- private String ALPHABET;
- private char[] DIGITS;
- private int MASK;
- private int SHIFT;
- private HashMap CHAR_MAP;
-
- static final String SEPARATOR = "-";
-
- protected Base32String(String alphabet) {
- this.ALPHABET = alphabet;
- DIGITS = ALPHABET.toCharArray();
- MASK = DIGITS.length - 1;
- SHIFT = Integer.numberOfTrailingZeros(DIGITS.length);
- CHAR_MAP = new HashMap();
- for (int i = 0; i < DIGITS.length; i++) {
- CHAR_MAP.put(DIGITS[i], i);
- }
- }
-
- public static byte[] decode(String encoded) throws DecodingException {
- return getInstance().decodeInternal(encoded);
- }
-
- protected byte[] decodeInternal(String encoded) throws DecodingException {
- // Remove whitespace and separators
- encoded = encoded.trim().replaceAll(SEPARATOR, "").replaceAll(" ", "");
-
- // Remove padding. Note: the padding is used as hint to determine how many
- // bits to decode from the last incomplete chunk (which is commented out
- // below, so this may have been wrong to start with).
- encoded = encoded.replaceFirst("[=]*$", "");
-
- // Canonicalize to all upper case
- encoded = encoded.toUpperCase(Locale.US);
- if (encoded.length() == 0) {
- return new byte[0];
- }
- int encodedLength = encoded.length();
- int outLength = encodedLength * SHIFT / 8;
- byte[] result = new byte[outLength];
- int buffer = 0;
- int next = 0;
- int bitsLeft = 0;
- for (char c : encoded.toCharArray()) {
- if (!CHAR_MAP.containsKey(c)) {
- throw new DecodingException("Illegal character: " + c);
- }
- buffer <<= SHIFT;
- buffer |= CHAR_MAP.get(c) & MASK;
- bitsLeft += SHIFT;
- if (bitsLeft >= 8) {
- result[next++] = (byte) (buffer >> (bitsLeft - 8));
- bitsLeft -= 8;
- }
- }
- // We'll ignore leftover bits for now.
- //
- // if (next != outLength || bitsLeft >= SHIFT) {
- // throw new DecodingException("Bits left: " + bitsLeft);
- // }
- return result;
- }
-
- public static String encode(byte[] data) {
- return getInstance().encodeInternal(data);
- }
-
- protected String encodeInternal(byte[] data) {
- if (data.length == 0) {
- return "";
- }
-
- // SHIFT is the number of bits per output character, so the length of the
- // output is the length of the input multiplied by 8/SHIFT, rounded up.
- if (data.length >= (1 << 28)) {
- // The computation below will fail, so don't do it.
- throw new IllegalArgumentException();
- }
-
- int outputLength = (data.length * 8 + SHIFT - 1) / SHIFT;
- StringBuilder result = new StringBuilder(outputLength);
-
- int buffer = data[0];
- int next = 1;
- int bitsLeft = 8;
- while (bitsLeft > 0 || next < data.length) {
- if (bitsLeft < SHIFT) {
- if (next < data.length) {
- buffer <<= 8;
- buffer |= (data[next++] & 0xff);
- bitsLeft += 8;
- } else {
- int pad = SHIFT - bitsLeft;
- buffer <<= pad;
- bitsLeft += pad;
- }
- }
- int index = MASK & (buffer >> (bitsLeft - SHIFT));
- bitsLeft -= SHIFT;
- result.append(DIGITS[index]);
- }
- return result.toString();
- }
-
- @Override
- // enforce that this class is a singleton
- public Object clone() throws CloneNotSupportedException {
- throw new CloneNotSupportedException();
- }
-
- public static class DecodingException extends Exception {
- public DecodingException(String message) {
- super(message);
- }
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/AboutActivity.java b/app/src/main/java/org/fedorahosted/freeotp/AboutActivity.java
deleted file mode 100644
index 22715d89..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/AboutActivity.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * 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.app.Activity;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.text.Html;
-import android.text.method.LinkMovementMethod;
-import android.widget.TextView;
-
-public class AboutActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.about);
- }
-
- @Override
- @SuppressWarnings("deprecation")
- //suppress because Html.fromHtml(String, int) requires minSdkVersion 24
- public void onStart() {
- super.onStart();
-
- Resources res = getResources();
- TextView tv;
-
- try {
- PackageManager pm = getPackageManager();
- PackageInfo info = pm.getPackageInfo(getPackageName(), 0);
- String version = res.getString(R.string.about_version, info.versionName, info.versionCode);
- tv = findViewById(R.id.about_version);
- tv.setText(version);
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
-
- String apache2 = res.getString(R.string.link_apache2);
- String license = res.getString(R.string.about_license, apache2);
- tv = findViewById(R.id.about_license);
- tv.setMovementMethod(LinkMovementMethod.getInstance());
- tv.setText(Html.fromHtml(license));
-
- String lwebsite = res.getString(R.string.link_website);
- String swebsite = res.getString(R.string.about_website, lwebsite);
- tv = findViewById(R.id.about_website);
- tv.setMovementMethod(LinkMovementMethod.getInstance());
- tv.setText(Html.fromHtml(swebsite));
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/BaseReorderableAdapter.java b/app/src/main/java/org/fedorahosted/freeotp/BaseReorderableAdapter.java
deleted file mode 100644
index b0bfe229..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/BaseReorderableAdapter.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * 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.ClipData;
-import android.view.DragEvent;
-import android.view.View;
-import android.view.View.DragShadowBuilder;
-import android.view.View.OnDragListener;
-import android.view.View.OnLongClickListener;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-public abstract class BaseReorderableAdapter extends BaseAdapter {
- private class Reference {
- public Reference(T t) {
- reference = t;
- }
-
- T reference;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- if (convertView == null) {
- int type = getItemViewType(position);
- convertView = createView(parent, type);
-
- convertView.setOnDragListener(new OnDragListener() {
- @Override
- @SuppressWarnings("unchecked")
- //unavoidable generic type problems -> Reference
- public boolean onDrag(View dstView, DragEvent event) {
- Reference ref = (Reference) event.getLocalState();
- final View srcView = ref.reference;
-
- switch (event.getAction()) {
- case DragEvent.ACTION_DRAG_ENTERED:
- srcView.setVisibility(View.VISIBLE);
- dstView.setVisibility(View.INVISIBLE);
-
- move(((Integer) srcView.getTag(R.id.reorder_key)),
- ((Integer) dstView.getTag(R.id.reorder_key)));
- ref.reference = dstView;
- break;
-
- case DragEvent.ACTION_DRAG_ENDED:
- srcView.post(new Runnable() {
- @Override
- public void run() {
- srcView.setVisibility(View.VISIBLE);
- }
- });
- break;
- }
-
- return true;
- }
- });
-
- convertView.setOnLongClickListener(new OnLongClickListener() {
- @Override
- public boolean onLongClick(final View view) {
- // 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
- @SuppressWarnings("deprecation")
- //startDrag() --> suppress deprecation because startDragAndDrop() requires minSdkVersion 24
- public void run() {
- ClipData data = ClipData.newPlainText("", "");
- DragShadowBuilder sb = new View.DragShadowBuilder(view);
- view.startDrag(data, sb, new Reference<>(view), 0);
- }
- });
-
- return true;
- }
- });
- }
-
- convertView.setTag(R.id.reorder_key, position);
- bindView(convertView, position);
- return convertView;
- }
-
- protected abstract void move(int fromPosition, int toPosition);
-
- protected abstract void bindView(View view, int position);
-
- protected abstract View createView(ViewGroup parent, int type);
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/MainActivity.java b/app/src/main/java/org/fedorahosted/freeotp/MainActivity.java
deleted file mode 100644
index 4f2444a8..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/MainActivity.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- * Authors: Siemens AG
- *
- * Copyright (C) 2013 Nathaniel McCallum, Red Hat
- * Copyright (C) 2017 Max Wittig, Siemens AG
- *
- * 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.
- */
-
-/*
- * Portions Copyright 2009 ZXing authors
- *
- * 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.Manifest;
-import android.widget.Toast;
-
-import org.fedorahosted.freeotp.add.ScanActivity;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.IntentFilter;
-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.view.WindowManager.LayoutParams;
-import android.widget.GridView;
-
-public class MainActivity extends Activity implements OnMenuItemClickListener {
- private TokenAdapter mTokenAdapter;
- public static final String ACTION_IMAGE_SAVED = "org.fedorahosted.freeotp.ACTION_IMAGE_SAVED";
- private DataSetObserver mDataSetObserver;
- private final int PERMISSIONS_REQUEST_CAMERA = 1;
- private RefreshListBroadcastReceiver receiver;
-
- private class RefreshListBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- mTokenAdapter.notifyDataSetChanged();
- }
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- onNewIntent(getIntent());
- setContentView(R.layout.main);
-
- mTokenAdapter = new TokenAdapter(this);
- receiver = new RefreshListBroadcastReceiver();
- registerReceiver(receiver, new IntentFilter(ACTION_IMAGE_SAVED));
- ((GridView) findViewById(R.id.grid)).setAdapter(mTokenAdapter);
-
- // Don't permit screenshots since these might contain OTP codes.
- getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
-
- mDataSetObserver = new DataSetObserver() {
- @Override
- public void onChanged() {
- super.onChanged();
- if (mTokenAdapter.getCount() == 0)
- findViewById(android.R.id.empty).setVisibility(View.VISIBLE);
- else
- findViewById(android.R.id.empty).setVisibility(View.GONE);
- }
- };
- mTokenAdapter.registerDataSetObserver(mDataSetObserver);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- mTokenAdapter.notifyDataSetChanged();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mTokenAdapter.notifyDataSetChanged();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mTokenAdapter.unregisterDataSetObserver(mDataSetObserver);
- unregisterReceiver(receiver);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.main, menu);
- menu.findItem(R.id.action_scan).setVisible(ScanActivity.hasCamera(this));
- menu.findItem(R.id.action_scan).setOnMenuItemClickListener(this);
- menu.findItem(R.id.action_about).setOnMenuItemClickListener(this);
- return true;
- }
-
- private void tryOpenCamera() {
- if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
- requestPermissions(new String[]{Manifest.permission.CAMERA}, PERMISSIONS_REQUEST_CAMERA);
- }
- else {
- // permission is already granted
- openCamera();
- }
- }
-
- private void openCamera() {
- startActivity(new Intent(this, ScanActivity.class));
- overridePendingTransition(R.anim.fadein, R.anim.fadeout);
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_scan:
- tryOpenCamera();
- return true;
-
- case R.id.action_about:
- startActivity(new Intent(this, AboutActivity.class));
- return true;
- }
-
- return false;
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode,
- String permissions[], int[] grantResults) {
- switch (requestCode) {
- case PERMISSIONS_REQUEST_CAMERA: {
- if (grantResults.length > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- openCamera();
- } else {
- Toast.makeText(MainActivity.this, R.string.error_permission_camera_open, Toast.LENGTH_LONG).show();
- }
- return;
- }
- }
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
-
- Uri uri = intent.getData();
- if (uri != null) {
- try {
- TokenPersistence.saveAsync(this, new Token(uri));
- } catch (Token.TokenUriInvalidException e) {
- e.printStackTrace();
- }
- }
-
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/ProgressCircle.java b/app/src/main/java/org/fedorahosted/freeotp/ProgressCircle.java
deleted file mode 100644
index ed7bad62..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/ProgressCircle.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * 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.content.res.Resources.Theme;
-import android.content.res.TypedArray;
-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.util.DisplayMetrics;
-import android.util.TypedValue;
-import android.view.View;
-
-public class ProgressCircle extends View {
- private Paint mPaint;
- private RectF mRectF;
- private Rect mRect;
- private int mProgress;
- private int mMax;
- private boolean mHollow;
- private float mPadding;
- private float mStrokeWidth;
-
- public ProgressCircle(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- setup(context, attrs);
- }
-
- public ProgressCircle(Context context, AttributeSet attrs) {
- super(context, attrs);
- setup(context, attrs);
- }
-
- public ProgressCircle(Context context) {
- super(context);
- setup(context, null);
- }
-
- private void setup(Context context, AttributeSet attrs) {
- DisplayMetrics dm = getResources().getDisplayMetrics();
- mPadding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm);
- mStrokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, dm);
-
- mRectF = new RectF();
- mRect = new Rect();
-
- mPaint = new Paint();
- mPaint.setARGB(0x99, 0x33, 0x33, 0x33);
- mPaint.setAntiAlias(true);
- mPaint.setStrokeCap(Paint.Cap.BUTT);
-
- if (attrs != null) {
- Theme t = context.getTheme();
- TypedArray a = t.obtainStyledAttributes(attrs, R.styleable.ProgressCircle, 0, 0);
-
- try {
- setMax(a.getInteger(R.styleable.ProgressCircle_max, 100));
- setHollow(a.getBoolean(R.styleable.ProgressCircle_hollow, false));
- } finally {
- a.recycle();
- }
- }
- }
-
- public void setMax(int max) {
- this.mMax = max;
- }
-
- public int getMax() {
- return mMax;
- }
-
- public void setHollow(boolean hollow) {
- mHollow = hollow;
- mPaint.setStyle(hollow ? Style.STROKE : Style.FILL);
- mPaint.setStrokeWidth(hollow ? mStrokeWidth : 0);
- }
-
- public boolean getHollow() {
- return mHollow;
- }
-
- public void setProgress(int progress) {
- mProgress = progress;
-
- int percent = mProgress * 100 / getMax();
- if (percent > 25 || mProgress == 0)
- mPaint.setARGB(0x99, 0x33, 0x33, 0x33);
- else
- mPaint.setARGB(0x99, 0xff, 0xe0 * percent / 25, 0x00);
-
- invalidate();
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- getDrawingRect(mRect);
-
- mRect.left += getPaddingLeft() + mPadding;
- mRect.top += getPaddingTop() + mPadding;
- mRect.right -= getPaddingRight() + mPadding;
- mRect.bottom -= getPaddingBottom() + mPadding;
- mRectF.set(mRect);
-
- canvas.drawArc(mRectF, -90, mProgress * 360 / getMax(), !mHollow, mPaint);
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/Token.java b/app/src/main/java/org/fedorahosted/freeotp/Token.java
deleted file mode 100644
index e6c8ac0a..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/Token.java
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * 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.io.File;
-import java.nio.ByteBuffer;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Locale;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-
-import android.net.Uri;
-
-import com.google.android.apps.authenticator.Base32String;
-import com.google.android.apps.authenticator.Base32String.DecodingException;
-
-public class Token {
- public static class TokenUriInvalidException extends Exception {
- private static final long serialVersionUID = -1108624734612362345L;
- }
-
- public static enum TokenType {
- HOTP, TOTP
- }
-
- private static char[] STEAMCHARS = new char[] {
- '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
- 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
- 'R', 'T', 'V', 'W', 'X', 'Y'};
-
- private String issuerInt;
- private String issuerExt;
- private String issuerAlt;
- private String label;
- private String labelAlt;
- private String image;
- private String imageAlt;
- private TokenType type;
- private String algo;
- private byte[] secret;
- private int digits;
- private long counter;
- private int period;
-
- private Token(Uri uri, boolean internal) throws TokenUriInvalidException {
- validateTokenURI(uri);
-
- String path = uri.getPath();
- // Strip the path of its leading '/'
- path = path.replaceFirst("/","");
-
- if (path.length() == 0)
- 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);
- try {
- Mac.getInstance("Hmac" + algo);
- } catch (NoSuchAlgorithmException e1) {
- throw new TokenUriInvalidException();
- }
-
- try {
- String d = uri.getQueryParameter("digits");
- if (d == null)
- d = "6";
- digits = Integer.parseInt(d);
- if (!issuerExt.equals("Steam") && digits != 6 && digits != 8)
- throw new TokenUriInvalidException();
- } catch (NumberFormatException e) {
- throw new TokenUriInvalidException();
- }
-
- try {
- String p = uri.getQueryParameter("period");
- if (p == null)
- p = "30";
- period = Integer.parseInt(p);
- period = (period > 0) ? period : 30; // Avoid divide-by-zero
- } catch (NumberFormatException e) {
- throw new TokenUriInvalidException();
- }
-
- if (type == TokenType.HOTP) {
- try {
- String c = uri.getQueryParameter("counter");
- if (c == null)
- c = "0";
- counter = Long.parseLong(c);
- } catch (NumberFormatException e) {
- throw new TokenUriInvalidException();
- }
- }
-
- try {
- String s = uri.getQueryParameter("secret");
- secret = Base32String.decode(s);
- } catch (DecodingException e) {
- throw new TokenUriInvalidException();
- } catch (NullPointerException e) {
- throw new TokenUriInvalidException();
- }
-
- image = uri.getQueryParameter("image");
-
- if (internal) {
- setIssuer(uri.getQueryParameter("issueralt"));
- setLabel(uri.getQueryParameter("labelalt"));
- }
- }
-
- private void validateTokenURI(Uri uri) throws TokenUriInvalidException{
- if (uri == null) throw new TokenUriInvalidException();
-
- if (uri.getScheme() == null || !uri.getScheme().equals("otpauth")){
- throw new TokenUriInvalidException();
- }
-
- if (uri.getAuthority() == null) throw new TokenUriInvalidException();
-
- if (uri.getAuthority().equals("totp")) {
- type = TokenType.TOTP;
- } else if (uri.getAuthority().equals("hotp"))
- type = TokenType.HOTP;
- else {
- throw new TokenUriInvalidException();
- }
-
- if (uri.getPath() == null) throw new TokenUriInvalidException();
- }
-
- private String getHOTP(long counter) {
- // Encode counter in network byte order
- ByteBuffer bb = ByteBuffer.allocate(8);
- bb.putLong(counter);
-
- // Create digits divisor
- int div = 1;
- for (int i = digits; i > 0; i--)
- div *= 10;
-
- // Create the HMAC
- try {
- Mac mac = Mac.getInstance("Hmac" + algo);
- mac.init(new SecretKeySpec(secret, "Hmac" + algo));
-
- // Do the hashing
- byte[] digest = mac.doFinal(bb.array());
-
- // Truncate
- int binary;
- int off = digest[digest.length - 1] & 0xf;
- binary = (digest[off] & 0x7f) << 0x18;
- binary |= (digest[off + 1] & 0xff) << 0x10;
- binary |= (digest[off + 2] & 0xff) << 0x08;
- binary |= (digest[off + 3] & 0xff);
-
- String hotp = "";
- if (issuerExt.equals("Steam")) {
- for (int i = 0; i < digits; i++) {
- hotp += STEAMCHARS[binary % STEAMCHARS.length];
- binary /= STEAMCHARS.length;
- }
- } else {
- binary = binary % div;
-
- // Zero pad
- hotp = Integer.toString(binary);
- while (hotp.length() != digits)
- hotp = "0" + hotp;
- }
-
- return hotp;
- } catch (InvalidKeyException e) {
- e.printStackTrace();
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- }
-
- return "";
- }
-
- public Token(String uri, boolean internal) throws TokenUriInvalidException {
- this(Uri.parse(uri), internal);
- }
-
- public Token(Uri uri) throws TokenUriInvalidException {
- this(uri, false);
- }
-
- public Token(String uri) throws TokenUriInvalidException {
- this(Uri.parse(uri));
- }
-
- public String getID() {
- String id;
- if (issuerInt != null && !issuerInt.equals(""))
- id = issuerInt + ":" + label;
- else if (issuerExt != null && !issuerExt.equals(""))
- id = issuerExt + ":" + label;
- else
- id = label;
-
- return id;
- }
-
- // NOTE: This changes internal data. You MUST save the token immediately.
- public void setIssuer(String issuer) {
- issuerAlt = (issuer == null || issuer.equals(this.issuerExt)) ? null : issuer;
- }
-
- public String getIssuer() {
- if (issuerAlt != null)
- return issuerAlt;
- return issuerExt != null ? issuerExt : "";
- }
-
- // NOTE: This changes internal data. You MUST save the token immediately.
- public void setLabel(String label) {
- labelAlt = (label == null || label.equals(this.label)) ? null : label;
- }
-
- public String getLabel() {
- if (labelAlt != null)
- return labelAlt;
- return label != null ? label : "";
- }
-
- public int getDigits() {
- return digits;
- }
-
- // NOTE: This may change internal data. You MUST save the token immediately.
- public TokenCode generateCodes() {
- long cur = System.currentTimeMillis();
-
- switch (type) {
- case HOTP:
- return new TokenCode(getHOTP(counter++), cur, cur + (period * 1000));
-
- case TOTP:
- long counter = cur / 1000 / period;
- return new TokenCode(getHOTP(counter + 0),
- (counter + 0) * period * 1000,
- (counter + 1) * period * 1000,
- new TokenCode(getHOTP(counter + 1),
- (counter + 1) * period * 1000,
- (counter + 2) * period * 1000));
- }
-
- return null;
- }
-
- public TokenType getType() {
- return type;
- }
-
- public Uri toUri() {
- String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
-
- Uri.Builder builder = new Uri.Builder().scheme("otpauth").path(issuerLabel)
- .appendQueryParameter("secret", Base32String.encode(secret))
- .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
- .appendQueryParameter("algorithm", algo)
- .appendQueryParameter("digits", Integer.toString(digits))
- .appendQueryParameter("period", Integer.toString(period));
-
- switch (type) {
- case HOTP:
- builder.authority("hotp");
- builder.appendQueryParameter("counter", Long.toString(counter + 1));
- break;
- case TOTP:
- builder.authority("totp");
- break;
- }
-
- return builder.build();
- }
-
- @Override
- public String toString() {
- return toUri().toString();
- }
-
- /**
- * delete image, which is attached to the token from storage
- */
- public void deleteImage() {
- Uri imageUri = getImage();
- if (imageUri != null) {
- File image = new File(imageUri.getPath());
- if (image.exists())
- image.delete();
- }
- }
-
- public void setImage(Uri image) {
- //delete old token image, before assigning the new one
- deleteImage();
-
- imageAlt = null;
- if (image == null)
- return;
-
- if (this.image == null || !Uri.parse(this.image).equals(image))
- imageAlt = image.toString();
- }
-
- public Uri getImage() {
- if (imageAlt != null)
- return Uri.parse(imageAlt);
-
- if (image != null)
- return Uri.parse(image);
-
- return null;
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/TokenAdapter.java b/app/src/main/java/org/fedorahosted/freeotp/TokenAdapter.java
deleted file mode 100644
index cdf1c7a4..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/TokenAdapter.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * 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.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.Intent;
-import android.database.DataSetObserver;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.PopupMenu;
-import android.widget.Toast;
-
-import org.fedorahosted.freeotp.edit.DeleteActivity;
-import org.fedorahosted.freeotp.edit.EditActivity;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class TokenAdapter extends BaseReorderableAdapter {
- private final TokenPersistence mTokenPersistence;
- private final LayoutInflater mLayoutInflater;
- private final ClipboardManager mClipMan;
- private final Map mTokenCodes;
-
- public TokenAdapter(Context ctx) {
- mTokenPersistence = new TokenPersistence(ctx);
- mLayoutInflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mClipMan = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE);
- mTokenCodes = new HashMap<>();
- registerDataSetObserver(new DataSetObserver() {
- @Override
- public void onChanged() {
- mTokenCodes.clear();
- }
-
- @Override
- public void onInvalidated() {
- mTokenCodes.clear();
- }
- });
- }
-
- @Override
- public int getCount() {
- return mTokenPersistence.length();
- }
-
- @Override
- public Token getItem(int position) {
- return mTokenPersistence.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- protected void move(int fromPosition, int toPosition) {
- mTokenPersistence.move(fromPosition, toPosition);
- notifyDataSetChanged();
- }
-
- @Override
- protected void bindView(View view, final int position) {
- final Context ctx = view.getContext();
- TokenLayout tl = (TokenLayout) view;
- Token token = getItem(position);
-
- tl.bind(token, R.menu.token, new PopupMenu.OnMenuItemClickListener() {
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- Intent i;
-
- switch (item.getItemId()) {
- case R.id.action_edit:
- i = new Intent(ctx, EditActivity.class);
- i.putExtra(EditActivity.EXTRA_POSITION, position);
- ctx.startActivity(i);
- break;
-
- case R.id.action_delete:
- i = new Intent(ctx, DeleteActivity.class);
- i.putExtra(DeleteActivity.EXTRA_POSITION, position);
- ctx.startActivity(i);
- break;
- }
-
- return true;
- }
- });
-
- tl.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- TokenPersistence tp = new TokenPersistence(ctx);
-
- // Increment the token.
- Token token = tp.get(position);
- TokenCode codes = token.generateCodes();
- //save token. Image wasn't changed here, so just save it in sync
- new TokenPersistence(ctx).save(token);
-
- // Copy code to clipboard.
- mClipMan.setPrimaryClip(ClipData.newPlainText(null, codes.getCurrentCode()));
- Toast.makeText(v.getContext().getApplicationContext(),
- R.string.code_copied,
- Toast.LENGTH_SHORT).show();
-
- mTokenCodes.put(token.getID(), codes);
- ((TokenLayout) v).start(token.getType(), codes, true);
- }
- });
-
- TokenCode tc = mTokenCodes.get(token.getID());
- if (tc != null && tc.getCurrentCode() != null)
- tl.start(token.getType(), tc, false);
- }
-
- @Override
- protected View createView(ViewGroup parent, int type) {
- return mLayoutInflater.inflate(R.layout.token, parent, false);
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/TokenCode.java b/app/src/main/java/org/fedorahosted/freeotp/TokenCode.java
deleted file mode 100644
index 772e59b4..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/TokenCode.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * Copyright (C) 2014 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;
-
-public class TokenCode {
- private final String mCode;
- private final long mStart;
- private final long mUntil;
- private TokenCode mNext;
-
- public TokenCode(String code, long start, long until) {
- mCode = code;
- mStart = start;
- mUntil = until;
- }
-
- public TokenCode(TokenCode prev, String code, long start, long until) {
- this(code, start, until);
- prev.mNext = this;
- }
-
- public TokenCode(String code, long start, long until, TokenCode next) {
- this(code, start, until);
- mNext = next;
- }
-
- public String getCurrentCode() {
- TokenCode active = getActive(System.currentTimeMillis());
- if (active == null)
- return null;
- return active.mCode;
- }
-
- public int getTotalProgress() {
- long cur = System.currentTimeMillis();
- long total = getLast().mUntil - mStart;
- long state = total - (cur - mStart);
- return (int) (state * 1000 / total);
- }
-
- public int getCurrentProgress() {
- long cur = System.currentTimeMillis();
- TokenCode active = getActive(cur);
- if (active == null)
- return 0;
-
- long total = active.mUntil - active.mStart;
- long state = total - (cur - active.mStart);
- return (int) (state * 1000 / total);
- }
-
- private TokenCode getActive(long curTime) {
- if (curTime >= mStart && curTime < mUntil)
- return this;
-
- if (mNext == null)
- return null;
-
- return this.mNext.getActive(curTime);
- }
-
- private TokenCode getLast() {
- if (mNext == null)
- return this;
- return this.mNext.getLast();
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/TokenLayout.java b/app/src/main/java/org/fedorahosted/freeotp/TokenLayout.java
deleted file mode 100644
index 64751080..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/TokenLayout.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * 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.util.AttributeSet;
-import android.view.View;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.PopupMenu;
-import android.widget.TextView;
-
-import com.squareup.picasso.Picasso;
-
-public class TokenLayout extends FrameLayout implements View.OnClickListener, Runnable {
- private ProgressCircle mProgressInner;
- private ProgressCircle mProgressOuter;
- private ImageView mImage;
- private TextView mCode;
- private TextView mIssuer;
- private TextView mLabel;
- private ImageView mMenu;
- private PopupMenu mPopupMenu;
-
- private TokenCode mCodes;
- private Token.TokenType mType;
- private String mPlaceholder;
- private long mStartTime;
-
- public TokenLayout(Context context) {
- super(context);
- }
-
- public TokenLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public TokenLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- mProgressInner = findViewById(R.id.progressInner);
- mProgressOuter = findViewById(R.id.progressOuter);
- mImage = findViewById(R.id.image);
- mCode = findViewById(R.id.code);
- mIssuer = findViewById(R.id.issuer);
- mLabel = findViewById(R.id.label);
- mMenu = findViewById(R.id.menu);
-
- mPopupMenu = new PopupMenu(getContext(), mMenu);
- mMenu.setOnClickListener(this);
- }
-
- public void bind(Token token, int menu, PopupMenu.OnMenuItemClickListener micl) {
- mCodes = null;
-
- // Setup menu.
- mPopupMenu.getMenu().clear();
- mPopupMenu.getMenuInflater().inflate(menu, mPopupMenu.getMenu());
- mPopupMenu.setOnMenuItemClickListener(micl);
-
- // Cancel all active animations.
- setEnabled(true);
- removeCallbacks(this);
- mImage.clearAnimation();
- mProgressInner.clearAnimation();
- mProgressOuter.clearAnimation();
- mProgressInner.setVisibility(View.GONE);
- mProgressOuter.setVisibility(View.GONE);
-
- // Get the code placeholder.
- char[] placeholder = new char[token.getDigits()];
- for (int i = 0; i < placeholder.length; i++)
- placeholder[i] = '-';
- mPlaceholder = new String(placeholder);
-
- // Show the image.
- Picasso.with(getContext())
- .load(token.getImage())
- .placeholder(R.mipmap.ic_freeotp_logo_foreground)
- .fit()
- .into(mImage);
-
- // Set the labels.
- mLabel.setText(token.getLabel());
- mIssuer.setText(token.getIssuer());
- mCode.setText(mPlaceholder);
- if (mIssuer.getText().length() == 0) {
- mIssuer.setText(token.getLabel());
- mLabel.setVisibility(View.GONE);
- } else {
- mLabel.setVisibility(View.VISIBLE);
- }
- }
-
- private void animate(View view, int anim, boolean animate) {
- Animation a = AnimationUtils.loadAnimation(view.getContext(), anim);
- if (!animate)
- a.setDuration(0);
- view.startAnimation(a);
- }
-
- public void start(Token.TokenType type, TokenCode codes, boolean animate) {
- mCodes = codes;
- mType = type;
-
- // Start animations.
- mProgressInner.setVisibility(View.VISIBLE);
- animate(mProgressInner, R.anim.fadein, animate);
- animate(mImage, R.anim.token_image_fadeout, animate);
-
- // Handle type-specific UI.
- switch (type) {
- case HOTP:
- setEnabled(false);
- break;
- case TOTP:
- mProgressOuter.setVisibility(View.VISIBLE);
- animate(mProgressOuter, R.anim.fadein, animate);
- break;
- }
-
- mStartTime = System.currentTimeMillis();
- post(this);
- }
-
- @Override
- public void onClick(View v) {
- mPopupMenu.show();
- }
-
- @Override
- public void run() {
- // Get the current data
- String code = mCodes == null ? null : mCodes.getCurrentCode();
- if (code != null) {
- // Determine whether to enable/disable the view.
- if (!isEnabled())
- setEnabled(System.currentTimeMillis() - mStartTime > 5000);
-
- // Update the fields
- mCode.setText(code);
- mProgressInner.setProgress(mCodes.getCurrentProgress());
- if (mType != Token.TokenType.HOTP)
- mProgressOuter.setProgress(mCodes.getTotalProgress());
-
- postDelayed(this, 100);
- return;
- }
-
- mCode.setText(mPlaceholder);
- mProgressInner.setVisibility(View.GONE);
- mProgressOuter.setVisibility(View.GONE);
- animate(mImage, R.anim.token_image_fadein, true);
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java b/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java
deleted file mode 100644
index 253ac158..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * 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.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.AsyncTask;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonSyntaxException;
-import com.google.gson.reflect.TypeToken;
-import com.squareup.picasso.Picasso;
-
-import org.fedorahosted.freeotp.Token.TokenUriInvalidException;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.UUID;
-
-public class TokenPersistence {
- private static final String NAME = "tokens";
- private static final String ORDER = "tokenOrder";
- private final SharedPreferences prefs;
- private final Gson gson;
-
- private List getTokenOrder() {
- Type type = new TypeToken>(){}.getType();
- String str = prefs.getString(ORDER, "[]");
- List order = gson.fromJson(str, type);
- return order == null ? new LinkedList() : order;
- }
-
- private SharedPreferences.Editor setTokenOrder(List order) {
- return prefs.edit().putString(ORDER, gson.toJson(order));
- }
-
- public TokenPersistence(Context ctx) {
- prefs = ctx.getApplicationContext().getSharedPreferences(NAME, Context.MODE_PRIVATE);
- gson = new Gson();
- }
-
- public int length() {
- return getTokenOrder().size();
- }
-
- public boolean tokenExists(Token token) {
- return prefs.contains(token.getID());
- }
-
- public Token get(int position) {
- String key = getTokenOrder().get(position);
- String str = prefs.getString(key, null);
-
- try {
- return gson.fromJson(str, Token.class);
- } catch (JsonSyntaxException jse) {
- // Backwards compatibility for URL-based persistence.
- try {
- return new Token(str, true);
- } catch (TokenUriInvalidException tuie) {
- tuie.printStackTrace();
- }
- }
-
- return null;
- }
-
- public void save(Token token) {
- String key = token.getID();
-
- //if token exists, just update it
- if (prefs.contains(key)) {
- prefs.edit().putString(token.getID(), gson.toJson(token)).apply();
- return;
- }
-
- List order = getTokenOrder();
- order.add(0, key);
- setTokenOrder(order).putString(key, gson.toJson(token)).apply();
- }
-
- public void move(int fromPosition, int toPosition) {
- if (fromPosition == toPosition)
- return;
-
- List 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();
- }
-
- public void delete(int position) {
- List order = getTokenOrder();
- String key = order.remove(position);
- setTokenOrder(order).remove(key).apply();
- }
-
- /**
- * Save token async, because Image needs to be downloaded/copied to storage
- * @param context Application Context
- * @param token Token (with Image, Image will be saved by the async task)
- */
- public static void saveAsync(Context context, final Token token) {
- File outFile = null;
- if(token.getImage() != null)
- outFile = new File(context.getFilesDir(), "img_" + UUID.randomUUID().toString() + ".png");
- new SaveTokenTask().execute(new TaskParams(token, outFile, context));
- }
-
- /**
- * Data class for SaveTokenTask
- */
- private static class ReturnParams {
- private final Token token;
- private final Context context;
-
- public ReturnParams(Token token, Context context) {
- this.token = token;
- this.context = context;
- }
-
- public Token getToken() {
- return token;
- }
-
- public Context getContext() {
- return context;
- }
- }
-
- /**
- * Data class for SaveTokenTask
- */
- private static class TaskParams {
- private final File outFile;
- private final Context mContext;
- private final Token token;
-
- public TaskParams(Token token, File outFile, Context mContext) {
- this.token = token;
- this.outFile = outFile;
- this.mContext = mContext;
- }
-
- public Context getContext() {
- return mContext;
- }
-
- public Token getToken() {
- return token;
- }
-
- public File getOutFile() {
- return outFile;
- }
- }
-
- /**
- * Downloads/copies images to FreeOTP storage
- * Saves token in PostExecute
- */
- private static class SaveTokenTask extends AsyncTask {
- protected ReturnParams doInBackground(TaskParams... params) {
- final TaskParams taskParams = params[0];
- if(taskParams.getToken().getImage() != null) {
- try {
- Bitmap bitmap = Picasso.with(taskParams.getContext())
- .load(taskParams.getToken()
- .getImage())
- .resize(200, 200) // it's just an icon
- .onlyScaleDown() //resize image, if bigger than 200x200
- .get();
- File outFile = taskParams.getOutFile();
- //saveAsync image
- FileOutputStream out = new FileOutputStream(outFile);
- bitmap.compress(Bitmap.CompressFormat.PNG, 50, out);
- out.close();
- taskParams.getToken().setImage(Uri.fromFile(outFile));
- } catch (IOException e) {
- e.printStackTrace();
- //set image to null to prevent internet link in image, in case image
- //was scanned, when no connection existed
- taskParams.getToken().setImage(null);
- }
- }
- return new ReturnParams(taskParams.getToken(), taskParams.getContext());
- }
-
- @Override
- protected void onPostExecute(ReturnParams returnParams) {
- super.onPostExecute(returnParams);
- //we downloaded the image, now save it normally
- new TokenPersistence(returnParams.getContext()).save(returnParams.getToken());
- //refresh TokenAdapter
- returnParams.context.sendBroadcast(new Intent(MainActivity.ACTION_IMAGE_SAVED));
- }
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/add/ScanActivity.java b/app/src/main/java/org/fedorahosted/freeotp/add/ScanActivity.java
deleted file mode 100644
index e1b37565..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/add/ScanActivity.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- * Authors: Siemens AG
- *
- * Copyright (C) 2013 Nathaniel McCallum, Red Hat
- * Copyright (C) 2017 Max Wittig, Siemens AG
- *
- * 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.add;
-
-import org.fedorahosted.freeotp.R;
-import org.fedorahosted.freeotp.Token;
-import org.fedorahosted.freeotp.TokenPersistence;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.ImageView;
-import com.squareup.picasso.Callback;
-import com.squareup.picasso.Picasso;
-import io.fotoapparat.Fotoapparat;
-import io.fotoapparat.parameter.ScaleType;
-import io.fotoapparat.parameter.selector.FocusModeSelectors;
-import io.fotoapparat.view.CameraView;
-import static io.fotoapparat.parameter.selector.FocusModeSelectors.autoFocus;
-import static io.fotoapparat.parameter.selector.FocusModeSelectors.fixed;
-import static io.fotoapparat.parameter.selector.LensPositionSelectors.back;
-import static io.fotoapparat.parameter.selector.Selectors.firstAvailable;
-import static io.fotoapparat.parameter.selector.SizeSelectors.biggestSize;
-
-public class ScanActivity extends Activity {
- private Fotoapparat fotoapparat;
- private static ScanBroadcastReceiver receiver;
-
- public class ScanBroadcastReceiver extends BroadcastReceiver {
- public static final String ACTION = "org.fedorahosted.freeotp.ACTION_CODE_SCANNED";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- String text = intent.getStringExtra("scanResult");
- addTokenAndFinish(text);
- }
- }
-
- public static boolean hasCamera(Context context) {
- PackageManager pm = context.getPackageManager();
- return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
- }
-
- private void addTokenAndFinish(String text) {
- Token token = null;
- try {
- token = new Token(text);
- } catch (Token.TokenUriInvalidException e) {
- e.printStackTrace();
- }
-
- //do not receive any more broadcasts
- this.unregisterReceiver(receiver);
-
- //check if token already exists
- if (new TokenPersistence(ScanActivity.this).tokenExists(token)) {
- finish();
- return;
- }
-
- TokenPersistence.saveAsync(ScanActivity.this, token);
- if (token == null || token.getImage() == null) {
- finish();
- return;
- }
-
- final ImageView image = (ImageView) findViewById(R.id.image);
- Picasso.with(ScanActivity.this)
- .load(token.getImage())
- .placeholder(R.drawable.scan)
- .into(image, new Callback() {
- @Override
- public void onSuccess() {
- findViewById(R.id.progress).setVisibility(View.INVISIBLE);
- image.setAlpha(0.9f);
- image.postDelayed(new Runnable() {
- @Override
- public void run() {
- finish();
- }
- }, 2000);
- }
-
- @Override
- public void onError() {
- finish();
- }
- });
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- try {
- this.unregisterReceiver(receiver);
- }
- catch (IllegalArgumentException e) {
- // catch exception, when trying to unregister receiver again
- // there seems to be no way to check, if receiver if registered
- }
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- receiver = new ScanBroadcastReceiver();
- this.registerReceiver(receiver, new IntentFilter(ScanBroadcastReceiver.ACTION));
- setContentView(R.layout.scan);
- CameraView cameraView = findViewById(R.id.camera_view);
-
- fotoapparat = Fotoapparat
- .with(this)
- .into(cameraView)
- .previewScaleType(ScaleType.CENTER_CROP)
- .photoSize(biggestSize())
- .lensPosition(back())
- .focusMode(firstAvailable(
- FocusModeSelectors.continuousFocus(),
- autoFocus(),
- fixed()
- ))
- .frameProcessor(new ScanFrameProcessor(this))
- .build();
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- fotoapparat.start();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- fotoapparat.stop();
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/add/ScanFrameProcessor.java b/app/src/main/java/org/fedorahosted/freeotp/add/ScanFrameProcessor.java
deleted file mode 100644
index 13b4cd70..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/add/ScanFrameProcessor.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- * Authors: Siemens AG
- *
- * Copyright (C) 2013 Nathaniel McCallum, Red Hat
- * Copyright (C) 2017 Max Wittig, Siemens AG
- *
- * 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.add;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Handler;
-import android.os.Looper;
-import com.google.zxing.*;
-import com.google.zxing.common.HybridBinarizer;
-import com.google.zxing.qrcode.QRCodeReader;
-import io.fotoapparat.preview.Frame;
-import io.fotoapparat.preview.FrameProcessor;
-
-public class ScanFrameProcessor implements FrameProcessor {
-
- private static Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper());
- private Reader reader;
- private Context scanActivityContext;
-
- public ScanFrameProcessor(Context context) {
- scanActivityContext = context;
- }
-
- @Override
- public void processFrame(final Frame frame) {
- MAIN_THREAD_HANDLER.post(new Runnable() {
- @Override
- public void run() {
- try {
- reader = new QRCodeReader();
- LuminanceSource ls = new PlanarYUVLuminanceSource(
- frame.image, frame.size.width, frame.size.height,
- 0, 0, frame.size.width, frame.size.height, false);
- Result r = reader.decode(new BinaryBitmap(new HybridBinarizer(ls)));
- sendTextToActivity(r.getText());
- }
- catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
-
- private void sendTextToActivity(String text) {
- Intent intent = new Intent();
- intent.setAction(ScanActivity.ScanBroadcastReceiver.ACTION);
- intent.putExtra("scanResult", text);
- scanActivityContext.sendBroadcast(intent);
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/edit/BaseActivity.java b/app/src/main/java/org/fedorahosted/freeotp/edit/BaseActivity.java
deleted file mode 100644
index 6f635bdf..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/edit/BaseActivity.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.fedorahosted.freeotp.edit;
-
-import android.app.Activity;
-import android.os.Bundle;
-import org.fedorahosted.freeotp.BuildConfig;
-
-public abstract class BaseActivity extends Activity {
- public static final String EXTRA_POSITION = "position";
- private int mPosition;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Get the position of the token. This MUST exist.
- mPosition = getIntent().getIntExtra(EXTRA_POSITION, -1);
- if(BuildConfig.DEBUG && mPosition < 0)
- throw new RuntimeException("Could not create BaseActivity");
- }
-
- protected int getPosition() {
- return mPosition;
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/edit/DeleteActivity.java b/app/src/main/java/org/fedorahosted/freeotp/edit/DeleteActivity.java
deleted file mode 100644
index 51403cd3..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/edit/DeleteActivity.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.fedorahosted.freeotp.edit;
-
-import org.fedorahosted.freeotp.R;
-import org.fedorahosted.freeotp.Token;
-import org.fedorahosted.freeotp.TokenPersistence;
-
-import android.os.Bundle;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.squareup.picasso.Picasso;
-
-public class DeleteActivity extends BaseActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.delete);
-
- final Token token = new TokenPersistence(this).get(getPosition());
- ((TextView) findViewById(R.id.issuer)).setText(token.getIssuer());
- ((TextView) findViewById(R.id.label)).setText(token.getLabel());
- Picasso.with(this)
- .load(token.getImage())
- .placeholder(R.mipmap.ic_freeotp_logo_foreground)
- .into((ImageView) findViewById(R.id.image));
-
- findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- finish();
- }
- });
-
- findViewById(R.id.delete).setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- //delete the image that was copied to storage, before deleting the token
- token.deleteImage();
- new TokenPersistence(DeleteActivity.this).delete(getPosition());
- finish();
- }
- });
- }
-}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/edit/EditActivity.java b/app/src/main/java/org/fedorahosted/freeotp/edit/EditActivity.java
deleted file mode 100644
index 4ecb5622..00000000
--- a/app/src/main/java/org/fedorahosted/freeotp/edit/EditActivity.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * FreeOTP
- *
- * Authors: Nathaniel McCallum
- *
- * Copyright (C) 2014 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.edit;
-
-import android.widget.Toast;
-
-import org.fedorahosted.freeotp.R;
-import org.fedorahosted.freeotp.Token;
-import org.fedorahosted.freeotp.TokenPersistence;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.view.View;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageButton;
-
-import com.squareup.picasso.Picasso;
-
-public class EditActivity extends BaseActivity implements TextWatcher, View.OnClickListener {
- private EditText mIssuer;
- private EditText mLabel;
- private ImageButton mImage;
- private Button mRestore;
- private Button mSave;
-
- private String mIssuerCurrent;
- private String mIssuerDefault;
- private String mLabelCurrent;
- private String mLabelDefault;
- private Uri mImageCurrent;
- private Uri mImageDefault;
- private Uri mImageDisplay;
- private Token token;
- private final int REQUEST_IMAGE_OPEN = 1;
-
- private void showImage(Uri uri) {
- mImageDisplay = uri;
- onTextChanged(null, 0, 0, 0);
- Picasso.with(this)
- .load(uri)
- .placeholder(R.mipmap.ic_freeotp_logo_foreground)
- .into(mImage);
- }
-
- private boolean imageIs(Uri uri) {
- if (uri == null)
- return mImageDisplay == null;
-
- return uri.equals(mImageDisplay);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.edit);
-
- // Get token values.
- token = new TokenPersistence(this).get(getPosition());
- mIssuerCurrent = token.getIssuer();
- mLabelCurrent = token.getLabel();
- mImageCurrent = token.getImage();
- mIssuerDefault = token.getIssuer();
- mLabelDefault = token.getLabel();
- mImageDefault = token.getImage();
-
- // Get references to widgets.
- mIssuer = findViewById(R.id.issuer);
- mLabel = findViewById(R.id.label);
- mImage = findViewById(R.id.image);
- mRestore = findViewById(R.id.restore);
- mSave = findViewById(R.id.save);
-
- // Setup text changed listeners.
- mIssuer.addTextChangedListener(this);
- mLabel.addTextChangedListener(this);
-
- // Setup click callbacks.
- findViewById(R.id.cancel).setOnClickListener(this);
- findViewById(R.id.save).setOnClickListener(this);
- findViewById(R.id.restore).setOnClickListener(this);
- mImage.setOnClickListener(this);
-
- // Setup initial state.
- showImage(mImageCurrent);
- mLabel.setText(mLabelCurrent);
- mIssuer.setText(mIssuerCurrent);
- mIssuer.setSelection(mIssuer.getText().length());
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- if (resultCode == RESULT_OK) {
- if (requestCode == REQUEST_IMAGE_OPEN) {
- //mImageDisplay is set in showImage
- showImage(data.getData());
- token.setImage(mImageDisplay);
- }
- else {
- Toast.makeText(EditActivity.this, R.string.error_image_open, Toast.LENGTH_LONG).show();
- }
- }
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- String label = mLabel.getText().toString();
- String issuer = mIssuer.getText().toString();
- mSave.setEnabled(!label.equals(mLabelCurrent) || !issuer.equals(mIssuerCurrent) || !imageIs(mImageCurrent));
- mRestore.setEnabled(!label.equals(mLabelDefault) || !issuer.equals(mIssuerDefault) || !imageIs(mImageDefault));
- }
-
- @Override
- public void afterTextChanged(Editable s) {
-
- }
-
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.image:
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.setType("image/*");
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- startActivityForResult(intent, REQUEST_IMAGE_OPEN);
- break;
-
- case R.id.restore:
- mLabel.setText(mLabelDefault);
- mIssuer.setText(mIssuerDefault);
- mIssuer.setSelection(mIssuer.getText().length());
- showImage(mImageDefault);
- break;
-
- case R.id.save:
- TokenPersistence tp = new TokenPersistence(this);
- Token token = tp.get(getPosition());
- token.setIssuer(mIssuer.getText().toString());
- token.setLabel(mLabel.getText().toString());
- token.setImage(mImageDisplay);
- TokenPersistence.saveAsync(this, token);
-
- case R.id.cancel:
- finish();
- break;
- }
- }
-}
diff --git a/app/src/main/res/anim/fadein.xml b/app/src/main/res/anim/fadein.xml
deleted file mode 100644
index df8e48b1..00000000
--- a/app/src/main/res/anim/fadein.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/anim/fadeout.xml b/app/src/main/res/anim/fadeout.xml
deleted file mode 100644
index b04fa183..00000000
--- a/app/src/main/res/anim/fadeout.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/anim/token_image_fadein.xml b/app/src/main/res/anim/token_image_fadein.xml
deleted file mode 100644
index 7e0b4f58..00000000
--- a/app/src/main/res/anim/token_image_fadein.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
diff --git a/app/src/main/res/anim/token_image_fadeout.xml b/app/src/main/res/anim/token_image_fadeout.xml
deleted file mode 100644
index 2ebbc41d..00000000
--- a/app/src/main/res/anim/token_image_fadeout.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/color/menu.xml b/app/src/main/res/color/menu.xml
deleted file mode 100644
index cefc6c32..00000000
--- a/app/src/main/res/color/menu.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/ic_action_edit.png b/app/src/main/res/drawable-hdpi/ic_action_edit.png
deleted file mode 100644
index 5f7c6eff..00000000
Binary files a/app/src/main/res/drawable-hdpi/ic_action_edit.png and /dev/null differ
diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index 0112f764..00000000
Binary files a/app/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/drawable-hdpi/qrcode.png b/app/src/main/res/drawable-hdpi/qrcode.png
deleted file mode 100644
index e5806e64..00000000
Binary files a/app/src/main/res/drawable-hdpi/qrcode.png and /dev/null differ
diff --git a/app/src/main/res/drawable-ldpi/ic_launcher.png b/app/src/main/res/drawable-ldpi/ic_launcher.png
deleted file mode 100644
index 34bf9dc9..00000000
Binary files a/app/src/main/res/drawable-ldpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_edit.png b/app/src/main/res/drawable-mdpi/ic_action_edit.png
deleted file mode 100644
index 650b4d89..00000000
Binary files a/app/src/main/res/drawable-mdpi/ic_action_edit.png and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index 6fd3f45e..00000000
Binary files a/app/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/qrcode.png b/app/src/main/res/drawable-mdpi/qrcode.png
deleted file mode 100644
index b4d964d9..00000000
Binary files a/app/src/main/res/drawable-mdpi/qrcode.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_edit.png b/app/src/main/res/drawable-xhdpi/ic_action_edit.png
deleted file mode 100644
index 8ab436d8..00000000
Binary files a/app/src/main/res/drawable-xhdpi/ic_action_edit.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index 14cc3905..00000000
Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xhdpi/qrcode.png b/app/src/main/res/drawable-xhdpi/qrcode.png
deleted file mode 100644
index b9a691c3..00000000
Binary files a/app/src/main/res/drawable-xhdpi/qrcode.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_edit.png b/app/src/main/res/drawable-xxhdpi/ic_action_edit.png
deleted file mode 100644
index f2b2078b..00000000
Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_edit.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xxhdpi/logo.png b/app/src/main/res/drawable-xxhdpi/logo.png
deleted file mode 100644
index 8939e47b..00000000
Binary files a/app/src/main/res/drawable-xxhdpi/logo.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xxhdpi/qrcode.png b/app/src/main/res/drawable-xxhdpi/qrcode.png
deleted file mode 100644
index 1f3b6263..00000000
Binary files a/app/src/main/res/drawable-xxhdpi/qrcode.png and /dev/null differ
diff --git a/app/src/main/res/drawable/ic_freeotp.xml b/app/src/main/res/drawable/ic_freeotp.xml
deleted file mode 100644
index 03c14358..00000000
--- a/app/src/main/res/drawable/ic_freeotp.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_freeotp_logo_background.xml b/app/src/main/res/drawable/ic_freeotp_logo_background.xml
deleted file mode 100644
index 01f0af0a..00000000
--- a/app/src/main/res/drawable/ic_freeotp_logo_background.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml
deleted file mode 100644
index d66b1090..00000000
--- a/app/src/main/res/drawable/menu.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/scan.xml b/app/src/main/res/drawable/scan.xml
deleted file mode 100644
index ad7b1852..00000000
--- a/app/src/main/res/drawable/scan.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml
deleted file mode 100644
index a3691ee5..00000000
--- a/app/src/main/res/layout/about.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/delete.xml b/app/src/main/res/layout/delete.xml
deleted file mode 100644
index a8c51ae0..00000000
--- a/app/src/main/res/layout/delete.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/edit.xml b/app/src/main/res/layout/edit.xml
deleted file mode 100644
index 133e6ada..00000000
--- a/app/src/main/res/layout/edit.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml
deleted file mode 100644
index a1770ae3..00000000
--- a/app/src/main/res/layout/main.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/metadata.xml b/app/src/main/res/layout/metadata.xml
deleted file mode 100644
index 433f767f..00000000
--- a/app/src/main/res/layout/metadata.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/titles.xml b/app/src/main/res/layout/titles.xml
deleted file mode 100644
index 7fd037ac..00000000
--- a/app/src/main/res/layout/titles.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/token.xml b/app/src/main/res/layout/token.xml
deleted file mode 100644
index ace2619e..00000000
--- a/app/src/main/res/layout/token.xml
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_freeotp_logo.xml b/app/src/main/res/mipmap-anydpi-v26/ic_freeotp_logo.xml
deleted file mode 100644
index 68c9c828..00000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_freeotp_logo.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_freeotp_logo_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_freeotp_logo_round.xml
deleted file mode 100644
index 68c9c828..00000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_freeotp_logo_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_freeotp_logo.png b/app/src/main/res/mipmap-hdpi/ic_freeotp_logo.png
deleted file mode 100644
index 73365486..00000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_freeotp_logo.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_freeotp_logo_foreground.png b/app/src/main/res/mipmap-hdpi/ic_freeotp_logo_foreground.png
deleted file mode 100644
index a44a510d..00000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_freeotp_logo_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_freeotp_logo_round.png b/app/src/main/res/mipmap-hdpi/ic_freeotp_logo_round.png
deleted file mode 100644
index e6e8a2e5..00000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_freeotp_logo_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_freeotp_logo.png b/app/src/main/res/mipmap-mdpi/ic_freeotp_logo.png
deleted file mode 100644
index 28081a77..00000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_freeotp_logo.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_freeotp_logo_foreground.png b/app/src/main/res/mipmap-mdpi/ic_freeotp_logo_foreground.png
deleted file mode 100644
index 20ed8997..00000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_freeotp_logo_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_freeotp_logo_round.png b/app/src/main/res/mipmap-mdpi/ic_freeotp_logo_round.png
deleted file mode 100644
index 83ab24ba..00000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_freeotp_logo_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo.png b/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo.png
deleted file mode 100644
index 9c8bdbf0..00000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo_foreground.png
deleted file mode 100644
index 100c0353..00000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo_round.png b/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo_round.png
deleted file mode 100644
index 48902e8e..00000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_freeotp_logo_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo.png b/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo.png
deleted file mode 100644
index a37b1eeb..00000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo_foreground.png
deleted file mode 100644
index a7dff87e..00000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo_round.png b/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo_round.png
deleted file mode 100644
index ecbf974a..00000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_freeotp_logo_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo.png b/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo.png
deleted file mode 100644
index 3115696f..00000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo_foreground.png
deleted file mode 100644
index 0bb63692..00000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo_round.png
deleted file mode 100644
index 99bd14d0..00000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_freeotp_logo_round.png and /dev/null differ
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
deleted file mode 100644
index 20aaf312..00000000
--- a/app/src/main/res/values-hu/strings.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
- FreeOTP
- A megadott jelsor érvénytelen volt!
- Törlés
- Szerkesztés
- Alapértékek visszaállítása
- Nincsenek OTP kulcsok telepítve.
- Jelsor hozzáadása
- QR-kód beolvasása
- Időköz
- Számláló
- Titok
- Típus
- Algoritmus
- Számjegyek
- Mentés
- Kód vágólapra másolva.
-
- A FreeOTP névjegye
- FreeOTP verzió: %1$s (%2$d)
- © 2013 - Red Hat, Inc., et al.
- A FreeOTP %1$s licenc szerint érhető el.
- További információkért nézze meg a %s.
- Az alkalmazásban használt néhány ikon az Android nyílt forráskódú projekt által létrehozott és megosztott munkából származik, és a Creative Commons 2.5 Nevezd meg! licencben leírtak szerint használható.
-
- <a href="http://freeotp.github.io">weboldalunkat</a>
- <a href="http://www.apache.org/licenses/LICENSE-2.0.html">Apache 2.0</a>
-
- Hiba a kamera megnyitásakor!
- Nincs jogosultság a kamera megnyitásához
- Hiba a kép megnyitásakor!
-
- Ez az utolsó esélye: ha törli ezt a jelsort, akkor örökre eltűnik. Nem lesz letiltva a kiszolgálón.
- Törli ezt a jelsort?
-
-
- - MD5
- - SHA1
- - SHA256
- - SHA512
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-v11/styles.xml b/app/src/main/res/values-v11/styles.xml
deleted file mode 100644
index 3c02242a..00000000
--- a/app/src/main/res/values-v11/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/values-v14/styles.xml b/app/src/main/res/values-v14/styles.xml
deleted file mode 100644
index a91fd037..00000000
--- a/app/src/main/res/values-v14/styles.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
deleted file mode 100644
index 026f667c..00000000
--- a/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
- FreeOTP
- The token specified was invalid!
- Delete
- Edit
- Restore Defaults
- No OTP keys installed.
- Add Token
- Scan QR Code
- Interval
- Counter
- Secret
- Type
- Algorithm
- Digits
- Save
- Code copied to clipboard.
-
- About FreeOTP
- FreeOTP Version %1$s (%2$d)
- © 2013 - Red Hat, Inc., et al.
- FreeOTP is licensed under %1$s.
- For more information, see our %s.
- Some icons used by this application are reproduced from work created and shared by the Android Open Source Project and used according to terms described in the Creative Commons 2.5 Attribution License.
-
- <a href="http://freeotp.github.io">website</a>
- <a href="http://www.apache.org/licenses/LICENSE-2.0.html">Apache 2.0</a>
-
- Error while opening camera!
- No permission to open the camera
- Error while opening image!
-
- This is your last chance: if you delete this token, it will be gone forever. It will not be disabled on the server.
- Delete this token?
-
-
- - MD5
- - SHA1
- - SHA256
- - SHA512
-
-
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index c384c81e..00000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/test/java/org/fedorahosted/freeotp/TokenCodeTest.java b/app/src/test/java/org/fedorahosted/freeotp/TokenCodeTest.java
deleted file mode 100644
index 157cb180..00000000
--- a/app/src/test/java/org/fedorahosted/freeotp/TokenCodeTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.fedorahosted.freeotp;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.assertEquals;
-
-@RunWith(MockitoJUnitRunner.class)
-public class TokenCodeTest {
-
- private Token totpToken, hotpToken;
-
- @Before
- public void setUp() throws Exception {
- // otpauth://totp/FreeOTP:joe@google.com?secret=JBSWY3DPEHPK3PXP&issuer=FreeOTP
- totpToken = TokenTestUtils.mockToken("totp", null);
-
- // otpauth://hotp/FreeOTP:joe@google.com?secret=JBSWY3DPEHPK3PXP&issuer=FreeOTP
- hotpToken = TokenTestUtils.mockToken("hotp", "0");
- }
-
- @Test
- public void getCurrentCode_totpToken_returns6DigitCode() throws Exception {
- String code = totpToken.generateCodes().getCurrentCode();
- assertTrue(code.length() == 6);
- }
-
- @Test
- public void getTotalProgress_totpToken_returnsValidIntRange() throws Exception {
- int progress = totpToken.generateCodes().getTotalProgress();
- assertTrue(progress > 0);
- assertTrue(progress < 1000);
- }
-
- @Test
- public void getCurrentProgress_totpToken_returnsValidIntRange() throws Exception {
- int progress = totpToken.generateCodes().getCurrentProgress();
- assertTrue(progress > 0);
- assertTrue(progress < 1000);
- }
-
- @Test
- public void getCurrentCode_hotpToken_returnsFixedHOTP() throws Exception {
- String code = hotpToken.generateCodes().getCurrentCode();
- assertTrue(code.length() == 6);
- assertEquals("282760", code);
- }
-
-}
\ No newline at end of file
diff --git a/app/src/test/java/org/fedorahosted/freeotp/TokenPersistenceTest.java b/app/src/test/java/org/fedorahosted/freeotp/TokenPersistenceTest.java
deleted file mode 100644
index d81d5d9e..00000000
--- a/app/src/test/java/org/fedorahosted/freeotp/TokenPersistenceTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-package org.fedorahosted.freeotp;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.runners.MockitoJUnitRunner;
-import org.mockito.stubbing.Answer;
-
-import java.util.LinkedHashMap;
-
-import static org.fedorahosted.freeotp.TokenTestUtils.mockToken;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class TokenPersistenceTest {
- @Mock
- Context mockContext;
-
- @Mock
- Context mockApplicationContext;
-
- @Mock
- SharedPreferences mockSharedPreferences;
-
- private LinkedHashMap mockStore;
- private TokenPersistence tokenPersistence;
- private Token mockToken;
-
- @Before
- public void setUp() throws Exception {
- when(mockContext.getApplicationContext())
- .thenReturn(mockApplicationContext);
- when(mockApplicationContext.getSharedPreferences(anyString(), anyInt()))
- .thenReturn(mockSharedPreferences);
- tokenPersistence = new TokenPersistence(mockContext);
- mockToken = mockToken("totp", null);
-
- mockStore = new LinkedHashMap<>();
-
- final SharedPreferences.Editor mockEditor = Mockito.mock(SharedPreferences.Editor.class);
- when(mockSharedPreferences.edit()).thenReturn(mockEditor);
- when(mockEditor.putString(anyString(), anyString()))
- .thenAnswer(new Answer() {
- @Override
- public SharedPreferences.Editor answer(InvocationOnMock invocation) throws Throwable {
- mockStore.put(invocation.getArgumentAt(0, String.class),
- invocation.getArgumentAt(1, String.class));
- return mockEditor;
- }
- });
- when(mockEditor.remove(anyString()))
- .thenAnswer(new Answer() {
- @Override
- public SharedPreferences.Editor answer(InvocationOnMock invocation) throws Throwable {
- mockStore.remove(invocation.getArgumentAt(0, String.class));
- return mockEditor;
- }
- });
-
- when(mockSharedPreferences.contains(anyString()))
- .thenAnswer(new Answer() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
- return mockStore.containsKey(invocation.getArgumentAt(0, String.class));
- }
- });
-
- when(mockSharedPreferences.getString(anyString(), any(String.class)))
- .thenAnswer(new Answer() {
- @Override
- public String answer(InvocationOnMock invocation) throws Throwable {
- try {
- return mockStore.getOrDefault(invocation.getArgumentAt(0, String.class),
- invocation.getArgumentAt(1, String.class));
- } catch (NullPointerException np) {
- return mockStore.getOrDefault(invocation.getArgumentAt(0, String.class), null);
- }
- }
- });
- }
-
- @Test
- public void add_sameTokenTwice_isIdempotent() throws Exception {
- int sizeBefore = mockStore.size();
- tokenPersistence.save(mockToken);
- assertEquals(sizeBefore + 2, mockStore.size());
-
- JsonObject token = new JsonObject();
- token.addProperty("issuerInt", "FreeOTP");
- token.addProperty("issuerExt", "FreeOTP");
- token.addProperty("label", "joe@google.com");
- token.addProperty("type", "TOTP");
- token.addProperty("algo", "SHA1");
- token.add("secret", new JsonParser().parse("[72,101,108,108,111,33,-34,-83,-66,-17]"));
- token.addProperty("digits", 6);
- token.addProperty("counter", 0);
- token.addProperty("period", 30);
-
- // language=JSON
- assertEquals(token, new JsonParser().parse(mockStore.get("FreeOTP:joe@google.com")));
-
- // language=JSON
- JsonArray order = new JsonArray();
- order.add("FreeOTP:joe@google.com");
- assertEquals(order, new JsonParser().parse(mockStore.get("tokenOrder")));
-
- sizeBefore = mockStore.size();
- tokenPersistence.save(mockToken);
- assertEquals(sizeBefore, mockStore.size());
- }
-
- @Test
- public void length_ofStoreWith1Token_returns1() throws Exception {
- int sizeBefore = mockStore.size();
- tokenPersistence.save(mockToken);
- assertEquals(sizeBefore + 2, mockStore.size());
-
- assertEquals(sizeBefore + 1, tokenPersistence.length());
- }
-
- @Test
- public void get_WithValidPosition_returnsToken() throws Exception {
- tokenPersistence.save(mockToken);
- Token hotpMockToken = mockToken("hotp", "1", "FreeOTP:mail@google.com");
- tokenPersistence.save(hotpMockToken);
-
- assertEquals(mockToken.getType(), tokenPersistence.get(1).getType());
- assertEquals(mockToken.getID(), tokenPersistence.get(1).getID());
- assertEquals(mockToken.getDigits(), tokenPersistence.get(1).getDigits());
- assertEquals(mockToken.getLabel(), tokenPersistence.get(1).getLabel());
- assertEquals(mockToken.getIssuer(), tokenPersistence.get(1).getIssuer());
- assertEquals(mockToken.getImage(), tokenPersistence.get(1).getImage());
-
- assertEquals(hotpMockToken.getType(), tokenPersistence.get(0).getType());
- assertEquals(hotpMockToken.getID(), tokenPersistence.get(0).getID());
- assertEquals(hotpMockToken.getDigits(), tokenPersistence.get(0).getDigits());
- assertEquals(hotpMockToken.getLabel(), tokenPersistence.get(0).getLabel());
- assertEquals(hotpMockToken.getIssuer(), tokenPersistence.get(0).getIssuer());
- assertEquals(hotpMockToken.getImage(), tokenPersistence.get(0).getImage());
- }
-
- @Test(expected = IndexOutOfBoundsException.class)
- public void get_WithInvalidPosition_ThrowsIndexOutOfBounds() throws Exception {
- tokenPersistence.get(0);
- }
-
- @Test
- public void move_fromSamePosition_DoesNothing() throws Exception {
- tokenPersistence.save(mockToken);
- Token hotpMockToken = mockToken("hotp", "1", "FreeOTP:mail@google.com");
- tokenPersistence.save(hotpMockToken);
-
- tokenPersistence.move(0, 0);
- assertEquals(mockToken.getID(), tokenPersistence.get(1).getID());
- assertEquals(hotpMockToken.getID(), tokenPersistence.get(0).getID());
- }
-
- @Test
- public void move_twoValidTokens_SwitchesTokenOrder() throws Exception {
- tokenPersistence.save(mockToken);
- Token hotpMockToken = mockToken("hotp", "1", "FreeOTP:mail@google.com");
- tokenPersistence.save(hotpMockToken);
-
- assertEquals(mockToken.getID(), tokenPersistence.get(1).getID());
- assertEquals(hotpMockToken.getID(), tokenPersistence.get(0).getID());
-
- tokenPersistence.move(0, 1);
- assertEquals(mockToken.getID(), tokenPersistence.get(0).getID());
- assertEquals(hotpMockToken.getID(), tokenPersistence.get(1).getID());
- }
-
- @Test
- public void delete_tokenFromValidPosition_removesIt() throws Exception {
- tokenPersistence.save(mockToken);
- Token hotpMockToken = mockToken("hotp", "1", "FreeOTP:mail@google.com");
- tokenPersistence.save(hotpMockToken);
-
- assertEquals(hotpMockToken.getID(), tokenPersistence.get(0).getID());
- assertEquals(2, tokenPersistence.length());
-
- tokenPersistence.delete(0);
- assertEquals(mockToken.getID(), tokenPersistence.get(0).getID());
- assertEquals(1, tokenPersistence.length());
- }
-
- @Test
- public void save_updatingExistingToken_changesCounter() throws Exception {
- Token hotpMockToken = mockToken("hotp", "1", "FreeOTP:mail@google.com");
- tokenPersistence.save(hotpMockToken);
-
- Token hotpMockTokenUpdated = mockToken("totp", "1", "FreeOTP:mail@google.com");
- tokenPersistence.save(hotpMockTokenUpdated);
-
- assertEquals(1, tokenPersistence.length());
- assertEquals(Token.TokenType.TOTP, tokenPersistence.get(0).getType());
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/org/fedorahosted/freeotp/TokenTest.java b/app/src/test/java/org/fedorahosted/freeotp/TokenTest.java
deleted file mode 100644
index 32c33950..00000000
--- a/app/src/test/java/org/fedorahosted/freeotp/TokenTest.java
+++ /dev/null
@@ -1,232 +0,0 @@
-package org.fedorahosted.freeotp;
-
-import android.net.Uri;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.mockito.Mockito;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import java.lang.reflect.Field;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class TokenTest {
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_nullScheme_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn(null);
-
- new Token(mockUri);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_invalidScheme_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("borked");
-
- new Token(mockUri);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_nullAuthority_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn(null);
-
- new Token(mockUri);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_invalidAuthority_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("borked");
-
- new Token(mockUri);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_nullPath_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn(null);
-
- new Token(mockUri);
- }
-
- @RunWith(Parameterized.class)
- public static class ParameterizedPathsTest {
- private String path;
-
- public ParameterizedPathsTest (String path) {
- this.path = path;
- }
-
- @Parameterized.Parameters
- public static String[] paths() {
- return new String[]{
- "//////",
- "/agda/asdg/sag/aga:AGSDSAgA@@@:://adg",
- "@email/",
- ""
- };
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_invalidPath_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn(path);
-
- new Token(mockUri);
- }
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_nullIssuer_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn("FreeOTP:joe@cool.com");
- when(mockUri.getQueryParameter("issuer")).thenReturn(null);
-
- new Token(mockUri);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_invalidIssuer_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn("FreeOTP:joe@cool.com");
- when(mockUri.getQueryParameter("issuer")).thenReturn("borked");
-
- new Token(mockUri);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_nullSecret_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("secret")).thenReturn(null);
-
- new Token(mockUri);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_invalidSecret_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn("borked");
- when(mockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("secret")).thenReturn("κόσμε");
-
- new Token(mockUri);
- }
-
- @Test
- public void TokenCtor_nullPeriod_setsDefaultValue() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("secret")).thenReturn("foobar");
- when(mockUri.getQueryParameter("period")).thenReturn(null);
-
- // When period is not defined it should get set to the default value.
-
- Token token = new Token(mockUri);
- Field f = Token.class.getDeclaredField("period");
- f.setAccessible(true);
- Integer period = (Integer) f.get(token);
- assertEquals((Integer) 30, period);
- }
-
- @Test
- public void TokenCtor_invalidPeriod_setsDefaultValue() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn("borked");
- when(mockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("secret")).thenReturn("foobar");
- when(mockUri.getQueryParameter("period")).thenReturn("-1");
-
- // When period is invalid it should get set to the default value.
-
- Token token = new Token(mockUri);
- Field f = Token.class.getDeclaredField("period");
- f.setAccessible(true);
- Integer period = (Integer) f.get(token);
- assertEquals((Integer) 30, period);
- }
-
- @Test(expected = Token.TokenUriInvalidException.class)
- public void TokenCtor_invalidPeriodType_throwsTokenUriInvalidException() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("totp");
- when(mockUri.getPath()).thenReturn("borked");
- when(mockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("secret")).thenReturn("foobar");
- when(mockUri.getQueryParameter("period")).thenReturn("not a number");
-
- new Token(mockUri);
- }
-
- @Test
- public void TokenCtor_ZeroPeriod_ShouldNotThrowArithmeticException() throws Exception {
- Token totpToken = TokenTestUtils.mockToken("totp", null, "foo", 0);
- // Shouldn't throw from a divide by zero!
- totpToken.generateCodes();
- }
-
- @Test
- public void TokenCtor_hotp_nullCounter_setsDefaultValue() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("hotp");
- when(mockUri.getPath()).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("secret")).thenReturn("foobar");
- when(mockUri.getQueryParameter("counter")).thenReturn(null);
-
- // When counter is not defined it should get set to the default value.
-
- Token token = new Token(mockUri);
- Field f = Token.class.getDeclaredField("counter");
- f.setAccessible(true);
- Long counter = (Long) f.get(token);
- assertEquals(Long.valueOf(0), counter);
- }
-
- @Test
- public void TokenCtor_hotp_setsCounterIfValid() throws Exception {
- Uri mockUri = Mockito.mock(Uri.class);
- when(mockUri.getScheme()).thenReturn("otpauth");
- when(mockUri.getAuthority()).thenReturn("hotp");
- when(mockUri.getPath()).thenReturn("borked");
- when(mockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mockUri.getQueryParameter("secret")).thenReturn("foobar");
- when(mockUri.getQueryParameter("counter")).thenReturn("-999");
-
- // When counter is invalid it should get set to the default value.
-
- Token token = new Token(mockUri);
- Field f = Token.class.getDeclaredField("counter");
- f.setAccessible(true);
- Long counter = (Long) f.get(token);
- assertEquals(Long.valueOf(-999), counter);
- }
-}
diff --git a/app/src/test/java/org/fedorahosted/freeotp/TokenTestUtils.java b/app/src/test/java/org/fedorahosted/freeotp/TokenTestUtils.java
deleted file mode 100644
index 94d1c8f4..00000000
--- a/app/src/test/java/org/fedorahosted/freeotp/TokenTestUtils.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.fedorahosted.freeotp;
-
-import android.net.Uri;
-
-import org.mockito.Mockito;
-
-import static org.mockito.Mockito.when;
-
-// Who tests the test utils...?
-class TokenTestUtils {
-
- public static Token mockToken(String authority, String counter) {
- return mockToken(authority, counter, "FreeOTP:joe@google.com");
- }
-
- public static Token mockToken(String authority, String counter, String id) {
- return mockToken(authority, counter, id, null);
- }
-
- public static Token mockToken(String authority, String counter, String id, Integer period) {
- // https://github.com/google/google-authenticator/wiki/Key-Uri-Format
- Uri mMockUri = Mockito.mock(Uri.class);
-
- when(mMockUri.getScheme()).thenReturn("otpauth");
- when(mMockUri.getAuthority()).thenReturn(authority);
- when(mMockUri.getPath()).thenReturn(id);
- when(mMockUri.getQueryParameter("issuer")).thenReturn("FreeOTP");
- when(mMockUri.getQueryParameter("secret")).thenReturn("JBSWY3DPEHPK3PXP");
-
- if (period != null) {
- when(mMockUri.getQueryParameter("period")).thenReturn(String.valueOf(period.intValue()));
- }
-
- if (authority.equals("hotp")) {
- when(mMockUri.getQueryParameter("counter")).thenReturn(counter);
- }
-
- try {
- return new Token(mMockUri);
- } catch (Token.TokenUriInvalidException e) {
- e.printStackTrace();
- }
-
- return null;
- }
-}
diff --git a/build.gradle b/build.gradle
index 184f922d..d62bb2b2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,4 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
buildscript {
repositories {
google()
@@ -11,6 +12,7 @@ buildscript {
allprojects {
repositories {
+ google()
jcenter()
maven { url 'https://jitpack.io' }
// Android studio requires google m2 to obtain support libraries, even though
@@ -18,3 +20,7 @@ allprojects {
google()
}
}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..743d692c
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 8c0fb64a..7a3265ee 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index af32b29f..00000000
--- a/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Fri Oct 04 08:34:48 EDT 2019
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/gradlew b/gradlew
index 91a7e269..cccdd3d5 100755
--- a/gradlew
+++ b/gradlew
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
##############################################################################
##
@@ -6,20 +6,38 @@
##
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
-warn ( ) {
+warn () {
echo "$*"
}
-die ( ) {
+die () {
echo
echo "$*"
echo
@@ -30,6 +48,7 @@ die ( ) {
cygwin=false
msys=false
darwin=false
+nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
@@ -40,31 +59,11 @@ case "`uname`" in
MINGW* )
msys=true
;;
+ NONSTOP* )
+ nonstop=true
+ ;;
esac
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -90,7 +89,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -114,6 +113,7 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -154,11 +154,19 @@ if $cygwin ; then
esac
fi
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index aec99730..e95643d6 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,10 +46,9 @@ echo location of your Java installation.
goto fail
:init
-@rem Get command-line arguments, handling Windowz variants
+@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
diff --git a/ic_launcher-web.png b/ic_launcher-web.png
deleted file mode 100644
index 7e07ab7f..00000000
Binary files a/ic_launcher-web.png and /dev/null differ
diff --git a/mobile/build.gradle b/mobile/build.gradle
new file mode 100644
index 00000000..cd21bc70
--- /dev/null
+++ b/mobile/build.gradle
@@ -0,0 +1,52 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 29
+ defaultConfig {
+ applicationId "org.fedorahosted.freeotp"
+ minSdkVersion 23
+ targetSdkVersion 29
+ versionCode 19
+ versionName "1.99"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ testOptions {
+ unitTests.all {
+ testLogging {
+ events "passed", "skipped", "failed", "standardOut", "standardError"
+ outputs.upToDateWhen {false}
+ showStandardStreams = true
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation 'com.android.support.constraint:constraint-layout:1.1.2'
+ implementation 'com.android.support:recyclerview-v7:27.1.1'
+ implementation 'com.android.support:exifinterface:27.1.1'
+ implementation 'com.android.support:appcompat-v7:27.1.1'
+ implementation 'com.android.support:cardview-v7:27.1.1'
+ implementation 'com.android.support:design:27.1.1'
+
+ implementation 'io.fotoapparat.fotoapparat:library:2.2.0'
+ implementation 'com.squareup.picasso:picasso:2.71828'
+ implementation 'com.google.code.gson:gson:2.8.5'
+ implementation 'com.google.zxing:core:3.3.3'
+
+ testImplementation 'junit:junit:4.12'
+ testImplementation 'org.mockito:mockito-core:2.19.0'
+
+ androidTestImplementation 'org.mockito:mockito-android:2.19.0'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/mobile/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/src/androidTest/java/org/fedorahosted/freeotp/Context.java b/mobile/src/androidTest/java/org/fedorahosted/freeotp/Context.java
new file mode 100644
index 00000000..f6d2ffb2
--- /dev/null
+++ b/mobile/src/androidTest/java/org/fedorahosted/freeotp/Context.java
@@ -0,0 +1,567 @@
+package org.fedorahosted.freeotp;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.Display;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Context extends android.content.Context {
+ private Map mMemorySharedPreferences = new HashMap<>();
+
+ @Override
+ public AssetManager getAssets() {
+ return null;
+ }
+
+ @Override
+ public Resources getResources() {
+ return null;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return null;
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return null;
+ }
+
+ @Override
+ public Looper getMainLooper() {
+ return null;
+ }
+
+ @Override
+ public android.content.Context getApplicationContext() {
+ return null;
+ }
+
+ @Override
+ public void setTheme(int resid) {
+
+ }
+
+ @Override
+ public Resources.Theme getTheme() {
+ return null;
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return null;
+ }
+
+ @Override
+ public String getPackageName() {
+ return null;
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo() {
+ return null;
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ return null;
+ }
+
+ @Override
+ public String getPackageCodePath() {
+ return null;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ if (!mMemorySharedPreferences.containsKey(name))
+ mMemorySharedPreferences.put(name, new MemorySharedPreferences());
+ return mMemorySharedPreferences.get(name);
+ }
+
+ @Override
+ public boolean moveSharedPreferencesFrom(android.content.Context sourceContext, String name) {
+ return false;
+ }
+
+ @Override
+ public boolean deleteSharedPreferences(String name) {
+ return false;
+ }
+
+ @Override
+ public FileInputStream openFileInput(String name) throws FileNotFoundException {
+ return null;
+ }
+
+ @Override
+ public FileOutputStream openFileOutput(String name, int mode) throws FileNotFoundException {
+ return null;
+ }
+
+ @Override
+ public boolean deleteFile(String name) {
+ return false;
+ }
+
+ @Override
+ public File getFileStreamPath(String name) {
+ return null;
+ }
+
+ @Override
+ public File getDataDir() {
+ return null;
+ }
+
+ @Override
+ public File getFilesDir() {
+ return null;
+ }
+
+ @Override
+ public File getNoBackupFilesDir() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public File getExternalFilesDir(@Nullable String type) {
+ return null;
+ }
+
+ @Override
+ public File[] getExternalFilesDirs(String type) {
+ return new File[0];
+ }
+
+ @Override
+ public File getObbDir() {
+ return null;
+ }
+
+ @Override
+ public File[] getObbDirs() {
+ return new File[0];
+ }
+
+ @Override
+ public File getCacheDir() {
+ return null;
+ }
+
+ @Override
+ public File getCodeCacheDir() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public File getExternalCacheDir() {
+ return null;
+ }
+
+ @Override
+ public File[] getExternalCacheDirs() {
+ return new File[0];
+ }
+
+ @Override
+ public File[] getExternalMediaDirs() {
+ return new File[0];
+ }
+
+ @Override
+ public String[] fileList() {
+ return new String[0];
+ }
+
+ @Override
+ public File getDir(String name, int mode) {
+ return null;
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) {
+ return null;
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, @Nullable DatabaseErrorHandler errorHandler) {
+ return null;
+ }
+
+ @Override
+ public boolean moveDatabaseFrom(android.content.Context sourceContext, String name) {
+ return false;
+ }
+
+ @Override
+ public boolean deleteDatabase(String name) {
+ return false;
+ }
+
+ @Override
+ public File getDatabasePath(String name) {
+ return null;
+ }
+
+ @Override
+ public String[] databaseList() {
+ return new String[0];
+ }
+
+ @Override
+ public Drawable getWallpaper() {
+ return null;
+ }
+
+ @Override
+ public Drawable peekWallpaper() {
+ return null;
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumHeight() {
+ return 0;
+ }
+
+ @Override
+ public void setWallpaper(Bitmap bitmap) throws IOException {
+
+ }
+
+ @Override
+ public void setWallpaper(InputStream data) throws IOException {
+
+ }
+
+ @Override
+ public void clearWallpaper() throws IOException {
+
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+
+ }
+
+ @Override
+ public void startActivity(Intent intent, @Nullable Bundle options) {
+
+ }
+
+ @Override
+ public void startActivities(Intent[] intents) {
+
+ }
+
+ @Override
+ public void startActivities(Intent[] intents, Bundle options) {
+
+ }
+
+ @Override
+ public void startIntentSender(IntentSender intent, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws IntentSender.SendIntentException {
+
+ }
+
+ @Override
+ public void startIntentSender(IntentSender intent, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException {
+
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent) {
+
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent, @Nullable String receiverPermission) {
+
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission) {
+
+ }
+
+ @Override
+ public void sendOrderedBroadcast(@NonNull Intent intent, @Nullable String receiverPermission, @Nullable BroadcastReceiver resultReceiver, @Nullable Handler scheduler, int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
+
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user) {
+
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user, @Nullable String receiverPermission) {
+
+ }
+
+ @Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, @Nullable String receiverPermission, BroadcastReceiver resultReceiver, @Nullable Handler scheduler, int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
+
+ }
+
+ @Override
+ public void sendStickyBroadcast(Intent intent) {
+
+ }
+
+ @Override
+ public void sendStickyOrderedBroadcast(Intent intent, BroadcastReceiver resultReceiver, @Nullable Handler scheduler, int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
+
+ }
+
+ @Override
+ public void removeStickyBroadcast(Intent intent) {
+
+ }
+
+ @Override
+ public void sendStickyBroadcastAsUser(Intent intent, UserHandle user) {
+
+ }
+
+ @Override
+ public void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, BroadcastReceiver resultReceiver, @Nullable Handler scheduler, int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
+
+ }
+
+ @Override
+ public void removeStickyBroadcastAsUser(Intent intent, UserHandle user) {
+
+ }
+
+ @Nullable
+ @Override
+ public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter, int flags) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, @Nullable String broadcastPermission, @Nullable Handler scheduler) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, @Nullable String broadcastPermission, @Nullable Handler scheduler, int flags) {
+ return null;
+ }
+
+ @Override
+ public void unregisterReceiver(BroadcastReceiver receiver) {
+
+ }
+
+ @Nullable
+ @Override
+ public ComponentName startService(Intent service) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public ComponentName startForegroundService(Intent service) {
+ return null;
+ }
+
+ @Override
+ public boolean stopService(Intent service) {
+ return false;
+ }
+
+ @Override
+ public boolean bindService(Intent service, @NonNull ServiceConnection conn, int flags) {
+ return false;
+ }
+
+ @Override
+ public void unbindService(@NonNull ServiceConnection conn) {
+
+ }
+
+ @Override
+ public boolean startInstrumentation(@NonNull ComponentName className, @Nullable String profileFile, @Nullable Bundle arguments) {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public Object getSystemService(@NonNull String name) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getSystemServiceName(@NonNull Class> serviceClass) {
+ return null;
+ }
+
+ @Override
+ public int checkPermission(@NonNull String permission, int pid, int uid) {
+ return 0;
+ }
+
+ @Override
+ public int checkCallingPermission(@NonNull String permission) {
+ return 0;
+ }
+
+ @Override
+ public int checkCallingOrSelfPermission(@NonNull String permission) {
+ return 0;
+ }
+
+ @Override
+ public int checkSelfPermission(@NonNull String permission) {
+ return 0;
+ }
+
+ @Override
+ public void enforcePermission(@NonNull String permission, int pid, int uid, @Nullable String message) {
+
+ }
+
+ @Override
+ public void enforceCallingPermission(@NonNull String permission, @Nullable String message) {
+
+ }
+
+ @Override
+ public void enforceCallingOrSelfPermission(@NonNull String permission, @Nullable String message) {
+
+ }
+
+ @Override
+ public void grantUriPermission(String toPackage, Uri uri, int modeFlags) {
+
+ }
+
+ @Override
+ public void revokeUriPermission(Uri uri, int modeFlags) {
+
+ }
+
+ @Override
+ public void revokeUriPermission(String toPackage, Uri uri, int modeFlags) {
+
+ }
+
+ @Override
+ public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
+ return 0;
+ }
+
+ @Override
+ public int checkCallingUriPermission(Uri uri, int modeFlags) {
+ return 0;
+ }
+
+ @Override
+ public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags) {
+ return 0;
+ }
+
+ @Override
+ public int checkUriPermission(@Nullable Uri uri, @Nullable String readPermission, @Nullable String writePermission, int pid, int uid, int modeFlags) {
+ return 0;
+ }
+
+ @Override
+ public void enforceUriPermission(Uri uri, int pid, int uid, int modeFlags, String message) {
+
+ }
+
+ @Override
+ public void enforceCallingUriPermission(Uri uri, int modeFlags, String message) {
+
+ }
+
+ @Override
+ public void enforceCallingOrSelfUriPermission(Uri uri, int modeFlags, String message) {
+
+ }
+
+ @Override
+ public void enforceUriPermission(@Nullable Uri uri, @Nullable String readPermission, @Nullable String writePermission, int pid, int uid, int modeFlags, @Nullable String message) {
+
+ }
+
+ @Override
+ public android.content.Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
+ return null;
+ }
+
+ @Override
+ public android.content.Context createContextForSplit(String splitName) throws PackageManager.NameNotFoundException {
+ return null;
+ }
+
+ @Override
+ public android.content.Context createConfigurationContext(@NonNull Configuration overrideConfiguration) {
+ return null;
+ }
+
+ @Override
+ public android.content.Context createDisplayContext(@NonNull Display display) {
+ return null;
+ }
+
+ @Override
+ public android.content.Context createDeviceProtectedStorageContext() {
+ return null;
+ }
+
+ @Override
+ public boolean isDeviceProtectedStorage() {
+ return false;
+ }
+}
diff --git a/mobile/src/androidTest/java/org/fedorahosted/freeotp/MemorySharedPreferences.java b/mobile/src/androidTest/java/org/fedorahosted/freeotp/MemorySharedPreferences.java
new file mode 100644
index 00000000..d0c4f2c5
--- /dev/null
+++ b/mobile/src/androidTest/java/org/fedorahosted/freeotp/MemorySharedPreferences.java
@@ -0,0 +1,197 @@
+package org.fedorahosted.freeotp;
+
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class MemorySharedPreferences implements SharedPreferences {
+ private static abstract class Editor implements SharedPreferences.Editor {
+ static class Instruction {
+ final boolean put;
+ final String key;
+ final Object val;
+
+ Instruction() {
+ put = false;
+ val = null;
+ key = null;
+ }
+
+ Instruction(String key) {
+ this.put = false;
+ this.val = null;
+ this.key = key;
+ }
+
+ Instruction(String key, Object val) {
+ this.put = true;
+ this.val = val;
+ this.key = key;
+ }
+ }
+
+ List mInstructions = new LinkedList<>();
+
+ @Override
+ public SharedPreferences.Editor putString(String key, @Nullable String value) {
+ mInstructions.add(new Editor.Instruction(key, value));
+ return this;
+ }
+
+ @Override
+ public SharedPreferences.Editor putStringSet(String key, @Nullable Set values) {
+ mInstructions.add(new Editor.Instruction(key, values));
+ return this;
+ }
+
+ @Override
+ public SharedPreferences.Editor putInt(String key, int value) {
+ mInstructions.add(new Editor.Instruction(key, value));
+ return this;
+ }
+
+ @Override
+ public SharedPreferences.Editor putLong(String key, long value) {
+ mInstructions.add(new Editor.Instruction(key, value));
+ return this;
+ }
+
+ @Override
+ public SharedPreferences.Editor putFloat(String key, float value) {
+ mInstructions.add(new Editor.Instruction(key, value));
+ return this;
+ }
+
+ @Override
+ public SharedPreferences.Editor putBoolean(String key, boolean value) {
+ mInstructions.add(new Editor.Instruction(key, value));
+ return this;
+ }
+
+ @Override
+ public SharedPreferences.Editor remove(String key) {
+ mInstructions.add(new Editor.Instruction(key));
+ return this;
+ }
+
+ @Override
+ public SharedPreferences.Editor clear() {
+ mInstructions.add(new Editor.Instruction());
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ apply();
+ return true;
+ }
+ }
+
+ private Set mListeners = new HashSet<>();
+ private Map mPreferences = new HashMap<>();
+
+ @Override
+ public Map getAll() {
+ return mPreferences;
+ }
+
+ @Nullable
+ @Override
+ public String getString(String key, @Nullable String defValue) {
+ if (!mPreferences.containsKey(key))
+ return defValue;
+ return (String) mPreferences.get(key);
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ public Set getStringSet(String key, @Nullable Set defValues) {
+ if (!mPreferences.containsKey(key))
+ return defValues;
+ return (Set) mPreferences.get(key);
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ if (!mPreferences.containsKey(key))
+ return defValue;
+ return (Integer) mPreferences.get(key);
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ if (!mPreferences.containsKey(key))
+ return defValue;
+ return (Long) mPreferences.get(key);
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ if (!mPreferences.containsKey(key))
+ return defValue;
+ return (Float) mPreferences.get(key);
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ if (!mPreferences.containsKey(key))
+ return defValue;
+ return (Boolean) mPreferences.get(key);
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return mPreferences.containsKey(key);
+ }
+
+ private void onChange(String key) {
+ for (OnSharedPreferenceChangeListener l : mListeners)
+ l.onSharedPreferenceChanged(this, key);
+ }
+
+ @Override
+ public SharedPreferences.Editor edit() {
+ return new Editor() {
+ @Override
+ public void apply() {
+ for (Iterator iitr = mInstructions.iterator(); iitr.hasNext(); ) {
+ Instruction i = iitr.next();
+ if (i.key != null) {
+ if (i.put)
+ mPreferences.put(i.key, i.val);
+ else
+ mPreferences.remove(i.key);
+
+ onChange(i.key);
+ continue;
+ }
+
+ for (Iterator sitr = mPreferences.keySet().iterator(); sitr.hasNext(); ) {
+ onChange(sitr.next());
+ sitr.remove();
+ }
+
+ iitr.remove();
+ }
+ }
+ };
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ mListeners.remove(listener);
+ }
+}
diff --git a/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenAdapterTest.java b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenAdapterTest.java
new file mode 100644
index 00000000..effc630b
--- /dev/null
+++ b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenAdapterTest.java
@@ -0,0 +1,181 @@
+package org.fedorahosted.freeotp;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import android.util.Pair;
+
+import junit.framework.TestCase;
+
+import org.fedorahosted.freeotp.main.Adapter;
+import org.fedorahosted.freeotp.utils.SelectableAdapter;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.UUID;
+
+import javax.crypto.SecretKey;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TokenAdapterTest extends TestCase implements SelectableAdapter.EventListener {
+ private static final String HOTP = "otpauth://hotp/foo:bar?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ";
+
+ @Mock
+ private Context mockContext;
+
+ private Map mockStore = new HashMap<>();
+ private Pair pair = Token.parseUnsafe(HOTP);
+
+ private String getItem(String item) {
+ return mockStore.get("tokenStore").getString(item, null);
+ }
+
+ private int getSize() {
+ return mockStore.get("tokenStore").getAll().size();
+ }
+
+ public TokenAdapterTest() throws Token.InvalidUriException {
+ }
+
+ @Before
+ public void setup() {
+ when(mockContext.getSharedPreferences(anyString(), anyInt()))
+ .thenAnswer(new Answer() {
+ @Override
+ public SharedPreferences answer(InvocationOnMock invocation) {
+ String arg0 = invocation.getArgument(0);
+ if (!mockStore.containsKey(arg0))
+ mockStore.put(arg0, new MemorySharedPreferences());
+ return mockStore.get(arg0);
+ }
+ });
+ }
+
+ private Pair reset() throws GeneralSecurityException, IOException {
+ KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
+ ks.load(null);
+
+ for (String alias : Collections.list(ks.aliases()))
+ ks.deleteEntry(alias);
+
+ JSONArray array = new JSONArray();
+ for (int i = 0; i < 4; i++)
+ array.put(UUID.randomUUID().toString());
+
+ mockStore.clear();
+ mockContext.getSharedPreferences("tokenStore", Context.MODE_PRIVATE)
+ .edit().putString("tokenOrder", array.toString()).commit();
+
+ return new Pair<>(ks, new Adapter(mockContext, this));
+ }
+
+ @Test
+ public void addToken() throws GeneralSecurityException, IOException, JSONException {
+ Pair state = reset();
+
+ Log.e(getClass().getCanonicalName(), getItem("tokenOrder"));
+
+ JSONArray prev = new JSONArray(getItem("tokenOrder"));
+ assertEquals(4, prev.length());
+
+ assertEquals(1, getSize());
+ state.second.add(pair.first, pair.second);
+ assertEquals(2, getSize());
+
+ JSONArray next = new JSONArray(getItem("tokenOrder"));
+ assertEquals(5, next.length());
+
+ String uuid = (String) next.remove(next.length() - 1);
+ assertEquals(prev, next);
+
+ assertEquals(pair.second.serialize(), getItem(uuid));
+
+ assertTrue(state.first.containsAlias(uuid));
+ assertTrue(!state.first.containsAlias("foo"));
+ }
+
+ @Test
+ public void deleteToken() throws GeneralSecurityException, IOException, JSONException {
+ Pair state = reset();
+
+ Log.e(getClass().getCanonicalName(), getItem("tokenOrder"));
+
+ JSONArray prev = new JSONArray(getItem("tokenOrder"));
+ state.second.add(pair.first, pair.second);
+ JSONArray next = new JSONArray(getItem("tokenOrder"));
+
+ String uuid = (String) next.remove(next.length() - 1);
+ assertEquals(prev, next);
+
+ assertEquals(2, getSize());
+ state.second.delete(state.second.getItemCount() - 1);
+ assertEquals(1, getSize());
+ assertEquals(prev, new JSONArray(getItem("tokenOrder")));
+
+ assertTrue(!mockStore.get("tokenStore").getAll().containsKey(uuid));
+ assertTrue(!state.first.containsAlias(uuid));
+ assertTrue(!state.first.containsAlias("foo"));
+ }
+
+ @Test
+ public void moveToken() throws GeneralSecurityException, IOException, JSONException {
+ Pair state = reset();
+
+ Log.e(getClass().getCanonicalName(), getItem("tokenOrder"));
+
+ JSONArray prev = new JSONArray(getItem("tokenOrder"));
+ JSONArray next = new JSONArray(getItem("tokenOrder"));
+ String one = (String) next.remove(1);
+ String two = (String) next.get(2);
+ next.put(2, one);
+ next.put(3, two);
+
+ assertEquals(1, getSize());
+ assertEquals(4, prev.length());
+ state.second.move(1, 2);
+ assertEquals(4, new JSONArray(getItem("tokenOrder")).length());
+ assertEquals(1, getSize());
+
+ assertEquals(next, new JSONArray(getItem("tokenOrder")));
+ }
+
+ @Test
+ public void moveTokenSame() throws GeneralSecurityException, IOException, JSONException {
+ Pair state = reset();
+
+ Log.e(getClass().getCanonicalName(), getItem("tokenOrder"));
+
+ JSONArray prev = new JSONArray(getItem("tokenOrder"));
+
+ assertEquals(1, getSize());
+ assertEquals(4, prev.length());
+ state.second.move(1, 1);
+ assertEquals(4, new JSONArray(getItem("tokenOrder")).length());
+ assertEquals(1, getSize());
+
+ assertEquals(prev, new JSONArray(getItem("tokenOrder")));
+ }
+
+ @Override
+ public void onSelectEvent(NavigableSet selected) {
+
+ }
+}
diff --git a/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenCompatTest.java b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenCompatTest.java
new file mode 100644
index 00000000..ec70088b
--- /dev/null
+++ b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenCompatTest.java
@@ -0,0 +1,307 @@
+package org.fedorahosted.freeotp;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Looper;
+import android.util.Pair;
+
+import junit.framework.TestCase;
+
+import org.fedorahosted.freeotp.main.Adapter;
+import org.fedorahosted.freeotp.utils.Base32;
+import org.fedorahosted.freeotp.utils.SelectableAdapter;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.UUID;
+
+import javax.crypto.SecretKey;
+
+@RunWith(Parameterized.class)
+public class TokenCompatTest extends TestCase implements SelectableAdapter.EventListener {
+ static {
+ Looper.prepare();
+ }
+
+ private enum Layout { NONE, PATH, PARAM, BOTH };
+
+ private static final Layout[] LAYOUT = { Layout.PARAM, Layout.NONE, Layout.PATH, Layout.BOTH };
+ private static final String[] TYPES = { "hotp", "HOTP", "totp", "TOTP" };
+ private static final String[] ISSUERS = { "foo", "είμαι πάπια", "Steam" };
+ private static final String[] LABELS = { "baz", "είμαι αρκούδα" };
+ private static final String[] SECRETS = { "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", "gezdgnbvgy3tqojqgezdgnbvgy3tqojq" };
+ private static final String[] ALGORITHMS = { null, "SHA1", "SHA224", "SHA256", "SHA384", "SHA512" };
+ private static final String[] DIGITS = { null, "6", "7", "8", "9" };
+ private static final String[] PERIODS = { null, "30", "60" };
+ private static final String[] COUNTERS = { null, "0", "1234143" };
+ private static final String[] IMAGES = { null, };
+ private static final String[] COLORS = { null, "000000", "FFFFFF" };
+ private static final String[] LOCKS = { null, "true", "false" };
+
+ private static Object[][] compose(Object[] ... sets) {
+ int size = 1;
+ for (int i = 0; i < sets.length; i++)
+ size += sets[i].length;
+
+ Object[][] all = new Object[size][sets.length];
+
+ for (int i = 0; i < sets.length; i++)
+ all[0][i] = sets[i][0];
+
+ int off = 1;
+ for (int i = 0; i < sets.length; i++) {
+ for (int j = 0; j < sets[i].length; j++, off++) {
+ for (int k = 0; k < sets.length; k++) {
+ all[off][k] = i == k ? sets[k][j] : sets[k][0];
+ }
+ }
+ }
+
+ return all;
+ }
+
+ @Parameterized.Parameters
+ public static Collection data() {
+ return Arrays.asList(compose(LAYOUT, TYPES, ISSUERS, LABELS, SECRETS, ALGORITHMS, DIGITS,
+ PERIODS, COUNTERS, IMAGES, COLORS, LOCKS));
+ }
+
+ private Context mContext = new org.fedorahosted.freeotp.Context();
+
+ private Layout mLayout;
+ private String mType;
+ private String mIssuer;
+ private String mLabel;
+ private String mSecret;
+ private String mAlgorithm;
+ private String mDigits;
+ private String mPeriod;
+ private String mCounter;
+ private String mImage;
+ private String mColor;
+ private String mLock;
+
+ private String fix(String str) {
+ return "null".equals(str) ? null : str;
+ }
+
+ public TokenCompatTest(Layout layout, String type, String issuer, String label, String secret,
+ String algorithm, String digits, String period, String counter,
+ String image, String color, String lock) {
+ mLayout = layout;
+ mType = fix(type);
+ mIssuer = fix(issuer);
+ mLabel = fix(label);
+ mSecret = fix(secret);
+ mAlgorithm = fix(algorithm);
+ mDigits = fix(digits);
+ mPeriod = fix(period);
+ mCounter = fix(counter);
+ mImage = fix(image);
+ mColor = fix(color);
+ mLock = fix(lock);
+ }
+
+ private JSONObject makeJson() throws JSONException, Base32.DecodingException {
+ JSONObject obj = new JSONObject();
+
+ JSONArray arr = new JSONArray();
+ for (byte b : Base32.RFC4648.decode(mSecret.toUpperCase()))
+ arr.put((int) b);
+
+ obj.put("counter", mCounter == null ? 0 : Integer.parseInt(mCounter));
+ obj.put("period", mPeriod == null ? 30 : Integer.parseInt(mPeriod));
+ obj.put("digits", mDigits == null ? 6 : Integer.parseInt(mDigits));
+ obj.put("algo", mAlgorithm == null ? "SHA1" : mAlgorithm);
+ obj.put("type", mType.toUpperCase());
+ obj.put("label", mLabel);
+ obj.put("secret", arr);
+
+ switch (mLayout) {
+ case NONE:
+ break;
+
+ case PATH:
+ obj.put("issuerExt", mIssuer);
+ break;
+
+ case PARAM:
+ obj.put("issuerInt", mIssuer);
+ break;
+
+ case BOTH:
+ obj.put("issuerInt", mIssuer);
+ obj.put("issuerExt", mIssuer);
+ break;
+ }
+
+ if (mImage != null)
+ obj.put("image", mImage);
+
+ return obj;
+ }
+
+ private Uri makeUri() {
+ Uri.Builder b = new Uri.Builder().scheme("otpauth").authority(mType);
+ switch (mLayout) {
+ case NONE:
+ b.appendEncodedPath(mLabel);
+ break;
+
+ case PATH:
+ b.appendEncodedPath(mIssuer + ":" + mLabel);
+ break;
+
+ case PARAM:
+ b.appendEncodedPath(mLabel).appendQueryParameter("issuer", mIssuer);
+ break;
+
+ case BOTH:
+ b.appendEncodedPath(mIssuer + ":" + mLabel).appendQueryParameter("issuer", mIssuer);
+ break;
+
+ default:
+ throw new NullPointerException();
+ }
+
+ b.appendQueryParameter("secret", mSecret);
+
+ if (mAlgorithm != null)
+ b.appendQueryParameter("algorithm", mAlgorithm);
+ if (mDigits != null)
+ b.appendQueryParameter("digits", mDigits);
+ if (mPeriod != null)
+ b.appendQueryParameter("period", mPeriod);
+ if (mCounter != null)
+ b.appendQueryParameter("counter", mCounter);
+ if (mImage != null)
+ b.appendQueryParameter("image", mImage);
+ if (mColor != null)
+ b.appendQueryParameter("color", mColor);
+ if (mLock != null)
+ b.appendQueryParameter("lock", mLock);
+
+ return b.build();
+ }
+
+ @Test
+ public void uriToken() throws Token.UnsafeUriException, Token.InvalidUriException, InvalidKeyException {
+ Pair pair = Token.parse(makeUri());
+
+ assertEquals(Token.Type.valueOf(mType.toUpperCase()), pair.second.getType());
+ assertEquals(mLayout == Layout.NONE ? null : mIssuer, pair.second.getIssuer());
+ assertEquals(mLabel, pair.second.getLabel());
+
+ assertEquals(mImage, pair.second.getImage());
+ assertEquals(mColor, pair.second.getColor());
+ assertEquals(Boolean.valueOf(mLock).booleanValue(), pair.second.getLock());
+
+ assertTrue(pair.second.getCode(pair.first) != null);
+ }
+
+ @Test
+ public void uriCompat() throws GeneralSecurityException, IOException, JSONException {
+ SharedPreferences old = mContext.getSharedPreferences("tokens", Context.MODE_PRIVATE);
+ SharedPreferences cur = mContext.getSharedPreferences("tokenStore", Context.MODE_PRIVATE);
+ old.edit()
+ .clear()
+ .putString("tokenOrder", "[\"foo\"]")
+ .putString("foo", makeUri().toString())
+ .commit();
+
+ Adapter a = new Adapter(mContext, this);
+
+ // Ensure the migration happened.
+ assertEquals(0, old.getAll().size());
+ assertEquals(2, cur.getAll().size());
+
+ // Make sure tokenOrder is well formed.
+ JSONArray order = new JSONArray(cur.getString("tokenOrder", null));
+ assertEquals(1, order.length());
+
+ // Make sure it is a UUID.
+ UUID uuid = UUID.fromString(order.getString(0));
+
+ // Make sure the secret is stored in the key store.
+ KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
+ ks.load(null);
+ assertTrue(ks.containsAlias(uuid.toString()));
+ Key key = ks.getKey(uuid.toString(), null);
+
+ // Make sure that the token is valid.
+ Token token = Token.deserialize(cur.getString(uuid.toString(), null));
+
+ // Check token values.
+ assertEquals(Token.Type.valueOf(mType.toUpperCase()), token.getType());
+ assertEquals(mLayout == Layout.NONE ? null : mIssuer, token.getIssuer());
+ assertEquals(mLabel, token.getLabel());
+ assertEquals(mImage, token.getImage());
+ assertEquals(mColor, token.getColor());
+ assertEquals(Boolean.valueOf(mLock).booleanValue(), token.getLock());
+ assertTrue(token.getCode(key) != null);
+ }
+
+ @Test
+ public void jsonCompat() throws GeneralSecurityException, IOException, JSONException, Base32.DecodingException {
+ SharedPreferences old = mContext.getSharedPreferences("tokens", Context.MODE_PRIVATE);
+ SharedPreferences cur = mContext.getSharedPreferences("tokenStore", Context.MODE_PRIVATE);
+ old.edit()
+ .clear()
+ .putString("tokenOrder", "[\"foo\"]")
+ .putString("foo", makeJson().toString())
+ .commit();
+
+ Adapter a = new Adapter(mContext, this);
+
+ for (Map.Entry e: old.getAll().entrySet()) {
+ System.out.println(String.format("%s = %s", e.getKey(), e.getValue().toString()));
+ System.out.flush();
+ }
+
+ // Ensure the migration happened.
+ assertEquals(0, old.getAll().size());
+ assertEquals(2, cur.getAll().size());
+
+ // Make sure tokenOrder is well formed.
+ JSONArray order = new JSONArray(cur.getString("tokenOrder", null));
+ assertEquals(1, order.length());
+
+ // Make sure it is a UUID.
+ UUID uuid = UUID.fromString(order.getString(0));
+
+ // Make sure the secret is stored in the key store.
+ KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
+ ks.load(null);
+ assertTrue(ks.containsAlias(uuid.toString()));
+ Key key = ks.getKey(uuid.toString(), null);
+
+ // Make sure that the token is valid.
+ Token token = Token.deserialize(cur.getString(uuid.toString(), null));
+
+ // Check token values.
+ assertEquals(Token.Type.valueOf(mType.toUpperCase()), token.getType());
+ assertEquals(mLayout == Layout.NONE ? null : mIssuer, token.getIssuer());
+ assertEquals(mLabel, token.getLabel());
+ assertEquals(mImage, token.getImage());
+ assertTrue(token.getCode(key) != null);
+ }
+
+ @Override
+ public void onSelectEvent(NavigableSet selected) {
+
+ }
+}
diff --git a/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenRFC4226Test.java b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenRFC4226Test.java
new file mode 100644
index 00000000..2cb3dde3
--- /dev/null
+++ b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenRFC4226Test.java
@@ -0,0 +1,40 @@
+package org.fedorahosted.freeotp;
+
+
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import junit.framework.TestCase;
+
+import org.fedorahosted.freeotp.utils.Base32;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.InvalidKeyException;
+
+import javax.crypto.SecretKey;
+
+@RunWith(AndroidJUnit4.class)
+public class TokenRFC4226Test extends TestCase {
+ @Test
+ public void test() throws Token.InvalidUriException, InvalidKeyException {
+ // Note: we are implicitly testing defaults of:
+ // * digits = 6
+ // * counter = 0
+ // * algorithm = SHA1
+ String fmt = "otpauth://hotp/foo?secret=%s";
+ String uri = String.format(fmt, Base32.RFC4648.encode("12345678901234567890".getBytes()));
+
+ Pair pair = Token.parseUnsafe(uri);
+ assertEquals("755224", pair.second.getCode(pair.first).getCode());
+ assertEquals("287082", pair.second.getCode(pair.first).getCode());
+ assertEquals("359152", pair.second.getCode(pair.first).getCode());
+ assertEquals("969429", pair.second.getCode(pair.first).getCode());
+ assertEquals("338314", pair.second.getCode(pair.first).getCode());
+ assertEquals("254676", pair.second.getCode(pair.first).getCode());
+ assertEquals("287922", pair.second.getCode(pair.first).getCode());
+ assertEquals("162583", pair.second.getCode(pair.first).getCode());
+ assertEquals("399871", pair.second.getCode(pair.first).getCode());
+ assertEquals("520489", pair.second.getCode(pair.first).getCode());
+ }
+}
diff --git a/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenRFC6238Test.java b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenRFC6238Test.java
new file mode 100644
index 00000000..2b7d683f
--- /dev/null
+++ b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenRFC6238Test.java
@@ -0,0 +1,76 @@
+package org.fedorahosted.freeotp;
+
+
+import android.util.Pair;
+
+import junit.framework.TestCase;
+
+import org.fedorahosted.freeotp.utils.Base32;
+import org.fedorahosted.freeotp.utils.Time;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.security.InvalidKeyException;
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.crypto.SecretKey;
+
+@RunWith(Parameterized.class)
+public class TokenRFC6238Test extends TestCase {
+ @Parameterized.Parameters
+ public static Collection data() {
+ return Arrays.asList(new Object[][] {
+ { 59L, "SHA1", "94287082", "12345678901234567890" },
+ { 59L, "SHA256", "46119246", "12345678901234567890123456789012" },
+ { 59L, "SHA512", "90693936", "1234567890123456789012345678901234567890123456789012345678901234" },
+ { 1111111109L, "SHA1", "07081804", "12345678901234567890" },
+ { 1111111109L, "SHA256", "68084774", "12345678901234567890123456789012" },
+ { 1111111109L, "SHA512", "25091201", "1234567890123456789012345678901234567890123456789012345678901234" },
+ { 1111111111L, "SHA1", "14050471", "12345678901234567890" },
+ { 1111111111L, "SHA256", "67062674", "12345678901234567890123456789012" },
+ { 1111111111L, "SHA512", "99943326", "1234567890123456789012345678901234567890123456789012345678901234" },
+ { 1234567890L, "SHA1", "89005924", "12345678901234567890" },
+ { 1234567890L, "SHA256", "91819424", "12345678901234567890123456789012" },
+ { 1234567890L, "SHA512", "93441116", "1234567890123456789012345678901234567890123456789012345678901234" },
+ { 2000000000L, "SHA1", "69279037", "12345678901234567890" },
+ { 2000000000L, "SHA256", "90698825", "12345678901234567890123456789012" },
+ { 2000000000L, "SHA512", "38618901", "1234567890123456789012345678901234567890123456789012345678901234" },
+ { 20000000000L, "SHA1", "65353130", "12345678901234567890" },
+ { 20000000000L, "SHA256", "77737706", "12345678901234567890123456789012" },
+ { 20000000000L, "SHA512", "47863826", "1234567890123456789012345678901234567890123456789012345678901234" },
+ });
+ }
+
+ private final String mAlgorithm;
+ private final String mSecret;
+ private final String mCode;
+ private final long mTime;
+
+ public TokenRFC6238Test(long time, String algorithm, String code, String secret) {
+ mAlgorithm = algorithm;
+ mSecret = secret;
+ mCode = code;
+ mTime = time;
+ }
+
+ @Test
+ public void test() throws Token.InvalidUriException, InvalidKeyException {
+ // Note: we are implicitly testing default period = 30
+ String fmt = "otpauth://totp/foo?secret=%s&digits=8&algorithm=%s";
+ String uri = String.format(fmt, Base32.RFC4648.encode(mSecret.getBytes()), mAlgorithm);
+ Pair pair = Token.parseUnsafe(uri);
+
+ Time.INSTANCE = new Time() {
+ @Override
+ public long current() {
+ return mTime * 1000;
+ }
+ };
+
+ assertEquals(mCode, pair.second.getCode(pair.first).getCode());
+
+ Time.INSTANCE = new Time();
+ }
+}
diff --git a/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenUriInvalidTest.java b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenUriInvalidTest.java
new file mode 100644
index 00000000..3ba3b35d
--- /dev/null
+++ b/mobile/src/androidTest/java/org/fedorahosted/freeotp/TokenUriInvalidTest.java
@@ -0,0 +1,91 @@
+package org.fedorahosted.freeotp;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class TokenUriInvalidTest extends TestCase {
+ @Parameterized.Parameters
+ public static Collection data() {
+ return Arrays.asList(new Object[][] {
+ { "foo",
+ Token.InvalidSchemeException.class },
+ { "https://google.com",
+ Token.InvalidSchemeException.class },
+ { "otpauth://potp",
+ Token.InvalidTypeException.class },
+ { "otpauth://totp",
+ Token.InvalidLabelException.class },
+ { "otpauth://totp/",
+ Token.InvalidLabelException.class },
+ { "otpauth://totp/foo:bar:baz",
+ Token.InvalidLabelException.class },
+ { "otpauth://totp/bar",
+ Token.InvalidSecretException.class },
+ { "otpauth://totp/bar?secret=00000000000000000000000000",
+ Token.InvalidSecretException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZU",
+ Token.UnsafeSecretException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&algorithm=foo",
+ Token.InvalidAlgorithmException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&algorithm=MD5",
+ Token.UnsafeAlgorithmException.class },
+ { "otpauth://hotp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&counter=-1",
+ Token.InvalidCounterException.class },
+ { "otpauth://hotp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&counter=18446744073709551615",
+ Token.InvalidCounterException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&digits=-1",
+ Token.InvalidDigitsException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&digits=5",
+ Token.UnsafeDigitsException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&digits=10",
+ Token.UnsafeDigitsException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&period=-1",
+ Token.InvalidPeriodException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&period=4",
+ Token.InvalidPeriodException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&period=2147483648",
+ Token.InvalidPeriodException.class },
+
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&color=00000",
+ Token.InvalidColorException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&color=0000000",
+ Token.InvalidColorException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&color=00000000",
+ Token.InvalidColorException.class },
+ { "otpauth://totp/bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&color=GHIJKL",
+ Token.InvalidColorException.class },
+
+ /* Test digit bounds with an alternate token code alphabet. */
+ { "otpauth://totp/Steam:bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&digits=4",
+ Token.UnsafeDigitsException.class },
+ { "otpauth://totp/Steam:bar?secret=GAYTEMZUGU3DOOBZGAYTEMZUGU&digits=7",
+ Token.UnsafeDigitsException.class },
+ });
+ }
+
+ private final Class extends Token.InvalidUriException> mException;
+ private final String mUri;
+
+ public TokenUriInvalidTest(String uri, Class extends Token.InvalidUriException> exception) {
+ mException = exception;
+ mUri = uri;
+ }
+
+ @Test
+ public void test() throws Token.InvalidUriException, Token.UnsafeUriException {
+ try {
+ Token.parse(mUri);
+ Assert.fail("Unexpectedly succeeded.");
+ } catch (Exception e) {
+ assertEquals(mException, e.getClass());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
similarity index 66%
rename from app/src/main/AndroidManifest.xml
rename to mobile/src/main/AndroidManifest.xml
index 342a9b4b..bb3a3363 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -4,7 +4,7 @@
-
- Authors: Nathaniel McCallum
-
- - Copyright (C) 2013 Nathaniel McCallum, Red Hat
+ - Copyright (C) 2018 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.
@@ -20,9 +20,7 @@
-->
+ package="org.fedorahosted.freeotp">
-
+
+
+
+
-
+
+
+
+
-
-
-
-
-
-
-
-
+ android:theme="@style/AppTheme"
+ android:allowBackup="false"
+ android:supportsRtl="true"
+ android:label="@string/app_name">
+ android:launchMode="singleTask">
+
-
+
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/Code.java b/mobile/src/main/java/org/fedorahosted/freeotp/Code.java
new file mode 100644
index 00000000..29e23724
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/Code.java
@@ -0,0 +1,113 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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 org.fedorahosted.freeotp.utils.Time;
+import org.jetbrains.annotations.Nullable;
+
+public class Code {
+ static class Factory {
+ // See: RFC 4226, Section 4, R4
+ private static final double RFC_MIN = Math.pow(10, 6);
+ private static final double RFC_MAX = Integer.MAX_VALUE;
+
+ static Factory fromIssuer(String issuer) {
+ if (issuer == null)
+ return new Factory("0123456789");
+
+ if (issuer.equals("Steam"))
+ return new Factory("23456789BCDFGHJKMNPQRTVWXY");
+
+ return new Factory("0123456789");
+ }
+
+ private final char[] mAlphabet;
+ private final int mDigits;
+
+ private Factory(String alphabet) {
+ mAlphabet = alphabet.toCharArray();
+ mDigits = getDigitsMin();
+ }
+
+ private Factory(String alphabet, int defaultDigits) {
+ mAlphabet = alphabet.toCharArray();
+ mDigits = defaultDigits;
+ }
+
+ int getDigitsDefault() {
+ return mDigits;
+ }
+
+ int getDigitsMin() {
+ return (int) Math.ceil(Math.log(RFC_MIN) / Math.log(mAlphabet.length));
+ }
+
+ int getDigitsMax() {
+ return (int) Math.floor(Math.log(RFC_MAX) / Math.log(mAlphabet.length));
+ }
+
+ Code makeCode(int code, @Nullable Integer digits, int period) {
+ if (digits == null)
+ digits = mDigits;
+
+ char[] buffer = new char[digits];
+
+ for (int i = 0; i < digits; i++) {
+ buffer[digits - i - 1] = mAlphabet[code % mAlphabet.length];
+ code /= mAlphabet.length;
+ }
+
+ return new Code(new String(buffer), period);
+ }
+ }
+
+ private final String mCode;
+ private final long mPeriod;
+ private final long mStart;
+
+ public Code(String code, long period) {
+ mStart = Time.INSTANCE.current();
+ mPeriod = period * 1000;
+ mCode = code;
+ }
+
+ public String getCode() {
+ return mCode;
+ }
+
+ public long timeValid() {
+ return mPeriod;
+ }
+
+ public long timeLeft() {
+ long now = Time.INSTANCE.current();
+ long left = mStart + mPeriod - now;
+ return left < 0 ? 0 : left;
+ }
+
+ public int getProgress(int max) {
+ return (int) (timeLeft() * max / timeValid());
+ }
+
+ public boolean isValid() {
+ return timeLeft() > 0;
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/Token.java b/mobile/src/main/java/org/fedorahosted/freeotp/Token.java
new file mode 100644
index 00000000..58bb280c
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/Token.java
@@ -0,0 +1,375 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.net.Uri;
+import android.util.Pair;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+
+import org.fedorahosted.freeotp.utils.Base32;
+import org.fedorahosted.freeotp.utils.Time;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+public class Token {
+ public static class UnsafeUriException extends Exception {}
+ public static class UnsafeSecretException extends UnsafeUriException {}
+ public static class UnsafeDigitsException extends UnsafeUriException {}
+ public static class UnsafeAlgorithmException extends UnsafeUriException {}
+
+ public static class InvalidUriException extends Exception {}
+ public static class InvalidCounterException extends InvalidUriException {}
+ public static class InvalidDigitsException extends InvalidUriException {}
+ public static class InvalidPeriodException extends InvalidUriException {}
+ public static class InvalidSecretException extends InvalidUriException {}
+ public static class InvalidLabelException extends InvalidUriException {}
+ public static class InvalidAlgorithmException extends InvalidUriException {}
+ public static class InvalidSchemeException extends InvalidUriException {}
+ public static class InvalidTypeException extends InvalidUriException {}
+ public static class InvalidColorException extends InvalidUriException {}
+
+ public enum Type { HOTP, TOTP }
+
+ private static final String[] SAFE_ALGOS = { "SHA1", "SHA224", "SHA256", "SHA384", "SHA512" };
+ private static final Pattern PATTERN = Pattern.compile("^/(?:([^:]+):)?([^:]+)$");
+
+ @SerializedName("algo")
+ private final String mAlgorithm;
+
+ @SerializedName("issuerExt")
+ private final String mIssuer;
+
+ @SerializedName("issuerInt")
+ private final String mIssuerParam;
+
+ @SerializedName("label")
+ private final String mLabel;
+
+ @SerializedName("image")
+ private final String mImage;
+
+ @SerializedName("color")
+ private final String mColor;
+
+ @SerializedName("lock")
+ private final Boolean mLock;
+
+ @SerializedName("period")
+ private final Integer mPeriod;
+
+ @SerializedName("digits")
+ private final Integer mDigits;
+
+ @SerializedName("type")
+ private final Type mType;
+
+ @SerializedName("counter")
+ private Long mCounter;
+
+ private class Secret {
+ private byte[] secret;
+ }
+
+ public static Token deserialize(String json) {
+ return new Gson().fromJson(json, Token.class);
+ }
+
+ public static Pair compat(String str) throws InvalidUriException {
+ try {
+ Token t = Token.deserialize(str);
+ Secret s = new Gson().fromJson(str, Secret.class);
+ SecretKey key = new SecretKeySpec(s.secret, "Hmac" + t.getAlgorithm());
+ return new Pair(key, t);
+ } catch (JsonSyntaxException e) {
+ // Backwards compatibility for URL-based persistence.
+ return Token.parseUnsafe(str);
+ }
+ }
+
+ public static Pair parse(String uri) throws UnsafeUriException, InvalidUriException {
+ return parse(Uri.parse(uri));
+ }
+
+ public static Pair parseUnsafe(String uri) throws InvalidUriException {
+ return parseUnsafe(Uri.parse(uri));
+ }
+
+ public static Pair parse(Uri uri) throws UnsafeUriException, InvalidUriException {
+ Pair pair = parseUnsafe(uri);
+
+ // RFC 4226, Section 4, R6
+ if (pair.first.getEncoded().length < 16)
+ throw new UnsafeSecretException();
+
+ boolean safeAlgo = false;
+ for (String algo : SAFE_ALGOS) {
+ if (pair.second.getAlgorithm().equals(algo)) {
+ safeAlgo = true;
+ break;
+ }
+ }
+ if (!safeAlgo)
+ throw new UnsafeAlgorithmException();
+
+ if (pair.second.mDigits != null) {
+ Code.Factory f = Code.Factory.fromIssuer(pair.second.mIssuer);
+ if (pair.second.mDigits < f.getDigitsMin() || pair.second.mDigits > f.getDigitsMax())
+ throw new UnsafeDigitsException();
+ }
+
+ return pair;
+ }
+
+ public static Pair parseUnsafe(Uri uri) throws InvalidUriException {
+ Token t = new Token(uri);
+
+ try {
+ String secret = uri.getQueryParameter("secret").toUpperCase(Locale.US);
+ byte[] bytes = Base32.RFC4648.decode(secret);
+ return new Pair(new SecretKeySpec(bytes, "Hmac" + t.getAlgorithm()), t);
+ } catch (Base32.DecodingException | NullPointerException e) {
+ throw new InvalidSecretException();
+ }
+ }
+
+ public static Pair random() {
+ Random r = ThreadLocalRandom.current();
+ Token t = new Token(r);
+
+ byte[] bytes = new byte[16 + r.nextInt(16)];
+ r.nextBytes(bytes);
+ return new Pair(new SecretKeySpec(bytes, "Hmac" + t.getAlgorithm()), t);
+ }
+
+ private static final String[] ISSUERS = { "Buffer", "Google+", "HootSuite", "Mastodon",
+ "Reddit", "Tumbler", "Twitter", "WordPress.com", "FreeIPA", "Facebook", "Steam",
+ "Bitbucket", "gitlab.com", "Code Climate", "GitHub", "Launchpad", "Mapbox" };
+
+ private Token(Random r) {
+ mIssuer = r.nextInt(5) < 1 ? null : ISSUERS[r.nextInt(ISSUERS.length)];
+ mIssuerParam = mIssuer;
+ mAlgorithm = SAFE_ALGOS[r.nextInt(SAFE_ALGOS.length)];
+ mType = r.nextBoolean() ? Type.TOTP : Type.HOTP;
+ mLabel = UUID.randomUUID().toString();
+ mPeriod = 5 + r.nextInt(55);
+ mCounter = (long) r.nextInt(1000);
+ mLock = r.nextBoolean();
+
+ Code.Factory f = Code.Factory.fromIssuer(mIssuer);
+ mDigits = f.getDigitsMin() + r.nextInt(f.getDigitsMax() - f.getDigitsMin());
+
+ mImage = null;
+ mColor = null;
+ }
+
+ private Token(Uri uri) throws InvalidUriException {
+ if (uri.getScheme() == null || !uri.getScheme().equals("otpauth"))
+ throw new InvalidSchemeException();
+
+ try {
+ mType = Type.valueOf(uri.getAuthority().toUpperCase(Locale.US));
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new InvalidTypeException();
+ }
+
+ try {
+ Matcher matcher = PATTERN.matcher(uri.getPath());
+ if (!matcher.find())
+ throw new InvalidLabelException();
+
+ mIssuer = matcher.group(1);
+ mLabel = matcher.group(2);
+ } catch (NullPointerException e) {
+ throw new InvalidLabelException();
+ }
+
+ mIssuerParam = uri.getQueryParameter("issuer");
+
+ try {
+ mAlgorithm = uri.getQueryParameter("algorithm");
+ if (mAlgorithm != null)
+ Mac.getInstance("Hmac" + mAlgorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new InvalidAlgorithmException();
+ }
+
+ if (mType == Type.HOTP) {
+ try {
+ String s = uri.getQueryParameter("counter");
+ mCounter = Long.parseLong(s == null ? "0" : s);
+ if (mCounter < 0)
+ throw new InvalidCounterException();
+ } catch (NumberFormatException x) {
+ throw new InvalidCounterException();
+ }
+ }
+
+ try {
+ String s = uri.getQueryParameter("period");
+ mPeriod = s == null ? null : Integer.parseInt(s);
+ if (mPeriod != null && mPeriod < 5)
+ throw new InvalidPeriodException();
+ } catch (NumberFormatException x) {
+ throw new InvalidPeriodException();
+ }
+
+ try {
+ String s = uri.getQueryParameter("digits");
+ mDigits = s == null ? null : Integer.parseInt(s);
+ if (mDigits != null && mDigits < 0)
+ throw new InvalidDigitsException();
+ } catch (NumberFormatException x) {
+ throw new InvalidDigitsException();
+ }
+
+ if (uri.getQueryParameter("lock") != null)
+ mLock = uri.getBooleanQueryParameter("lock", false);
+ else
+ mLock = null;
+
+ mImage = uri.getQueryParameter("image");
+
+ mColor = uri.getQueryParameter("color");
+ if (mColor != null && !mColor.matches("^[0-9a-fA-F]{6}$"))
+ throw new InvalidColorException();
+ }
+
+ private String getAlgorithm() {
+ return mAlgorithm == null ? "SHA1" : mAlgorithm;
+ }
+
+ public String serialize() {
+ return new Gson().toJson(this);
+ }
+
+ public String getIssuer() {
+ return mIssuer == null ? mIssuerParam : mIssuer;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public int getPeriod() {
+ return mPeriod == null ? 30 : mPeriod;
+ }
+
+ public String getImage() {
+ return mImage;
+ }
+
+ public String getColor() {
+ return mColor;
+ }
+
+ public Type getType() {
+ return mType;
+ }
+
+ public boolean getLock() {
+ return mLock == null ? false : mLock;
+ }
+
+ public Code getCode(Key key) throws InvalidKeyException {
+ Mac mac;
+
+ // Prepare the input.
+ ByteBuffer bb = ByteBuffer.allocate(8);
+ bb.order(ByteOrder.BIG_ENDIAN);
+ switch (mType) {
+ case HOTP: bb.putLong(mCounter++); break;
+ case TOTP: bb.putLong(Time.INSTANCE.current() / 1000 / getPeriod()); break;
+ }
+
+ try {
+ mac = Mac.getInstance("Hmac" + getAlgorithm());
+ } catch (NoSuchAlgorithmException e) {
+ mac = null; // This should never happen since we check validity in the constructor.
+ }
+
+ // Do the hashing.
+ mac.init(key);
+ byte[] digest = mac.doFinal(bb.array());
+
+ // Truncate.
+ int off = digest[digest.length - 1] & 0xf;
+ int code = (digest[off] & 0x7f) << 0x18;
+ code |= (digest[off + 1] & 0xff) << 0x10;
+ code |= (digest[off + 2] & 0xff) << 0x08;
+ code |= (digest[off + 3] & 0xff);
+
+ return Code.Factory.fromIssuer(mIssuer).makeCode(code, mDigits, getPeriod());
+ }
+
+ public Uri toUri() {
+ return toUri(null);
+ }
+
+ public Uri toUri(SecretKey key) {
+ Uri.Builder ub = new Uri.Builder().scheme("otpauth");
+
+ ub.authority(mType.toString().toLowerCase());
+ ub.appendEncodedPath(mIssuer != null ? mIssuer + ":" + mLabel : mLabel);
+
+ if (key != null)
+ ub.appendQueryParameter("secret", Base32.RFC4648.encode(key.getEncoded()));
+
+ if (mAlgorithm != null)
+ ub.appendQueryParameter("algorithm", mAlgorithm);
+
+ if (mPeriod != null)
+ ub.appendQueryParameter("period", Integer.toString(mPeriod));
+
+ if (mDigits != null)
+ ub.appendQueryParameter("digits", Integer.toString(mDigits));
+
+ if (mLock != null)
+ ub.appendQueryParameter("lock", Boolean.toString(mLock));
+
+ if (mColor != null)
+ ub.appendQueryParameter("color", mColor);
+
+ if (mImage != null)
+ ub.appendQueryParameter("image", mImage);
+
+ if (mType == Type.HOTP)
+ ub.appendQueryParameter("counter", Long.toString(mCounter));
+
+ return ub.build();
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/Activity.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/Activity.java
new file mode 100644
index 00000000..b397d1f4
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/Activity.java
@@ -0,0 +1,407 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main;
+
+import android.Manifest;
+import android.app.KeyguardManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.UserNotAuthenticatedException;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.text.Html;
+import android.util.Pair;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.widget.TextView;
+
+import org.fedorahosted.freeotp.R;
+import org.fedorahosted.freeotp.Token;
+import org.fedorahosted.freeotp.main.share.ShareFragment;
+import org.fedorahosted.freeotp.utils.GridLayoutItemDecoration;
+import org.fedorahosted.freeotp.utils.SelectableAdapter;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.security.GeneralSecurityException;
+import java.security.KeyStoreException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NavigableSet;
+import java.util.TreeSet;
+
+import javax.crypto.SecretKey;
+
+public class Activity extends AppCompatActivity
+ implements SelectableAdapter.EventListener, View.OnClickListener, View.OnLongClickListener {
+ private List> mViewHolders = new LinkedList<>();
+ private int mLongClickCount = 0;
+
+ private FloatingActionButton mFloatingActionButton;
+ private RecyclerView mRecyclerView;
+ private Adapter mTokenAdapter;
+ private TextView mEmpty;
+ private Menu mMenu;
+
+ private final RecyclerView.AdapterDataObserver mAdapterDataObserver =
+ new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ mEmpty.setVisibility(mTokenAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ onChanged();
+ }
+ };
+
+ private void onActivate(ViewHolder vh) {
+ try {
+ vh.displayCode(mTokenAdapter.getCode(vh.getAdapterPosition()));
+ } catch (UserNotAuthenticatedException e) {
+ KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
+ Intent i = km.createConfirmDeviceCredentialIntent(vh.getIssuer(), vh.getLabel());
+
+ mViewHolders.add(new WeakReference(vh));
+ startActivityForResult(i, mViewHolders.size() - 1);
+ } catch (KeyPermanentlyInvalidatedException e) {
+ try {
+ mTokenAdapter.delete(vh.getAdapterPosition());
+ } catch (GeneralSecurityException | IOException f) {
+ f.printStackTrace();
+ }
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.main_invalidated_title)
+ .setMessage(R.string.main_invalidated_message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ while (mViewHolders.size() > 0) {
+ ViewHolder holder = mViewHolders.remove(requestCode).get();
+
+ if (resultCode == Activity.RESULT_OK && holder != null)
+ onActivate(holder);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_main);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ // Don't let other apps screenshot token codes...
+ getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
+
+ mFloatingActionButton = findViewById(R.id.fab);
+ mRecyclerView = findViewById(R.id.recycler);
+ mEmpty = findViewById(android.R.id.empty);
+
+ try {
+ mTokenAdapter = new Adapter(getApplicationContext(), this) {
+ @Override
+ public void onActivated(ViewHolder holder) {
+ Activity.this.onActivate(holder);
+ }
+
+ @Override
+ public void onShare(String code) {
+ Bundle b = new Bundle();
+ b.putString(ShareFragment.CODE_ID, code);
+
+ ShareFragment sf = new ShareFragment();
+ sf.setArguments(b);
+ sf.show(getSupportFragmentManager(), sf.getTag());
+ }
+ };
+ } catch (GeneralSecurityException | IOException e) {
+ }
+
+ mFloatingActionButton.setOnClickListener(this);
+ mFloatingActionButton.setOnLongClickListener(this);
+ if (!ScanDialogFragment.hasCamera(getApplicationContext()))
+ mFloatingActionButton.hide();
+
+ int margin = getResources().getDimensionPixelSize(R.dimen.margin);
+ mRecyclerView.setAdapter(mTokenAdapter);
+ mRecyclerView.addItemDecoration(new GridLayoutItemDecoration(margin));
+ mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+
+ /* Hide FAB on scroll-down, revealing the bottom token. */
+ if (mFloatingActionButton.getVisibility() == View.VISIBLE) {
+ if (dy > 0)
+ mFloatingActionButton.hide();
+ } else if (dy < 0 && ScanDialogFragment.hasCamera(getApplicationContext())) {
+ mFloatingActionButton.show();
+ }
+ }
+ });
+
+ mTokenAdapter.registerAdapterDataObserver(mAdapterDataObserver);
+ mAdapterDataObserver.onChanged();
+
+ onNewIntent(getIntent());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ int p = ContextCompat.checkSelfPermission(this, Manifest.permission.USE_FINGERPRINT);
+ if (p != PackageManager.PERMISSION_GRANTED)
+ requestPermissions(new String[] { Manifest.permission.USE_FINGERPRINT }, 0);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ mMenu = menu;
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_about:
+ try {
+ Resources r = getResources();
+ PackageManager pm = getPackageManager();
+ PackageInfo info = pm.getPackageInfo(getPackageName(), 0);
+
+ new AlertDialog.Builder(this)
+ .setTitle(r.getString(R.string.main_about_title,
+ info.versionName, info.versionCode))
+ .setMessage(Html.fromHtml(r.getString(R.string.main_about_message)))
+ .setPositiveButton(R.string.close, null)
+ .show();
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ return true;
+
+ case R.id.action_down:
+ if (mTokenAdapter.isSelected(mTokenAdapter.getItemCount() - 1))
+ return true;
+
+ for (Integer i : new TreeSet<>(mTokenAdapter.getSelected().descendingSet()))
+ mTokenAdapter.move(i, i + 1);
+
+ mRecyclerView.scrollToPosition(mTokenAdapter.getSelected().first());
+ return true;
+
+ case R.id.action_up:
+ if (mTokenAdapter.isSelected(0))
+ return true;
+
+ for (Integer i : new TreeSet<>(mTokenAdapter.getSelected()))
+ mTokenAdapter.move(i, i - 1);
+
+ mRecyclerView.scrollToPosition(mTokenAdapter.getSelected().first());
+ return true;
+
+ case R.id.action_delete:
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.main_deletion_title)
+ .setMessage(R.string.main_deletion_message)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ for (Integer i : new TreeSet<>(mTokenAdapter.getSelected().descendingSet())) {
+ try { mTokenAdapter.delete(i); }
+ catch (GeneralSecurityException | IOException e) { }
+ }
+
+ mFloatingActionButton.show();
+ }
+ }).show();
+
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onSelectEvent(NavigableSet selected) {
+ if (mMenu == null)
+ return;
+
+ for (int i = 0; i < mMenu.size(); i++) {
+ MenuItem mi = mMenu.getItem(i);
+
+ switch (mi.getItemId()) {
+ case R.id.action_about:
+ mi.setVisible(selected.size() == 0);
+ break;
+
+ case R.id.action_up:
+ mi.setVisible(selected.size() > 0);
+ mi.setEnabled(!mTokenAdapter.isSelected(0));
+ break;
+
+ case R.id.action_down:
+ mi.setVisible(selected.size() > 0);
+ mi.setEnabled(!mTokenAdapter.isSelected(mTokenAdapter.getItemCount() - 1));
+ break;
+
+ case R.id.action_delete:
+ mi.setVisible(selected.size() > 0);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ switch (mLongClickCount++) {
+ case 0:
+ /* You have 15 seconds from the first click to enter random mode. */
+ mFloatingActionButton.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (mLongClickCount < 3)
+ mLongClickCount = 0;
+ }
+ }, 15000);
+ break;
+
+ case 2:
+ /* Random mode lasts for 15 seconds... */
+ mFloatingActionButton.setImageResource(R.drawable.ic_add);
+ mFloatingActionButton.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mFloatingActionButton.setImageResource(R.drawable.ic_scan);
+ mLongClickCount = 0;
+ }
+ }, 15000);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mLongClickCount >= 3) {
+ Pair pair = Token.random();
+ addToken(pair.second.toUri(pair.first), true);
+ } else {
+ ScanDialogFragment scan = new ScanDialogFragment();
+ scan.show(getSupportFragmentManager(), scan.getTag());
+ }
+ }
+
+ void addToken(final Uri uri, boolean enableSecurity) {
+ try {
+ Pair pair = enableSecurity ? Token.parse(uri) : Token.parseUnsafe(uri);
+ mRecyclerView.scrollToPosition(mTokenAdapter.add(pair.first, pair.second));
+ } catch (Token.UnsafeUriException e) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.main_unsafe_title)
+ .setMessage(R.string.main_unsafe_message)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.add_anyway, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ addToken(uri, false);
+ }
+ }).show();
+ } catch (Token.InvalidUriException e) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.main_invalid_title)
+ .setMessage(R.string.main_invalid_message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ } catch (GeneralSecurityException | IOException e) {
+ if (!e.getClass().equals(KeyStoreException.class) ||
+ !e.getCause().getClass().equals(IllegalStateException.class)) {
+ e.printStackTrace();
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.main_error_title)
+ .setMessage(R.string.main_error_message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ return;
+ }
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.main_lock_title)
+ .setMessage(R.string.main_lock_message)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.enable_lock_screen, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
+ startActivity(intent);
+ }
+ })
+ .show();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ Uri uri = intent.getData();
+ if (uri != null)
+ addToken(uri, true);
+ }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/Adapter.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/Adapter.java
new file mode 100644
index 00000000..a170ac69
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/Adapter.java
@@ -0,0 +1,253 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+import android.security.keystore.KeyProtection;
+import android.security.keystore.UserNotAuthenticatedException;
+import android.support.annotation.NonNull;
+import android.util.LongSparseArray;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import org.fedorahosted.freeotp.Code;
+import org.fedorahosted.freeotp.R;
+import org.fedorahosted.freeotp.Token;
+import org.fedorahosted.freeotp.utils.SelectableAdapter;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.crypto.SecretKey;
+
+public class Adapter extends SelectableAdapter implements ViewHolder.EventListener {
+ private static final String COMPAT = "tokens";
+ private static final String ORDER = "tokenOrder";
+ private static final String NAME = "tokenStore";
+ private static final Gson GSON = new Gson();
+
+ private final LongSparseArray mActive = new LongSparseArray<>();
+ private final Handler mHandler = new Handler();
+
+ private final SharedPreferences mSharedPreferences;
+ private final List mItems;
+ private final KeyStore mKeyStore;
+
+ private SharedPreferences.Editor storeItems() {
+ return mSharedPreferences.edit().putString(ORDER, new Gson().toJson(mItems));
+ }
+
+ private void compat(Context context) {
+ SharedPreferences sp = context.getSharedPreferences(COMPAT, Context.MODE_PRIVATE);
+ Type type = new TypeToken>(){}.getType();
+ List items = GSON.fromJson(sp.getString(ORDER, "[]"), type);
+ int size = items.size();
+
+ for (int i = size; i > 0; i--) {
+ String key = items.remove(i - 1);
+ String val = sp.getString(key, null);
+ if (val == null) {
+ if (!sp.edit().putString(ORDER, GSON.toJson(items)).commit())
+ items.add(i - 1, key);
+ continue;
+ }
+
+ try {
+ Pair pair = Token.compat(val);
+ add(pair.first, pair.second, false);
+ } catch (GeneralSecurityException | IOException | Token.InvalidUriException e) {
+ items.add(i - 1, key);
+ e.printStackTrace();
+ continue;
+ }
+
+ if (!sp.edit().putString(ORDER, GSON.toJson(items)).remove(key).commit()) {
+ items.add(i - 1, key);
+ try { delete(0); }
+ catch (GeneralSecurityException | IOException e) { e.printStackTrace(); }
+ }
+ }
+
+ if (size > 0 && items.size() == 0)
+ sp.edit().remove(ORDER).apply();
+ }
+
+ public Adapter(Context context, EventListener listener) throws GeneralSecurityException, IOException {
+ super(listener);
+ setHasStableIds(true);
+
+ mSharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE);
+ mKeyStore = KeyStore.getInstance("AndroidKeyStore");
+ mKeyStore.load(null);
+
+ Type type = new TypeToken>(){}.getType();
+ String str = mSharedPreferences.getString(ORDER, "[]");
+ mItems = GSON.fromJson(str, type);
+
+ compat(context);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return UUID.fromString(mItems.get(position)).getMostSignificantBits();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ LayoutInflater li = LayoutInflater.from(parent.getContext());
+ View v = li.inflate(R.layout.token, parent, false);
+ return new ViewHolder(v, this);
+ }
+
+ @Override
+ public void onViewRecycled(@NonNull ViewHolder holder) {
+ super.onViewRecycled(holder);
+ holder.reset();
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
+ String uuid = mItems.get(position);
+ Token token = Token.deserialize(mSharedPreferences.getString(uuid, null));
+ holder.bind(token, mActive.get(getItemId(position)), isSelected(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ private int add(SecretKey key, Token token, boolean lock) throws GeneralSecurityException, IOException {
+ String uuid = UUID.randomUUID().toString();
+
+ // Save key.
+ mKeyStore.setEntry(uuid, new KeyStore.SecretKeyEntry(key),
+ new KeyProtection.Builder(KeyProperties.PURPOSE_SIGN)
+ .setUserAuthenticationValidityDurationSeconds(token.getPeriod())
+ .setUserAuthenticationRequired(token.getLock() && lock)
+ .build());
+
+ // Save everything else.
+ mItems.add(uuid);
+ if (!storeItems().putString(uuid, token.serialize()).commit()) {
+ mItems.remove(mItems.size() - 1);
+ mKeyStore.deleteEntry(uuid);
+ throw new IOException();
+ }
+
+ this.notifyItemInserted(mItems.size() - 1);
+ return mItems.size() - 1;
+ }
+
+ public int add(SecretKey key, Token token) throws GeneralSecurityException, IOException {
+ return add(key, token, true);
+ }
+
+ public void delete(int position) throws GeneralSecurityException, IOException {
+ String uuid = mItems.remove(position);
+ if (!storeItems().remove(uuid).commit()) {
+ mItems.add(position, uuid);
+ throw new IOException();
+ }
+
+ notifyItemRemoved(position);
+ mKeyStore.deleteEntry(uuid);
+ }
+
+ public void move(int fromPosition, int toPosition) {
+ String uuid = mItems.remove(fromPosition);
+ mItems.add(toPosition, uuid);
+ if (storeItems().commit()) {
+ notifyItemMoved(fromPosition, toPosition);
+ return;
+ }
+
+ uuid = mItems.remove(toPosition);
+ mItems.add(fromPosition, uuid);
+ }
+
+ @Override
+ public boolean onSelectionToggled(ViewHolder holder) {
+ boolean selected = !isSelected(holder.getAdapterPosition());
+ setSelected(holder.getAdapterPosition(), selected);
+ return selected;
+ }
+
+ public Code getCode(int position)
+ throws UserNotAuthenticatedException, KeyPermanentlyInvalidatedException {
+ String uuid = mItems.get(position);
+ Code code;
+
+ try {
+ Token token = Token.deserialize(mSharedPreferences.getString(uuid, null));
+ Key key = mKeyStore.getKey(uuid, null);
+ code = token.getCode(key);
+ mSharedPreferences.edit().putString(uuid, token.serialize()).apply();
+ } catch (UserNotAuthenticatedException | KeyPermanentlyInvalidatedException e) {
+ throw e;
+ } catch (GeneralSecurityException e) {
+ e.printStackTrace();
+ return new Code("ERROR", 15);
+ }
+
+ final Long id = getItemId(position);
+ mActive.put(id, code);
+
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Code code = mActive.get(id);
+ if (code == null)
+ return;
+ if (!code.isValid())
+ mActive.remove(id);
+ else
+ mHandler.postDelayed(this, code.timeLeft());
+ }
+ }, code.timeLeft());
+
+ return code;
+ }
+
+ @Override
+ public void onActivated(ViewHolder holder) {
+ }
+
+ @Override
+ public void onShare(String code) {
+ }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/ScanDialogFragment.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/ScanDialogFragment.java
new file mode 100644
index 00000000..5167481a
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/ScanDialogFragment.java
@@ -0,0 +1,240 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ * Authors: Siemens AG
+ *
+ * Copyright (C) 2013-2018 Nathaniel McCallum, Red Hat
+ * Copyright (C) 2017 Max Wittig, Siemens AG
+ *
+ * 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.main;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatDialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.ChecksumException;
+import com.google.zxing.FormatException;
+import com.google.zxing.LuminanceSource;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.common.HybridBinarizer;
+import com.google.zxing.qrcode.QRCodeReader;
+import com.google.zxing.qrcode.QRCodeWriter;
+
+import org.fedorahosted.freeotp.R;
+
+import io.fotoapparat.Fotoapparat;
+import io.fotoapparat.error.CameraErrorListener;
+import io.fotoapparat.exception.camera.CameraException;
+import io.fotoapparat.parameter.Resolution;
+import io.fotoapparat.parameter.ScaleType;
+import io.fotoapparat.preview.Frame;
+import io.fotoapparat.preview.FrameProcessor;
+import io.fotoapparat.view.CameraView;
+
+import static io.fotoapparat.selector.FocusModeSelectorsKt.autoFocus;
+import static io.fotoapparat.selector.FocusModeSelectorsKt.continuousFocusPicture;
+import static io.fotoapparat.selector.FocusModeSelectorsKt.fixed;
+import static io.fotoapparat.selector.LensPositionSelectorsKt.back;
+import static io.fotoapparat.selector.LensPositionSelectorsKt.external;
+import static io.fotoapparat.selector.LensPositionSelectorsKt.front;
+import static io.fotoapparat.selector.SelectorsKt.firstAvailable;
+
+public class ScanDialogFragment extends AppCompatDialogFragment
+ implements FrameProcessor, CameraErrorListener {
+ private Fotoapparat mFotoapparat;
+ private ProgressBar mProgress;
+ private CameraView mCamera;
+ private ImageView mImage;
+ private TextView mError;
+
+ public static boolean hasCamera(Context context) {
+ PackageManager pm = context.getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View v = View.inflate(getContext(), R.layout.fragment_scan, null);
+
+ mProgress = v.findViewById(R.id.progress);
+ mCamera = v.findViewById(R.id.camera);
+ mImage = v.findViewById(R.id.image);
+ mError = v.findViewById(R.id.error);
+
+ mFotoapparat = Fotoapparat.with(getContext())
+ .focusMode(firstAvailable(continuousFocusPicture(), autoFocus(), fixed()))
+ .lensPosition(firstAvailable(back(), external(), front()))
+ .previewScaleType(ScaleType.CenterCrop)
+ .cameraErrorCallback(this)
+ .frameProcessor(this)
+ .into(mCamera)
+ .build();
+
+ return v;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ switch (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA)) {
+ case PackageManager.PERMISSION_GRANTED:
+ onRequestPermissionsResult(0,
+ new String[] { Manifest.permission.CAMERA },
+ new int[] { PackageManager.PERMISSION_GRANTED });
+ break;
+
+ default:
+ requestPermissions(new String[] { Manifest.permission.CAMERA }, 0);
+ break;
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ for (int i = 0; i < permissions.length; i++) {
+ if (!permissions[i].equals(Manifest.permission.CAMERA))
+ continue;
+
+ switch (grantResults[i]) {
+ case PackageManager.PERMISSION_GRANTED:
+ mFotoapparat.start();
+ mCamera.animate()
+ .setInterpolator(new AccelerateInterpolator())
+ .setDuration(2000)
+ .alpha(1.0f)
+ .start();
+ break;
+
+ default:
+ dismiss();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mFotoapparat.stop();
+ }
+
+ @Override
+ public void process(Frame frame) {
+ byte[] i = frame.getImage();
+ Resolution r = frame.getSize();
+
+ LuminanceSource ls = new PlanarYUVLuminanceSource(
+ i, r.width, r.height, 0, 0, r.width, r.height, false);
+
+ try {
+ BinaryBitmap bb = new BinaryBitmap(new HybridBinarizer(ls));
+ final String uri = new QRCodeReader().decode(bb).getText();
+
+ int size = mImage.getWidth();
+ if (size > mImage.getHeight())
+ size = mImage.getHeight();
+
+ BitMatrix bm = new QRCodeWriter().encode(uri, BarcodeFormat.QR_CODE, size, size);
+ mFotoapparat.stop();
+
+ final Bitmap b = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ for (int x = 0; x < size; x++) {
+ for (int y = 0; y < size; y++) {
+ b.setPixel(x, y, bm.get(x, y) ? Color.BLACK : Color.WHITE);
+ }
+ }
+
+ Vibrator v = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ v.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE));
+ } else {
+ v.vibrate(250);
+ }
+
+ mImage.post(new Runnable() {
+ @Override
+ public void run() {
+ mProgress.setVisibility(View.INVISIBLE);
+ mCamera.animate()
+ .setInterpolator(new DecelerateInterpolator())
+ .setDuration(2000)
+ .alpha(0.0f)
+ .start();
+
+ mImage.setImageBitmap(b);
+ mImage.animate()
+ .setInterpolator(new DecelerateInterpolator())
+ .setDuration(2000)
+ .alpha(1.0f)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mImage.post(new Runnable() {
+ @Override
+ public void run() {
+ Activity a = (Activity) getActivity();
+ a.addToken(Uri.parse(uri), true);
+ }
+ });
+ dismiss();
+ }
+ })
+ .start();
+ }
+ });
+ } catch (NotFoundException | ChecksumException | FormatException | WriterException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onError(CameraException e) {
+ mProgress.setVisibility(View.INVISIBLE);
+ mCamera.setVisibility(View.INVISIBLE);
+ mImage.setVisibility(View.INVISIBLE);
+ mError.setVisibility(View.VISIBLE);
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/ViewHolder.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/ViewHolder.java
new file mode 100644
index 00000000..35ff2b98
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/ViewHolder.java
@@ -0,0 +1,304 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.squareup.picasso.Picasso;
+
+import org.fedorahosted.freeotp.Code;
+import org.fedorahosted.freeotp.R;
+import org.fedorahosted.freeotp.Token;
+
+import java.util.Locale;
+
+class ViewHolder extends RecyclerView.ViewHolder {
+ interface EventListener {
+ boolean onSelectionToggled(ViewHolder holder);
+ void onActivated(ViewHolder holder);
+ void onShare(String code);
+ }
+
+ private EventListener mEventListener;
+ private ObjectAnimator mCountdown;
+
+ private ProgressBar mProgress;
+ private ImageButton mShare;
+ private ViewGroup mPassive;
+ private ViewGroup mActive;
+ private ViewGroup mIcons;
+ private ImageView mCheck;
+ private ImageView mImage;
+ private ImageView mLock;
+ private TextView mIssuer;
+ private TextView mLabel;
+ private TextView mCode;
+ private View mView;
+
+ private final View.OnClickListener mViewClick = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = getAdapterPosition();
+ mEventListener.onActivated(ViewHolder.this);
+ }
+ };
+
+ private final View.OnClickListener mShareClick = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String code = mCode.getText().toString();
+ mEventListener.onShare(code.replaceAll("\\s+", ""));
+ }
+ };
+
+ private final View.OnClickListener mSelectClick = new View.OnClickListener() {
+ @Override
+ public void onClick(final View v) {
+ v.animate()
+ .setInterpolator(new AccelerateInterpolator())
+ .setDuration(100)
+ .rotationY(90)
+ .withLayer()
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ int pos = getAdapterPosition();
+ setSelected(mEventListener.onSelectionToggled(ViewHolder.this));
+ v.setRotationY(-90);
+ v.animate()
+ .setInterpolator(new DecelerateInterpolator())
+ .setDuration(100)
+ .rotationY(0)
+ .withLayer()
+ .start();
+ }
+ }).start();
+ }
+ };
+
+ private static void fade(final View view, final boolean in, int duration) {
+ view.setVisibility(View.VISIBLE);
+ view.animate()
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .setDuration(duration)
+ .alpha(in ? 1f : 0f)
+ .withLayer()
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (in)
+ view.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!in)
+ view.setVisibility(View.GONE);
+ }
+ })
+ .start();
+ }
+
+ private void displayCode(Code code, int animationDuration) {
+ if (code == null)
+ return;
+
+ String text = code.getCode();
+ for (int segment : new int[] { 7, 5, 4, 3 }) {
+ if (text.length() % segment != 0)
+ continue;
+
+ int count = text.length() / segment;
+ if (count < 2)
+ break;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(" ");
+ for (int i = 0; i < text.length(); i += segment) {
+ if (i % 13 > (i + segment) % 13)
+ sb.append("\n ");
+
+ sb.append(text.substring(i, i + segment));
+ sb.append(" ");
+ }
+
+ text = sb.toString();
+ break;
+ }
+
+ mView.setEnabled(false);
+ fade(mPassive, false, animationDuration);
+ fade(mActive, true, animationDuration);
+
+ mCode.setText(text);
+ mCountdown.setDuration(code.timeLeft());
+ mCountdown.setIntValues(code.getProgress(mProgress.getMax()), 0);
+ mCountdown.start();
+ }
+
+ ViewHolder(View itemView, EventListener listener) {
+ super(itemView);
+
+ mEventListener = listener;
+ mCountdown = new ObjectAnimator();
+ mProgress = itemView.findViewById(R.id.progress);
+ mPassive = itemView.findViewById(R.id.passive);
+ mActive = itemView.findViewById(R.id.active);
+ mIssuer = itemView.findViewById(R.id.issuer);
+ mLabel = itemView.findViewById(R.id.label);
+ mIcons = itemView.findViewById(R.id.icons);
+ mCheck = itemView.findViewById(R.id.check);
+ mImage = itemView.findViewById(R.id.image);
+ mLock = itemView.findViewById(R.id.lock);
+ mShare = itemView.findViewById(R.id.share);
+ mCode = itemView.findViewById(R.id.code);
+ mView = itemView;
+
+ mCountdown.setInterpolator(new LinearInterpolator());
+ mCountdown.setPropertyName("progress");
+ mCountdown.setTarget(mProgress);
+ mCountdown.setAutoCancel(true);
+ mCountdown.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mPassive.clearAnimation();
+ mActive.clearAnimation();
+ mView.setEnabled(true);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ fade(mPassive, true, 500);
+ fade(mActive, false, 500);
+ mView.setEnabled(true);
+ }
+ });
+
+ mIcons.setOnClickListener(mSelectClick);
+ mShare.setOnClickListener(mShareClick);
+ mView.setOnClickListener(mViewClick);
+ }
+
+ private int getIdentifier(String type, String prefix, Token token) {
+ String issuer = token.getIssuer();
+ if (issuer == null)
+ return 0;
+
+ String name = prefix + issuer.toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", "_");
+ String pkg = mView.getContext().getPackageName();
+
+ return mView.getResources().getIdentifier(name, type, pkg);
+ }
+
+ private void setBackgroundColor(Token token) {
+ try {
+ // If the token specified a color, use it.
+ mImage.setBackgroundColor(Color.parseColor("#" + token.getColor()));
+ } catch (NumberFormatException e) {
+ Resources r = mView.getResources();
+ try {
+ // If the token didn't specify a color but we know the Issuer's color, use it.
+ mImage.setBackgroundColor(r.getColor(getIdentifier("color", "brand_", token)));
+ } catch (Resources.NotFoundException f) {
+ // Otherwise, pick a color that will be constant for the same Issuer string.
+ int[] backgrounds = r.getIntArray(R.array.backgrounds);
+ int idx = 0;
+
+ try { idx = Math.abs(token.getIssuer().hashCode()); }
+ catch (NullPointerException g) { }
+
+ mImage.setBackgroundColor(backgrounds[idx % backgrounds.length]);
+ }
+ }
+ }
+
+ private void setImage(Token token) {
+ int id = getIdentifier("drawable", "fa_", token);
+ if (id == 0) {
+ switch (token.getType()) {
+ case HOTP: id = R.drawable.ic_hotp; break;
+ case TOTP: id = R.drawable.ic_totp; break;
+ }
+ }
+
+ String url = token.getImage();
+ if (url == null) {
+ mImage.setImageResource(id);
+ } else {
+ Picasso.get().load(url).error(id).into(mImage);
+ }
+ }
+
+ private void setSelected(boolean selected) {
+ mImage.setVisibility(selected ? View.INVISIBLE : View.VISIBLE);
+ mCheck.setVisibility(selected ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ void reset() {
+ mCountdown.cancel();
+ }
+
+ void bind(Token token, Code code, boolean selected) {
+ reset();
+
+ String issuer = token.getIssuer();
+ if (issuer == null)
+ issuer = mView.getResources().getString(R.string.unknown_issuer);
+
+ mLock.setVisibility(token.getLock() ? View.VISIBLE : View.GONE);
+ mIssuer.setText(issuer);
+ mLabel.setText(token.getLabel());
+ setBackgroundColor(token);
+ setImage(token);
+
+ setSelected(selected);
+ if (code != null)
+ displayCode(code, 0);
+ }
+
+ void displayCode(Code code) {
+ displayCode(code, 500);
+ }
+
+ CharSequence getIssuer() {
+ return mIssuer.getText();
+ }
+
+ CharSequence getLabel() {
+ return mLabel.getText();
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Adapter.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Adapter.java
new file mode 100644
index 00000000..1e41dec0
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Adapter.java
@@ -0,0 +1,165 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main.share;
+
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.fedorahosted.freeotp.R;
+
+class Adapter extends SortableItemAdapter {
+ static class Item extends SortableItem- {
+ public interface OnClickListener {
+ void onClick(Item item);
+ }
+
+ private OnClickListener mOnClickListener;
+ private boolean mEnabled = true;
+ private int mPriority = 0;
+ private String mSubtitle;
+ private String mTitle;
+ private int mImage;
+
+ synchronized void setOnClickListener(OnClickListener onClickListener) {
+ mOnClickListener = onClickListener;
+ notifyOnChangeListeners();
+ }
+
+ synchronized String getSubtitle() {
+ return mSubtitle;
+ }
+
+ synchronized void setSubtitle(String subtitle) {
+ mSubtitle = subtitle;
+ notifyOnChangeListeners();
+ }
+
+ synchronized String getTitle() {
+ return mTitle;
+ }
+
+ synchronized void setTitle(String title) {
+ mTitle = title;
+ notifyOnChangeListeners();
+ }
+
+ synchronized boolean getEnabled() {
+ return mEnabled;
+ }
+
+ synchronized void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ notifyOnChangeListeners();
+ }
+
+ synchronized int getPriority() {
+ return mPriority;
+ }
+
+ synchronized void setPriority(int priority) {
+ mPriority = priority;
+ notifyOnChangeListeners();
+ }
+
+ synchronized int getImage() {
+ return mImage;
+ }
+
+ synchronized void setImage(int image) {
+ mImage = image;
+ notifyOnChangeListeners();
+ }
+
+ @Override
+ public int compareTo(@NonNull Item item) {
+ int type = getPriority() - item.getPriority();
+ if (type != 0)
+ return type;
+
+ if (getTitle() == null || item.getTitle() == null)
+ return 0;
+
+ int title = getTitle().compareTo(item.getTitle());
+ if (title != 0)
+ return type;
+
+ if (getSubtitle() == null || item.getSubtitle() == null)
+ return 0;
+
+ return getSubtitle().compareTo(item.getSubtitle());
+ }
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ ProgressBar mProgress;
+ TextView mSubtitle;
+ ImageView mImage;
+ TextView mTitle;
+ View mRow;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ mRow = itemView;
+ mImage = itemView.findViewById(R.id.image);
+ mTitle = itemView.findViewById(R.id.title);
+ mSubtitle = itemView.findViewById(R.id.subtitle);
+ mProgress = itemView.findViewById(R.id.progress);
+ }
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater li = LayoutInflater.from(parent.getContext());
+ return new ViewHolder(li.inflate(R.layout.target, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ final Item item = get(position);
+
+ holder.mProgress.setVisibility(item.mOnClickListener == null ? View.VISIBLE : View.GONE);
+ holder.mImage.setVisibility(item.mOnClickListener != null ? View.VISIBLE : View.GONE);
+ holder.mSubtitle.setText(item.getSubtitle());
+ holder.mTitle.setText(item.getTitle());
+
+ holder.mRow.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ item.mOnClickListener.onClick(item);
+ }
+ });
+
+ if (item.mOnClickListener != null)
+ holder.mImage.setImageResource(item.getImage());
+
+ holder.mProgress.setEnabled(item.mOnClickListener != null && item.getEnabled());
+ holder.mSubtitle.setEnabled(item.mOnClickListener != null && item.getEnabled());
+ holder.mImage.setEnabled(item.mOnClickListener != null && item.getEnabled());
+ holder.mTitle.setEnabled(item.mOnClickListener != null && item.getEnabled());
+ holder.mRow.setEnabled(item.mOnClickListener != null && item.getEnabled());
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Clipboard.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Clipboard.java
new file mode 100644
index 00000000..e02f8ca8
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Clipboard.java
@@ -0,0 +1,53 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main.share;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.fedorahosted.freeotp.R;
+
+class Clipboard extends Discoverable {
+ private ClipboardManager mClipboardManager;
+
+ Clipboard(@NonNull Context context, @NonNull DiscoveryCallback discoveryCallback) {
+ super(context, discoveryCallback);
+
+ mClipboardManager = context.getSystemService(ClipboardManager.class);
+ if (mClipboardManager == null)
+ return;
+
+ Adapter.Item item = new Adapter.Item();
+ item.setImage(R.drawable.ic_copy);
+ item.setTitle(mContext.getResources().getString(R.string.share_clipboard_copy_to));
+ item.setSubtitle(mContext.getResources().getString(R.string.share_clipboard_clipboard));
+
+ appear(item, new Shareable() {
+ @Override
+ public void share(String token, ShareCallback shareCallback) {
+ mClipboardManager.setPrimaryClip(ClipData.newPlainText(null, token));
+ shareCallback.onShareCompleted(true);
+ }
+ });
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Discoverable.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Discoverable.java
new file mode 100644
index 00000000..ba775800
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Discoverable.java
@@ -0,0 +1,113 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main.share;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+
+class Discoverable {
+ interface Shareable {
+ interface ShareCallback {
+ void onShareCompleted(boolean success);
+ }
+
+ void share(String token, ShareCallback shareCallback);
+ }
+
+ interface DiscoveryCallback {
+ void onShareAppeared(Discoverable discoverable, Adapter.Item item, Shareable shareable);
+ void onShareDisappeared(Discoverable discoverable, Adapter.Item item);
+ }
+
+ private Handler mHandler = new Handler(Looper.getMainLooper());
+ private DiscoveryCallback mDiscoveryCallback;
+ Context mContext;
+
+ Discoverable(@NonNull Context context, @NonNull DiscoveryCallback discoveryCallback) {
+ mDiscoveryCallback = discoveryCallback;
+ mContext = context;
+ }
+
+ void post(Runnable runnable) {
+ mHandler.post(runnable);
+ }
+
+ void post(Runnable runnable, long delayMillis) {
+ mHandler.postDelayed(runnable, delayMillis);
+ }
+
+ void appear(final Adapter.Item item, final Shareable shareable) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mDiscoveryCallback.onShareAppeared(Discoverable.this, item, shareable);
+ }
+ });
+ }
+
+ void disappear(final Adapter.Item item) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mDiscoveryCallback.onShareDisappeared(Discoverable.this, item);
+ }
+ });
+ }
+
+ /* Determines whether or not this device supports this type of share. */
+ boolean supported() {
+ return true;
+ }
+
+ /* The permissions required to use this type of share. */
+ String[] permissions() {
+ return new String[0];
+ }
+
+ boolean permitted() {
+ if (!supported())
+ return false;
+
+ for (String p : permissions()) {
+ if (mContext.checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED)
+ return false;
+ }
+
+ return true;
+ }
+
+ /* The intent to use with startActivityForResult() if enabled is required. */
+ Intent enablement() {
+ return null;
+ }
+
+ /* Start discovery of sharables. */
+ void startDiscovery() {}
+
+ /* Stop discovery of sharables. */
+ void stopDiscovery() {}
+
+ boolean isDiscovering() { return true; }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Jelling.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Jelling.java
new file mode 100644
index 00000000..d5643bb5
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/Jelling.java
@@ -0,0 +1,349 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main.share;
+
+import android.Manifest;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.ParcelUuid;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import org.fedorahosted.freeotp.R;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+class Jelling extends Discoverable {
+ private class GattCallback extends BluetoothGattCallback {
+ Shareable.ShareCallback mShareCallback;
+ boolean mRegistered = false;
+ boolean mSuccess = false;
+ boolean mRestart = false;
+ String mToken;
+
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent i) {
+ BluetoothDevice dev = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ int ns = i.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
+ int os = i.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);
+
+ Log.d("LOG", String.format("Bond: %s (%d => %d)", dev.getAddress(), os, ns));
+
+ if (ns != BluetoothDevice.BOND_BONDED)
+ return;
+
+ if (mBluetoothGatt == null)
+ return;
+
+ if (!dev.equals(mBluetoothGatt.getDevice()))
+ return;
+
+ mRestart = true;
+ mBluetoothGatt.disconnect();
+ }
+ };
+
+ GattCallback(String token, Shareable.ShareCallback shareCallback) {
+ mShareCallback = shareCallback;
+ mToken = token;
+ }
+
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int state) {
+ switch (state) {
+ case BluetoothGatt.STATE_CONNECTED:
+ if (!mRegistered) {
+ IntentFilter f = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, f);
+ mRegistered = true;
+ }
+ gatt.discoverServices();
+ break;
+
+ case BluetoothGatt.STATE_DISCONNECTED:
+ if (mRestart) {
+ // The remote pairing dialog has stolen focus from the input.
+ // Give time for the dialog to dismiss and refocus.
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mBluetoothGatt.connect();
+ }
+ }, 3000);
+ mRestart = false;
+ return;
+ }
+
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mShareCallback.onShareCompleted(mSuccess);
+ }
+ });
+
+ if (mRegistered) {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ mRegistered = false;
+ }
+
+ mBluetoothGatt = null;
+ gatt.close();
+ break;
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ BluetoothGattService svc = gatt.getService(JELLING_SVC);
+ if (svc == null) {
+ Log.d(getClass().getSimpleName(), "Service not found!");
+ gatt.disconnect();
+ return;
+ }
+
+ final BluetoothGattCharacteristic chr = svc.getCharacteristic(JELLING_CHR);
+ if (chr == null) {
+ Log.d(getClass().getSimpleName(), "Characteristic not found!");
+ gatt.disconnect();
+ return;
+ }
+
+ gatt.beginReliableWrite();
+ chr.setValue(mToken);
+ if (!gatt.writeCharacteristic(chr)) {
+ Log.d(getClass().getSimpleName(), "Error during write!");
+ gatt.abortReliableWrite();
+ gatt.disconnect();
+ }
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic chr, int status) {
+ switch (status) {
+ case BluetoothGatt.GATT_SUCCESS:
+ if (gatt.executeReliableWrite())
+ return;
+
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
+ case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION:
+ default:
+ Log.d(getClass().getSimpleName(), String.format("Chr. Write failed: %d", status));
+ gatt.abortReliableWrite();
+ gatt.disconnect();
+ break;
+ }
+ }
+
+ @Override
+ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+ mSuccess = status == BluetoothGatt.GATT_SUCCESS;
+ gatt.disconnect();
+ }
+ }
+
+ private static final UUID JELLING_SVC = UUID.fromString("B670003C-0079-465C-9BA7-6C0539CCD67F");
+ private static final UUID JELLING_CHR = UUID.fromString("F4186B06-D796-4327-AF39-AC22C50BDCA8");
+
+ private static final List FILTERS = Collections.singletonList(
+ new ScanFilter.Builder().setServiceUuid(
+ new ParcelUuid(JELLING_SVC),
+ new ParcelUuid(UUID.fromString("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"))
+ ).build()
+ );
+
+ private static final ScanSettings SCAN_SETTINGS = new ScanSettings.Builder()
+ .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
+ .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+ .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build();
+
+ private final ScanCallback mScanCallback = new ScanCallback() {
+ private final Map mDevices = new ConcurrentHashMap<>();
+ private final long TIMEOUT = 10000;
+
+ @Override
+ public void onBatchScanResults(List results) {
+ super.onBatchScanResults(results);
+ for (ScanResult result : results)
+ onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result);
+ }
+
+ @Override
+ public void onScanResult(int callbackType, final ScanResult result) {
+ final BluetoothDevice dev = result.getDevice();
+
+ if (!mDevices.containsKey(dev)) {
+ Adapter.Item item = new Adapter.Item();
+ item.setTitle(mContext.getResources().getString(R.string.share_jelling_send_to));
+ item.setSubtitle(dev.getName());
+ item.setImage(R.drawable.ic_bluetooth);
+
+ switch (dev.getBondState()) {
+ case BluetoothDevice.BOND_BONDED:
+ case BluetoothDevice.BOND_BONDING:
+ item.setPriority(100);
+ break;
+ default:
+ item.setPriority(101);
+ }
+
+ mDeviceItemMap.put(dev, item);
+ appear(item, new Shareable() {
+ @Override
+ public void share(String token, ShareCallback shareCallback) {
+ GattCallback gc = new GattCallback(token, shareCallback);
+ mBluetoothGatt = dev.connectGatt(mContext, false, gc);
+ }
+ });
+ }
+
+ mDevices.put(dev, System.currentTimeMillis());
+
+ post(new Runnable() {
+ @Override
+ public void run() {
+ for (BluetoothDevice d : mDevices.keySet()) {
+ if (mDevices.get(d) < System.currentTimeMillis() - TIMEOUT) {
+ disappear(mDeviceItemMap.get(d));
+ mDevices.remove(d);
+ }
+ }
+ }
+ }, TIMEOUT);
+ }
+ };
+
+ private Map mDeviceItemMap = new ConcurrentHashMap<>();
+ private Adapter.Item mBluetoothItem = new Adapter.Item();
+ private BluetoothGatt mBluetoothGatt;
+ private boolean mScanning = false;
+
+ Jelling(@NonNull Context context, @NonNull DiscoveryCallback discoveryCallback) {
+ super(context, discoveryCallback);
+
+ mBluetoothItem = new Adapter.Item();
+ mBluetoothItem.setSubtitle(mContext.getResources().getString(R.string.share_jelling_bluetooth_devices));
+ mBluetoothItem.setTitle(mContext.getResources().getString(R.string.share_jelling_scan_for));
+ mBluetoothItem.setImage(R.drawable.ic_bluetooth);
+ mBluetoothItem.setPriority(102);
+ if (supported())
+ appear(mBluetoothItem, null);
+ }
+
+ @Override
+ public boolean supported() {
+ PackageManager pm = mContext.getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
+ }
+
+ @Override
+ public String[] permissions() {
+ return new String[] {
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.BLUETOOTH_ADMIN,
+ Manifest.permission.BLUETOOTH,
+ };
+ }
+
+ public Intent enablement() {
+ BluetoothManager bm = mContext.getSystemService(BluetoothManager.class);
+ if (bm != null) {
+ BluetoothAdapter ba = bm.getAdapter();
+ if (ba != null && ba.isEnabled())
+ return null;
+ }
+
+ return new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ }
+
+ @Override
+ public void startDiscovery() {
+ if (mScanning)
+ return;
+
+ BluetoothManager bm = mContext.getSystemService(BluetoothManager.class);
+ if (bm == null)
+ return;
+
+ BluetoothAdapter ba = bm.getAdapter();
+ if (ba == null)
+ return;
+
+ ba.getBluetoothLeScanner().startScan(FILTERS, SCAN_SETTINGS, mScanCallback);
+ mScanning = true;
+
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mBluetoothItem.setTitle(mContext.getResources().getString(R.string.share_jelling_scanning_for));
+ mBluetoothItem.setOnClickListener(null);
+ }
+ });
+ }
+
+ public void stopDiscovery() {
+ if (!mScanning)
+ return;
+
+ BluetoothManager bm = mContext.getSystemService(BluetoothManager.class);
+ if (bm == null)
+ return;
+
+ BluetoothAdapter ba = bm.getAdapter();
+ if (ba == null)
+ return;
+
+ ba.getBluetoothLeScanner().stopScan(mScanCallback);
+ mScanning = false;
+
+ if (mBluetoothGatt != null)
+ mBluetoothGatt.disconnect();
+
+ mBluetoothItem.setTitle(mContext.getResources().getString(R.string.share_jelling_scan_for));
+ disappear(mBluetoothItem);
+ appear(mBluetoothItem, null);
+ }
+
+ @Override
+ boolean isDiscovering() {
+ return mScanning;
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/share/ShareFragment.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/ShareFragment.java
new file mode 100644
index 00000000..0bfbb443
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/ShareFragment.java
@@ -0,0 +1,146 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main.share;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomSheetDialogFragment;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.fedorahosted.freeotp.R;
+
+public class ShareFragment extends BottomSheetDialogFragment implements Discoverable.DiscoveryCallback {
+ public static final String CODE_ID = "CODE";
+
+ private final Adapter mShareTokenAdapter = new Adapter();
+ private Discoverable[] mDiscoverables;
+ private String mCode;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ mCode = getArguments().getString(CODE_ID);
+ mDiscoverables = new Discoverable[] {
+ new Clipboard(getContext(), this),
+ new Jelling(getContext(), this),
+ };
+
+ View v = View.inflate(getContext(), R.layout.fragment_share, null);
+
+ RecyclerView rv = v.findViewById(R.id.targets);
+ rv.setAdapter(mShareTokenAdapter);
+ rv.setHasFixedSize(false);
+
+ return v;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ for (Discoverable d : mDiscoverables) {
+ if (d.permissions().length == 0)
+ onRequestPermissionsResult(0, d.permissions(), new int[0]);
+ else if (d.permitted())
+ requestPermissions(d.permissions(), 0);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
+ @NonNull int[] results) {
+ for (Discoverable d : mDiscoverables) {
+ if (!d.permitted() || d.isDiscovering())
+ continue;
+
+ Intent intent = d.enablement();
+ if (intent != null)
+ startActivityForResult(intent, 0);
+ else
+ onActivityResult(0, 0, null);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ for (Discoverable d : mDiscoverables) {
+ if (!d.permitted() || d.isDiscovering())
+ continue;
+
+ Intent intent = d.enablement();
+ if (intent == null)
+ d.startDiscovery();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ for (Discoverable d : mDiscoverables) {
+ if (d.isDiscovering())
+ d.stopDiscovery();
+ }
+ }
+
+ @Override
+ public void onShareAppeared(final Discoverable discoverable, final Adapter.Item item,
+ final Discoverable.Shareable shareable) {
+ if (shareable == null) {
+ item.setOnClickListener(new Adapter.Item.OnClickListener() {
+ @Override
+ public void onClick(Adapter.Item item) {
+ requestPermissions(discoverable.permissions(), 0);
+ }
+ });
+ } else {
+ item.setOnClickListener(new Adapter.Item.OnClickListener() {
+ @Override
+ public void onClick(Adapter.Item item) {
+ item.setOnClickListener(null);
+ for (int i = 0; i < mShareTokenAdapter.getItemCount(); i++)
+ mShareTokenAdapter.get(i).setEnabled(false);
+
+ shareable.share(mCode, new Discoverable.Shareable.ShareCallback() {
+ @Override
+ public void onShareCompleted(boolean success) {
+ dismiss();
+ }
+ });
+ }
+ });
+ }
+
+ mShareTokenAdapter.add(item);
+ }
+
+ @Override
+ public void onShareDisappeared(Discoverable discoverable, final Adapter.Item item) {
+ mShareTokenAdapter.remove(item);
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/share/SortableItem.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/SortableItem.java
new file mode 100644
index 00000000..5b543a3f
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/SortableItem.java
@@ -0,0 +1,56 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main.share;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public abstract class SortableItem> implements Comparable {
+ interface OnChangeListener> {
+ void onChange(SortableItem item);
+ }
+
+ private Set> mOnChangeListeners =
+ Collections.synchronizedSet(new HashSet>());
+ private Handler mHandler = new Handler(Looper.getMainLooper());
+
+ public void removeOnChangeListener(OnChangeListener onChangeListener) {
+ mOnChangeListeners.remove(onChangeListener);
+ }
+
+ public void addOnChangeListener(OnChangeListener onChangeListener) {
+ mOnChangeListeners.add(onChangeListener);
+ }
+
+ public void notifyOnChangeListeners() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (OnChangeListener onChangeListener : mOnChangeListeners)
+ onChangeListener.onChange(SortableItem.this);
+ }
+ });
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/main/share/SortableItemAdapter.java b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/SortableItemAdapter.java
new file mode 100644
index 00000000..d2e80023
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/main/share/SortableItemAdapter.java
@@ -0,0 +1,69 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.main.share;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public abstract class SortableItemAdapter, VH extends RecyclerView.ViewHolder>
+ extends RecyclerView.Adapter implements SortableItem.OnChangeListener {
+ private List mItems = new ArrayList<>();
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ public T get(int pos) {
+ return mItems.get(pos);
+ }
+
+ public void add(T item) {
+ item.addOnChangeListener(this);
+
+ int pos = Collections.binarySearch(mItems, item);
+ if (pos < 0)
+ pos += mItems.size() + 1;
+
+ notifyItemInserted(pos);
+ mItems.add(pos, item);
+ }
+
+ public boolean remove(T item) {
+ int pos = Collections.binarySearch(mItems, item);
+ if (pos < 0)
+ return false;
+
+ item.removeOnChangeListener(this);
+ notifyItemRemoved(pos);
+ mItems.remove(pos);
+ return true;
+ }
+
+ @Override
+ public void onChange(SortableItem item) {
+ Collections.sort(mItems);
+ notifyDataSetChanged();
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/utils/Base32.java b/mobile/src/main/java/org/fedorahosted/freeotp/utils/Base32.java
new file mode 100644
index 00000000..b02c866c
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/utils/Base32.java
@@ -0,0 +1,94 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.utils;
+
+public class Base32 {
+ public static class DecodingException extends Exception {}
+
+ public final static Base32 RFC4648
+ = new Base32("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567");
+
+ private char[] alphabet;
+
+ private Base32(String alphabet) {
+ this.alphabet = alphabet.toCharArray();
+ assert alphabet.length() == 32;
+ }
+
+ public int encodedLength(int declen) {
+ return (declen * 8 + 4) / 5;
+ }
+
+ public int decodedLength(int enclen) {
+ return enclen * 5 / 8;
+ }
+
+ public String encode(byte[] decoded) {
+ StringBuilder sb = new StringBuilder(encodedLength(decoded.length));
+ int carry = 0;
+ int bits = 0;
+
+ for (int i = 0; i < decoded.length; i++) {
+ bits += 8;
+ carry <<= 8;
+ carry |= decoded[i];
+
+ while (bits >= 5) {
+ sb.append(alphabet[carry >> bits - 5 & 0b00011111]);
+ bits -= 5;
+ }
+ }
+
+ if (bits > 0) {
+ sb.append(alphabet[carry << 5 - bits & 0b00011111]);
+ }
+
+ return sb.toString();
+ }
+
+ private int find(char c) throws DecodingException {
+ for (int i = 0; i < alphabet.length; i++)
+ if (c == alphabet[i])
+ return i;
+
+ throw new DecodingException();
+ }
+
+ public byte[] decode(String encoded) throws DecodingException {
+ byte[] decoded = new byte[decodedLength(encoded.length())];
+ int carry = 0;
+ int bits = 0;
+ int i = 0;
+
+ for (char c : encoded.toCharArray()) {
+ bits += 5;
+ carry <<= 5;
+ carry |= find(c);
+
+ while (bits >= 8) {
+ decoded[i++] = (byte) (carry >> bits - 8 & 0xff);
+ bits -= 8;
+ }
+ }
+
+ return decoded;
+ }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/utils/GridLayoutItemDecoration.java b/mobile/src/main/java/org/fedorahosted/freeotp/utils/GridLayoutItemDecoration.java
new file mode 100644
index 00000000..ca8d4ba5
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/utils/GridLayoutItemDecoration.java
@@ -0,0 +1,54 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.utils;
+
+import android.graphics.Rect;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+public class GridLayoutItemDecoration extends RecyclerView.ItemDecoration {
+ int mMargin;
+
+ public GridLayoutItemDecoration(int margin) {
+ mMargin = margin;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+
+ final GridLayoutManager lm = (GridLayoutManager) parent.getLayoutManager();
+ final int position = parent.getChildAdapterPosition(view);
+ final int span = lm.getSpanCount();
+ final int x = position % span;
+ final int y = position / span;
+
+ if (x == 0)
+ outRect.left = mMargin;
+
+ if (y == 0)
+ outRect.top = mMargin;
+
+ outRect.right = mMargin;
+ outRect.bottom = mMargin;
+ }
+}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/utils/SelectableAdapter.java b/mobile/src/main/java/org/fedorahosted/freeotp/utils/SelectableAdapter.java
new file mode 100644
index 00000000..7446b8d3
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/utils/SelectableAdapter.java
@@ -0,0 +1,142 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.utils;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.util.NavigableSet;
+import java.util.TreeSet;
+
+public abstract class SelectableAdapter extends RecyclerView.Adapter {
+ public interface EventListener {
+ void onSelectEvent(NavigableSet selected);
+ }
+
+ private NavigableSet mSelected = new TreeSet<>();
+ private EventListener mListener;
+
+ private RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ mSelected.clear();
+ mListener.onSelectEvent(mSelected);
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ super.onItemRangeInserted(positionStart, itemCount);
+
+ for (Integer i : new TreeSet<>(mSelected.descendingSet())) {
+ if (i < positionStart)
+ break;
+
+ mSelected.remove(i);
+ mSelected.add(i + itemCount);
+ }
+
+ mListener.onSelectEvent(mSelected);
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ super.onItemRangeRemoved(positionStart, itemCount);
+
+ for (Integer i : new TreeSet<>(mSelected)) {
+ if (i < positionStart)
+ continue;
+
+ mSelected.remove(i);
+ if (i > positionStart + itemCount)
+ mSelected.add(i - itemCount);
+ }
+
+ mListener.onSelectEvent(mSelected);
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ super.onItemRangeMoved(fromPosition, toPosition, itemCount);
+
+ NavigableSet selected = new TreeSet<>();
+ for (Integer i : mSelected) {
+ int j;
+
+ /* Before all affected items: no change. */
+ if (i < fromPosition && i < toPosition)
+ j = i;
+
+ /* After all affected items: no change. */
+ else if (i >= fromPosition + itemCount && i >= toPosition + itemCount)
+ j = i;
+
+ /* In the old position: move to new position. */
+ else if (i >= fromPosition && i < fromPosition + itemCount)
+ j = i + toPosition - fromPosition;
+
+ /* Shift upward. */
+ else if (fromPosition < toPosition)
+ j = i - itemCount;
+
+ /* Shift downward.*/
+ else
+ j = i + itemCount;
+
+ selected.add(j);
+ }
+
+ mListener.onSelectEvent(mSelected = selected);
+ }
+ };
+
+ @Override
+ public void setHasStableIds(boolean hasStableIds) {
+ unregisterAdapterDataObserver(mObserver);
+ super.setHasStableIds(hasStableIds);
+ registerAdapterDataObserver(mObserver);
+ }
+
+ public SelectableAdapter(EventListener listener) {
+ super();
+ mListener = listener;
+ registerAdapterDataObserver(mObserver);
+ }
+
+ public NavigableSet getSelected() {
+ return mSelected;
+ }
+
+ public void setSelected(int position, boolean selected) {
+ boolean change;
+
+ if (selected)
+ change = mSelected.add(position);
+ else
+ change = mSelected.remove(position);
+
+ if (change)
+ mListener.onSelectEvent(mSelected);
+ }
+
+ public boolean isSelected(int position) {
+ return mSelected.contains(position);
+ }
+}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/add/ScanWindowFrameLayout.java b/mobile/src/main/java/org/fedorahosted/freeotp/utils/SquareFrameLayout.java
similarity index 80%
rename from app/src/main/java/org/fedorahosted/freeotp/add/ScanWindowFrameLayout.java
rename to mobile/src/main/java/org/fedorahosted/freeotp/utils/SquareFrameLayout.java
index 71660e57..850cf45a 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/add/ScanWindowFrameLayout.java
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/utils/SquareFrameLayout.java
@@ -18,22 +18,22 @@
* limitations under the License.
*/
-package org.fedorahosted.freeotp.add;
+package org.fedorahosted.freeotp.utils;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
-public class ScanWindowFrameLayout extends FrameLayout {
- public ScanWindowFrameLayout(Context context) {
+public class SquareFrameLayout extends FrameLayout {
+ public SquareFrameLayout(Context context) {
super(context);
}
- public ScanWindowFrameLayout(Context context, AttributeSet attrs) {
+ public SquareFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
- public ScanWindowFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+ public SquareFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
diff --git a/mobile/src/main/java/org/fedorahosted/freeotp/utils/Time.java b/mobile/src/main/java/org/fedorahosted/freeotp/utils/Time.java
new file mode 100644
index 00000000..91818eb2
--- /dev/null
+++ b/mobile/src/main/java/org/fedorahosted/freeotp/utils/Time.java
@@ -0,0 +1,29 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Nathaniel McCallum
+ *
+ * Copyright (C) 2018 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.utils;
+
+public class Time {
+ public static Time INSTANCE = new Time();
+
+ public long current() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/app/src/main/res/drawable/token.xml b/mobile/src/main/res/drawable/button.xml
similarity index 69%
rename from app/src/main/res/drawable/token.xml
rename to mobile/src/main/res/drawable/button.xml
index 47aedb01..e5cc7dce 100644
--- a/app/src/main/res/drawable/token.xml
+++ b/mobile/src/main/res/drawable/button.xml
@@ -4,7 +4,7 @@
-
- Authors: Nathaniel McCallum
-
- - Copyright (C) 2013 Nathaniel McCallum, Red Hat
+ - Copyright (C) 2018 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.
@@ -20,13 +20,6 @@
-->
- -
-
-
- -
-
-
- -
-
-
-
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/fa_500px.xml b/mobile/src/main/res/drawable/fa_500px.xml
new file mode 100644
index 00000000..a64a84da
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_500px.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_accessible_icon.xml b/mobile/src/main/res/drawable/fa_accessible_icon.xml
new file mode 100644
index 00000000..647bc7dc
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_accessible_icon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_accusoft.xml b/mobile/src/main/res/drawable/fa_accusoft.xml
new file mode 100644
index 00000000..a65c9699
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_accusoft.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_adn.xml b/mobile/src/main/res/drawable/fa_adn.xml
new file mode 100644
index 00000000..bce9c802
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_adn.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_adversal.xml b/mobile/src/main/res/drawable/fa_adversal.xml
new file mode 100644
index 00000000..aa6ba0db
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_adversal.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_affiliatetheme.xml b/mobile/src/main/res/drawable/fa_affiliatetheme.xml
new file mode 100644
index 00000000..4a76fb71
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_affiliatetheme.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_algolia.xml b/mobile/src/main/res/drawable/fa_algolia.xml
new file mode 100644
index 00000000..d3f652ba
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_algolia.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_amazon.xml b/mobile/src/main/res/drawable/fa_amazon.xml
new file mode 100644
index 00000000..f5032ac3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_amazon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_amazon_pay.xml b/mobile/src/main/res/drawable/fa_amazon_pay.xml
new file mode 100644
index 00000000..a60dd192
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_amazon_pay.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_amilia.xml b/mobile/src/main/res/drawable/fa_amilia.xml
new file mode 100644
index 00000000..30f6f478
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_amilia.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_android.xml b/mobile/src/main/res/drawable/fa_android.xml
new file mode 100644
index 00000000..af0f01a8
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_android.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_angellist.xml b/mobile/src/main/res/drawable/fa_angellist.xml
new file mode 100644
index 00000000..eb690623
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_angellist.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_angrycreative.xml b/mobile/src/main/res/drawable/fa_angrycreative.xml
new file mode 100644
index 00000000..7486bde2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_angrycreative.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_angular.xml b/mobile/src/main/res/drawable/fa_angular.xml
new file mode 100644
index 00000000..68453674
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_angular.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_app_store.xml b/mobile/src/main/res/drawable/fa_app_store.xml
new file mode 100644
index 00000000..f07ccee3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_app_store.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_app_store_ios.xml b/mobile/src/main/res/drawable/fa_app_store_ios.xml
new file mode 100644
index 00000000..ced4cfb0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_app_store_ios.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_apper.xml b/mobile/src/main/res/drawable/fa_apper.xml
new file mode 100644
index 00000000..5da61e3c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_apper.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_apple.xml b/mobile/src/main/res/drawable/fa_apple.xml
new file mode 100644
index 00000000..f462fc61
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_apple.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_apple_pay.xml b/mobile/src/main/res/drawable/fa_apple_pay.xml
new file mode 100644
index 00000000..da9dae54
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_apple_pay.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_asymmetrik.xml b/mobile/src/main/res/drawable/fa_asymmetrik.xml
new file mode 100644
index 00000000..aeea3834
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_asymmetrik.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_audible.xml b/mobile/src/main/res/drawable/fa_audible.xml
new file mode 100644
index 00000000..110482a0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_audible.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_autoprefixer.xml b/mobile/src/main/res/drawable/fa_autoprefixer.xml
new file mode 100644
index 00000000..ad9285ef
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_autoprefixer.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_avianex.xml b/mobile/src/main/res/drawable/fa_avianex.xml
new file mode 100644
index 00000000..bf6ab484
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_avianex.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_aws.xml b/mobile/src/main/res/drawable/fa_aws.xml
new file mode 100644
index 00000000..4db988fa
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_aws.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_bandcamp.xml b/mobile/src/main/res/drawable/fa_bandcamp.xml
new file mode 100644
index 00000000..c254d0e8
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_bandcamp.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_behance.xml b/mobile/src/main/res/drawable/fa_behance.xml
new file mode 100644
index 00000000..bb768fa3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_behance.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_bimobject.xml b/mobile/src/main/res/drawable/fa_bimobject.xml
new file mode 100644
index 00000000..1f961bc6
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_bimobject.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_bitbucket.xml b/mobile/src/main/res/drawable/fa_bitbucket.xml
new file mode 100644
index 00000000..6cbadc66
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_bitbucket.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_bitcoin.xml b/mobile/src/main/res/drawable/fa_bitcoin.xml
new file mode 100644
index 00000000..1aaefeae
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_bitcoin.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_bity.xml b/mobile/src/main/res/drawable/fa_bity.xml
new file mode 100644
index 00000000..da258437
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_bity.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_black_tie.xml b/mobile/src/main/res/drawable/fa_black_tie.xml
new file mode 100644
index 00000000..36129803
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_black_tie.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_blackberry.xml b/mobile/src/main/res/drawable/fa_blackberry.xml
new file mode 100644
index 00000000..048de665
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_blackberry.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_blogger.xml b/mobile/src/main/res/drawable/fa_blogger.xml
new file mode 100644
index 00000000..baea7681
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_blogger.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_blogger_b.xml b/mobile/src/main/res/drawable/fa_blogger_b.xml
new file mode 100644
index 00000000..5523367d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_blogger_b.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_bluetooth.xml b/mobile/src/main/res/drawable/fa_bluetooth.xml
new file mode 100644
index 00000000..a892a204
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_bluetooth.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_bluetooth_b.xml b/mobile/src/main/res/drawable/fa_bluetooth_b.xml
new file mode 100644
index 00000000..27baa40a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_bluetooth_b.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_btc.xml b/mobile/src/main/res/drawable/fa_btc.xml
new file mode 100644
index 00000000..461c9a52
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_btc.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_buromobelexperte.xml b/mobile/src/main/res/drawable/fa_buromobelexperte.xml
new file mode 100644
index 00000000..2f4c15e0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_buromobelexperte.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_buysellads.xml b/mobile/src/main/res/drawable/fa_buysellads.xml
new file mode 100644
index 00000000..618dad8d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_buysellads.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_centercode.xml b/mobile/src/main/res/drawable/fa_centercode.xml
new file mode 100644
index 00000000..a48ba8f3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_centercode.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_chrome.xml b/mobile/src/main/res/drawable/fa_chrome.xml
new file mode 100644
index 00000000..a7fa5e6b
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_chrome.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_cloudscale.xml b/mobile/src/main/res/drawable/fa_cloudscale.xml
new file mode 100644
index 00000000..0707ebff
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_cloudscale.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_cloudsmith.xml b/mobile/src/main/res/drawable/fa_cloudsmith.xml
new file mode 100644
index 00000000..0f0f1816
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_cloudsmith.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_cloudversify.xml b/mobile/src/main/res/drawable/fa_cloudversify.xml
new file mode 100644
index 00000000..0b6db304
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_cloudversify.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_codepen.xml b/mobile/src/main/res/drawable/fa_codepen.xml
new file mode 100644
index 00000000..79629192
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_codepen.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_codiepie.xml b/mobile/src/main/res/drawable/fa_codiepie.xml
new file mode 100644
index 00000000..92aa9140
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_codiepie.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_connectdevelop.xml b/mobile/src/main/res/drawable/fa_connectdevelop.xml
new file mode 100644
index 00000000..7bea442d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_connectdevelop.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_contao.xml b/mobile/src/main/res/drawable/fa_contao.xml
new file mode 100644
index 00000000..6000bdb7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_contao.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_cpanel.xml b/mobile/src/main/res/drawable/fa_cpanel.xml
new file mode 100644
index 00000000..47a5ee39
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_cpanel.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_css3.xml b/mobile/src/main/res/drawable/fa_css3.xml
new file mode 100644
index 00000000..b4ac77d2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_css3.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_cuttlefish.xml b/mobile/src/main/res/drawable/fa_cuttlefish.xml
new file mode 100644
index 00000000..236f8744
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_cuttlefish.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_d_and_d.xml b/mobile/src/main/res/drawable/fa_d_and_d.xml
new file mode 100644
index 00000000..3f079eaf
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_d_and_d.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_dashcube.xml b/mobile/src/main/res/drawable/fa_dashcube.xml
new file mode 100644
index 00000000..64eb25a9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_dashcube.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_delicious.xml b/mobile/src/main/res/drawable/fa_delicious.xml
new file mode 100644
index 00000000..a0492c97
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_delicious.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_deploydog.xml b/mobile/src/main/res/drawable/fa_deploydog.xml
new file mode 100644
index 00000000..d98e36e2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_deploydog.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_deskpro.xml b/mobile/src/main/res/drawable/fa_deskpro.xml
new file mode 100644
index 00000000..25863b20
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_deskpro.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_deviantart.xml b/mobile/src/main/res/drawable/fa_deviantart.xml
new file mode 100644
index 00000000..74e42bbd
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_deviantart.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_digg.xml b/mobile/src/main/res/drawable/fa_digg.xml
new file mode 100644
index 00000000..34411e1f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_digg.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_digital_ocean.xml b/mobile/src/main/res/drawable/fa_digital_ocean.xml
new file mode 100644
index 00000000..884196e2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_digital_ocean.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_discord.xml b/mobile/src/main/res/drawable/fa_discord.xml
new file mode 100644
index 00000000..5d2868fd
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_discord.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_discourse.xml b/mobile/src/main/res/drawable/fa_discourse.xml
new file mode 100644
index 00000000..08059b5c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_discourse.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_dochub.xml b/mobile/src/main/res/drawable/fa_dochub.xml
new file mode 100644
index 00000000..e416581c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_dochub.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_docker.xml b/mobile/src/main/res/drawable/fa_docker.xml
new file mode 100644
index 00000000..2898b34d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_docker.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_draft2digital.xml b/mobile/src/main/res/drawable/fa_draft2digital.xml
new file mode 100644
index 00000000..e8d2de9a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_draft2digital.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_dribbble.xml b/mobile/src/main/res/drawable/fa_dribbble.xml
new file mode 100644
index 00000000..3dd73dd0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_dribbble.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_dropbox.xml b/mobile/src/main/res/drawable/fa_dropbox.xml
new file mode 100644
index 00000000..75a8ebda
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_dropbox.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_drupal.xml b/mobile/src/main/res/drawable/fa_drupal.xml
new file mode 100644
index 00000000..dfe162f5
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_drupal.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_dyalog.xml b/mobile/src/main/res/drawable/fa_dyalog.xml
new file mode 100644
index 00000000..80f45897
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_dyalog.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_earlybirds.xml b/mobile/src/main/res/drawable/fa_earlybirds.xml
new file mode 100644
index 00000000..b88c7224
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_earlybirds.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_ebay.xml b/mobile/src/main/res/drawable/fa_ebay.xml
new file mode 100644
index 00000000..00388d24
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_ebay.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_edge.xml b/mobile/src/main/res/drawable/fa_edge.xml
new file mode 100644
index 00000000..928457a4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_edge.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_elementor.xml b/mobile/src/main/res/drawable/fa_elementor.xml
new file mode 100644
index 00000000..8c00001c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_elementor.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_ember.xml b/mobile/src/main/res/drawable/fa_ember.xml
new file mode 100644
index 00000000..e49662d9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_ember.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_empire.xml b/mobile/src/main/res/drawable/fa_empire.xml
new file mode 100644
index 00000000..de3a071d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_empire.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_envira.xml b/mobile/src/main/res/drawable/fa_envira.xml
new file mode 100644
index 00000000..01eb9fb7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_envira.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_erlang.xml b/mobile/src/main/res/drawable/fa_erlang.xml
new file mode 100644
index 00000000..c07df50a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_erlang.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_ethereum.xml b/mobile/src/main/res/drawable/fa_ethereum.xml
new file mode 100644
index 00000000..e40a4e4a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_ethereum.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_etsy.xml b/mobile/src/main/res/drawable/fa_etsy.xml
new file mode 100644
index 00000000..a6c3ec8f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_etsy.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_expeditedssl.xml b/mobile/src/main/res/drawable/fa_expeditedssl.xml
new file mode 100644
index 00000000..311f8ef2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_expeditedssl.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_facebook.xml b/mobile/src/main/res/drawable/fa_facebook.xml
new file mode 100644
index 00000000..d730faf6
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_facebook.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_facebook_f.xml b/mobile/src/main/res/drawable/fa_facebook_f.xml
new file mode 100644
index 00000000..f1c20e1d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_facebook_f.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_facebook_messenger.xml b/mobile/src/main/res/drawable/fa_facebook_messenger.xml
new file mode 100644
index 00000000..c265cbf8
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_facebook_messenger.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_firefox.xml b/mobile/src/main/res/drawable/fa_firefox.xml
new file mode 100644
index 00000000..8931b38f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_firefox.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_firstdraft.xml b/mobile/src/main/res/drawable/fa_firstdraft.xml
new file mode 100644
index 00000000..bb70f01a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_firstdraft.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_flickr.xml b/mobile/src/main/res/drawable/fa_flickr.xml
new file mode 100644
index 00000000..e4f46df8
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_flickr.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_flipboard.xml b/mobile/src/main/res/drawable/fa_flipboard.xml
new file mode 100644
index 00000000..69c054dc
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_flipboard.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_fly.xml b/mobile/src/main/res/drawable/fa_fly.xml
new file mode 100644
index 00000000..ede46557
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_fly.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_font_awesome.xml b/mobile/src/main/res/drawable/fa_font_awesome.xml
new file mode 100644
index 00000000..3cd27755
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_font_awesome.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_font_awesome_flag.xml b/mobile/src/main/res/drawable/fa_font_awesome_flag.xml
new file mode 100644
index 00000000..1d3268b1
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_font_awesome_flag.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_font_awesome_logo_full.xml b/mobile/src/main/res/drawable/fa_font_awesome_logo_full.xml
new file mode 100644
index 00000000..6294beb9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_font_awesome_logo_full.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_fonticons.xml b/mobile/src/main/res/drawable/fa_fonticons.xml
new file mode 100644
index 00000000..5af19093
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_fonticons.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_fonticons_fi.xml b/mobile/src/main/res/drawable/fa_fonticons_fi.xml
new file mode 100644
index 00000000..2c439329
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_fonticons_fi.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_fort_awesome.xml b/mobile/src/main/res/drawable/fa_fort_awesome.xml
new file mode 100644
index 00000000..43306cc9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_fort_awesome.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_forumbee.xml b/mobile/src/main/res/drawable/fa_forumbee.xml
new file mode 100644
index 00000000..1ff775d0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_forumbee.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_foursquare.xml b/mobile/src/main/res/drawable/fa_foursquare.xml
new file mode 100644
index 00000000..3b8878eb
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_foursquare.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_free_code_camp.xml b/mobile/src/main/res/drawable/fa_free_code_camp.xml
new file mode 100644
index 00000000..c6be3350
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_free_code_camp.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_freebsd.xml b/mobile/src/main/res/drawable/fa_freebsd.xml
new file mode 100644
index 00000000..aa9166a4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_freebsd.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_fulcrum.xml b/mobile/src/main/res/drawable/fa_fulcrum.xml
new file mode 100644
index 00000000..0e82a2b4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_fulcrum.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_get_pocket.xml b/mobile/src/main/res/drawable/fa_get_pocket.xml
new file mode 100644
index 00000000..094a7790
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_get_pocket.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gg.xml b/mobile/src/main/res/drawable/fa_gg.xml
new file mode 100644
index 00000000..a4b79139
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gg.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_git.xml b/mobile/src/main/res/drawable/fa_git.xml
new file mode 100644
index 00000000..b63b141d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_git.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_github.xml b/mobile/src/main/res/drawable/fa_github.xml
new file mode 100644
index 00000000..c5572697
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_github.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gitkraken.xml b/mobile/src/main/res/drawable/fa_gitkraken.xml
new file mode 100644
index 00000000..4cfe458b
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gitkraken.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gitlab.xml b/mobile/src/main/res/drawable/fa_gitlab.xml
new file mode 100644
index 00000000..87d7e673
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gitlab.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gitter.xml b/mobile/src/main/res/drawable/fa_gitter.xml
new file mode 100644
index 00000000..dee244b9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gitter.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_glide.xml b/mobile/src/main/res/drawable/fa_glide.xml
new file mode 100644
index 00000000..69ad305f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_glide.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_glide_g.xml b/mobile/src/main/res/drawable/fa_glide_g.xml
new file mode 100644
index 00000000..9e4d62ef
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_glide_g.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gofore.xml b/mobile/src/main/res/drawable/fa_gofore.xml
new file mode 100644
index 00000000..e22a1062
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gofore.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_goodreads.xml b/mobile/src/main/res/drawable/fa_goodreads.xml
new file mode 100644
index 00000000..da210ae1
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_goodreads.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_goodreads_g.xml b/mobile/src/main/res/drawable/fa_goodreads_g.xml
new file mode 100644
index 00000000..6b24dead
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_goodreads_g.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_google.xml b/mobile/src/main/res/drawable/fa_google.xml
new file mode 100644
index 00000000..1f8aaf62
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_google.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_google_drive.xml b/mobile/src/main/res/drawable/fa_google_drive.xml
new file mode 100644
index 00000000..d00bffad
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_google_drive.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_google_play.xml b/mobile/src/main/res/drawable/fa_google_play.xml
new file mode 100644
index 00000000..409400df
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_google_play.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_google_plus.xml b/mobile/src/main/res/drawable/fa_google_plus.xml
new file mode 100644
index 00000000..ad250f69
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_google_plus.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_google_plus_g.xml b/mobile/src/main/res/drawable/fa_google_plus_g.xml
new file mode 100644
index 00000000..2d203aaa
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_google_plus_g.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_google_wallet.xml b/mobile/src/main/res/drawable/fa_google_wallet.xml
new file mode 100644
index 00000000..4b94f18a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_google_wallet.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gratipay.xml b/mobile/src/main/res/drawable/fa_gratipay.xml
new file mode 100644
index 00000000..ebf5aaa0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gratipay.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_grav.xml b/mobile/src/main/res/drawable/fa_grav.xml
new file mode 100644
index 00000000..ba607837
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_grav.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gripfire.xml b/mobile/src/main/res/drawable/fa_gripfire.xml
new file mode 100644
index 00000000..6f587ad7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gripfire.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_grunt.xml b/mobile/src/main/res/drawable/fa_grunt.xml
new file mode 100644
index 00000000..ac38e70c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_grunt.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_gulp.xml b/mobile/src/main/res/drawable/fa_gulp.xml
new file mode 100644
index 00000000..3b1bcf8a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_gulp.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_hacker_news.xml b/mobile/src/main/res/drawable/fa_hacker_news.xml
new file mode 100644
index 00000000..99da0894
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_hacker_news.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_hips.xml b/mobile/src/main/res/drawable/fa_hips.xml
new file mode 100644
index 00000000..0f1fc334
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_hips.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_hire_a_helper.xml b/mobile/src/main/res/drawable/fa_hire_a_helper.xml
new file mode 100644
index 00000000..2143288f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_hire_a_helper.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_hornbill.xml b/mobile/src/main/res/drawable/fa_hornbill.xml
new file mode 100644
index 00000000..41984d06
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_hornbill.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_hotjar.xml b/mobile/src/main/res/drawable/fa_hotjar.xml
new file mode 100644
index 00000000..afc6c150
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_hotjar.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_houzz.xml b/mobile/src/main/res/drawable/fa_houzz.xml
new file mode 100644
index 00000000..871ce65c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_houzz.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_html5.xml b/mobile/src/main/res/drawable/fa_html5.xml
new file mode 100644
index 00000000..a49fa1a4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_html5.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_hubspot.xml b/mobile/src/main/res/drawable/fa_hubspot.xml
new file mode 100644
index 00000000..b824866e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_hubspot.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_imdb.xml b/mobile/src/main/res/drawable/fa_imdb.xml
new file mode 100644
index 00000000..e56e7ad5
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_imdb.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_instagram.xml b/mobile/src/main/res/drawable/fa_instagram.xml
new file mode 100644
index 00000000..718decaa
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_instagram.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_internet_explorer.xml b/mobile/src/main/res/drawable/fa_internet_explorer.xml
new file mode 100644
index 00000000..c53b3ccb
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_internet_explorer.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_ioxhost.xml b/mobile/src/main/res/drawable/fa_ioxhost.xml
new file mode 100644
index 00000000..1a24dca7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_ioxhost.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_itunes.xml b/mobile/src/main/res/drawable/fa_itunes.xml
new file mode 100644
index 00000000..5505ace0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_itunes.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_itunes_note.xml b/mobile/src/main/res/drawable/fa_itunes_note.xml
new file mode 100644
index 00000000..e81ca6b3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_itunes_note.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_java.xml b/mobile/src/main/res/drawable/fa_java.xml
new file mode 100644
index 00000000..ee127426
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_java.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_jenkins.xml b/mobile/src/main/res/drawable/fa_jenkins.xml
new file mode 100644
index 00000000..c6628cde
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_jenkins.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_joget.xml b/mobile/src/main/res/drawable/fa_joget.xml
new file mode 100644
index 00000000..61ee7595
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_joget.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_joomla.xml b/mobile/src/main/res/drawable/fa_joomla.xml
new file mode 100644
index 00000000..281cf711
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_joomla.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_js.xml b/mobile/src/main/res/drawable/fa_js.xml
new file mode 100644
index 00000000..adf65620
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_js.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_jsfiddle.xml b/mobile/src/main/res/drawable/fa_jsfiddle.xml
new file mode 100644
index 00000000..9eb1e0ba
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_jsfiddle.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_keybase.xml b/mobile/src/main/res/drawable/fa_keybase.xml
new file mode 100644
index 00000000..bd2af758
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_keybase.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_keycdn.xml b/mobile/src/main/res/drawable/fa_keycdn.xml
new file mode 100644
index 00000000..91289e21
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_keycdn.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_kickstarter.xml b/mobile/src/main/res/drawable/fa_kickstarter.xml
new file mode 100644
index 00000000..15fd439d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_kickstarter.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_kickstarter_k.xml b/mobile/src/main/res/drawable/fa_kickstarter_k.xml
new file mode 100644
index 00000000..016cf826
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_kickstarter_k.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_korvue.xml b/mobile/src/main/res/drawable/fa_korvue.xml
new file mode 100644
index 00000000..4d5ed4f7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_korvue.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_laravel.xml b/mobile/src/main/res/drawable/fa_laravel.xml
new file mode 100644
index 00000000..efa23f1b
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_laravel.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_lastfm.xml b/mobile/src/main/res/drawable/fa_lastfm.xml
new file mode 100644
index 00000000..ec63bfc3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_lastfm.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_leanpub.xml b/mobile/src/main/res/drawable/fa_leanpub.xml
new file mode 100644
index 00000000..049a20dc
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_leanpub.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_less.xml b/mobile/src/main/res/drawable/fa_less.xml
new file mode 100644
index 00000000..a6891bca
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_less.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_line.xml b/mobile/src/main/res/drawable/fa_line.xml
new file mode 100644
index 00000000..693565c0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_line.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_linkedin.xml b/mobile/src/main/res/drawable/fa_linkedin.xml
new file mode 100644
index 00000000..17b867bf
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_linkedin.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_linkedin_in.xml b/mobile/src/main/res/drawable/fa_linkedin_in.xml
new file mode 100644
index 00000000..1b621e4c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_linkedin_in.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_linode.xml b/mobile/src/main/res/drawable/fa_linode.xml
new file mode 100644
index 00000000..7bdb16f6
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_linode.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_linux.xml b/mobile/src/main/res/drawable/fa_linux.xml
new file mode 100644
index 00000000..713e33ec
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_linux.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_lyft.xml b/mobile/src/main/res/drawable/fa_lyft.xml
new file mode 100644
index 00000000..6cdde514
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_lyft.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_magento.xml b/mobile/src/main/res/drawable/fa_magento.xml
new file mode 100644
index 00000000..bb965162
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_magento.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_mailchimp.xml b/mobile/src/main/res/drawable/fa_mailchimp.xml
new file mode 100644
index 00000000..a11dbb74
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_mailchimp.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_mandalorian.xml b/mobile/src/main/res/drawable/fa_mandalorian.xml
new file mode 100644
index 00000000..9f0c752c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_mandalorian.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_mastodon.xml b/mobile/src/main/res/drawable/fa_mastodon.xml
new file mode 100644
index 00000000..e08ddf0c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_mastodon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_maxcdn.xml b/mobile/src/main/res/drawable/fa_maxcdn.xml
new file mode 100644
index 00000000..53d41d01
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_maxcdn.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_medapps.xml b/mobile/src/main/res/drawable/fa_medapps.xml
new file mode 100644
index 00000000..33928456
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_medapps.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_medium.xml b/mobile/src/main/res/drawable/fa_medium.xml
new file mode 100644
index 00000000..a2daa9d3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_medium.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_medium_m.xml b/mobile/src/main/res/drawable/fa_medium_m.xml
new file mode 100644
index 00000000..83d1ff8f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_medium_m.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_medrt.xml b/mobile/src/main/res/drawable/fa_medrt.xml
new file mode 100644
index 00000000..07df571d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_medrt.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_meetup.xml b/mobile/src/main/res/drawable/fa_meetup.xml
new file mode 100644
index 00000000..9b2f9198
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_meetup.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_megaport.xml b/mobile/src/main/res/drawable/fa_megaport.xml
new file mode 100644
index 00000000..8a96c5b7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_megaport.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_microsoft.xml b/mobile/src/main/res/drawable/fa_microsoft.xml
new file mode 100644
index 00000000..3c8f2d40
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_microsoft.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_mix.xml b/mobile/src/main/res/drawable/fa_mix.xml
new file mode 100644
index 00000000..69d97d96
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_mix.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_mixcloud.xml b/mobile/src/main/res/drawable/fa_mixcloud.xml
new file mode 100644
index 00000000..2ac89998
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_mixcloud.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_mizuni.xml b/mobile/src/main/res/drawable/fa_mizuni.xml
new file mode 100644
index 00000000..4b222dd2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_mizuni.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_modx.xml b/mobile/src/main/res/drawable/fa_modx.xml
new file mode 100644
index 00000000..d10fac10
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_modx.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_monero.xml b/mobile/src/main/res/drawable/fa_monero.xml
new file mode 100644
index 00000000..10ac650e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_monero.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_napster.xml b/mobile/src/main/res/drawable/fa_napster.xml
new file mode 100644
index 00000000..301dbf60
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_napster.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_nimblr.xml b/mobile/src/main/res/drawable/fa_nimblr.xml
new file mode 100644
index 00000000..c081ca2d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_nimblr.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_nintendo_switch.xml b/mobile/src/main/res/drawable/fa_nintendo_switch.xml
new file mode 100644
index 00000000..05ca0484
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_nintendo_switch.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_node.xml b/mobile/src/main/res/drawable/fa_node.xml
new file mode 100644
index 00000000..e60c69a9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_node.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_node_js.xml b/mobile/src/main/res/drawable/fa_node_js.xml
new file mode 100644
index 00000000..f370f31e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_node_js.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_npm.xml b/mobile/src/main/res/drawable/fa_npm.xml
new file mode 100644
index 00000000..32ce56d5
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_npm.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_ns8.xml b/mobile/src/main/res/drawable/fa_ns8.xml
new file mode 100644
index 00000000..1384298b
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_ns8.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_nutritionix.xml b/mobile/src/main/res/drawable/fa_nutritionix.xml
new file mode 100644
index 00000000..9feb864e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_nutritionix.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_odnoklassniki.xml b/mobile/src/main/res/drawable/fa_odnoklassniki.xml
new file mode 100644
index 00000000..1e73eeea
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_odnoklassniki.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_opencart.xml b/mobile/src/main/res/drawable/fa_opencart.xml
new file mode 100644
index 00000000..9b7b1d43
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_opencart.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_openid.xml b/mobile/src/main/res/drawable/fa_openid.xml
new file mode 100644
index 00000000..99185684
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_openid.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_opera.xml b/mobile/src/main/res/drawable/fa_opera.xml
new file mode 100644
index 00000000..ebf21ce0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_opera.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_optin_monster.xml b/mobile/src/main/res/drawable/fa_optin_monster.xml
new file mode 100644
index 00000000..c1768d52
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_optin_monster.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_osi.xml b/mobile/src/main/res/drawable/fa_osi.xml
new file mode 100644
index 00000000..1f8e0fff
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_osi.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_page4.xml b/mobile/src/main/res/drawable/fa_page4.xml
new file mode 100644
index 00000000..74a9b970
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_page4.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_pagelines.xml b/mobile/src/main/res/drawable/fa_pagelines.xml
new file mode 100644
index 00000000..1fcb7a7f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_pagelines.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_palfed.xml b/mobile/src/main/res/drawable/fa_palfed.xml
new file mode 100644
index 00000000..dde757ec
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_palfed.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_patreon.xml b/mobile/src/main/res/drawable/fa_patreon.xml
new file mode 100644
index 00000000..9c9e6116
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_patreon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_paypal.xml b/mobile/src/main/res/drawable/fa_paypal.xml
new file mode 100644
index 00000000..a59ea8ce
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_paypal.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_periscope.xml b/mobile/src/main/res/drawable/fa_periscope.xml
new file mode 100644
index 00000000..d8034049
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_periscope.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_phabricator.xml b/mobile/src/main/res/drawable/fa_phabricator.xml
new file mode 100644
index 00000000..e72a3d80
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_phabricator.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_phoenix_framework.xml b/mobile/src/main/res/drawable/fa_phoenix_framework.xml
new file mode 100644
index 00000000..06e00d8f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_phoenix_framework.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_phoenix_squadron.xml b/mobile/src/main/res/drawable/fa_phoenix_squadron.xml
new file mode 100644
index 00000000..a8644fee
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_phoenix_squadron.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_php.xml b/mobile/src/main/res/drawable/fa_php.xml
new file mode 100644
index 00000000..a33642f9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_php.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_pinterest.xml b/mobile/src/main/res/drawable/fa_pinterest.xml
new file mode 100644
index 00000000..df655895
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_pinterest.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_pinterest_p.xml b/mobile/src/main/res/drawable/fa_pinterest_p.xml
new file mode 100644
index 00000000..417e759d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_pinterest_p.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_playstation.xml b/mobile/src/main/res/drawable/fa_playstation.xml
new file mode 100644
index 00000000..000d2575
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_playstation.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_product_hunt.xml b/mobile/src/main/res/drawable/fa_product_hunt.xml
new file mode 100644
index 00000000..d812d44c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_product_hunt.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_pushed.xml b/mobile/src/main/res/drawable/fa_pushed.xml
new file mode 100644
index 00000000..c163ddcd
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_pushed.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_python.xml b/mobile/src/main/res/drawable/fa_python.xml
new file mode 100644
index 00000000..4cff3e54
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_python.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_qq.xml b/mobile/src/main/res/drawable/fa_qq.xml
new file mode 100644
index 00000000..d63db979
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_qq.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_quinscape.xml b/mobile/src/main/res/drawable/fa_quinscape.xml
new file mode 100644
index 00000000..5f9e201d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_quinscape.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_quora.xml b/mobile/src/main/res/drawable/fa_quora.xml
new file mode 100644
index 00000000..92f8813e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_quora.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_r_project.xml b/mobile/src/main/res/drawable/fa_r_project.xml
new file mode 100644
index 00000000..8ab5a2e8
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_r_project.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_ravelry.xml b/mobile/src/main/res/drawable/fa_ravelry.xml
new file mode 100644
index 00000000..7a50d0e5
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_ravelry.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_react.xml b/mobile/src/main/res/drawable/fa_react.xml
new file mode 100644
index 00000000..cf1103b8
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_react.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_readme.xml b/mobile/src/main/res/drawable/fa_readme.xml
new file mode 100644
index 00000000..d2486fcb
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_readme.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_rebel.xml b/mobile/src/main/res/drawable/fa_rebel.xml
new file mode 100644
index 00000000..74a60719
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_rebel.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_red_river.xml b/mobile/src/main/res/drawable/fa_red_river.xml
new file mode 100644
index 00000000..86dfb624
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_red_river.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_reddit.xml b/mobile/src/main/res/drawable/fa_reddit.xml
new file mode 100644
index 00000000..b94d488a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_reddit.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_reddit_alien.xml b/mobile/src/main/res/drawable/fa_reddit_alien.xml
new file mode 100644
index 00000000..c7c588df
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_reddit_alien.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_rendact.xml b/mobile/src/main/res/drawable/fa_rendact.xml
new file mode 100644
index 00000000..dd2c680d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_rendact.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_renren.xml b/mobile/src/main/res/drawable/fa_renren.xml
new file mode 100644
index 00000000..1fc87fd9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_renren.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_replyd.xml b/mobile/src/main/res/drawable/fa_replyd.xml
new file mode 100644
index 00000000..096d8d92
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_replyd.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_researchgate.xml b/mobile/src/main/res/drawable/fa_researchgate.xml
new file mode 100644
index 00000000..4e22830c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_researchgate.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_resolving.xml b/mobile/src/main/res/drawable/fa_resolving.xml
new file mode 100644
index 00000000..c9cc58fe
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_resolving.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_rocketchat.xml b/mobile/src/main/res/drawable/fa_rocketchat.xml
new file mode 100644
index 00000000..7f3a4bd9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_rocketchat.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_rockrms.xml b/mobile/src/main/res/drawable/fa_rockrms.xml
new file mode 100644
index 00000000..3c54c655
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_rockrms.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_safari.xml b/mobile/src/main/res/drawable/fa_safari.xml
new file mode 100644
index 00000000..0fb6cb8e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_safari.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_sass.xml b/mobile/src/main/res/drawable/fa_sass.xml
new file mode 100644
index 00000000..1d31dee9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_sass.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_schlix.xml b/mobile/src/main/res/drawable/fa_schlix.xml
new file mode 100644
index 00000000..a9f1ac60
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_schlix.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_scribd.xml b/mobile/src/main/res/drawable/fa_scribd.xml
new file mode 100644
index 00000000..7e010929
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_scribd.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_searchengin.xml b/mobile/src/main/res/drawable/fa_searchengin.xml
new file mode 100644
index 00000000..02eaaaf7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_searchengin.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_sellcast.xml b/mobile/src/main/res/drawable/fa_sellcast.xml
new file mode 100644
index 00000000..588106a7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_sellcast.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_sellsy.xml b/mobile/src/main/res/drawable/fa_sellsy.xml
new file mode 100644
index 00000000..374ee4e0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_sellsy.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_servicestack.xml b/mobile/src/main/res/drawable/fa_servicestack.xml
new file mode 100644
index 00000000..901c2780
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_servicestack.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_shirtsinbulk.xml b/mobile/src/main/res/drawable/fa_shirtsinbulk.xml
new file mode 100644
index 00000000..0ec11b29
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_shirtsinbulk.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_shopware.xml b/mobile/src/main/res/drawable/fa_shopware.xml
new file mode 100644
index 00000000..009f1b1e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_shopware.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_simplybuilt.xml b/mobile/src/main/res/drawable/fa_simplybuilt.xml
new file mode 100644
index 00000000..60f3dc73
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_simplybuilt.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_sistrix.xml b/mobile/src/main/res/drawable/fa_sistrix.xml
new file mode 100644
index 00000000..55debf51
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_sistrix.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_sith.xml b/mobile/src/main/res/drawable/fa_sith.xml
new file mode 100644
index 00000000..5c42a3b7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_sith.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_skyatlas.xml b/mobile/src/main/res/drawable/fa_skyatlas.xml
new file mode 100644
index 00000000..75e5b404
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_skyatlas.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_skype.xml b/mobile/src/main/res/drawable/fa_skype.xml
new file mode 100644
index 00000000..65c6d301
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_skype.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_slack.xml b/mobile/src/main/res/drawable/fa_slack.xml
new file mode 100644
index 00000000..d3996238
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_slack.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_slack_hash.xml b/mobile/src/main/res/drawable/fa_slack_hash.xml
new file mode 100644
index 00000000..f4c7a061
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_slack_hash.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_slideshare.xml b/mobile/src/main/res/drawable/fa_slideshare.xml
new file mode 100644
index 00000000..1cce230f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_slideshare.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_snapchat.xml b/mobile/src/main/res/drawable/fa_snapchat.xml
new file mode 100644
index 00000000..ac6dd32c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_snapchat.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_snapchat_ghost.xml b/mobile/src/main/res/drawable/fa_snapchat_ghost.xml
new file mode 100644
index 00000000..3f868d05
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_snapchat_ghost.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_soundcloud.xml b/mobile/src/main/res/drawable/fa_soundcloud.xml
new file mode 100644
index 00000000..cbef4c1a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_soundcloud.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_speakap.xml b/mobile/src/main/res/drawable/fa_speakap.xml
new file mode 100644
index 00000000..3b00ace5
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_speakap.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_spotify.xml b/mobile/src/main/res/drawable/fa_spotify.xml
new file mode 100644
index 00000000..045ce37d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_spotify.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_squarespace.xml b/mobile/src/main/res/drawable/fa_squarespace.xml
new file mode 100644
index 00000000..27dcffc0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_squarespace.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_stack_exchange.xml b/mobile/src/main/res/drawable/fa_stack_exchange.xml
new file mode 100644
index 00000000..fefc53bf
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_stack_exchange.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_stack_overflow.xml b/mobile/src/main/res/drawable/fa_stack_overflow.xml
new file mode 100644
index 00000000..1e56a4c3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_stack_overflow.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_staylinked.xml b/mobile/src/main/res/drawable/fa_staylinked.xml
new file mode 100644
index 00000000..dbd19da2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_staylinked.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_steam.xml b/mobile/src/main/res/drawable/fa_steam.xml
new file mode 100644
index 00000000..66deae24
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_steam.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_steam_symbol.xml b/mobile/src/main/res/drawable/fa_steam_symbol.xml
new file mode 100644
index 00000000..97a21f2f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_steam_symbol.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_sticker_mule.xml b/mobile/src/main/res/drawable/fa_sticker_mule.xml
new file mode 100644
index 00000000..4a24698a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_sticker_mule.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_strava.xml b/mobile/src/main/res/drawable/fa_strava.xml
new file mode 100644
index 00000000..1c45909b
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_strava.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_stripe.xml b/mobile/src/main/res/drawable/fa_stripe.xml
new file mode 100644
index 00000000..b6378706
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_stripe.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_stripe_s.xml b/mobile/src/main/res/drawable/fa_stripe_s.xml
new file mode 100644
index 00000000..992ae9ee
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_stripe_s.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_studiovinari.xml b/mobile/src/main/res/drawable/fa_studiovinari.xml
new file mode 100644
index 00000000..94e98365
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_studiovinari.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_stumbleupon.xml b/mobile/src/main/res/drawable/fa_stumbleupon.xml
new file mode 100644
index 00000000..fcf0dc80
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_stumbleupon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_superpowers.xml b/mobile/src/main/res/drawable/fa_superpowers.xml
new file mode 100644
index 00000000..0c96fcd9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_superpowers.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_supple.xml b/mobile/src/main/res/drawable/fa_supple.xml
new file mode 100644
index 00000000..06732a78
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_supple.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_teamspeak.xml b/mobile/src/main/res/drawable/fa_teamspeak.xml
new file mode 100644
index 00000000..cec240e4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_teamspeak.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_telegram.xml b/mobile/src/main/res/drawable/fa_telegram.xml
new file mode 100644
index 00000000..87e9c124
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_telegram.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_telegram_plane.xml b/mobile/src/main/res/drawable/fa_telegram_plane.xml
new file mode 100644
index 00000000..4bee396d
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_telegram_plane.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_tencent_weibo.xml b/mobile/src/main/res/drawable/fa_tencent_weibo.xml
new file mode 100644
index 00000000..f5882916
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_tencent_weibo.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_themeco.xml b/mobile/src/main/res/drawable/fa_themeco.xml
new file mode 100644
index 00000000..8f56d646
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_themeco.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_themeisle.xml b/mobile/src/main/res/drawable/fa_themeisle.xml
new file mode 100644
index 00000000..f759cfce
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_themeisle.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_trade_federation.xml b/mobile/src/main/res/drawable/fa_trade_federation.xml
new file mode 100644
index 00000000..43048936
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_trade_federation.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_trello.xml b/mobile/src/main/res/drawable/fa_trello.xml
new file mode 100644
index 00000000..c872e1d0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_trello.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_tripadvisor.xml b/mobile/src/main/res/drawable/fa_tripadvisor.xml
new file mode 100644
index 00000000..30d51a3a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_tripadvisor.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_tumblr.xml b/mobile/src/main/res/drawable/fa_tumblr.xml
new file mode 100644
index 00000000..524c5bc9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_tumblr.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_twitch.xml b/mobile/src/main/res/drawable/fa_twitch.xml
new file mode 100644
index 00000000..900e3641
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_twitch.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_twitter.xml b/mobile/src/main/res/drawable/fa_twitter.xml
new file mode 100644
index 00000000..bd9478d3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_twitter.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_typo3.xml b/mobile/src/main/res/drawable/fa_typo3.xml
new file mode 100644
index 00000000..ea40065f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_typo3.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_uber.xml b/mobile/src/main/res/drawable/fa_uber.xml
new file mode 100644
index 00000000..3970dd47
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_uber.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_uikit.xml b/mobile/src/main/res/drawable/fa_uikit.xml
new file mode 100644
index 00000000..fdf27776
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_uikit.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_uniregistry.xml b/mobile/src/main/res/drawable/fa_uniregistry.xml
new file mode 100644
index 00000000..c8123f52
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_uniregistry.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_untappd.xml b/mobile/src/main/res/drawable/fa_untappd.xml
new file mode 100644
index 00000000..c703a7c9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_untappd.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_usb.xml b/mobile/src/main/res/drawable/fa_usb.xml
new file mode 100644
index 00000000..6af9b47f
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_usb.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_ussunnah.xml b/mobile/src/main/res/drawable/fa_ussunnah.xml
new file mode 100644
index 00000000..bbcbfac5
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_ussunnah.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_vaadin.xml b/mobile/src/main/res/drawable/fa_vaadin.xml
new file mode 100644
index 00000000..f7acee44
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_vaadin.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_viacoin.xml b/mobile/src/main/res/drawable/fa_viacoin.xml
new file mode 100644
index 00000000..2ab61d37
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_viacoin.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_viadeo.xml b/mobile/src/main/res/drawable/fa_viadeo.xml
new file mode 100644
index 00000000..b7be74c4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_viadeo.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_viber.xml b/mobile/src/main/res/drawable/fa_viber.xml
new file mode 100644
index 00000000..591d444a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_viber.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_vimeo.xml b/mobile/src/main/res/drawable/fa_vimeo.xml
new file mode 100644
index 00000000..a6de6e65
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_vimeo.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_vimeo_v.xml b/mobile/src/main/res/drawable/fa_vimeo_v.xml
new file mode 100644
index 00000000..d15494c8
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_vimeo_v.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_vine.xml b/mobile/src/main/res/drawable/fa_vine.xml
new file mode 100644
index 00000000..933bcbf4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_vine.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_vk.xml b/mobile/src/main/res/drawable/fa_vk.xml
new file mode 100644
index 00000000..6ce2e604
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_vk.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_vnv.xml b/mobile/src/main/res/drawable/fa_vnv.xml
new file mode 100644
index 00000000..91d6b8d9
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_vnv.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_vuejs.xml b/mobile/src/main/res/drawable/fa_vuejs.xml
new file mode 100644
index 00000000..20d81447
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_vuejs.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_weebly.xml b/mobile/src/main/res/drawable/fa_weebly.xml
new file mode 100644
index 00000000..61e6a7a2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_weebly.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_weibo.xml b/mobile/src/main/res/drawable/fa_weibo.xml
new file mode 100644
index 00000000..e52f3f7e
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_weibo.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_weixin.xml b/mobile/src/main/res/drawable/fa_weixin.xml
new file mode 100644
index 00000000..8ed08c14
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_weixin.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_whatsapp.xml b/mobile/src/main/res/drawable/fa_whatsapp.xml
new file mode 100644
index 00000000..fd4dade7
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_whatsapp.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_whmcs.xml b/mobile/src/main/res/drawable/fa_whmcs.xml
new file mode 100644
index 00000000..91b571a1
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_whmcs.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wikipedia_w.xml b/mobile/src/main/res/drawable/fa_wikipedia_w.xml
new file mode 100644
index 00000000..7bf88933
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wikipedia_w.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_windows.xml b/mobile/src/main/res/drawable/fa_windows.xml
new file mode 100644
index 00000000..c129c75c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_windows.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wix.xml b/mobile/src/main/res/drawable/fa_wix.xml
new file mode 100644
index 00000000..e1a57438
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wix.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wolf_pack_battalion.xml b/mobile/src/main/res/drawable/fa_wolf_pack_battalion.xml
new file mode 100644
index 00000000..d7add424
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wolf_pack_battalion.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wordpress.xml b/mobile/src/main/res/drawable/fa_wordpress.xml
new file mode 100644
index 00000000..d13b2dc4
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wordpress.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wordpress_simple.xml b/mobile/src/main/res/drawable/fa_wordpress_simple.xml
new file mode 100644
index 00000000..798278c3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wordpress_simple.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wpbeginner.xml b/mobile/src/main/res/drawable/fa_wpbeginner.xml
new file mode 100644
index 00000000..eb3375d0
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wpbeginner.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wpexplorer.xml b/mobile/src/main/res/drawable/fa_wpexplorer.xml
new file mode 100644
index 00000000..e51a1496
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wpexplorer.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_wpforms.xml b/mobile/src/main/res/drawable/fa_wpforms.xml
new file mode 100644
index 00000000..c6fd255c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_wpforms.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_xbox.xml b/mobile/src/main/res/drawable/fa_xbox.xml
new file mode 100644
index 00000000..4a936f01
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_xbox.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_xing.xml b/mobile/src/main/res/drawable/fa_xing.xml
new file mode 100644
index 00000000..61700751
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_xing.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_y_combinator.xml b/mobile/src/main/res/drawable/fa_y_combinator.xml
new file mode 100644
index 00000000..c54472ca
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_y_combinator.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_yahoo.xml b/mobile/src/main/res/drawable/fa_yahoo.xml
new file mode 100644
index 00000000..1f2fe30b
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_yahoo.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_yandex.xml b/mobile/src/main/res/drawable/fa_yandex.xml
new file mode 100644
index 00000000..6acc0e0c
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_yandex.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_yandex_international.xml b/mobile/src/main/res/drawable/fa_yandex_international.xml
new file mode 100644
index 00000000..b715d64a
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_yandex_international.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_yelp.xml b/mobile/src/main/res/drawable/fa_yelp.xml
new file mode 100644
index 00000000..bab325e2
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_yelp.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_yoast.xml b/mobile/src/main/res/drawable/fa_yoast.xml
new file mode 100644
index 00000000..178231d3
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_yoast.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/fa_youtube.xml b/mobile/src/main/res/drawable/fa_youtube.xml
new file mode 100644
index 00000000..e3bc7e67
--- /dev/null
+++ b/mobile/src/main/res/drawable/fa_youtube.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/mobile/src/main/res/drawable/ic_about.xml b/mobile/src/main/res/drawable/ic_about.xml
new file mode 100644
index 00000000..0a9df35e
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_about.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_add.xml b/mobile/src/main/res/drawable/ic_add.xml
new file mode 100644
index 00000000..09e5b83d
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_bluetooth.xml b/mobile/src/main/res/drawable/ic_bluetooth.xml
new file mode 100644
index 00000000..3863b7ec
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_bluetooth.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/mobile/src/main/res/drawable/ic_check.xml b/mobile/src/main/res/drawable/ic_check.xml
new file mode 100644
index 00000000..909bf5ec
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_check.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_copy.xml b/mobile/src/main/res/drawable/ic_copy.xml
new file mode 100644
index 00000000..87097c87
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_copy.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/mobile/src/main/res/drawable/ic_delete.xml b/mobile/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 00000000..f4bc2542
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_down.xml b/mobile/src/main/res/drawable/ic_down.xml
new file mode 100644
index 00000000..a4403b63
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_down.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_freeotp.png b/mobile/src/main/res/drawable/ic_freeotp.png
new file mode 100644
index 00000000..74d972b2
Binary files /dev/null and b/mobile/src/main/res/drawable/ic_freeotp.png differ
diff --git a/mobile/src/main/res/drawable/ic_hotp.xml b/mobile/src/main/res/drawable/ic_hotp.xml
new file mode 100644
index 00000000..e66c7a17
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_hotp.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_lock.xml b/mobile/src/main/res/drawable/ic_lock.xml
new file mode 100644
index 00000000..cf93ad09
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_lock.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_scan.xml b/mobile/src/main/res/drawable/ic_scan.xml
new file mode 100644
index 00000000..705683a4
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_scan.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_share.xml b/mobile/src/main/res/drawable/ic_share.xml
new file mode 100644
index 00000000..474b2493
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_totp.xml b/mobile/src/main/res/drawable/ic_totp.xml
new file mode 100644
index 00000000..89a358e1
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_totp.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_up.xml b/mobile/src/main/res/drawable/ic_up.xml
new file mode 100644
index 00000000..346a64a2
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_up.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/token.xml b/mobile/src/main/res/drawable/progress.xml
similarity index 62%
rename from app/src/main/res/menu/token.xml
rename to mobile/src/main/res/drawable/progress.xml
index a5a28e94..32931104 100644
--- a/app/src/main/res/menu/token.xml
+++ b/mobile/src/main/res/drawable/progress.xml
@@ -4,7 +4,7 @@
-
- Authors: Nathaniel McCallum
-
- - Copyright (C) 2014 Nathaniel McCallum, Red Hat
+ - Copyright (C) 2018 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.
@@ -18,16 +18,13 @@
- See the License for the specific language governing permissions and
- limitations under the License.
-->
-
-
-
-
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..4b878ec3
--- /dev/null
+++ b/mobile/src/main/res/layout/activity_main.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/scan.xml b/mobile/src/main/res/layout/fragment_scan.xml
similarity index 57%
rename from app/src/main/res/layout/scan.xml
rename to mobile/src/main/res/layout/fragment_scan.xml
index b5e6f57f..d5216af6 100644
--- a/app/src/main/res/layout/scan.xml
+++ b/mobile/src/main/res/layout/fragment_scan.xml
@@ -4,7 +4,7 @@
-
- Authors: Nathaniel McCallum
-
- - Copyright (C) 2013 Nathaniel McCallum, Red Hat
+ - Copyright (C) 2013-2018 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.
@@ -18,51 +18,44 @@
- See the License for the specific language governing permissions and
- limitations under the License.
-->
-
-
+ android:background="@android:color/black">
-
-
+ android:scaleType="fitCenter"
+ android:id="@+id/image"
+ android:alpha="0.0"
+ />
-
-
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/mobile/src/main/res/layout/fragment_share.xml
similarity index 57%
rename from app/src/main/res/menu/main.xml
rename to mobile/src/main/res/layout/fragment_share.xml
index 8de1ade0..664fd2f1 100644
--- a/app/src/main/res/menu/main.xml
+++ b/mobile/src/main/res/layout/fragment_share.xml
@@ -4,7 +4,7 @@
-
- Authors: Nathaniel McCallum
-
- - Copyright (C) 2013 Nathaniel McCallum, Red Hat
+ - Copyright (C) 2018 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.
@@ -19,16 +19,15 @@
- limitations under the License.
-->
-
-
+
-
+ app:layoutManager="android.support.v7.widget.LinearLayoutManager"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ />
diff --git a/mobile/src/main/res/layout/target.xml b/mobile/src/main/res/layout/target.xml
new file mode 100644
index 00000000..4e93bc5a
--- /dev/null
+++ b/mobile/src/main/res/layout/target.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/main/res/layout/token.xml b/mobile/src/main/res/layout/token.xml
new file mode 100644
index 00000000..d3c907d8
--- /dev/null
+++ b/mobile/src/main/res/layout/token.xml
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/main/res/menu/menu_main.xml b/mobile/src/main/res/menu/menu_main.xml
new file mode 100644
index 00000000..4c3fee4e
--- /dev/null
+++ b/mobile/src/main/res/menu/menu_main.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/mobile/src/main/res/values-de/strings.xml
similarity index 100%
rename from app/src/main/res/values-de/strings.xml
rename to mobile/src/main/res/values-de/strings.xml
diff --git a/app/src/main/res/values-es/strings.xml b/mobile/src/main/res/values-es/strings.xml
similarity index 100%
rename from app/src/main/res/values-es/strings.xml
rename to mobile/src/main/res/values-es/strings.xml
diff --git a/app/src/main/res/values-fi/strings.xml b/mobile/src/main/res/values-fi/strings.xml
similarity index 100%
rename from app/src/main/res/values-fi/strings.xml
rename to mobile/src/main/res/values-fi/strings.xml
diff --git a/app/src/main/res/values-he/strings.xml b/mobile/src/main/res/values-he/strings.xml
similarity index 100%
rename from app/src/main/res/values-he/strings.xml
rename to mobile/src/main/res/values-he/strings.xml
diff --git a/app/src/main/res/values-ko/strings.xml b/mobile/src/main/res/values-ko/strings.xml
similarity index 100%
rename from app/src/main/res/values-ko/strings.xml
rename to mobile/src/main/res/values-ko/strings.xml
diff --git a/app/src/main/res/values-nb/strings.xml b/mobile/src/main/res/values-nb/strings.xml
similarity index 100%
rename from app/src/main/res/values-nb/strings.xml
rename to mobile/src/main/res/values-nb/strings.xml
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/mobile/src/main/res/values-pt-rPT/strings.xml
similarity index 100%
rename from app/src/main/res/values-pt-rPT/strings.xml
rename to mobile/src/main/res/values-pt-rPT/strings.xml
diff --git a/app/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml
similarity index 100%
rename from app/src/main/res/values-ru/strings.xml
rename to mobile/src/main/res/values-ru/strings.xml
diff --git a/mobile/src/main/res/values-sw600dp/integers.xml b/mobile/src/main/res/values-sw600dp/integers.xml
new file mode 100644
index 00000000..691978f5
--- /dev/null
+++ b/mobile/src/main/res/values-sw600dp/integers.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ 2
+
\ No newline at end of file
diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/mobile/src/main/res/values-tr-rTR/strings.xml
similarity index 100%
rename from app/src/main/res/values-tr-rTR/strings.xml
rename to mobile/src/main/res/values-tr-rTR/strings.xml
diff --git a/mobile/src/main/res/values/colors.xml b/mobile/src/main/res/values/colors.xml
new file mode 100644
index 00000000..179d9d51
--- /dev/null
+++ b/mobile/src/main/res/values/colors.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ #cfd8dc
+ #546e7a
+ @color/md_deep_orange_500
+
+ #F44336
+ #673AB7
+ #03A9F4
+ #4CAF50
+ #FF5722
+ #E91E63
+ #3F51B5
+ #00BCD4
+ #8BC34A
+ #FFC107
+ #795548
+ #9C27B0
+ #2196F3
+ #009688
+ #CDDC39
+ #FF9800
+
+
+ - @color/md_red_500
+ - @color/md_deep_purple_500
+ - @color/md_light_blue_500
+ - @color/md_green_500
+ - @color/md_deep_orange_500
+ - @color/md_pink_500
+ - @color/md_indigo_500
+ - @color/md_cyan_500
+ - @color/md_light_green_500
+ - @color/md_amber_500
+ - @color/md_brown_500
+ - @color/md_purple_500
+ - @color/md_blue_500
+ - @color/md_teal_500
+ - @color/md_lime_500
+ - @color/md_orange_500
+
+
+ #0747A6
+ #3b5998
+ #24292E
+ #292961
+ #FF4500
+ #242424
+
diff --git a/app/src/main/res/values/tags.xml b/mobile/src/main/res/values/dimens.xml
similarity index 81%
rename from app/src/main/res/values/tags.xml
rename to mobile/src/main/res/values/dimens.xml
index bfde3542..f0952d6c 100644
--- a/app/src/main/res/values/tags.xml
+++ b/mobile/src/main/res/values/dimens.xml
@@ -4,7 +4,7 @@
-
- Authors: Nathaniel McCallum
-
- - Copyright (C) 2014 Nathaniel McCallum, Red Hat
+ - Copyright (C) 2018 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.
@@ -19,6 +19,6 @@
- limitations under the License.
-->
-
-
+
+ 16dp
diff --git a/app/src/main/res/values/attrs.xml b/mobile/src/main/res/values/drawables.xml
similarity index 72%
rename from app/src/main/res/values/attrs.xml
rename to mobile/src/main/res/values/drawables.xml
index 5ffc3e50..96291924 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/mobile/src/main/res/values/drawables.xml
@@ -4,7 +4,7 @@
-
- Authors: Nathaniel McCallum
-
- - Copyright (C) 2013 Nathaniel McCallum, Red Hat
+ - Copyright (C) 2018 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.
@@ -18,10 +18,7 @@
- See the License for the specific language governing permissions and
- limitations under the License.
-->
-
-
-
-
-
-
+
+
+ @drawable/fa_gitlab
diff --git a/mobile/src/main/res/values/integers.xml b/mobile/src/main/res/values/integers.xml
new file mode 100644
index 00000000..d35a5010
--- /dev/null
+++ b/mobile/src/main/res/values/integers.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ 1
+
\ No newline at end of file
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
new file mode 100644
index 00000000..c65d2b01
--- /dev/null
+++ b/mobile/src/main/res/values/strings.xml
@@ -0,0 +1,64 @@
+
+
+
+
+ FreeOTP
+
+
+ Ok
+ Close
+ About
+ Cancel
+ Delete
+ Move Up
+ Move Down
+ Add Anyway
+ Unknown Issuer
+ Enable Lock Screen
+
+
+ Send to…
+ Scan for…
+ Scanning for…
+ Bluetooth Devices
+ Copy to…
+ Clipboard
+
+
+ Error while opening camera!
+
+
+ FreeOTP Version %1$s (%2$d)
+ © 2013–2018 - Red Hat, Inc., et al.<br/><br/>FreeOTP is licensed under <a href="http://www.apache.org/licenses/LICENSE-2.0.html">Apache 2.0</a>.<br/><br/>For more information, see our <a href="http://freeotp.github.io">website</a>.
+ Welcome to FreeOTP!\n\nScan a QR code to get started.
+ Delete selected tokens?
+ Deleting a token does not disable it on the server. If you continue, you may be locked out of your account. Deleting a token cannot be undone.
+ Token is unsafe!
+ The token you are attempting to add contains weak cryptographic parameters. Use of this token is strongly discouraged! Please alert your token provider.
+ Token is invalid!
+ The token you are attempting to add is invalid. Please alert your token provider.
+ Error!
+ An error occurred when trying to add a token.
+ This token requires you to authenticate on each use. This means that your device\'s lock screen must be enabled. Please enable lock screen security and add the token again.
+ Lock screen required!
+ Key invalidated!
+ The key for this token has been invalidated by the Android Key Store. This may have been due to a change in your lock screen settings. The token has been removed. Please contact your token provider.
+
diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml
new file mode 100644
index 00000000..56866bee
--- /dev/null
+++ b/mobile/src/main/res/values/styles.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/test/java/org/fedorahosted/freeotp/Base32Test.java b/mobile/src/test/java/org/fedorahosted/freeotp/Base32Test.java
new file mode 100644
index 00000000..4a3b32ca
--- /dev/null
+++ b/mobile/src/test/java/org/fedorahosted/freeotp/Base32Test.java
@@ -0,0 +1,48 @@
+package org.fedorahosted.freeotp;
+
+import junit.framework.TestCase;
+
+import org.fedorahosted.freeotp.utils.Base32;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class Base32Test extends TestCase {
+ @Parameterized.Parameters
+ public static Collection data() {
+ /* RFC 4648, Section 10 */
+ return Arrays.asList(new Object[][] {
+ { "", "" },
+ { "f", "MY" },
+ { "fo", "MZXQ" },
+ { "foo", "MZXW6" },
+ { "foob", "MZXW6YQ" },
+ { "fooba", "MZXW6YTB" },
+ { "foobar", "MZXW6YTBOI" },
+ });
+ }
+
+ private final byte[] mDecoded;
+ private final String mEncoded;
+
+ public Base32Test(String decoded, String encoded) {
+ mDecoded = decoded.getBytes(StandardCharsets.US_ASCII);
+ mEncoded = encoded;
+ }
+
+ @Test
+ public void encode() {
+ assertEquals(mEncoded, Base32.RFC4648.encode(mDecoded));
+ }
+
+ @Test
+ public void decode() throws Base32.DecodingException {
+ Assert.assertArrayEquals(mDecoded, Base32.RFC4648.decode(mEncoded));
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index e7b4def4..6070d9b3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app'
+include ':mobile'