5 Commits
Author SHA1 Message Date
amir 1fa7144a7b feat: login-context approval screen, one-tap Deny, stale-request expiry
Release APK / build (push) Successful in 1m59s
- Approve now opens an in-app review screen showing app/user/IP/geo of the
  request (optional deep-link params: app, user, ip, geo, ts) before biometrics
- Add a Deny button on the approval screen (no biometric needed to reject)
- Reject requests older than 2 min via the ts param (replay safety net)
- README: context params table, new Features bullets, approve-context screenshot
- Bump to v1.2 (versionCode 3)
2026-06-21 02:44:27 +00:00
amir b5286ca6db docs: add 'login approved' screenshot to README 2026-06-19 13:26:00 +00:00
amir ad32e731fb ci: add Gitea Actions release workflow (build, sign, attach APK, auto-notes)
Release APK / build (push) Successful in 1m53s
2026-06-19 09:10:14 +00:00
amir 70ac1cd90e docs: add README + setup screenshot; add .gitignore, untrack build/gradle artifacts 2026-06-19 09:08:35 +00:00
amir 62d7c929fe v1.1: launcher icon, setup screen, biometric enrollment check 2026-06-04 13:03:12 +00:00
2952 changed files with 368 additions and 350457 deletions
+77
View File
@@ -0,0 +1,77 @@
name: Release APK
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Build release APK
run: |
chmod +x ./gradlew
./gradlew :app:assembleRelease --no-daemon
mkdir -p dist
cp app/build/outputs/apk/release/app-release.apk "dist/homelab-mfa.apk"
- name: Generate release notes
id: notes
run: |
TAG="${GITHUB_REF_NAME}"
PREV=$(git tag --sort=-creatordate | grep -A1 -x "$TAG" | tail -n1)
if [ -n "$PREV" ] && [ "$PREV" != "$TAG" ]; then
RANGE="$PREV..$TAG"
else
RANGE=""
fi
echo "## $TAG" > notes.md
echo "" >> notes.md
git log $RANGE --no-merges --pretty=format:'- %s' | grep -viE '^- (merge|wip|fixup)' >> notes.md || true
echo "" >> notes.md
echo "" >> notes.md
echo "**Install:** download \`homelab-mfa.apk\` below." >> notes.md
- name: Publish release + upload APK
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
TAG="${GITHUB_REF_NAME}"
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
BODY=$(python3 -c "import json,sys; print(json.dumps(open('notes.md').read()))")
# find existing release for this tag
REL=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG")
ID=$(echo "$REL" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || echo "")
if [ -z "$ID" ]; then
ID=$(curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$API/releases" -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":$BODY}" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
else
curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$API/releases/$ID" -d "{\"name\":\"$TAG\",\"body\":$BODY}" >/dev/null
fi
# remove existing same-named asset, then upload fresh
for AID in $(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
| python3 -c "import json,sys;[print(a['id']) for a in json.load(sys.stdin) if a['name']=='homelab-mfa.apk']" 2>/dev/null); do
curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$AID" >/dev/null
done
curl -s -X POST -H "Authorization: token $TOKEN" \
"$API/releases/$ID/assets?name=homelab-mfa.apk" \
-F attachment=@dist/homelab-mfa.apk >/dev/null
echo "Released $TAG (id $ID) with homelab-mfa.apk"
+13
View File
@@ -0,0 +1,13 @@
*.iml
.gradle/
.kotlin/
local.properties
.idea/
.DS_Store
/build/
app/build/
captures/
.externalNativeBuild/
.cxx/
*.keystore
*.jks
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
@@ -1,2 +0,0 @@
#Thu Jun 04 12:16:33 UTC 2026
gradle.version=8.6
Binary file not shown.
Binary file not shown.
View File
+73
View File
@@ -0,0 +1,73 @@
# Homelab MFA
Biometric push-approval for your homelab logins. When you sign in through Authentik, your phone
buzzes — approve with a fingerprint, or deny with one tap. A self-hosted alternative to Duo /
Authy push, with **no third-party cloud** in the loop.
## How it works
1. You log in via **Authentik**. A flow step fires an **ntfy** notification to your phone with two
action buttons: **Approve** and **Deny**.
2. The buttons are `homemfa://approve?token=…` / `homemfa://deny?token=…` deep links that open this app.
3. **Approve** opens an in-app review screen showing *who is asking* (see context params below), then
triggers a biometric prompt (fingerprint / face / device PIN). On success the app POSTs
`approve:<token>` to `ntfy.khodak.me/mfa-approve`; your Authentik flow is listening on that topic and
completes the login. You can also **Deny** right from that screen.
4. **Deny** POSTs `deny:<token>` to `mfa-deny` and blocks the login — no biometric needed to reject.
### Optional context params
The approve link may carry extra query params so you can see what you're approving before you do:
| Param | Shown as | Example |
|--------|---------------------|------------------------|
| `app` | 🖥 application name | `app=Grafana` |
| `user` | 👤 username | `user=amir` |
| `ip` | 📍 source IP | `ip=203.0.113.44` |
| `geo` | appended to the IP | `geo=Berlin, DE` |
| `ts` | request timestamp (epoch ms) — used to **expire** stale prompts after 2 min | `ts=1750000000000` |
All are optional and URL-encoded; with none present the app just shows a plain Approve / Deny prompt.
The activity is `showWhenLocked` + `turnScreenOn`, so an approval works straight from the lock screen.
## Features
- **Biometric gate** — `BIOMETRIC_STRONG` or device credential required to approve (deny is instant)
- **Login context** — see the app, user, source IP and geo of the request before approving
- **One-tap Deny** — reject a suspicious login straight from the approval screen, no biometric needed
- **Stale-request expiry** — prompts older than 2 minutes can't be approved (replay safety net)
- **Lock-screen ready** — wakes the screen and shows over the keyguard
- **Self-hosted** — talks only to your own ntfy instance; no Firebase, no vendor cloud
- **Zero stored secrets** — stateless; each request carries a one-time token
- **Setup self-check** — a "Test Biometric" button confirms enrollment before you rely on it
## Screenshots
<p align="center">
<img src="docs/screenshots/setup.png" width="240" alt="Setup / ready screen">
&nbsp;
<img src="docs/screenshots/approve-context.png" width="240" alt="Approval prompt with login context">
&nbsp;
<img src="docs/screenshots/approved.png" width="240" alt="Login approved">
</p>
## Install
1. Download `homelab-mfa.apk` from the [latest release](../../releases/latest)
2. On your Android phone: **Settings → Apps → Install unknown apps** → allow your browser/file manager
3. Open the APK and tap Install
4. Open **Homelab MFA** and tap **Test Biometric** to confirm your fingerprint is enrolled
## Server side
You need:
- An **ntfy** instance reachable at `ntfy.khodak.me` (or change the host in `MainActivity.kt`)
- An **Authentik** flow that, on login, publishes a notification with the two `homemfa://` action
buttons, then waits on the `mfa-approve` / `mfa-deny` topics for the decision
## Requirements
- Android 9.0+ (API 28)
- A fingerprint, face, or device PIN enrolled
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "me.khodak.mfa"
minSdk = 28
targetSdk = 34
versionCode = 1
versionName = "1.0"
versionCode = 3
versionName = "1.2"
}
signingConfigs {
@@ -1,90 +0,0 @@
// Generated by view binder compiler. Do not edit!
package me.khodak.mfa.databinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import androidx.viewbinding.ViewBindings;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;
import me.khodak.mfa.R;
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;
@NonNull
public final ImageView icon;
@NonNull
public final TextView statusText;
@NonNull
public final TextView subText;
private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull ImageView icon,
@NonNull TextView statusText, @NonNull TextView subText) {
this.rootView = rootView;
this.icon = icon;
this.statusText = statusText;
this.subText = subText;
}
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.icon;
ImageView icon = ViewBindings.findChildViewById(rootView, id);
if (icon == null) {
break missingId;
}
id = R.id.statusText;
TextView statusText = ViewBindings.findChildViewById(rootView, id);
if (statusText == null) {
break missingId;
}
id = R.id.subText;
TextView subText = ViewBindings.findChildViewById(rootView, id);
if (subText == null) {
break missingId;
}
return new ActivityMainBinding((LinearLayout) rootView, icon, statusText, subText);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
@@ -1,90 +0,0 @@
// Generated by view binder compiler. Do not edit!
package me.khodak.mfa.databinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import androidx.viewbinding.ViewBindings;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;
import me.khodak.mfa.R;
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;
@NonNull
public final ImageView icon;
@NonNull
public final TextView statusText;
@NonNull
public final TextView subText;
private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull ImageView icon,
@NonNull TextView statusText, @NonNull TextView subText) {
this.rootView = rootView;
this.icon = icon;
this.statusText = statusText;
this.subText = subText;
}
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.icon;
ImageView icon = ViewBindings.findChildViewById(rootView, id);
if (icon == null) {
break missingId;
}
id = R.id.statusText;
TextView statusText = ViewBindings.findChildViewById(rootView, id);
if (statusText == null) {
break missingId;
}
id = R.id.subText;
TextView subText = ViewBindings.findChildViewById(rootView, id);
if (subText == null) {
break missingId;
}
return new ActivityMainBinding((LinearLayout) rootView, icon, statusText, subText);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
@@ -1,92 +0,0 @@
-keep class androidx.core.app.CoreComponentFactory { <init>(); }
-keep class androidx.profileinstaller.ProfileInstallReceiver { <init>(); }
-keep class androidx.startup.InitializationProvider { <init>(); }
-keep class me.khodak.mfa.MainActivity { <init>(); }
-keep class android.widget.Space { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.app.AlertController$RecycleListView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ActionMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ExpandedMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ListMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarContainer { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarContextView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarOverlayLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActivityChooserView$InnerLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.AlertDialogLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ButtonBarLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ContentFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.DialogTitle { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.FitWindowsFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.FitWindowsLinearLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.SearchView$SearchAutoComplete { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.Toolbar { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ViewStubCompat { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.constraintlayout.helper.widget.Flow { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.constraintlayout.widget.ConstraintLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.coordinatorlayout.widget.CoordinatorLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.core.widget.NestedScrollView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.fragment.app.FragmentContainerView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.recyclerview.widget.RecyclerView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.appbar.MaterialToolbar { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.button.MaterialButton { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.button.MaterialButtonToggleGroup { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.chip.Chip { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.datepicker.MaterialCalendarGridView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.internal.BaselineLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.internal.CheckableImageButton { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.internal.ClippableRoundedCornerLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.internal.NavigationMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.internal.NavigationMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.internal.TouchObserverFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.snackbar.Snackbar$SnackbarLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.snackbar.SnackbarContentLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.textfield.TextInputEditText { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.textfield.TextInputLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.timepicker.ChipTextInputComboView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.timepicker.ClockFaceView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.timepicker.ClockHandView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class com.google.android.material.timepicker.TimePickerView { <init>(android.content.Context, android.util.AttributeSet); }
@@ -1,2 +0,0 @@
#- File Locator -
listingFile=../../../outputs/apk/debug/output-metadata.json
@@ -1,2 +0,0 @@
#- File Locator -
listingFile=../../../outputs/apk/release/output-metadata.json
@@ -1,2 +0,0 @@
appMetadataVersion=1.1
androidGradlePluginVersion=8.2.2
@@ -1,2 +0,0 @@
appMetadataVersion=1.1
androidGradlePluginVersion=8.2.2
@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.khodak.mfa"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="28"
android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- suppress DeprecatedClassUsageInspection -->
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<permission
android:name="me.khodak.mfa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature" />
<uses-permission android:name="me.khodak.mfa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />
<application
android:allowBackup="false"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:extractNativeLibs="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.HomelabMFA"
android:usesCleartextTraffic="false" >
<activity
android:name="me.khodak.mfa.MainActivity"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTop"
android:showWhenLocked="true"
android:turnScreenOn="true" >
<!-- Handle homemfa://approve?token=... and homemfa://deny?token=... -->
<intent-filter android:autoVerify="false" >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="homemfa" />
</intent-filter>
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="me.khodak.mfa.androidx-startup"
android:exported="false" >
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
android:value="androidx.startup" />
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup" />
<meta-data
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
android:value="androidx.startup" />
</provider>
<receiver
android:name="androidx.profileinstaller.ProfileInstallReceiver"
android:directBootAware="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.DUMP" >
<intent-filter>
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SKIP_FILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
</intent-filter>
</receiver>
</application>
</manifest>
@@ -1,10 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "COMPATIBLE_SCREEN_MANIFEST",
"kind": "Directory"
},
"applicationId": "me.khodak.mfa.debug",
"variantName": "debug",
"elements": []
}
@@ -1,10 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "COMPATIBLE_SCREEN_MANIFEST",
"kind": "Directory"
},
"applicationId": "me.khodak.mfa",
"variantName": "release",
"elements": []
}
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><Layout layout="activity_main" modulePackage="me.khodak.mfa" filePath="app/src/main/res/layout/activity_main.xml" directory="layout" isMerge="false" isBindingData="false" rootNodeType="android.widget.LinearLayout"><Targets><Target tag="layout/activity_main_0" view="LinearLayout"><Expressions/><location startLine="1" startOffset="0" endLine="36" endOffset="14"/></Target><Target id="@+id/icon" view="ImageView"><Expressions/><location startLine="9" startOffset="4" endLine="14" endOffset="50"/></Target><Target id="@+id/statusText" view="TextView"><Expressions/><location startLine="16" startOffset="4" endLine="24" endOffset="49"/></Target><Target id="@+id/subText" view="TextView"><Expressions/><location startLine="26" startOffset="4" endLine="34" endOffset="34"/></Target></Targets></Layout>
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><Layout layout="activity_main" modulePackage="me.khodak.mfa" filePath="app/src/main/res/layout/activity_main.xml" directory="layout" isMerge="false" isBindingData="false" rootNodeType="android.widget.LinearLayout"><Targets><Target tag="layout/activity_main_0" view="LinearLayout"><Expressions/><location startLine="1" startOffset="0" endLine="36" endOffset="14"/></Target><Target id="@+id/icon" view="ImageView"><Expressions/><location startLine="9" startOffset="4" endLine="14" endOffset="50"/></Target><Target id="@+id/statusText" view="TextView"><Expressions/><location startLine="16" startOffset="4" endLine="24" endOffset="49"/></Target><Target id="@+id/subText" view="TextView"><Expressions/><location startLine="26" startOffset="4" endLine="34" endOffset="34"/></Target></Targets></Layout>
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><Layout layout="activity_main" modulePackage="me.khodak.mfa" filePath="app/src/main/res/layout/activity_main.xml" directory="layout" isMerge="false" isBindingData="false" rootNodeType="android.widget.LinearLayout"><Targets><Target tag="layout/activity_main_0" view="LinearLayout"><Expressions/><location startLine="1" startOffset="0" endLine="36" endOffset="14"/></Target><Target id="@+id/icon" view="ImageView"><Expressions/><location startLine="9" startOffset="4" endLine="14" endOffset="50"/></Target><Target id="@+id/statusText" view="TextView"><Expressions/><location startLine="16" startOffset="4" endLine="24" endOffset="49"/></Target><Target id="@+id/subText" view="TextView"><Expressions/><location startLine="26" startOffset="4" endLine="34" endOffset="34"/></Target></Targets></Layout>
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><Layout layout="activity_main" modulePackage="me.khodak.mfa" filePath="app/src/main/res/layout/activity_main.xml" directory="layout" isMerge="false" isBindingData="false" rootNodeType="android.widget.LinearLayout"><Targets><Target tag="layout/activity_main_0" view="LinearLayout"><Expressions/><location startLine="1" startOffset="0" endLine="36" endOffset="14"/></Target><Target id="@+id/icon" view="ImageView"><Expressions/><location startLine="9" startOffset="4" endLine="14" endOffset="50"/></Target><Target id="@+id/statusText" view="TextView"><Expressions/><location startLine="16" startOffset="4" endLine="24" endOffset="49"/></Target><Target id="@+id/subText" view="TextView"><Expressions/><location startLine="26" startOffset="4" endLine="34" endOffset="34"/></Target></Targets></Layout>
@@ -1,117 +0,0 @@
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
# instead of this one, which turns off the optimization flags.
# Adding optimization introduces certain risks, since for example not all optimizations performed by
# ProGuard works on all versions of Dalvik. The following flags turn off various optimizations
# known to have issues, but the list may not be complete or up to date. (The "arithmetic"
# optimization can be used if you are only targeting Android 2.0 or later.) Make sure you test
# thoroughly if you go this route.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Preserve some attributes that may be required for reflection.
-keepattributes AnnotationDefault,
EnclosingMethod,
InnerClasses,
RuntimeVisibleAnnotations,
RuntimeVisibleParameterAnnotations,
RuntimeVisibleTypeAnnotations,
Signature
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**
# This class is deprecated, but remains for backward compatibility.
-dontwarn android.util.FloatMath
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <init>(...);
}
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-dontnote java.lang.invoke.**
@@ -1,116 +0,0 @@
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize steps (and performs some
# of these optimizations on its own).
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
-dontoptimize
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Preserve some attributes that may be required for reflection.
-keepattributes AnnotationDefault,
EnclosingMethod,
InnerClasses,
RuntimeVisibleAnnotations,
RuntimeVisibleParameterAnnotations,
RuntimeVisibleTypeAnnotations,
Signature
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**
# This class is deprecated, but remains for backward compatibility.
-dontwarn android.util.FloatMath
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <init>(...);
}
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-dontnote java.lang.invoke.**
@@ -1,117 +0,0 @@
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
# Optimizations can be turned on and off in the 'postProcessing' DSL block.
# The configuration below is applied if optimizations are enabled.
# Adding optimization introduces certain risks, since for example not all optimizations performed by
# ProGuard works on all versions of Dalvik. The following flags turn off various optimizations
# known to have issues, but the list may not be complete or up to date. (The "arithmetic"
# optimization can be used if you are only targeting Android 2.0 or later.) Make sure you test
# thoroughly if you go this route.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Preserve some attributes that may be required for reflection.
-keepattributes AnnotationDefault,
EnclosingMethod,
InnerClasses,
RuntimeVisibleAnnotations,
RuntimeVisibleParameterAnnotations,
RuntimeVisibleTypeAnnotations,
Signature
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**
# This class is deprecated, but remains for backward compatibility.
-dontwarn android.util.FloatMath
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <init>(...);
}
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-dontnote java.lang.invoke.**
@@ -1 +0,0 @@
8

Some files were not shown because too many files have changed in this diff Show More