Add null check in ScanManager.downgradeScanModeFromMaxDuty

In function ScanManager.downgradeScanModeFromMaxDuty,
ScanClient.stats.setScanDowngrade is called while ScanClient.stats maybe
null. This is resulting in a NullPointerException. All other functions
inside ScanManager using ScanClient.stats perform a null check
beforehand, and therefore a null check is added to fix the NPE issue.
Unit test to check NPE fix is added as well.

Bug: 279848544
Test: atest BluetoothInstrumentationTests
Change-Id: I2dc50d15f7c45b2cc954c00b8ccc4f4313d7e6da
diff --git a/android/app/src/com/android/bluetooth/gatt/ScanManager.java b/android/app/src/com/android/bluetooth/gatt/ScanManager.java
index ec01343..e29117b 100644
--- a/android/app/src/com/android/bluetooth/gatt/ScanManager.java
+++ b/android/app/src/com/android/bluetooth/gatt/ScanManager.java
@@ -829,7 +829,7 @@
         }
 
         private boolean downgradeScanModeFromMaxDuty(ScanClient client) {
-            if (mAdapterService.getScanDowngradeDurationMillis() == 0) {
+            if ((client.stats == null) || mAdapterService.getScanDowngradeDurationMillis() == 0) {
                 return false;
             }
             if (ScanSettings.SCAN_MODE_LOW_LATENCY == client.settings.getScanMode()) {
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/ScanManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/ScanManagerTest.java
index 4eb503e..9f42f3a 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/ScanManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/ScanManagerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.bluetooth.gatt;
 
-import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH;
 import static android.bluetooth.le.ScanSettings.SCAN_MODE_OPPORTUNISTIC;
 import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER;
@@ -28,7 +27,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.anyString;
@@ -42,13 +40,9 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
-import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProtoEnums;
 import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
 import android.bluetooth.le.ScanSettings;
-import android.bluetooth.le.TransportDiscoveryData;
 import android.content.Context;
 import android.location.LocationManager;
 import android.os.Binder;
@@ -62,12 +56,10 @@
 import androidx.test.rule.ServiceTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.bluetooth.R;
 import com.android.bluetooth.TestUtils;
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.btservice.BluetoothAdapterProxy;
 import com.android.bluetooth.btservice.MetricsLogger;
-import com.android.internal.util.ArrayUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -75,7 +67,6 @@
 import java.util.concurrent.TimeUnit;
 
 import org.junit.After;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -1236,4 +1227,52 @@
                 eq(BluetoothProtoEnums.SCREEN_OFF_EVENT), anyLong());
         Mockito.clearInvocations(mMetricsLogger);
     }
+
+    @Test
+    public void testDowngradeWithNonNullClientAppScanStats() {
+        // Set filtered scan flag
+        final boolean isFiltered = true;
+        // Set scan downgrade duration through Mock
+        when(mAdapterService.getScanDowngradeDurationMillis())
+                .thenReturn((long) DELAY_SCAN_DOWNGRADE_DURATION_MS);
+
+        // Turn off screen
+        sendMessageWaitForProcessed(createScreenOnOffMessage(false));
+        // Create scan client
+        ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_LATENCY);
+        // Start Scan
+        sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
+        assertThat(mScanManager.getRegularScanQueue().contains(client)).isTrue();
+        assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse();
+        assertThat(client.settings.getScanMode()).isEqualTo(SCAN_MODE_LOW_LATENCY);
+        // Set connecting state
+        sendMessageWaitForProcessed(createConnectingMessage(true));
+        // SCAN_MODE_LOW_LATENCY is now downgraded to SCAN_MODE_BALANCED
+        assertThat(client.settings.getScanMode()).isEqualTo(SCAN_MODE_BALANCED);
+    }
+
+    @Test
+    public void testDowngradeWithNullClientAppScanStats() {
+        // Set filtered scan flag
+        final boolean isFiltered = true;
+        // Set scan downgrade duration through Mock
+        when(mAdapterService.getScanDowngradeDurationMillis())
+                .thenReturn((long) DELAY_SCAN_DOWNGRADE_DURATION_MS);
+
+        // Turn off screen
+        sendMessageWaitForProcessed(createScreenOnOffMessage(false));
+        // Create scan client
+        ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_LATENCY);
+        // Start Scan
+        sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
+        assertThat(mScanManager.getRegularScanQueue().contains(client)).isTrue();
+        assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse();
+        assertThat(client.settings.getScanMode()).isEqualTo(SCAN_MODE_LOW_LATENCY);
+        // Set AppScanStats to null
+        client.stats = null;
+        // Set connecting state
+        sendMessageWaitForProcessed(createConnectingMessage(true));
+        // Since AppScanStats is null, no downgrade takes place for scan mode
+        assertThat(client.settings.getScanMode()).isEqualTo(SCAN_MODE_LOW_LATENCY);
+    }
 }