Commit a9a49f9f authored by peturbg's avatar peturbg
Browse files

feat(security): require PIN on launch and for tracking toggle

    When Admin Mode is enabled and a PIN is set, gate the main UI behind a PIN prompt and also require PIN to start/stop tracking from the main switch.
parent d450b0b7
Loading
Loading
Loading
Loading
Loading
+62 −3
Original line number Diff line number Diff line
@@ -77,6 +77,12 @@ public class MainActivity extends AppCompatActivity

    private DbAccess db;

    // When Admin Mode is enabled, we gate the main UI behind a PIN on app launch.
    // This is not meant as a strong security boundary, but it prevents kids / accidental taps
    // from stopping tracking or changing settings.
    private static final String STATE_ADMIN_UNLOCKED = "state_admin_unlocked";
    private boolean adminUnlocked = false;

    /**
     * Initialization
     * @param savedInstanceState Saved state
@@ -96,16 +102,69 @@ public class MainActivity extends AppCompatActivity
            toolbar.getLayoutParams().height = toolbarHeight + systemBars.top;
            return WindowInsetsCompat.CONSUMED;
        });
        if (savedInstanceState != null) {
            adminUnlocked = savedInstanceState.getBoolean(STATE_ADMIN_UNLOCKED, false);
        }

        // Only attach the main fragment after a successful PIN check (when Admin Mode is enabled).
        if (savedInstanceState == null) {
            MainFragment fragment = MainFragment.newInstance();
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.fragment_placeholder, fragment).commit();
            maybeGateOnLaunch();
        } else {
            // If activity is being recreated and we are still locked, re-gate.
            if (needsAdminGate() && !adminUnlocked) {
                maybeGateOnLaunch();
            }
        }
        getSupportFragmentManager().addOnBackStackChangedListener(this);
        //Handle when activity is recreated like on orientation Change
        setHomeUpButton();
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        outState.putBoolean(STATE_ADMIN_UNLOCKED, adminUnlocked);
        super.onSaveInstanceState(outState);
    }

    private boolean needsAdminGate() {
        return AdminLock.isEnabled(this) && AdminLock.isPinSet(this);
    }

    /**
     * If Admin Mode is enabled, ask for PIN before showing the main UI.
     * If user cancels or enters wrong PIN repeatedly and cancels, we close the app.
     */
    private void maybeGateOnLaunch() {
        if (!needsAdminGate()) {
            adminUnlocked = true;
            ensureMainFragment();
            return;
        }
        if (adminUnlocked) {
            ensureMainFragment();
            return;
        }

        AdminLock.showVerifyPinDialog(
                this,
                () -> {
                    adminUnlocked = true;
                    ensureMainFragment();
                },
                this::finish
        );
    }

    private void ensureMainFragment() {
        Fragment existing = getSupportFragmentManager().findFragmentById(R.id.fragment_placeholder);
        if (existing == null) {
            MainFragment fragment = MainFragment.newInstance();
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.fragment_placeholder, fragment)
                    .commit();
        }
    }

    /**
     * On resume
     */
+36 −4
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import net.fabiszewski.ulogger.TrackSummary;
import net.fabiszewski.ulogger.db.DbAccess;
import net.fabiszewski.ulogger.services.LoggerService;
import net.fabiszewski.ulogger.services.WebSyncService;
import net.fabiszewski.ulogger.utils.AdminLock;
import net.fabiszewski.ulogger.utils.PermissionHelper;

import java.text.NumberFormat;
@@ -191,10 +192,41 @@ public class MainFragment extends Fragment implements PermissionHelper.Permissio
     * @param view View
     */
    private void toggleLogging(@NonNull View view, boolean isChecked) {
        if (isChecked && !LoggerService.isRunning()) {
            startLogger(view.getContext());
        } else if (!isChecked && LoggerService.isRunning()) {
            stopLogger(view.getContext());
        final Context ctx = view.getContext();
        final boolean running = LoggerService.isRunning();
        final boolean shouldStart = isChecked && !running;
        final boolean shouldStop = !isChecked && running;

        // If Admin Mode is enabled, require PIN before allowing start/stop tracking.
        if ((shouldStart || shouldStop) && AdminLock.isEnabled(ctx) && AdminLock.isPinSet(ctx)) {
            // Immediately revert UI state while PIN dialog is shown.
            switchLogger.setOnCheckedChangeListener(null);
            switchLogger.setChecked(running);
            switchLogger.setOnCheckedChangeListener(this::toggleLogging);

            AdminLock.showVerifyPinDialog(
                    requireActivity(),
                    () -> {
                        if (shouldStart) {
                            startLogger(ctx);
                        } else {
                            stopLogger(ctx);
                        }
                        // Refresh switch state after action.
                        boolean nowRunning = LoggerService.isRunning();
                        switchLogger.setOnCheckedChangeListener(null);
                        switchLogger.setChecked(nowRunning);
                        switchLogger.setOnCheckedChangeListener(this::toggleLogging);
                    },
                    null
            );
            return;
        }

        if (shouldStart) {
            startLogger(ctx);
        } else if (shouldStop) {
            stopLogger(ctx);
        }
    }