Merge "Skip authentication if device was unlocked recently" into main
diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java
index c776987..1fc9101 100644
--- a/src/com/android/settings/network/NetworkProviderSettings.java
+++ b/src/com/android/settings/network/NetworkProviderSettings.java
@@ -706,7 +706,7 @@
                 forget(mSelectedWifiEntry);
                 return true;
             case MENU_ID_SHARE:
-                WifiDppUtils.showLockScreen(getContext(),
+                WifiDppUtils.showLockScreenForWifiSharing(getContext(),
                         () -> launchWifiDppConfiguratorActivity(mSelectedWifiEntry));
                 return true;
             case MENU_ID_MODIFY:
diff --git a/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java b/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java
index 8f9741a..4ffe279 100644
--- a/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java
+++ b/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java
@@ -57,7 +57,8 @@
     @Override
     public boolean handlePreferenceTreeClick(Preference preference) {
         if (KEY_ADD_DEVICE.equals(preference.getKey())) {
-            WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorQrCodeScanner());
+            WifiDppUtils.showLockScreenForWifiSharing(mContext,
+                    () -> launchWifiDppConfiguratorQrCodeScanner());
             return true; /* click is handled */
         }
 
diff --git a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
index a8d7f41..ecddecf 100644
--- a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
+++ b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
@@ -980,7 +980,8 @@
      * Share the wifi network with QR code.
      */
     private void shareNetwork() {
-        WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorActivity());
+        WifiDppUtils.showLockScreenForWifiSharing(mContext,
+                () -> launchWifiDppConfiguratorActivity());
     }
 
     /**
diff --git a/src/com/android/settings/wifi/dpp/WifiDppUtils.java b/src/com/android/settings/wifi/dpp/WifiDppUtils.java
index 23a6a54..24ab496 100644
--- a/src/com/android/settings/wifi/dpp/WifiDppUtils.java
+++ b/src/com/android/settings/wifi/dpp/WifiDppUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.settings.wifi.dpp;
 
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.Intent;
@@ -33,6 +35,9 @@
 import android.security.keystore.KeyGenParameterSpec;
 import android.security.keystore.KeyProperties;
 import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
 
 import com.android.settings.R;
 import com.android.settings.Utils;
@@ -58,6 +63,8 @@
  * @see WifiQrCode
  */
 public class WifiDppUtils {
+    private static final String TAG = "WifiDppUtils";
+
     /**
      * The fragment tag specified to FragmentManager for container activities to manage fragments.
      */
@@ -109,7 +116,15 @@
 
     private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3);
 
-    private static final String AES_CBC_PKCS7_PADDING = "AES/CBC/PKCS7Padding";
+    /**
+     * Parameters to check whether the device has been locked recently
+     */
+    @VisibleForTesting
+    public static final String AES_CBC_PKCS7_PADDING = "AES/CBC/PKCS7Padding";
+    @VisibleForTesting
+    public static final String WIFI_SHARING_KEY_ALIAS = "wifi_sharing_auth_key";
+    @VisibleForTesting
+    public static final int WIFI_SHARING_MAX_UNLOCK_SECONDS = 60;
 
     /**
      * Returns whether the device support WiFi DPP.
@@ -426,51 +441,75 @@
      * Shows authentication screen to confirm credentials (pin, pattern or password) for the current
      * user of the device.
      *
-     * @param context The {@code Context} used to get {@code KeyguardManager} service
+     * @param context         The {@code Context} used to get {@code KeyguardManager} service
      * @param successRunnable The {@code Runnable} which will be executed if the user does not setup
      *                        device security or if lock screen is unlocked
      */
-    public static void showLockScreen(Context context, Runnable successRunnable) {
-        final KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(
-                Context.KEYGUARD_SERVICE);
-
-        if (keyguardManager.isKeyguardSecure()) {
-            final BiometricPrompt.AuthenticationCallback authenticationCallback =
-                    new BiometricPrompt.AuthenticationCallback() {
-                        @Override
-                        public void onAuthenticationSucceeded(
-                                    BiometricPrompt.AuthenticationResult result) {
-                            successRunnable.run();
-                        }
-
-                        @Override
-                        public void onAuthenticationError(int errorCode, CharSequence errString) {
-                            //Do nothing
-                        }
-            };
-
-            final int userId = UserHandle.myUserId();
-
-            final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context)
-                    .setTitle(context.getText(R.string.wifi_dpp_lockscreen_title));
-
-            if (keyguardManager.isDeviceSecure()) {
-                builder.setDeviceCredentialAllowed(true);
-                builder.setTextForDeviceCredential(
-                        null /* title */,
-                        Utils.getConfirmCredentialStringForUser(
-                                context, userId, Utils.getCredentialType(context, userId)),
-                        null /* description */);
-            }
-
-            final BiometricPrompt bp = builder.build();
-            final Handler handler = new Handler(Looper.getMainLooper());
-            bp.authenticate(new CancellationSignal(),
-                    runnable -> handler.post(runnable),
-                    authenticationCallback);
-        } else {
+    public static void showLockScreen(@NonNull Context context, @NonNull Runnable successRunnable) {
+        KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+        if (keyguardManager == null || !keyguardManager.isKeyguardSecure()) {
             successRunnable.run();
+            return;
         }
+        showLockScreen(context, successRunnable, keyguardManager);
+    }
+
+    /**
+     * Shows authentication screen to confirm credentials (pin, pattern or password) for the
+     * current user of the device. But if the device has been unlocked recently, the
+     * authentication screen will be skipped.
+     *
+     * @param context         The {@code Context} used to get {@code KeyguardManager} service
+     * @param successRunnable The {@code Runnable} which will be executed if the user does not setup
+     *                        device security or if lock screen is unlocked
+     */
+    public static void showLockScreenForWifiSharing(@NonNull Context context,
+            @NonNull Runnable successRunnable) {
+        KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+        if (keyguardManager == null || !keyguardManager.isKeyguardSecure()) {
+            successRunnable.run();
+            return;
+        }
+        if (isUnlockedWithinSeconds(WIFI_SHARING_KEY_ALIAS, WIFI_SHARING_MAX_UNLOCK_SECONDS)) {
+            Log.d(TAG, "Bypassing the lock screen because the device was unlocked recently.");
+            successRunnable.run();
+            return;
+        }
+        showLockScreen(context, successRunnable, keyguardManager);
+    }
+
+    @SuppressLint("MissingPermission")
+    private static void showLockScreen(@NonNull Context context, @NonNull Runnable successRunnable,
+            @NonNull KeyguardManager keyguardManager) {
+        BiometricPrompt.AuthenticationCallback authenticationCallback =
+                new BiometricPrompt.AuthenticationCallback() {
+                    @Override
+                    public void onAuthenticationSucceeded(
+                            BiometricPrompt.AuthenticationResult result) {
+                        successRunnable.run();
+                    }
+
+                    @Override
+                    public void onAuthenticationError(int errorCode, CharSequence errString) {
+                        //Do nothing
+                    }
+                };
+        int userId = UserHandle.myUserId();
+        BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context)
+                .setTitle(context.getText(R.string.wifi_dpp_lockscreen_title));
+        if (keyguardManager.isDeviceSecure()) {
+            builder.setDeviceCredentialAllowed(true);
+            builder.setTextForDeviceCredential(
+                    null /* title */,
+                    Utils.getConfirmCredentialStringForUser(
+                            context, userId, Utils.getCredentialType(context, userId)),
+                    null /* description */);
+        }
+        BiometricPrompt bp = builder.build();
+        Handler handler = new Handler(Looper.getMainLooper());
+        bp.authenticate(new CancellationSignal(),
+                runnable -> handler.post(runnable),
+                authenticationCallback);
     }
 
     /**
diff --git a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java
index 1bcff1e..d2d26ab 100644
--- a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java
+++ b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java
@@ -123,7 +123,7 @@
     }
 
     private void shareHotspotNetwork(Intent intent) {
-        WifiDppUtils.showLockScreen(mContext, () -> {
+        WifiDppUtils.showLockScreenForWifiSharing(mContext, () -> {
             mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
                     SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_HOTSPOT_QR_CODE,
                     SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR,
diff --git a/tests/spa_unit/src/com/android/settings/spa/wifi/dpp/WifiDppUtilsTest.kt b/tests/spa_unit/src/com/android/settings/spa/wifi/dpp/WifiDppUtilsTest.kt
new file mode 100644
index 0000000..31ee9e6
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/wifi/dpp/WifiDppUtilsTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.android.settings.spa.wifi.dpp
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.hardware.biometrics.BiometricPrompt
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.wifi.dpp.WifiDppUtils
+import java.security.InvalidKeyException
+import java.security.Key
+import javax.crypto.Cipher
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoSession
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.doThrow
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+@RunWith(AndroidJUnit4::class)
+class WifiDppUtilsTest {
+    private lateinit var mockSession: MockitoSession
+
+    private val runnable = mock<Runnable>()
+    private val cipher = mock<Cipher>()
+    private var mockKeyguardManager = mock<KeyguardManager>()
+    private var context: Context =
+        spy(ApplicationProvider.getApplicationContext()) {
+            on { getSystemService(KeyguardManager::class.java) } doReturn mockKeyguardManager
+        }
+
+    @Before
+    fun setUp() {
+        mockSession =
+            ExtendedMockito.mockitoSession()
+                .initMocks(this)
+                .mockStatic(Cipher::class.java)
+                .mockStatic(BiometricPrompt::class.java)
+                .mockStatic(BiometricPrompt.Builder::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        whenever(context.applicationContext).thenReturn(context)
+    }
+
+    @After
+    fun tearDown() {
+        mockSession.finishMocking()
+    }
+
+    @Test
+    fun showLockScreen_notKeyguardSecure_runRunnable() {
+        mockKeyguardManager.stub { on { isKeyguardSecure } doReturn false }
+
+        WifiDppUtils.showLockScreen(context, runnable)
+
+        verify(runnable).run()
+    }
+
+    @Test
+    fun showLockScreen_isKeyguardSecure_doNotRunRunnable() {
+        mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true }
+
+        try {
+            WifiDppUtils.showLockScreen(context, runnable)
+        } catch (_: Exception) {}
+
+        verify(runnable, never()).run()
+    }
+
+    @Test
+    fun showLockScreenForWifiSharing_deviceUnlockedRecently_runRunnable() {
+        mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true }
+        whenever(Cipher.getInstance(WifiDppUtils.AES_CBC_PKCS7_PADDING)).thenReturn(cipher)
+
+        WifiDppUtils.showLockScreenForWifiSharing(context, runnable)
+
+        verify(runnable).run()
+    }
+
+    @Test
+    fun showLockScreenForWifiSharing_deviceNotUnlockedRecently_doNotRunRunnable() {
+        mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true }
+        whenever(Cipher.getInstance(WifiDppUtils.AES_CBC_PKCS7_PADDING)).thenReturn(cipher)
+        doThrow(InvalidKeyException()).whenever(cipher).init(anyInt(), any<Key>())
+
+        try {
+            WifiDppUtils.showLockScreenForWifiSharing(context, runnable)
+        } catch (_: Exception) {}
+
+        verify(runnable, never()).run()
+    }
+}