uwb(multi-device-tests): Add test for verify 3p app usage restrictions

Add a shell command to simulate app moving to foreground/background to
help with CTS testing

Also, use setExact for the bg timer to ensure we stop bg sessions on
time.

Bug: 250619496
Test: atest CtsUwbMultiDeviceTestCase_FiraRangingTests
Test: atest ServiceUwbTests
Change-Id: I5f241f764c14cbedde3dabc5905a451ed6d94e98
diff --git a/service/java/com/android/server/uwb/UwbSessionManager.java b/service/java/com/android/server/uwb/UwbSessionManager.java
index f3d1d54..03bc49e 100644
--- a/service/java/com/android/server/uwb/UwbSessionManager.java
+++ b/service/java/com/android/server/uwb/UwbSessionManager.java
@@ -115,7 +115,8 @@
 import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
-public class UwbSessionManager implements INativeUwbManager.SessionNotification {
+public class UwbSessionManager implements INativeUwbManager.SessionNotification,
+        ActivityManager.OnUidImportanceListener {
 
     private static final String TAG = "UwbSessionManager";
     private static final byte OPERATION_TYPE_INIT_SESSION = 0;
@@ -194,34 +195,35 @@
         return mIsRangeDataNtfConfigEnableDisableSupported;
     }
 
+    @Override
+    public void onUidImportance(final int uid, final int importance) {
+        Handler handler = new Handler(mLooper);
+        handler.post(() -> {
+            List<UwbSession> uwbSessions = mNonPrivilegedUidToFiraSessionsTable.get(uid);
+            // Not a uid in the watch list
+            if (uwbSessions == null) return;
+            // Feature not supported on device.
+            if (!isRangeDataNtfConfigEnableDisableSupported()) return;
+            boolean newModeHasNonPrivilegedFgApp =
+                    UwbInjector.isForegroundAppOrServiceImportance(importance);
+            for (UwbSession uwbSession : uwbSessions) {
+                // already at correct state.
+                if (newModeHasNonPrivilegedFgApp == uwbSession.hasNonPrivilegedFgApp()) {
+                    continue;
+                }
+                uwbSession.setHasNonPrivilegedFgApp(newModeHasNonPrivilegedFgApp);
+                // Reconfigure the session based on the new fg/bg state.
+                Log.i(TAG, "App state change. IsFg: " + newModeHasNonPrivilegedFgApp
+                        + ". Reconfiguring session ntf control");
+                uwbSession.reconfigureFiraSessionOnFgStateChange();
+            }
+        });
+    }
+
     // Detect UIDs going foreground/background
     private void registerUidImportanceTransitions() {
-        Handler handler = new Handler(mLooper);
-        mActivityManager.addOnUidImportanceListener(new ActivityManager.OnUidImportanceListener() {
-            @Override
-            public void onUidImportance(final int uid, final int importance) {
-                handler.post(() -> {
-                    List<UwbSession> uwbSessions = mNonPrivilegedUidToFiraSessionsTable.get(uid);
-                    // Not a uid in the watch list
-                    if (uwbSessions == null) return;
-                    // Feature not supported on device.
-                    if (!isRangeDataNtfConfigEnableDisableSupported()) return;
-                    boolean newModeHasNonPrivilegedFgApp =
-                            UwbInjector.isForegroundAppOrServiceImportance(importance);
-                    for (UwbSession uwbSession : uwbSessions) {
-                        // already at correct state.
-                        if (newModeHasNonPrivilegedFgApp == uwbSession.hasNonPrivilegedFgApp()) {
-                            continue;
-                        }
-                        uwbSession.setHasNonPrivilegedFgApp(newModeHasNonPrivilegedFgApp);
-                        // Reconfigure the session based on the new fg/bg state.
-                        Log.i(TAG, "App state change. IsFg: " + newModeHasNonPrivilegedFgApp
-                                + ". Reconfiguring session ntf control");
-                        uwbSession.reconfigureFiraSessionOnFgStateChange();
-                    }
-                });
-            }
-        }, IMPORTANCE_FOREGROUND_SERVICE);
+        mActivityManager.addOnUidImportanceListener(
+                UwbSessionManager.this, IMPORTANCE_FOREGROUND_SERVICE);
     }
 
     private static boolean hasAllRangingResultError(@NonNull UwbRangingData rangingData) {
@@ -435,7 +437,7 @@
         AttributionSource nonPrivilegedAppAttrSource =
                 uwbSession.getAnyNonPrivilegedAppInAttributionSource();
         if (nonPrivilegedAppAttrSource != null) {
-            Log.d(TAG, "Found a non fg 3p app/service in the attribution source of request: "
+            Log.d(TAG, "Found a 3p app/service in the attribution source of request: "
                     + nonPrivilegedAppAttrSource);
             // TODO(b/211445008): Move this operation to uwb thread.
             long identity = Binder.clearCallingIdentity();
@@ -1995,7 +1997,7 @@
                             + " Stopping session");
                     stopRangingInternal(mSessionHandle, true /* triggeredBySystemPolicy */);
                 };
-                mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                         mUwbInjector.getElapsedSinceBootMillis()
                                 + NON_PRIVILEGED_BG_APP_TIMEOUT_MS,
                         NON_PRIVILEGED_BG_APP_TIMER_TAG,
diff --git a/service/java/com/android/server/uwb/UwbShellCommand.java b/service/java/com/android/server/uwb/UwbShellCommand.java
index 72ad082..36cdb7a 100644
--- a/service/java/com/android/server/uwb/UwbShellCommand.java
+++ b/service/java/com/android/server/uwb/UwbShellCommand.java
@@ -16,6 +16,8 @@
 
 package com.android.server.uwb;
 
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.uwb.UwbAddress.SHORT_ADDRESS_BYTE_LENGTH;
 
 import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_3;
@@ -54,6 +56,7 @@
 import android.annotation.NonNull;
 import android.content.AttributionSource;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Looper;
@@ -935,6 +938,21 @@
                 case "get-country-code":
                     pw.println("Uwb Country Code = " + mUwbCountryCode.getCountryCode());
                     return 0;
+                case "simulate-app-state-change": {
+                    String appPackageName = getNextArgRequired();
+                    boolean isFg = getNextArgRequiredTrueOrFalse("foreground", "background");
+                    int uid = 0;
+                    try {
+                        uid = mContext.getPackageManager().getApplicationInfo(
+                                appPackageName, 0).uid;
+                    } catch (PackageManager.NameNotFoundException e) {
+                        pw.println("Unable to find package name: " + appPackageName);
+                        return -1;
+                    }
+                    mUwbInjector.getUwbSessionManager().onUidImportance(
+                            uid, isFg ? IMPORTANCE_FOREGROUND : IMPORTANCE_BACKGROUND);
+                    return 0;
+                }
                 case "set-log-mode": {
                     String logMode = getNextArgRequired();
                     if (!UciLogModeStore.isValid(logMode)) {
@@ -1167,6 +1185,8 @@
         pw.println("    Disable vendor diagnostics notification");
         pw.println("  take-bugreport");
         pw.println("    take bugreport through betterBug or alternatively bugreport manager");
+        pw.println("  simulate-app-state-change <package-name> foreground|background");
+        pw.println("    Simulate app moving to foreground/background to test stack handling");
     }
 
     private void onHelpPrivileged(PrintWriter pw) {
diff --git a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
index 3aef295..45e7480 100644
--- a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
+++ b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
@@ -1570,7 +1570,7 @@
         // Verify the appropriate timer is setup.
         ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListenerCaptor =
                 ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class);
-        verify(mAlarmManager).set(
+        verify(mAlarmManager).setExact(
                 anyInt(), anyLong(), eq(UwbSession.NON_PRIVILEGED_BG_APP_TIMER_TAG),
                 alarmListenerCaptor.capture(), any());
         assertThat(alarmListenerCaptor.getValue()).isNotNull();
diff --git a/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_decorator.py b/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_decorator.py
index 50711bb..8c2f137 100644
--- a/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_decorator.py
+++ b/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_decorator.py
@@ -70,9 +70,9 @@
                             (ranging_event, round(time.time() - start_time, 2)))
           self.clear_ranging_session_callback_events(session)
           return
-      except TimeoutError:
+      except errors.CallbackHandlerTimeoutError:
         self.log.warn("Failed to receive 'RangingSessionCallback' event")
-    raise TimeoutError("Failed to receive '%s' event" % ranging_event)
+        raise TimeoutError("Failed to receive '%s' event" % ranging_event)
 
   def open_fira_ranging(self,
                         params: uwb_ranging_params.UwbRangingParams,
diff --git a/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py b/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py
index 9a2b806..af871f1 100644
--- a/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py
+++ b/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py
@@ -3,6 +3,8 @@
 import logging
 import random
 import sys
+import time
+from threading import Thread
 from typing import List
 
 from mobly import asserts
@@ -33,6 +35,9 @@
     "test_stop_responder_ranging_nearby_share_profile",
     "test_ranging_device_tracker_profile_with_airplane_mode_toggle",
     "test_ranging_nearby_share_profile_with_airplane_mode_toggle",
+    "test_ranging_default_params_move_to_bg_and_fg",
+    "test_ranging_default_params_move_to_bg_and_stay_there_stops_session",
+    "test_ranging_default_params_no_valid_reports_stops_session",
 )
 
 
@@ -86,10 +91,17 @@
 
   def teardown_test(self):
     super().teardown_test()
-    self.responder.stop_ranging()
-    self.initiator.stop_ranging()
-    self.responder.close_ranging()
-    self.initiator.close_ranging()
+    try:
+      self.initiator.stop_ranging()
+      self.initiator.close_ranging()
+    except:
+      pass
+    try:
+      self.responder.stop_ranging()
+      self.responder.close_ranging()
+    except:
+      pass
+
 
   ### Helper Methods ###
 
@@ -280,6 +292,29 @@
     initiator.reconfigure_fira_ranging(reconfigure_params)
     uwb_test_utils.verify_peer_found(initiator, peer_addr)
 
+
+  @staticmethod
+  def _move_snippet_to_bg(device: uwb_ranging_decorator.UwbRangingDecorator):
+      """Simulate moving snippet app to background
+
+      Args:
+        device: The uwb device object.
+      """
+      device.ad.adb.shell(
+          ["cmd", "uwb", "simulate-app-state-change", "com.google.snippet.uwb", "background"])
+
+
+  @staticmethod
+  def _move_snippet_to_fg(device: uwb_ranging_decorator.UwbRangingDecorator):
+      """Simulate moving snippet app to foreground
+
+      Args:
+        device: The uwb device object.
+      """
+      device.ad.adb.shell(
+          ["cmd", "uwb", "simulate-app-state-change", "com.google.snippet.uwb", "foreground"])
+
+
   ### Test Cases ###
 
   def test_ranging_default_params(self):
@@ -1065,6 +1100,142 @@
         self.initiator, self.responder, initiator_params, responder_params,
         self.responder_addr)
 
+  def test_ranging_default_params_move_to_bg_and_fg(self):
+      """
+      1. Verifies ranging with default Fira parameters.
+      2. Move app to background (turn screen off).
+      3. Ensures the app does not receive range data notifications
+      4. Move app to foreground (turn screen on).
+      5. Ensures the app starts receiving range data notifications
+      """
+      initiator_params = uwb_ranging_params.UwbRangingParams(
+          device_role=uwb_ranging_params.FiraParamEnums.DEVICE_ROLE_INITIATOR,
+          device_type=uwb_ranging_params.FiraParamEnums.DEVICE_TYPE_CONTROLLER,
+          device_address=self.initiator_addr,
+          destination_addresses=[self.responder_addr],
+      )
+      responder_params = uwb_ranging_params.UwbRangingParams(
+          device_role=uwb_ranging_params.FiraParamEnums.DEVICE_ROLE_RESPONDER,
+          device_type=uwb_ranging_params.FiraParamEnums.DEVICE_TYPE_CONTROLEE,
+          device_address=self.responder_addr,
+          destination_addresses=[self.initiator_addr],
+      )
+      self._verify_one_to_one_ranging(self.initiator, self.responder,
+                                      initiator_params, responder_params,
+                                      self.responder_addr)
+
+      # Turn screen off to simulate app moving to background.
+      RangingTest._move_snippet_to_bg(self.initiator)
+      time.sleep(0.75)
+      self.initiator.clear_ranging_session_callback_events()
+      try:
+          self.initiator.verify_callback_received("ReportReceived")
+      except TimeoutError:
+          # Expect to get a timeout error
+          logging.info("Did not get any ranging reports as expected")
+      else:
+          asserts.fail("Should not receive ranging reports when the app is in background")
+
+      # Turn screen on to simulate app moving to foreground.
+      RangingTest._move_snippet_to_fg(self.initiator)
+      self.initiator.clear_ranging_session_callback_events()
+      try:
+          self.initiator.verify_callback_received("ReportReceived")
+      except TimeoutError:
+          asserts.fail("Should receive ranging reports when the app is in foreground")
+
+
+  def test_ranging_default_params_move_to_bg_and_stay_there_stops_session(self):
+      """
+      1. Verifies ranging with default Fira parameters.
+      2. Move app to background (turn screen off).
+      3. Ensures the app does not receive range data notifications
+      4. Remain in background.
+      5. Ensures the session is stopped within 4 mins.
+      """
+      initiator_params = uwb_ranging_params.UwbRangingParams(
+          device_role=uwb_ranging_params.FiraParamEnums.DEVICE_ROLE_INITIATOR,
+          device_type=uwb_ranging_params.FiraParamEnums.DEVICE_TYPE_CONTROLLER,
+          device_address=self.initiator_addr,
+          destination_addresses=[self.responder_addr],
+      )
+      responder_params = uwb_ranging_params.UwbRangingParams(
+          device_role=uwb_ranging_params.FiraParamEnums.DEVICE_ROLE_RESPONDER,
+          device_type=uwb_ranging_params.FiraParamEnums.DEVICE_TYPE_CONTROLEE,
+          device_address=self.responder_addr,
+          destination_addresses=[self.initiator_addr],
+      )
+      self._verify_one_to_one_ranging(self.initiator, self.responder,
+                                      initiator_params, responder_params,
+                                      self.responder_addr)
+
+      # Turn screen off to simulate app moving to background.
+      RangingTest._move_snippet_to_bg(self.initiator)
+      time.sleep(0.75)
+      self.initiator.clear_ranging_session_callback_events()
+      try:
+          self.initiator.verify_callback_received("ReportReceived")
+      except TimeoutError:
+          # Expect to get a timeout error
+          logging.info("Did not get any ranging reports as expected")
+      else:
+          asserts.fail("Should not receive ranging reports when the app is in background")
+
+      # Wait for 4 mins
+      try:
+          self.initiator.verify_callback_received("Stopped", timeout=60*4)
+      except TimeoutError:
+          asserts.fail("Should receive ranging reports when the app is in foreground")
+
+
+  def test_ranging_default_params_no_valid_reports_stops_session(self):
+      """
+      1. Verifies ranging with default Fira parameters.
+      2. Reboot the initiator to abruptly terminate session and cause ranging report errors.
+      3. Ensures the session is stopped within 2 mins.
+      """
+      initiator_params = uwb_ranging_params.UwbRangingParams(
+          device_role=uwb_ranging_params.FiraParamEnums.DEVICE_ROLE_INITIATOR,
+          device_type=uwb_ranging_params.FiraParamEnums.DEVICE_TYPE_CONTROLLER,
+          device_address=self.initiator_addr,
+          destination_addresses=[self.responder_addr],
+      )
+      responder_params = uwb_ranging_params.UwbRangingParams(
+          device_role=uwb_ranging_params.FiraParamEnums.DEVICE_ROLE_RESPONDER,
+          device_type=uwb_ranging_params.FiraParamEnums.DEVICE_TYPE_CONTROLEE,
+          device_address=self.responder_addr,
+          destination_addresses=[self.initiator_addr],
+      )
+      self._verify_one_to_one_ranging(self.initiator, self.responder,
+                                      initiator_params, responder_params,
+                                      self.responder_addr)
+
+      # Reboot responder and ensure peer is no longer seen in ranging reports
+      def reboot_responder():
+          self.responder.ad.reboot()
+          uwb_test_utils.initialize_uwb_country_code_if_not_set(self.responder.ad.adb)
+
+     # create a thread to reboot the responder and not block the main test.
+      thread = Thread(target=reboot_responder)
+      thread.start()
+
+      time.sleep(0.75)
+      self.initiator.clear_ranging_session_callback_events()
+      try:
+        uwb_test_utils.verify_peer_found(self.initiator, self.responder_addr)
+        asserts.fail("Peer found even though it was rebooted.")
+      except signals.TestFailure:
+        logging.info("Peer %s not found as expected", self.responder_addr)
+
+      # Wait for 2 mins to stop the session.
+      try:
+          self.initiator.verify_callback_received("Stopped", timeout=60*2)
+      except TimeoutError:
+         asserts.fail("Should receive ranging reports when the app is in foreground")
+      # Ensure the responder is back after reboot.
+      thread.join()
+
+
 if __name__ == "__main__":
   if "--" in sys.argv:
     index = sys.argv.index("--")