Skip to content

Commit

Permalink
Merge pull request #435 from doubleangels/dev
Browse files Browse the repository at this point in the history
Add biometric/pin lock for privacy
  • Loading branch information
doubleangels authored Feb 19, 2025
2 parents c3727a2 + 3d57efe commit 854c9c1
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 22 deletions.
8 changes: 5 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId "com.doubleangels.nextdnsmanagement"
minSdkVersion 32
targetSdk = 35
versionCode 254
versionName "5.5.5"
versionCode 255
versionName "5.5.6"
resourceConfigurations += ["en", "zh", "nl", "fi", "fr", "de", "in", "it", "ja", "pl", "pt", "es", "sv", "tr"]
}

Expand Down Expand Up @@ -65,17 +65,19 @@ android {
dependencies {
implementation "androidx.activity:activity:1.10.0"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation "androidx.biometric:biometric:1.2.0-alpha05"
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.webkit:webkit:1.12.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

// Only include Firebase Messaging in the gms flavor:
gmsImplementation 'com.google.firebase:firebase-messaging:24.1.0'

implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'io.sentry:sentry-android:8.2.0'
implementation 'jp.wasabeef:blurry:4.0.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}

Expand Down
167 changes: 158 additions & 9 deletions app/src/foss/java/com/doubleangels/nextdnsmanagement/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.Window;
import android.webkit.CookieManager;
import android.webkit.WebChromeClient;
Expand All @@ -27,11 +29,13 @@
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.webkit.WebSettingsCompat;
import androidx.webkit.WebViewFeature;

import com.doubleangels.nextdnsmanagement.biometriclock.BiometricLock;
import com.doubleangels.nextdnsmanagement.protocol.VisualIndicator;
import com.doubleangels.nextdnsmanagement.sentry.SentryInitializer;
import com.doubleangels.nextdnsmanagement.sentry.SentryManager;
Expand All @@ -40,10 +44,12 @@

import java.util.Locale;

import jp.wasabeef.blurry.Blurry;

/**
* Main Activity class that handles initialization of the UI, WebView, and various settings
* like dark mode, locale, etc. Also contains logic for handling low-memory events
* and navigation from the options menu.
* like dark mode, locale, etc. Also contains logic for handling low-memory events,
* biometric re‑authentication with a timeout, and a blurry overlay until authentication.
*/
public class MainActivity extends AppCompatActivity {

Expand All @@ -58,6 +64,10 @@ public class MainActivity extends AppCompatActivity {
// Used to save/restore WebView state across configuration changes
private Bundle webViewState = null;

// Biometric authentication timeout (2 minutes) and timestamp of last successful authentication
private static final long AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
private long lastAuthenticatedTime = 0;

/**
* Saves the current state of the activity, including the WebView state and dark mode setting.
*/
Expand All @@ -78,7 +88,7 @@ protected void onSaveInstanceState(@NonNull Bundle outState) {
* Called when the activity is first created. Restores saved state if available,
* initializes necessary components, and sets up the UI (toolbar, WebView, dark mode, etc).
*/
@SuppressLint("WrongThread")
@SuppressLint({"WrongThread", "SetJavaScriptEnabled"})
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
protected void onCreate(Bundle savedInstanceState) {
Expand All @@ -93,6 +103,9 @@ protected void onCreate(Bundle savedInstanceState) {
// Set the content view for this activity
setContentView(R.layout.activity_main);

// Apply the blurry overlay immediately
showBlurryOverlay();

// Initialize Sentry manager for error tracking and logging
SentryManager sentryManager = new SentryManager(this);

Expand All @@ -118,8 +131,8 @@ protected void onCreate(Bundle savedInstanceState) {

// Determine and apply dark mode preference
setupDarkModeForActivity(
sentryManager,
SharedPreferencesManager.getString("dark_mode", "match")
sentryManager,
SharedPreferencesManager.getString("dark_mode", "match")
);

// Set up any additional UI indicators
Expand Down Expand Up @@ -159,7 +172,8 @@ protected void onPause() {

/**
* Called when the activity is resumed. Resumes the WebView if it was previously initialized.
* If not initialized, sets up a new WebView instance.
* If not initialized, sets up a new WebView instance. Also handles biometric authentication
* with a timeout so that switching activities doesn't authenticate too frequently.
*/
@Override
protected void onResume() {
Expand All @@ -169,6 +183,93 @@ protected void onResume() {
} else if (!isWebViewInitialized) {
setupWebViewForActivity(getString(R.string.main_url));
}

// Hide toolbar buttons and menu items until authenticated.
hideToolbarButtons();
invalidateOptionsMenu(); // This will update the menu visibility via onPrepareOptionsMenu()

// Only prompt for biometric authentication if the timeout has passed.
if (shouldAuthenticate()) {
final BiometricLock biometricLock = new BiometricLock(this);
if (biometricLock.canAuthenticate()) {
showBlurryOverlay();
biometricLock.showPrompt(
"Unlock",
"Authenticate to access and change your settings.",
"",
new BiometricLock.BiometricLockCallback() {
@Override
public void onAuthenticationSucceeded() {
removeBlurryOverlay();
if (webView != null) {
webView.animate().alpha(1f).setDuration(300).start();
}
lastAuthenticatedTime = System.currentTimeMillis();
// Reveal the toolbar buttons and menu items.
showToolbarButtons();
invalidateOptionsMenu();
}

@Override
public void onAuthenticationError(String error) {
removeBlurryOverlay();
Toast.makeText(MainActivity.this, "Authentication error!", Toast.LENGTH_SHORT).show();
}

@Override
public void onAuthenticationFailed() {
Toast.makeText(MainActivity.this, "Authentication failed. Please try again.", Toast.LENGTH_SHORT).show();
}
}
);
} else {
lastAuthenticatedTime = System.currentTimeMillis();
showToolbarButtons();
invalidateOptionsMenu();
}
}
}

/**
* Determines if biometric authentication is required based on the timeout.
*
* @return true if the user should authenticate.
*/
private boolean shouldAuthenticate() {
return System.currentTimeMillis() - lastAuthenticatedTime > AUTH_TIMEOUT_MS;
}

/**
* Applies a blurry overlay over the entire content using the Blurry library.
* This method first checks that the root view has been laid out. If not, it posts a
* runnable to try again later.
*/
private void showBlurryOverlay() {
final ViewGroup container = findViewById(R.id.swipeRefreshLayout);
if (container.getWidth() == 0 || container.getHeight() == 0) {
// The container hasn't been laid out yet; try again on the next layout pass.
container.post(this::showBlurryOverlay);
return;
}
try {
int tintColor = ContextCompat.getColor(this, R.color.blur_tint);
Blurry.with(this)
.radius(10)
.sampling(2)
.color(tintColor)
.onto(container);
} catch (NullPointerException e) {
// Catch and log any exceptions from the Blurry library to avoid crashes.
Log.d("Blurry", "There was an error while applying a blur effect: " + e);
}
}

/**
* Removes the blurry overlay.
*/
private void removeBlurryOverlay() {
ViewGroup container = findViewById(R.id.swipeRefreshLayout);
Blurry.delete(container);
}

/**
Expand Down Expand Up @@ -305,6 +406,9 @@ public void setupWebViewForActivity(String url) {
// Find the WebView in the layout
webView = findViewById(R.id.webView);

// Hide the WebView until authentication is complete.
webView.setAlpha(0f);

// Restore any previously saved state
if (webViewState != null) {
webView.restoreState(webViewState);
Expand Down Expand Up @@ -401,9 +505,9 @@ private void setupDownloadManagerForActivity() {
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
// Set download destination to external files directory (Downloads)
request.setDestinationInExternalFilesDir(
this,
Environment.DIRECTORY_DOWNLOADS,
"NextDNS-Configuration.mobileconfig"
this,
Environment.DIRECTORY_DOWNLOADS,
"NextDNS-Configuration.mobileconfig"
);

// Enqueue the download request
Expand All @@ -426,6 +530,51 @@ private void startIntent(Class<?> targetClass) {
startActivity(intent);
}

/**
* Hides all child views (such as buttons) within the toolbar.
*/
private void hideToolbarButtons() {
// Get the toolbar by its ID.
Toolbar toolbar = findViewById(R.id.toolbar);
// Loop through all the child views in the toolbar.
for (int i = 0; i < toolbar.getChildCount(); i++) {
// Hide each child view.
toolbar.getChildAt(i).setVisibility(android.view.View.GONE);
}
}

/**
* Shows all child views (such as buttons) within the toolbar.
*/
private void showToolbarButtons() {
// Get the toolbar by its ID.
Toolbar toolbar = findViewById(R.id.toolbar);
// Loop through all the child views in the toolbar.
for (int i = 0; i < toolbar.getChildCount(); i++) {
// Make each child view visible.
toolbar.getChildAt(i).setVisibility(android.view.View.VISIBLE);
}
}

/**
* Prepares the options menu by updating the visibility of menu items based on the authentication status.
*
* @param menu The options menu to be prepared.
* @return true after the menu has been prepared.
*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// Determine if the user is authenticated by checking if the elapsed time since the last authentication
// is within the allowed authentication timeout.
boolean isAuthenticated = System.currentTimeMillis() - lastAuthenticatedTime <= AUTH_TIMEOUT_MS;

// Loop through each menu item and set its visibility based on the authentication status.
for (int i = 0; i < menu.size(); i++) {
menu.getItem(i).setVisible(isAuthenticated);
}
return super.onPrepareOptionsMenu(menu);
}

/**
* Initializes the options menu from a resource.
*
Expand Down
Loading

0 comments on commit 854c9c1

Please sign in to comment.