diff --git a/android.yaml b/android.yaml
new file mode 100644
index 00000000..fb76d25e
--- /dev/null
+++ b/android.yaml
@@ -0,0 +1,62 @@
+name: Android Alpha 2
+
+on:
+ push:
+ branches: [ feature/android-native ]
+ pull_request:
+ branches: [ feature/android-native ]
+
+jobs:
+ build:
+ name: Debug apk
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Yubikit Next
+ uses: actions/checkout@v2
+ with:
+ repository: Yubico/yubikit-android
+ ref: next
+ path: kit
+
+ - name: set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: '11'
+
+ - name: Build Yubikit-android
+ run: ./gradlew --stacktrace check test build javadocJar publishToMavenLocal
+ working-directory: ./kit
+
+ - name: Install Flutter
+ uses: subosito/flutter-action@v1
+ with:
+ channel: 'beta'
+ - run: |
+ flutter config
+ flutter --version
+
+ - uses: actions/checkout@v2
+ with:
+ path: 'app'
+
+ - name: Run tests
+ run: |
+ flutter test
+ flutter analyze
+ working-directory: ./app
+
+ - name: Build the App
+ run: flutter build apk --debug
+ working-directory: ./app
+
+ - name: Upload artifacts
+ run: |
+ mkdir artifacts
+ mv build/app/outputs/flutter-apk/app-debug.apk artifacts/yubico-authenticator-alpha2.apk
+ working-directory: ./app
+
+ - uses: actions/upload-artifact@v2
+ with:
+ name: yubico-authenticator-alpha2.apk.zip
+ path: app/artifacts/*
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 00000000..6f568019
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,13 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 00000000..9945d2fd
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,82 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: "org.jetbrains.kotlin.plugin.serialization"
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion flutter.compileSdkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ applicationId "com.yubico.authenticator"
+ minSdkVersion project.minSdkVersion
+ targetSdkVersion project.targetSdkVersion
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ api "com.yubico.yubikit:android:$project.yubiKitVersion"
+ api "com.yubico.yubikit:management:$project.yubiKitVersion"
+ api "com.yubico.yubikit:oath:$project.yubiKitVersion"
+
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
+
+ // Lifecycle
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
+ implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
+
+ implementation 'androidx.fragment:fragment-ktx:1.4.1'
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..fe5b5c89
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..6662ae67
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/com/yubico/authenticator/api/AppApiImpl.kt b/android/app/src/main/java/com/yubico/authenticator/api/AppApiImpl.kt
new file mode 100644
index 00000000..70d79445
--- /dev/null
+++ b/android/app/src/main/java/com/yubico/authenticator/api/AppApiImpl.kt
@@ -0,0 +1,28 @@
+package com.yubico.authenticator.api
+
+import com.yubico.authenticator.MainViewModel
+import com.yubico.authenticator.OperationContext
+import com.yubico.authenticator.ParameterException
+
+class AppApiImpl(private val modelView: MainViewModel) : Pigeon.AppApi {
+
+ override fun setContext(subPageIndex: Long?, result: Pigeon.Result?) {
+
+ result?.run {
+ if (subPageIndex == null) {
+ result.error(ParameterException())
+ return
+ }
+
+ val contextValue = OperationContext.getByValue(subPageIndex)
+ if (contextValue == OperationContext.Invalid) {
+ // returning success is all we can do here
+ result.success(null)
+ return
+ }
+
+ modelView.setContext(contextValue)
+ result.success(null)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/yubico/authenticator/api/HDialogApiImpl.kt b/android/app/src/main/java/com/yubico/authenticator/api/HDialogApiImpl.kt
new file mode 100644
index 00000000..5d67eda7
--- /dev/null
+++ b/android/app/src/main/java/com/yubico/authenticator/api/HDialogApiImpl.kt
@@ -0,0 +1,11 @@
+package com.yubico.authenticator.api
+
+import com.yubico.authenticator.MainViewModel
+
+class HDialogApiImpl(private val viewModel : MainViewModel) : Pigeon.HDialogApi {
+ override fun dialogClosed(result: Pigeon.Result?) {
+ result?.run {
+ viewModel.onDialogClosed(result)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/yubico/authenticator/api/OathApiImpl.kt b/android/app/src/main/java/com/yubico/authenticator/api/OathApiImpl.kt
new file mode 100644
index 00000000..2509d1cb
--- /dev/null
+++ b/android/app/src/main/java/com/yubico/authenticator/api/OathApiImpl.kt
@@ -0,0 +1,101 @@
+package com.yubico.authenticator.api
+
+import com.yubico.authenticator.MainViewModel
+import com.yubico.authenticator.api.Pigeon.OathApi
+import com.yubico.authenticator.api.Pigeon.Result
+
+class OathApiImpl(private val viewModel: MainViewModel) : OathApi {
+
+ override fun reset(result: Result?) {
+ result?.run {
+ viewModel.resetOathSession(result)
+ }
+ }
+
+ override fun unlock(
+ password: String?,
+ remember: Boolean?,
+ result: Result?
+ ) {
+ result?.run {
+ viewModel.unlockOathSession(password, remember, result)
+ }
+ }
+
+ override fun setPassword(
+ newPassword: String?,
+ result: Result?
+ ) {
+ result?.run {
+ viewModel.setOathPassword(null, newPassword, result)
+ }
+ }
+
+ override fun changePassword(
+ currentPassword: String?,
+ newPassword: String?,
+ result: Result?
+ ) {
+ result?.run {
+ viewModel.setOathPassword(currentPassword, newPassword, result)
+ }
+ }
+
+ override fun unsetPassword(currentPassword: String?, result: Result?) {
+ result?.run {
+ viewModel.unsetOathPassword(currentPassword, result)
+ }
+ }
+
+ override fun forgetPassword(result: Result?) {
+ result?.run {
+ viewModel.forgetPassword(result)
+ }
+ }
+
+ override fun addAccount(
+ uri: String?,
+ requireTouch: Boolean?,
+ result: Result?
+ ) {
+ result?.run {
+ viewModel.addAccount(uri, requireTouch, result)
+ }
+ }
+
+ override fun renameAccount(uri: String?, name: String?, result: Result?) {
+ result?.run {
+ viewModel.renameCredential(uri, name, null, result)
+ }
+ }
+
+ override fun renameAccountWithIssuer(
+ uri: String?,
+ name: String?,
+ issuer: String?,
+ result: Result?
+ ) {
+ result?.run {
+ viewModel.renameCredential(uri, name, issuer, result)
+ }
+ }
+
+ override fun deleteAccount(uri: String?, result: Result?) {
+ result?.run {
+ viewModel.deleteAccount(uri, result)
+ }
+ }
+
+ override fun refreshCodes(result: Result?) {
+ result?.run {
+ viewModel.refreshOathCodes(result)
+ }
+ }
+
+ override fun calculate(uri: String?, result: Result?) {
+ result?.run {
+ viewModel.calculate(uri, result)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/yubico/authenticator/api/Pigeon.java b/android/app/src/main/java/com/yubico/authenticator/api/Pigeon.java
new file mode 100644
index 00000000..75d90e5f
--- /dev/null
+++ b/android/app/src/main/java/com/yubico/authenticator/api/Pigeon.java
@@ -0,0 +1,673 @@
+// Autogenerated from Pigeon (v1.0.19), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package com.yubico.authenticator.api;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
+public class Pigeon {
+
+ public interface Result {
+ void success(T result);
+ void error(Throwable error);
+ }
+ private static class OathApiCodec extends StandardMessageCodec {
+ public static final OathApiCodec INSTANCE = new OathApiCodec();
+ private OathApiCodec() {}
+ }
+
+ /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
+ public interface OathApi {
+ void reset(Result result);
+ void unlock(String password, Boolean remember, Result result);
+ void setPassword(String newPassword, Result result);
+ void changePassword(String currentPassword, String newPassword, Result result);
+ void unsetPassword(String currentPassword, Result result);
+ void forgetPassword(Result result);
+ void addAccount(String uri, Boolean requireTouch, Result result);
+ void renameAccount(String uri, String name, Result result);
+ void renameAccountWithIssuer(String uri, String name, String issuer, Result result);
+ void deleteAccount(String uri, Result result);
+ void refreshCodes(Result result);
+ void calculate(String uri, Result result);
+
+ /** The codec used by OathApi. */
+ static MessageCodec