QnsTimer implementation for delayed events in QNS

Delayed messages in QNS needs to handle such that they will be
unimpacted even in doze mode.
Bug: 258753685
Test: atest QualifiedNetworksServiceTests and manual testing.

Change-Id: I65f95069e7a8099ba8c80c7a25e7963ab79e9a6a
Merged-In: I65f95069e7a8099ba8c80c7a25e7963ab79e9a6a
(cherry picked from commit 1a9a04b3658a785704e5d6f68eaa6d915f21cd2d)
diff --git a/services/QualifiedNetworksService/AndroidManifest.xml b/services/QualifiedNetworksService/AndroidManifest.xml
index 9b2444b..5abdbf9 100644
--- a/services/QualifiedNetworksService/AndroidManifest.xml
+++ b/services/QualifiedNetworksService/AndroidManifest.xml
@@ -27,6 +27,7 @@
   <uses-permission android:name="android.permission.READ_PRECISE_PHONE_STATE" />
   <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
   <uses-permission android:name="android.permission.REGISTER_STATS_PULL_ATOM" />
+  <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
   <application
       android:directBootAware="true"
       android:defaultToDeviceProtectedStorage="true">
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsCallStatusTracker.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsCallStatusTracker.java
index f1b6e37..f8b335a 100644
--- a/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsCallStatusTracker.java
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsCallStatusTracker.java
@@ -16,6 +16,8 @@
 
 package com.android.telephony.qns;
 
+import static com.android.telephony.qns.QnsConstants.INVALID_ID;
+
 import android.annotation.NonNull;
 import android.net.NetworkCapabilities;
 import android.os.Handler;
@@ -50,6 +52,7 @@
     private List<CallState> mCallStates = new ArrayList<>();
     private QnsRegistrant mCallTypeChangedEventListener;
     private QnsRegistrant mEmergencyCallTypeChangedEventListener;
+    private final QnsTimer mQnsTimer;
     private int mLastNormalCallType = QnsConstants.CALL_TYPE_IDLE;
     private int mLastEmergencyCallType = QnsConstants.CALL_TYPE_IDLE;
     private boolean mEmergencyOverIms;
@@ -144,7 +147,6 @@
             private static final int EVENT_PACKET_LOSS_TIMER_EXPIRED = 3402;
             private static final int EVENT_HYSTERESIS_FOR_NORMAL_QUALITY = 3403;
             private static final int EVENT_POLLING_CHECK_LOW_QUALITY = 3404;
-            private static final int EVENT_LOW_QUALITY_HANDLER_MAX = 3405;
 
             private static final int STATE_NORMAL_QUALITY = 0;
             private static final int STATE_SUSPECT_LOW_QUALITY = 1;
@@ -156,6 +158,9 @@
             private static final int LOW_QUALITY_REPORTED_TIME_INITIAL_VALUE = -1;
 
             private int mState = STATE_NORMAL_QUALITY;
+            private int mPacketLossTimerId = INVALID_ID;
+            private int mHysteresisTimerId = INVALID_ID;
+            private int mPollingCheckTimerId = INVALID_ID;
             private MediaQualityStatus mMediaQualityStatus;
             private String mTag;
 
@@ -204,12 +209,14 @@
                         return;
                     } else {
                         // check normal quality is stable or not.
-                        this.sendEmptyMessageDelayed(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY,
+                        mHysteresisTimerId = mQnsTimer.registerTimer(
+                                Message.obtain(this, EVENT_HYSTERESIS_FOR_NORMAL_QUALITY),
                                 HYSTERESIS_TIME_NORMAL_QUALITY_MILLIS);
                     }
                 } else {
                     // Threshold breached.
-                    this.removeMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY);
+                    mQnsTimer.unregisterTimer(mHysteresisTimerId);
+                    mHysteresisTimerId = INVALID_ID;
                     switch (mState) {
                         case STATE_NORMAL_QUALITY:
                         case STATE_SUSPECT_LOW_QUALITY:
@@ -223,7 +230,8 @@
                                     needNotify = true;
                                 }
                             } else {
-                                removeMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED);
+                                mQnsTimer.unregisterTimer(mPacketLossTimerId);
+                                mPacketLossTimerId = INVALID_ID;
                                 enterLowQualityState(status);
                                 needNotify = true;
                             }
@@ -250,28 +258,30 @@
             void enterLowQualityState(MediaQualityStatus status) {
                 Log.d(mTag, "enterLowQualityState " + status);
                 mState = STATE_LOW_QUALITY;
-                this.sendEmptyMessageDelayed(
-                        EVENT_POLLING_CHECK_LOW_QUALITY, LOW_QUALITY_CHECK_INTERVAL_MILLIS);
+                mPollingCheckTimerId = mQnsTimer.registerTimer(
+                        Message.obtain(this, EVENT_POLLING_CHECK_LOW_QUALITY),
+                        LOW_QUALITY_CHECK_INTERVAL_MILLIS);
             }
 
             void enterSuspectLowQualityState(int delayMillis) {
                 Log.d(mTag, "enterSuspectLowQualityState.");
-                if (!this.hasMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED)) {
-                    this.removeMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED);
-                }
+                mQnsTimer.unregisterTimer(mPacketLossTimerId);
                 Log.d(mTag, "Packet loss timer start. " + delayMillis);
                 Message msg = this.obtainMessage(
                         EVENT_PACKET_LOSS_TIMER_EXPIRED, mTransportType, 0);
-                this.sendMessageDelayed(msg, delayMillis);
+                mPacketLossTimerId = mQnsTimer.registerTimer(msg, delayMillis);
                 mState = STATE_SUSPECT_LOW_QUALITY;
             }
 
             void exitLowQualityState() {
                 mState = STATE_NORMAL_QUALITY;
-                for (int i = EVENT_PACKET_LOSS_TIMER_EXPIRED;
-                        i < EVENT_LOW_QUALITY_HANDLER_MAX; i++) {
-                    this.removeMessages(i);
-                }
+                this.removeCallbacksAndMessages(null);
+                mQnsTimer.unregisterTimer(mPacketLossTimerId);
+                mQnsTimer.unregisterTimer(mHysteresisTimerId);
+                mQnsTimer.unregisterTimer(mPollingCheckTimerId);
+                mPacketLossTimerId = INVALID_ID;
+                mHysteresisTimerId = INVALID_ID;
+                mPollingCheckTimerId = INVALID_ID;
                 notifyLowMediaQuality(0);
             }
 
@@ -283,9 +293,10 @@
                     int reason = thresholdBreached(mMediaQualityStatus);
                     if (reason > 0) {
                         notifyLowMediaQuality(thresholdBreached(mMediaQualityStatus));
-                    } else if (this.hasMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY)) {
+                    } else if (mHysteresisTimerId != INVALID_ID) {
                         // hysteresis time to be normal state is running. let's check after that.
-                        this.sendEmptyMessageDelayed(EVENT_POLLING_CHECK_LOW_QUALITY,
+                        mPollingCheckTimerId = mQnsTimer.registerTimer(
+                                Message.obtain(this, EVENT_POLLING_CHECK_LOW_QUALITY),
                                 HYSTERESIS_TIME_NORMAL_QUALITY_MILLIS);
                     } else {
                         Log.w(mTag, "Unexpected case.");
@@ -296,19 +307,22 @@
             void updateForHandover(int transportType) {
                 // restart timers that they need to be restarted on new transport type.
                 if (mState == STATE_SUSPECT_LOW_QUALITY) {
-                    this.removeMessages(EVENT_PACKET_LOSS_TIMER_EXPIRED);
+                    mQnsTimer.unregisterTimer(mPacketLossTimerId);
                     Message msg = this.obtainMessage(
                             EVENT_PACKET_LOSS_TIMER_EXPIRED, transportType, 0);
-                    this.sendMessageDelayed(msg, (mConfigManager.getRTPMetricsData()).mPktLossTime);
+                    mPacketLossTimerId = mQnsTimer.registerTimer(msg,
+                            (mConfigManager.getRTPMetricsData()).mPktLossTime);
                 }
-                if (this.hasMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY)) {
-                    this.removeMessages(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY);
-                    this.sendEmptyMessageDelayed(EVENT_HYSTERESIS_FOR_NORMAL_QUALITY,
+                if (mHysteresisTimerId != INVALID_ID) {
+                    mQnsTimer.unregisterTimer(mHysteresisTimerId);
+                    mHysteresisTimerId = mQnsTimer.registerTimer(
+                            Message.obtain(this, EVENT_HYSTERESIS_FOR_NORMAL_QUALITY),
                             HYSTERESIS_TIME_NORMAL_QUALITY_MILLIS);
                 }
                 if (mState == STATE_LOW_QUALITY) {
-                    this.removeMessages(EVENT_POLLING_CHECK_LOW_QUALITY);
-                    this.sendEmptyMessageDelayed(EVENT_POLLING_CHECK_LOW_QUALITY,
+                    mQnsTimer.unregisterTimer(mPollingCheckTimerId);
+                    mPollingCheckTimerId = mQnsTimer.registerTimer(
+                            Message.obtain(this, EVENT_POLLING_CHECK_LOW_QUALITY),
                             LOW_QUALITY_CHECK_AFTER_HO_MILLIS);
                 }
             }
@@ -727,17 +741,19 @@
     }
 
     QnsCallStatusTracker(QnsTelephonyListener telephonyListener,
-            QnsCarrierConfigManager configManager, int slotIndex) {
-        this(telephonyListener, configManager, slotIndex, null);
+            QnsCarrierConfigManager configManager, QnsTimer qnsTimer, int slotIndex) {
+        this(telephonyListener, configManager, qnsTimer, slotIndex, null);
     }
 
     /** Only for test */
     @VisibleForTesting
     QnsCallStatusTracker(QnsTelephonyListener telephonyListener,
-            QnsCarrierConfigManager configManager, int slotIndex, Looper looper) {
+            QnsCarrierConfigManager configManager, QnsTimer qnsTimer, int slotIndex,
+            Looper looper) {
         mLogTag = QnsCallStatusTracker.class.getSimpleName() + "_" + slotIndex;
         mTelephonyListener = telephonyListener;
         mConfigManager = configManager;
+        mQnsTimer = qnsTimer;
         mActiveCallTracker = new ActiveCallTracker(slotIndex, looper);
         mTelephonyListener.addCallStatesChangedCallback(mCallStatesConsumer);
         mTelephonyListener.addSrvccStateChangedCallback(mSrvccStateConsumer);
@@ -842,6 +858,7 @@
         } else {
             mActiveCallTracker.callStarted(callType, netCapability);
         }
+        mQnsTimer.updateCallState(callType);
     }
 
     boolean isCallIdle() {
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsComponents.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsComponents.java
index 04780e7..5862bc2 100644
--- a/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsComponents.java
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsComponents.java
@@ -22,6 +22,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -44,6 +45,7 @@
     private final SparseArray<WifiBackhaulMonitor> mWifiBackhaulMonitors;
     private final List<Integer> mSlotIds;
     private IwlanNetworkStatusTracker mIwlanNetworkStatusTracker;
+    private QnsTimer mQnsTimer;
     private WifiQualityMonitor mWifiQualityMonitor;
     private QnsMetrics mQnsMetrics;
 
@@ -88,20 +90,26 @@
         mQnsCarrierConfigManagers.put(
                 slotId,
                 new QnsCarrierConfigManager(mContext, mQnsEventDispatchers.get(slotId), slotId));
+        if (mQnsTimer == null) {
+            mQnsTimer = new QnsTimer(mContext);
+        }
         mQnsCallStatusTracker.put(
                 slotId,
-                new QnsCallStatusTracker(mQnsTelephonyListeners.get(slotId),
-                        mQnsCarrierConfigManagers.get(slotId), slotId));
+                new QnsCallStatusTracker(
+                        mQnsTelephonyListeners.get(slotId),
+                        mQnsCarrierConfigManagers.get(slotId),
+                        mQnsTimer,
+                        slotId));
         mWifiBackhaulMonitors.put(
                 slotId,
                 new WifiBackhaulMonitor(
                         mContext,
                         mQnsCarrierConfigManagers.get(slotId),
                         mQnsImsManagers.get(slotId),
+                        mQnsTimer,
                         slotId));
-
         if (mWifiQualityMonitor == null) {
-            mWifiQualityMonitor = new WifiQualityMonitor(mContext);
+            mWifiQualityMonitor = new WifiQualityMonitor(mContext, mQnsTimer);
         }
         if (mIwlanNetworkStatusTracker == null) {
             mIwlanNetworkStatusTracker = new IwlanNetworkStatusTracker(mContext);
@@ -131,6 +139,7 @@
             QnsProvisioningListener qnsProvisioningListener,
             QnsTelephonyListener qnsTelephonyListener,
             QnsCallStatusTracker qnsCallStatusTracker,
+            QnsTimer qnsTimer,
             WifiBackhaulMonitor wifiBackhaulMonitor,
             WifiQualityMonitor wifiQualityMonitor,
             QnsMetrics qnsMetrics,
@@ -149,6 +158,7 @@
         mWifiBackhaulMonitors.put(slotId, wifiBackhaulMonitor);
 
         mWifiQualityMonitor = wifiQualityMonitor;
+        mQnsTimer = qnsTimer;
         mIwlanNetworkStatusTracker = iwlanNetworkStatusTracker;
         mIwlanNetworkStatusTracker.initBySlotIndex(
                 qnsCarrierConfigManager,
@@ -214,6 +224,11 @@
         return mWifiQualityMonitor;
     }
 
+    /** Returns instance of QnsTimer. */
+    QnsTimer getQnsTimer() {
+        return mQnsTimer;
+    }
+
     /** Returns instance of WifiQualityMonitor. */
     QnsMetrics getQnsMetrics() {
         return mQnsMetrics;
@@ -247,6 +262,10 @@
             mQnsCallStatusTracker.remove(slotId);
             qnsCallStatusTracker.close();
         }
+        if (mSlotIds.size() == 1) {
+            mQnsTimer.close();
+            mQnsTimer = null;
+        }
         QnsCarrierConfigManager qnsCarrierConfigManager = mQnsCarrierConfigManagers.get(slotId);
         if (qnsCarrierConfigManager != null) {
             mQnsCarrierConfigManagers.remove(slotId);
@@ -286,4 +305,16 @@
         mSlotIds.remove(Integer.valueOf(slotId));
         Log.d(mLogTag, "QnsComponents closed for slot " + slotId);
     }
+
+    void dump(PrintWriter pw) {
+        if (mIwlanNetworkStatusTracker != null) {
+            mIwlanNetworkStatusTracker.dump(pw, "  ");
+        }
+        if (mIwlanNetworkStatusTracker != null) {
+            mWifiQualityMonitor.dump(pw, "  ");
+        }
+        if (mIwlanNetworkStatusTracker != null) {
+            mQnsTimer.dump(pw, " ");
+        }
+    }
 }
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsTimer.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsTimer.java
new file mode 100644
index 0000000..f2439bd
--- /dev/null
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/QnsTimer.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2023 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.telephony.qns;
+
+import static com.android.telephony.qns.QnsConstants.CALL_TYPE_IDLE;
+import static com.android.telephony.qns.QnsConstants.INVALID_ID;
+import static com.android.telephony.qns.QnsUtils.getSystemElapsedRealTime;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.PowerManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** This class handles the delayed events triggered in QNS. */
+class QnsTimer {
+
+    private static final String TAG = QnsTimer.class.getSimpleName();
+    private static final int EVENT_QNS_TIMER_EXPIRED = 1;
+    static final String ACTION_ALARM_TIMER_EXPIRED =
+            "com.android.telephony.qns.action.ALARM_TIMER_EXPIRED";
+
+    private static final AtomicInteger sTimerId = new AtomicInteger();
+    private final Context mContext;
+    private final AlarmManager mAlarmManager;
+    private final PowerManager mPowerManager;
+    private final HandlerThread mHandlerThread;
+    private final BroadcastReceiver mBroadcastReceiver;
+    private final PriorityQueue<TimerInfo> mTimerInfos;
+    private PendingIntent mPendingIntent;
+    private long mMinAlarmTimeMs = 10000;
+    private int mCurrentAlarmTimerId = INVALID_ID;
+    private int mCurrentHandlerTimerId = INVALID_ID;
+    private boolean mIsAlarmRequired;
+    @VisibleForTesting final Handler mHandler;
+    private long mLastAlarmTriggerAtMs = Long.MAX_VALUE;
+    private int mCallType = CALL_TYPE_IDLE;
+
+    QnsTimer(Context context) {
+        mContext = context;
+        mAlarmManager = mContext.getSystemService(AlarmManager.class);
+        mPowerManager = mContext.getSystemService(PowerManager.class);
+        mBroadcastReceiver = new AlarmReceiver();
+        mTimerInfos =
+                new PriorityQueue<>(Comparator.comparingLong(TimerInfo::getExpireAtElapsedMillis));
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new QnsTimerHandler();
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ACTION_ALARM_TIMER_EXPIRED);
+        intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+        intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+        intentFilter.addAction(PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED);
+        intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+        mContext.registerReceiver(mBroadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED);
+    }
+
+    /**
+     * This method uses AlarmManager to execute the delayed event passed as param.
+     *
+     * @param msg message to process.
+     * @param delayMs timer value for the delay.
+     * @return unique timer id associated with the registered timer.
+     */
+    int registerTimer(Message msg, long delayMs) {
+        int timerId = sTimerId.getAndIncrement();
+        TimerInfo timerInfo = new TimerInfo(timerId);
+        timerInfo.setMessage(msg);
+        timerInfo.setExpireAtElapsedMillis(getSystemElapsedRealTime() + delayMs);
+        logd("register timer for timerId=" + timerId + ", with delay=" + delayMs);
+        mHandler.post(
+                () -> {
+                    mTimerInfos.add(timerInfo);
+                    updateToShortestDelay(mIsAlarmRequired, true);
+                });
+        return timerId;
+    }
+
+    /**
+     * This method unregisters the timer associated to given timerId.
+     *
+     * @param timerId timer id associated with the running timer.
+     */
+    void unregisterTimer(int timerId) {
+        if (timerId == INVALID_ID) {
+            return;
+        }
+        logd("unregisterTimer for timerId=" + timerId);
+        mHandler.post(
+                () -> {
+                    logd("Cancel timerId=" + timerId);
+                    TimerInfo timerInfo = new TimerInfo(timerId);
+                    if (mTimerInfos.remove(timerInfo) && timerId == mCurrentAlarmTimerId) {
+                        updateToShortestDelay(mIsAlarmRequired, true);
+                    }
+                });
+    }
+
+    /**
+     * It updates the call state in QnsTimer. If the call is active the minimum timer value for an
+     * alarm is updated to 0ms. Otherwise the value will be based on device state (Idle, Light Idle
+     * or Screen off).
+     *
+     * @param type Call type {@code @QnsConstants.QnsCallType}
+     */
+    void updateCallState(@QnsConstants.QnsCallType int type) {
+        if (mCallType == CALL_TYPE_IDLE && type != CALL_TYPE_IDLE) {
+            mHandler.post(
+                    () -> {
+                        mMinAlarmTimeMs = 0;
+                        if (mIsAlarmRequired) {
+                            updateToShortestDelay(true, false);
+                        }
+                    });
+        }
+        mCallType = type;
+        if (mCallType == CALL_TYPE_IDLE && mIsAlarmRequired) {
+            if (mPowerManager.isDeviceIdleMode()) {
+                mMinAlarmTimeMs = 60000;
+            } else if (mPowerManager.isDeviceLightIdleMode()) {
+                mMinAlarmTimeMs = 30000;
+            } else {
+                mMinAlarmTimeMs = 10000; // SCREEN_OFF case
+            }
+        }
+    }
+
+    /**
+     * This method performs the following actions: 1. checks if the shortest timer is set for
+     * handler or alarm. If not it overrides the earlier set timer with the shortest one. 2. checks
+     * for timers in the list those have passed the current elapsed time; and notifies them to
+     * respective handlers.
+     *
+     * @param isAlarmRequired flag indicates if timer is need to setup with Alarm.
+     * @param skipTimerUpdate flag indicates if current scheduled alarm timers needs any change.
+     *     This flag will be false when call type changes or device moves or come out of idle state
+     *     because such cases mandates timer update.
+     */
+    private void updateToShortestDelay(boolean isAlarmRequired, boolean skipTimerUpdate) {
+        TimerInfo timerInfo = mTimerInfos.peek();
+        long elapsedTime = getSystemElapsedRealTime();
+        while (timerInfo != null && timerInfo.getExpireAtElapsedMillis() <= elapsedTime) {
+            logd("Notify timerInfo=" + timerInfo);
+            timerInfo.getMessage().sendToTarget();
+            mTimerInfos.poll();
+            timerInfo = mTimerInfos.peek();
+        }
+        if (timerInfo == null) {
+            logd("No timers are pending to run");
+            clearAllTimers();
+            return;
+        }
+        long delay = timerInfo.getExpireAtElapsedMillis() - elapsedTime;
+        // Delayed Handler will always set for shortest delay.
+        if (timerInfo.getTimerId() != mCurrentHandlerTimerId) {
+            mHandler.removeMessages(EVENT_QNS_TIMER_EXPIRED);
+            mHandler.sendEmptyMessageDelayed(EVENT_QNS_TIMER_EXPIRED, delay);
+            mCurrentHandlerTimerId = timerInfo.getTimerId();
+        }
+
+        // Alarm will always set for shortest from Math.max(delay, mMinAlarmTimeMs)
+        if (timerInfo.getTimerId() != mCurrentAlarmTimerId || !skipTimerUpdate) {
+            if (isAlarmRequired) {
+                delay = Math.max(delay, mMinAlarmTimeMs);
+                // check if smaller timer alarm is already running for active timer info.
+                if (mTimerInfos.contains(new TimerInfo(mCurrentAlarmTimerId))
+                        && mLastAlarmTriggerAtMs - elapsedTime < delay
+                        && mPendingIntent != null) {
+                    logd(
+                            "Skip update since minimum Alarm Timer already running for timerId="
+                                    + mCurrentAlarmTimerId);
+                    return;
+                }
+                logd("Setup alarm for delay " + delay);
+                mLastAlarmTriggerAtMs = elapsedTime + delay;
+                setupAlarmFor(mLastAlarmTriggerAtMs);
+            } else if (mPendingIntent != null) {
+                mAlarmManager.cancel(mPendingIntent);
+                mPendingIntent = null;
+            }
+            mCurrentAlarmTimerId = timerInfo.getTimerId();
+            logd("Update timer to timer id=" + mCurrentAlarmTimerId);
+        }
+    }
+
+    private void clearAllTimers() {
+        mHandler.removeMessages(EVENT_QNS_TIMER_EXPIRED);
+        if (mPendingIntent != null) {
+            logd("Cancel Alarm");
+            mAlarmManager.cancel(mPendingIntent);
+        }
+        mPendingIntent = null;
+    }
+
+    private void setupAlarmFor(long triggerAtMillis) {
+        mPendingIntent =
+                PendingIntent.getBroadcast(
+                        mContext,
+                        0,
+                        new Intent(ACTION_ALARM_TIMER_EXPIRED),
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        mAlarmManager.setExactAndAllowWhileIdle(
+                AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, mPendingIntent);
+    }
+
+    private class AlarmReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            logd("onReceive action=" + action);
+            switch (action) {
+                case ACTION_ALARM_TIMER_EXPIRED:
+                    mHandler.sendEmptyMessage(EVENT_QNS_TIMER_EXPIRED);
+                    break;
+                case Intent.ACTION_SCREEN_OFF:
+                    mHandler.post(
+                            () -> {
+                                mMinAlarmTimeMs = (mCallType == CALL_TYPE_IDLE) ? 10000 : 0;
+                                if (!mIsAlarmRequired) {
+                                    mIsAlarmRequired = true;
+                                    updateToShortestDelay(true, false);
+                                }
+                            });
+                    break;
+                case Intent.ACTION_SCREEN_ON:
+                    mHandler.post(
+                            () -> {
+                                if (mIsAlarmRequired) {
+                                    mIsAlarmRequired = false;
+                                    updateToShortestDelay(false, false);
+                                }
+                            });
+                    break;
+                case PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED:
+                    mHandler.post(
+                            () -> {
+                                if (mPowerManager.isDeviceLightIdleMode()) {
+                                    mMinAlarmTimeMs = (mCallType == CALL_TYPE_IDLE) ? 30000 : 0;
+                                    if (!mIsAlarmRequired) {
+                                        mIsAlarmRequired = true;
+                                        updateToShortestDelay(true, false);
+                                    }
+                                } else {
+                                    mMinAlarmTimeMs = (mCallType == CALL_TYPE_IDLE) ? 10000 : 0;
+                                }
+                            });
+                    break;
+                case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED:
+                    mHandler.post(
+                            () -> {
+                                if (mPowerManager.isDeviceIdleMode()) {
+                                    mMinAlarmTimeMs = (mCallType == CALL_TYPE_IDLE) ? 60000 : 0;
+                                    if (!mIsAlarmRequired) {
+                                        mIsAlarmRequired = true;
+                                        updateToShortestDelay(true, false);
+                                    }
+                                } else {
+                                    mMinAlarmTimeMs = (mCallType == CALL_TYPE_IDLE) ? 10000 : 0;
+                                }
+                            });
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private class QnsTimerHandler extends Handler {
+        QnsTimerHandler() {
+            super(mHandlerThread.getLooper());
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            super.handleMessage(msg);
+            logd("handleMessage msg.what=" + msg.what);
+            switch (msg.what) {
+                case EVENT_QNS_TIMER_EXPIRED:
+                    logd("Timer expired");
+                    updateToShortestDelay(mIsAlarmRequired, true);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    static class TimerInfo {
+        private final int mTimerId;
+        private long mExpireAtElapsedMillis;
+        private Message mMsg;
+
+        TimerInfo(int timerId) {
+            mTimerId = timerId;
+        }
+
+        public int getTimerId() {
+            return mTimerId;
+        }
+
+        public Message getMessage() {
+            return mMsg;
+        }
+
+        public void setMessage(Message msg) {
+            mMsg = msg;
+        }
+
+        public long getExpireAtElapsedMillis() {
+            return mExpireAtElapsedMillis;
+        }
+
+        public void setExpireAtElapsedMillis(long expireAtElapsedMillis) {
+            mExpireAtElapsedMillis = expireAtElapsedMillis;
+        }
+
+        /** Timers are equals if they share the same timer id. */
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof TimerInfo)) return false;
+            TimerInfo timerInfo = (TimerInfo) o;
+            return mTimerId == timerInfo.mTimerId;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mTimerId);
+        }
+
+        @Override
+        public String toString() {
+            return "TimerInfo{"
+                    + "mTimerId="
+                    + mTimerId
+                    + ", mExpireAtElapsedMillis="
+                    + mExpireAtElapsedMillis
+                    + ", mMsg="
+                    + mMsg
+                    + '}';
+        }
+    }
+
+    @VisibleForTesting
+    PriorityQueue<TimerInfo> getTimersInfo() {
+        return mTimerInfos;
+    }
+
+    void close() {
+        logd("Closing QnsTimer");
+        mHandlerThread.quitSafely();
+        mContext.unregisterReceiver(mBroadcastReceiver);
+        mTimerInfos.clear();
+        clearAllTimers();
+    }
+
+    private void logd(String s) {
+        Log.d(TAG, s);
+    }
+
+    /**
+     * Dumps the state of {@link QnsTimer}
+     *
+     * @param pw {@link PrintWriter} to write the state of the object.
+     * @param prefix String to append at start of dumped log.
+     */
+    void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "------------------------------");
+        pw.println(prefix + "QnsTimer:");
+        pw.println(
+                prefix
+                        + "mIsAlarmRequired="
+                        + mIsAlarmRequired
+                        + ", mCurrentAlarmTimerId="
+                        + mCurrentAlarmTimerId
+                        + ", mCurrentHandlerTimerId="
+                        + mCurrentHandlerTimerId
+                        + ", latest timerId="
+                        + sTimerId.get()
+                        + ", Current elapsed time="
+                        + getSystemElapsedRealTime());
+        pw.println(prefix + "mTimerInfos=" + mTimerInfos);
+        pw.println(prefix + "mPendingIntent=" + mPendingIntent);
+    }
+}
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/QualifiedNetworksServiceImpl.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/QualifiedNetworksServiceImpl.java
index 9ac37b1..679098e 100644
--- a/services/QualifiedNetworksService/src/com/android/telephony/qns/QualifiedNetworksServiceImpl.java
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/QualifiedNetworksServiceImpl.java
@@ -358,14 +358,7 @@
             NetworkAvailabilityProviderImpl provider = providerMap.getValue();
             provider.dump(pw, "  ");
         }
-        IwlanNetworkStatusTracker iwlanNst = mQnsComponents.getIwlanNetworkStatusTracker();
-        if (iwlanNst != null) {
-            iwlanNst.dump(pw, "  ");
-        }
-        WifiQualityMonitor wQM = mQnsComponents.getWifiQualityMonitor();
-        if (wQM != null) {
-            wQM.dump(pw, "  ");
-        }
+        mQnsComponents.dump(pw);
         pw.println("==============================");
     }
 }
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/RestrictManager.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/RestrictManager.java
index 1c15346..21eaf7c 100644
--- a/services/QualifiedNetworksService/src/com/android/telephony/qns/RestrictManager.java
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/RestrictManager.java
@@ -18,6 +18,7 @@
 
 import static com.android.telephony.qns.DataConnectionStatusTracker.STATE_CONNECTED;
 import static com.android.telephony.qns.DataConnectionStatusTracker.STATE_HANDOVER;
+import static com.android.telephony.qns.QnsConstants.INVALID_ID;
 
 import android.annotation.IntDef;
 import android.net.NetworkCapabilities;
@@ -161,6 +162,7 @@
     private QnsCallStatusTracker mQnsCallStatusTracker;
     private QnsCallStatusTracker.ActiveCallTracker mActiveCallTracker;
     private QnsImsManager mQnsImsManager;
+    private QnsTimer mQnsTimer;
     private WifiBackhaulMonitor mWifiBackhaulMonitor;
     private QnsMetrics mQnsMetrics;
     private int mNetCapability;
@@ -174,6 +176,7 @@
     private int mFallbackCounterOnDataConnectionFail;
     private boolean mIsRttStatusCheckRegistered = false;
     private int mLastDataConnectionTransportType;
+    private int mFallbackTimerId = -1;
     private boolean mIsTimerRunningOnDataConnectionFail = false;
     private Pair<Integer, Long> mDeferredThrottlingEvent = null;
 
@@ -183,6 +186,7 @@
     @Annotation.CallState private int mCallState;
 
     private Map<Integer, RestrictInfo> mRestrictInfos = new ConcurrentHashMap<>();
+    private Map<Restriction, Integer> mRestrictionTimers = new ConcurrentHashMap<>();
 
     private class RestrictManagerHandler extends Handler {
         RestrictManagerHandler(Looper l) {
@@ -239,6 +243,9 @@
                                     .getRestrictionMap()
                                     .get(restriction.mRestrictType)) {
                         releaseRestriction(transportType, restriction.mRestrictType);
+                        mQnsTimer.unregisterTimer(mRestrictionTimers
+                                .getOrDefault(restriction, INVALID_ID));
+                        mRestrictionTimers.remove(restriction);
                     }
                     break;
 
@@ -248,6 +255,7 @@
                             "Initial Data Connection fail timer expired"
                                     + mIsTimerRunningOnDataConnectionFail);
 
+                    mQnsTimer.unregisterTimer(mFallbackTimerId);
                     if (mIsTimerRunningOnDataConnectionFail) {
                         int currTransportType = message.arg1;
                         fallbackToOtherTransportOnDataConnectionFail(currTransportType);
@@ -452,6 +460,7 @@
         mTelephonyListener = qnsComponents.getQnsTelephonyListener(mSlotId);
         mQnsEventDispatcher = qnsComponents.getQnsEventDispatcher(mSlotId);
         mQnsCarrierConfigManager = qnsComponents.getQnsCarrierConfigManager(mSlotId);
+        mQnsTimer = qnsComponents.getQnsTimer();
         mHandler = new RestrictManagerHandler(loop);
         mNetCapability = netCapability;
         mDataConnectionStatusTracker = dcst;
@@ -520,6 +529,7 @@
         if (mNetCapability == NetworkCapabilities.NET_CAPABILITY_IMS) {
             mQnsImsManager.unregisterImsRegistrationStatusChanged(mHandler);
         }
+        mRestrictionTimers.clear();
     }
 
     private void onWfcModeChanged(int prefMode, @QnsConstants.CellularCoverage int coverage) {
@@ -778,9 +788,7 @@
         mIsTimerRunningOnDataConnectionFail = false;
         mRetryCounterOnDataConnectionFail = 0;
 
-        if (mHandler.hasMessages(EVENT_INITIAL_DATA_CONNECTION_FAIL_RETRY_TIMER_EXPIRED)) {
-            mHandler.removeMessages(EVENT_INITIAL_DATA_CONNECTION_FAIL_RETRY_TIMER_EXPIRED);
-        }
+        mQnsTimer.unregisterTimer(mFallbackTimerId);
     }
 
     private void processDataConnectionDisconnected() {
@@ -918,7 +926,7 @@
                             transportType,
                             0,
                             null);
-            mHandler.sendMessageDelayed(msg, (long) fallbackRetryTimer);
+            mFallbackTimerId = mQnsTimer.registerTimer(msg, fallbackRetryTimer);
             mIsTimerRunningOnDataConnectionFail = true;
         }
     }
@@ -1293,6 +1301,7 @@
                 removeReleaseRestrictionMessage(restriction);
             }
             restrictionMap.remove(restriction.mRestrictType);
+            mRestrictionTimers.remove(restriction);
             needNotify = true;
         }
         if (needNotify && !skipNotify) {
@@ -1329,7 +1338,8 @@
         Message msg =
                 mHandler.obtainMessage(EVENT_RELEASE_RESTRICTION, transportType, 0, restriction);
         long delayInMillis = restriction.mReleaseTime - SystemClock.elapsedRealtime();
-        mHandler.sendMessageDelayed(msg, delayInMillis);
+        int timerId = mQnsTimer.registerTimer(msg, delayInMillis);
+        mRestrictionTimers.put(restriction, timerId);
         Log.d(
                 mLogTag,
                 restrictTypeToString(restriction.mRestrictType)
@@ -1343,7 +1353,8 @@
             Log.e(mLogTag, "removeReleaseRestrictionMessage restriction is null");
             return;
         }
-        mHandler.removeMessages(EVENT_RELEASE_RESTRICTION, restriction);
+        mQnsTimer.unregisterTimer(mRestrictionTimers.getOrDefault(restriction, INVALID_ID));
+        mRestrictionTimers.remove(restriction);
     }
 
     void registerRestrictInfoChanged(Handler h, int what) {
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiBackhaulMonitor.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiBackhaulMonitor.java
index be3c22c..6bef150 100644
--- a/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiBackhaulMonitor.java
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiBackhaulMonitor.java
@@ -16,6 +16,8 @@
 
 package com.android.telephony.qns;
 
+import static com.android.telephony.qns.QnsConstants.INVALID_ID;
+
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.LinkProperties;
@@ -28,6 +30,8 @@
 import android.telephony.AccessNetworkConstants;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -56,6 +60,7 @@
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
     private final QnsCarrierConfigManager mConfigManager;
+    private final QnsTimer mQnsTimer;
     private boolean mRttResult = false;
 
     ArrayList<InetAddress> mValidIpList = new ArrayList<>();
@@ -65,6 +70,7 @@
     private boolean mIsIwlanConnected = false;
     private boolean mIsRttRunning = false;
     private String mInterfaceName = null;
+    private int mRttTimerId = INVALID_ID;
 
     private class BackhaulHandler extends Handler {
         BackhaulHandler() {
@@ -118,6 +124,7 @@
             Context context,
             QnsCarrierConfigManager configManager,
             QnsImsManager imsManager,
+            QnsTimer qnstimer,
             int slotIndex) {
         mSlotIndex = slotIndex;
         mTag = WifiBackhaulMonitor.class.getSimpleName() + "[" + mSlotIndex + "]";
@@ -125,6 +132,7 @@
         mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
         mConfigManager = configManager;
         mQnsImsManager = imsManager;
+        mQnsTimer = qnstimer;
         mNetworkCallback = new WiFiStatusCallback();
         mRegistrantList = new QnsRegistrantList();
         mHandlerThread = new HandlerThread(mTag);
@@ -174,8 +182,9 @@
     /** Triggers the request to check RTT. */
     void requestRttCheck() {
         if (!mIsRttRunning) {
-            if (mHandler.hasMessages(EVENT_START_RTT_CHECK)) {
-                mHandler.removeMessages(EVENT_START_RTT_CHECK);
+            if (mRttTimerId != INVALID_ID) {
+                mQnsTimer.unregisterTimer(mRttTimerId);
+                mRttTimerId = INVALID_ID;
             }
             mHandler.sendEmptyMessage(EVENT_START_RTT_CHECK);
         } else {
@@ -232,13 +241,17 @@
     }
 
     private void startRttSchedule(int delay) {
-        mHandler.sendEmptyMessageDelayed(EVENT_START_RTT_CHECK, delay);
+        log("start RTT schedule for " + delay);
+        mRttTimerId = mQnsTimer.registerTimer(Message.obtain(mHandler, EVENT_START_RTT_CHECK),
+                delay);
         mIsRttScheduled = true;
     }
 
     private void stopRttSchedule() {
         if (mIsRttScheduled) {
-            mHandler.removeMessages(EVENT_START_RTT_CHECK);
+            log("stop RTT schedule");
+            mQnsTimer.unregisterTimer(mRttTimerId);
+            mRttTimerId = INVALID_ID;
             mIsRttScheduled = false;
         }
     }
@@ -350,6 +363,11 @@
         mIsRttScheduled = false;
     }
 
+    @VisibleForTesting
+    int getRttTimerId() {
+        return mRttTimerId;
+    }
+
     private void log(String s) {
         Log.d(mTag, s);
     }
diff --git a/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiQualityMonitor.java b/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiQualityMonitor.java
index bf758a9..0c5ea2a 100644
--- a/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiQualityMonitor.java
+++ b/services/QualifiedNetworksService/src/com/android/telephony/qns/WifiQualityMonitor.java
@@ -53,6 +53,8 @@
     private final ConnectivityManager mConnectivityManager;
     private final WiFiThresholdCallback mWiFiThresholdCallback;
     private final NetworkRequest.Builder mBuilder;
+    private final QnsTimer mQnsTimer;
+    private final List<Integer> mTimerIds;
 
     private int mWifiRssi;
     @VisibleForTesting Handler mHandler;
@@ -60,6 +62,7 @@
     private static final int BACKHAUL_TIMER_DEFAULT = 3000;
     static final int INVALID_RSSI = -127;
     private boolean mIsRegistered = false;
+    private boolean mIsBackhaulRunning;
 
     private class WiFiThresholdCallback extends ConnectivityManager.NetworkCallback {
         /** Callback Received based on meeting Wifi RSSI Threshold Registered or Wifi Lost */
@@ -94,11 +97,7 @@
     synchronized void validateWqmStatus(int wifiRssi) {
         if (isWifiRssiValid(wifiRssi)) {
             Log.d(mTag, "Registered Threshold @ Wqm Status check =" + mRegisteredThreshold);
-            if (!mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED)) {
-                mHandler.obtainMessage(EVENT_WIFI_RSSI_CHANGED, wifiRssi, 0).sendToTarget();
-            } else {
-                Log.d(mTag, "BackhaulCheck in Progress , skip validation");
-            }
+            mHandler.obtainMessage(EVENT_WIFI_RSSI_CHANGED, wifiRssi, 0).sendToTarget();
         } else {
             Log.d(mTag, "Cancel backhaul if running for invalid SS received");
             clearBackHaulTimer();
@@ -117,21 +116,24 @@
     }
 
     private void clearBackHaulTimer() {
-        if (mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED)) {
-            Log.d(mTag, "Stop all active backhaul timers");
-            mHandler.removeMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED);
-            mWaitingThresholds.clear();
+        Log.d(mTag, "Stop all active backhaul timers");
+        for (int timerId : mTimerIds) {
+            mQnsTimer.unregisterTimer(timerId);
         }
+        mTimerIds.clear();
+        mWaitingThresholds.clear();
     }
 
     /**
      * Create WifiQualityMonitor object for accessing WifiManager, ConnectivityManager to monitor
      * RSSI, build parameters for registering threshold & callback listening.
      */
-    WifiQualityMonitor(Context context) {
+    WifiQualityMonitor(Context context, QnsTimer qnsTimer) {
         super(QualityMonitor.class.getSimpleName() + "-I");
         mTag = WifiQualityMonitor.class.getSimpleName() + "-I";
         mContext = context;
+        mQnsTimer = qnsTimer;
+        mTimerIds = new ArrayList<>();
         HandlerThread handlerThread = new HandlerThread(mTag);
         handlerThread.start();
         mHandler = new WiFiEventsHandler(handlerThread.getLooper());
@@ -239,9 +241,7 @@
     }
 
     private void validateForWifiBackhaul(int wifiRssi) {
-        if (mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED)) {
-            mHandler.removeMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED);
-        }
+        mIsBackhaulRunning = false;
         for (Map.Entry<String, List<Threshold>> entry : mThresholdsList.entrySet()) {
             if (mWaitingThresholds.getOrDefault(entry.getKey(), false)) {
                 continue;
@@ -262,9 +262,13 @@
         }
         if (backhaul > 0) {
             mWaitingThresholds.put(key, true);
-            if (!mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED)) {
-                Log.d(mTag, "Starting backhaul timer = " + backhaul);
-                mHandler.sendEmptyMessageDelayed(EVENT_WIFI_NOTIFY_TIMER_EXPIRED, backhaul);
+            Log.d(mTag, "Starting backhaul timer = " + backhaul);
+            if (!mIsBackhaulRunning) {
+                mTimerIds.add(
+                        mQnsTimer.registerTimer(
+                                Message.obtain(mHandler, EVENT_WIFI_NOTIFY_TIMER_EXPIRED),
+                                backhaul));
+                mIsBackhaulRunning = true;
             }
         } else {
             Log.d(mTag, "Notify for RSSI Threshold Registered w/o Backhaul = " + backhaul);
@@ -302,7 +306,6 @@
             unregisterCallback();
             if (mThresholdsList.isEmpty()) {
                 clearBackHaulTimer();
-                mWaitingThresholds.clear();
             }
         } else {
             Log.d(mTag, "Listening to threshold = " + mRegisteredThreshold);
@@ -378,8 +381,8 @@
                 prefix
                         + ", mIsRegistered="
                         + mIsRegistered
-                        + ", backhaulstatus ="
-                        + mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED));
+                        + ", mIsBackhaulRunning="
+                        + mIsBackhaulRunning);
         pw.println(
                 prefix
                         + "mWifiRssi="
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsCallStatusTrackerTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsCallStatusTrackerTest.java
index 9dea914..5d7f405 100644
--- a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsCallStatusTrackerTest.java
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsCallStatusTrackerTest.java
@@ -24,6 +24,10 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.when;
 
@@ -53,8 +57,10 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
+import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 
 @RunWith(JUnit4.class)
@@ -69,6 +75,8 @@
     private Handler mLowQualityHandler;
     private MockitoSession mMockSession;
     List<CallState> mTestCallStateList = new ArrayList<>();
+    int mId = 0;
+    HashMap<Integer, Message> mMessageHashMap = new HashMap<>();
 
     @Before
     public void setUp() throws Exception {
@@ -82,8 +90,27 @@
         mImsHandler = new Handler(mTestLooperListener.getLooper());
         mEmergencyHandler = new Handler(mTestLooperListener.getLooper());
         mLowQualityHandler = new Handler(mLowQualityListenerLooper.getLooper());
+        mMessageHashMap = new HashMap<>();
+        when(mMockQnsTimer.registerTimer(isA(Message.class), anyLong())).thenAnswer(
+                (Answer<Integer>) invocation -> {
+                    Message msg = (Message) invocation.getArguments()[0];
+                    long delay = (long) invocation.getArguments()[1];
+                    msg.getTarget().sendMessageDelayed(msg, delay);
+                    mMessageHashMap.put(++mId, msg);
+                    return mId;
+                });
+
+        doAnswer(invocation -> {
+            int timerId = (int) invocation.getArguments()[0];
+            Message msg = mMessageHashMap.get(timerId);
+            if (msg != null && msg.getTarget() != null) {
+                msg.getTarget().removeMessages(msg.what, msg.obj);
+            }
+            return null;
+        }).when(mMockQnsTimer).unregisterTimer(anyInt());
         mCallTracker = new QnsCallStatusTracker(
-                mMockQnsTelephonyListener, mMockQnsConfigManager, 0, mTestLooper.getLooper());
+                mMockQnsTelephonyListener, mMockQnsConfigManager, mMockQnsTimer, 0,
+                mTestLooper.getLooper());
         mCallTracker.registerCallTypeChangedListener(
                 NetworkCapabilities.NET_CAPABILITY_IMS, mImsHandler, 1, null);
         mCallTracker.registerCallTypeChangedListener(
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsComponentsTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsComponentsTest.java
index 8d88d75..73a5249 100644
--- a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsComponentsTest.java
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsComponentsTest.java
@@ -51,6 +51,7 @@
         assertNull(qnsComponents.getWifiBackhaulMonitor(slotId));
         assertNull(qnsComponents.getWifiQualityMonitor());
         assertNull(qnsComponents.getIwlanNetworkStatusTracker());
+        assertNull(qnsComponents.getQnsTimer());
 
         qnsComponents.createQnsComponents(slotId);
 
@@ -65,6 +66,7 @@
         assertNotNull(qnsComponents.getWifiBackhaulMonitor(slotId));
         assertNotNull(qnsComponents.getWifiQualityMonitor());
         assertNotNull(qnsComponents.getIwlanNetworkStatusTracker());
+        assertNotNull(qnsComponents.getQnsTimer());
     }
 
 
@@ -82,6 +84,7 @@
                 mMockQnsProvisioningListener,
                 mMockQnsTelephonyListener,
                 mMockQnsCallStatusTracker,
+                mMockQnsTimer,
                 mMockWifiBm,
                 mMockWifiQm,
                 mMockQnsMetrics,
@@ -98,6 +101,7 @@
         assertNotNull(qnsComponents.getWifiBackhaulMonitor(slotId));
         assertNotNull(qnsComponents.getWifiQualityMonitor());
         assertNotNull(qnsComponents.getIwlanNetworkStatusTracker());
+        assertNotNull(qnsComponents.getQnsTimer());
         assertNotNull(qnsComponents.getQnsMetrics());
 
         qnsComponents.closeComponents(slotId);
@@ -113,6 +117,7 @@
         assertNull(qnsComponents.getWifiBackhaulMonitor(slotId));
         assertNull(qnsComponents.getWifiQualityMonitor());
         assertNull(qnsComponents.getIwlanNetworkStatusTracker());
+        assertNull(qnsComponents.getQnsTimer());
         assertNull(qnsComponents.getQnsMetrics());
 
         verify(mMockQnsTelephonyListener).close();
@@ -126,6 +131,7 @@
         verify(mMockWifiBm).close();
         verify(mMockWifiQm).close();
         verify(mMockIwlanNetworkStatusTracker).close();
+        verify(mMockQnsTimer).close();
         verify(mMockQnsMetrics).close();
     }
 }
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsTest.java
index c973a25..0dff376 100644
--- a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsTest.java
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsTest.java
@@ -32,6 +32,7 @@
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.os.Handler;
+import android.os.PowerManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
@@ -73,6 +74,7 @@
     @Mock protected WifiQualityMonitor mMockWifiQm;
     @Mock protected CellularNetworkStatusTracker mMockCellNetStatusTracker;
     @Mock protected CellularQualityMonitor mMockCellularQm;
+    @Mock protected PowerManager mMockPowerManager;
     @Mock protected QnsImsManager mMockQnsImsManager;
     @Mock protected QnsCarrierConfigManager mMockQnsConfigManager;
     @Mock protected QnsEventDispatcher mMockQnsEventDispatcher;
@@ -80,6 +82,7 @@
     @Mock protected QnsTelephonyListener mMockQnsTelephonyListener;
     @Mock protected QnsCallStatusTracker mMockQnsCallStatusTracker;
     @Mock protected WifiBackhaulMonitor mMockWifiBm;
+    @Mock protected QnsTimer mMockQnsTimer;
     @Mock protected QnsMetrics mMockQnsMetrics;
 
     protected QnsComponents[] mQnsComponents = new QnsComponents[2];
@@ -109,6 +112,7 @@
                         mMockQnsProvisioningListener,
                         mMockQnsTelephonyListener,
                         mMockQnsCallStatusTracker,
+                        mMockQnsTimer,
                         mMockWifiBm,
                         mMockWifiQm,
                         mMockQnsMetrics,
@@ -126,6 +130,7 @@
                         mMockQnsProvisioningListener,
                         mMockQnsTelephonyListener,
                         mMockQnsCallStatusTracker,
+                        mMockQnsTimer,
                         mMockWifiBm,
                         mMockWifiQm,
                         mMockQnsMetrics,
@@ -144,7 +149,7 @@
         when(sMockContext.getSystemService(ImsManager.class)).thenReturn(mMockImsManager);
         when(sMockContext.getSystemService(WifiManager.class)).thenReturn(mMockWifiManager);
         when(sMockContext.getSystemService(CountryDetector.class)).thenReturn(mMockCountryDetector);
-
+        when(sMockContext.getSystemService(PowerManager.class)).thenReturn(mMockPowerManager);
         when(sMockContext.getResources()).thenReturn(mMockResources);
     }
 
@@ -165,6 +170,7 @@
 
         when(mMockCountryDetector.detectCountry())
                 .thenReturn(new Country("US", Country.COUNTRY_SOURCE_LOCATION));
+        when(mMockPowerManager.isDeviceIdleMode()).thenReturn(false);
     }
 
     private void stubOthers() {
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsTimerTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsTimerTest.java
new file mode 100644
index 0000000..c4acd5a
--- /dev/null
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QnsTimerTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 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.telephony.qns;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.test.TestLooper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class QnsTimerTest extends QnsTest {
+
+    private static final int EVENT_QNS_TIMER_EXPIRED = 1;
+    static final String ACTION_ALARM_TIMER_EXPIRED =
+            "com.android.telephony.qns.action.ALARM_TIMER_EXPIRED";
+    static final String KEY_TIMER_ID = "key_timer_id";
+    @Mock private Context mContext;
+    @Mock private AlarmManager mAlarmManager;
+    @Mock private PowerManager mPowerManager;
+    @Mock Message mMessage;
+    private QnsTimer mQnsTimer;
+    private BroadcastReceiver mBroadcastReceiver;
+    int mTimerId;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager);
+        when(mContext.getSystemService(PowerManager.class)).thenReturn(mPowerManager);
+        mQnsTimer = new QnsTimer(mContext);
+        ArgumentCaptor<BroadcastReceiver> args = ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mContext).registerReceiver(args.capture(), isA(IntentFilter.class), anyInt());
+        mBroadcastReceiver = args.getValue();
+    }
+
+    @After
+    public void tearDown() {
+        mQnsTimer.close();
+    }
+
+    @Test
+    public void testRegisterTimerForScreenOff() {
+        mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        mTimerId = mQnsTimer.registerTimer(mMessage, 30000);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        assertTrue(mQnsTimer.getTimersInfo().contains(new QnsTimer.TimerInfo(mTimerId)));
+        assertTrue(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+        verify(mAlarmManager)
+                .setExactAndAllowWhileIdle(anyInt(), anyLong(), isA(PendingIntent.class));
+    }
+
+    @Test
+    public void testRegisterTimerForScreenOn() {
+        mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_ON));
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        mTimerId = mQnsTimer.registerTimer(mMessage, 80000);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        assertTrue(mQnsTimer.getTimersInfo().contains(new QnsTimer.TimerInfo(mTimerId)));
+        assertTrue(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+        verify(mAlarmManager, never())
+                .setExactAndAllowWhileIdle(anyInt(), anyLong(), isA(PendingIntent.class));
+    }
+
+    @Test
+    public void testUnregisterForInvalidId() {
+        testRegisterTimerForScreenOn();
+        int timerInfoSize = mQnsTimer.getTimersInfo().size();
+        mQnsTimer.unregisterTimer(QnsConstants.INVALID_ID);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+        assertEquals(timerInfoSize, mQnsTimer.getTimersInfo().size());
+    }
+
+    @Test
+    public void testUnregisterTimerForScreenOff() {
+        testRegisterTimerForScreenOff();
+        mQnsTimer.unregisterTimer(mTimerId);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+        assertFalse(mQnsTimer.getTimersInfo().contains(new QnsTimer.TimerInfo(mTimerId)));
+        assertFalse(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+        verify(mAlarmManager).cancel(isA(PendingIntent.class));
+    }
+
+    @Test
+    public void testUnregisterTimerForScreenOn() {
+        testRegisterTimerForScreenOn();
+        mQnsTimer.unregisterTimer(mTimerId);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+        assertFalse(mQnsTimer.getTimersInfo().contains(new QnsTimer.TimerInfo(mTimerId)));
+        assertFalse(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+        verify(mAlarmManager, never()).cancel(isA(PendingIntent.class));
+    }
+
+    @Test
+    public void testUpdateTimerTypeToAlarm() {
+        testRegisterTimerForScreenOn();
+
+        mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        verify(mAlarmManager)
+                .setExactAndAllowWhileIdle(anyInt(), anyLong(), isA(PendingIntent.class));
+    }
+
+    @Test
+    public void testTimerExpired() {
+        mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        mTimerId = mQnsTimer.registerTimer(mMessage, 50);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        assertTrue(mQnsTimer.getTimersInfo().contains(new QnsTimer.TimerInfo(mTimerId)));
+        assertTrue(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+        verify(mAlarmManager)
+                .setExactAndAllowWhileIdle(anyInt(), anyLong(), isA(PendingIntent.class));
+
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 40, 200);
+        mBroadcastReceiver.onReceive(mContext, new Intent(ACTION_ALARM_TIMER_EXPIRED));
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        verify(mMessage).sendToTarget();
+        assertFalse(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+        verify(mAlarmManager).cancel(isA(PendingIntent.class));
+    }
+
+    @Test
+    public void testMultipleTimerRegistered() {
+        mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+        TestLooper testLooper = new TestLooper();
+        Handler h = new Handler(testLooper.getLooper());
+
+        mQnsTimer.registerTimer(Message.obtain(h, 4), 300);
+        mQnsTimer.registerTimer(Message.obtain(h, 3), 200);
+        mQnsTimer.registerTimer(Message.obtain(h, 1), 50);
+        mQnsTimer.registerTimer(Message.obtain(h, 1), 50);
+        mQnsTimer.registerTimer(Message.obtain(h, 2), 100);
+        mQnsTimer.registerTimer(Message.obtain(h, 2), 100);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 320, 100);
+
+        // alarm timer should update for shortest delay and since the minimum timer value is 10
+        // secs for screen off condition, the alarm timer will not replace be replaced until new
+        // timer requested for less than 10 secs.
+        verify(mAlarmManager)
+                .setExactAndAllowWhileIdle(anyInt(), anyLong(), isA(PendingIntent.class));
+
+        // verify order of message received:
+        Message msg = testLooper.nextMessage();
+        assertEquals(1, msg.what);
+        msg = testLooper.nextMessage();
+        assertEquals(1, msg.what);
+        msg = testLooper.nextMessage();
+        assertEquals(2, msg.what);
+        msg = testLooper.nextMessage();
+        assertEquals(2, msg.what);
+        msg = testLooper.nextMessage();
+        assertEquals(3, msg.what);
+        msg = testLooper.nextMessage();
+        assertEquals(4, msg.what);
+    }
+
+    @Test
+    public void testCancelOngoingAlarm() {
+        mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+        TestLooper testLooper = new TestLooper();
+        Handler h = new Handler(testLooper.getLooper());
+
+        int timerId1 = mQnsTimer.registerTimer(Message.obtain(h, 1), 61 * 1000);
+        int timerId2 = mQnsTimer.registerTimer(Message.obtain(h, 2), 1000);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+
+        assertEquals(2, mQnsTimer.getTimersInfo().size());
+        assertEquals(timerId2, mQnsTimer.getTimersInfo().peek().getTimerId());
+        assertTrue(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+        verify(mAlarmManager, times(2))
+                .setExactAndAllowWhileIdle(anyInt(), anyLong(), isA(PendingIntent.class));
+
+        mQnsTimer.unregisterTimer(timerId2);
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+        assertEquals(1, mQnsTimer.getTimersInfo().size());
+        assertEquals(timerId1, mQnsTimer.getTimersInfo().peek().getTimerId());
+        assertTrue(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
+
+        verify(mAlarmManager, times(3))
+                .setExactAndAllowWhileIdle(anyInt(), anyLong(), isA(PendingIntent.class));
+    }
+
+    @Test
+    public void testAlarmOnLiteIdleModeMinDelay() {
+        int setDelay = 20000;
+        when(mPowerManager.isDeviceLightIdleMode()).thenReturn(true);
+        mBroadcastReceiver.onReceive(
+                sMockContext, new Intent(PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        long delay = setupAlarmForDelay(setDelay);
+
+        // assume 100ms as max delay in execution
+        assertTrue(delay < 30000 && delay > 30000 - 100);
+    }
+
+    @Test
+    public void testAlarmOnLiteIdleMode() {
+        int setDelay = 40000;
+        when(mPowerManager.isDeviceLightIdleMode()).thenReturn(true);
+        mBroadcastReceiver.onReceive(
+                sMockContext, new Intent(PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        long delay = setupAlarmForDelay(setDelay);
+
+        // assume 100ms as max delay in execution
+        assertTrue(delay < setDelay && delay > setDelay - 100);
+    }
+
+    @Test
+    public void testAlarmOnIdleModeMinDelay() {
+        int setDelay = 50000;
+        when(mPowerManager.isDeviceIdleMode()).thenReturn(true);
+        mBroadcastReceiver.onReceive(
+                sMockContext, new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+        long delay = setupAlarmForDelay(setDelay);
+
+        // assume 100ms as max delay in execution
+        assertTrue(delay < 60000 && delay > 60000 - 100);
+    }
+
+    @Test
+    public void testAlarmOnIdleMode() {
+        int setDelay = 70000;
+        when(mPowerManager.isDeviceIdleMode()).thenReturn(true);
+        mBroadcastReceiver.onReceive(
+                sMockContext, new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+        long delay = setupAlarmForDelay(setDelay);
+
+        // assume 100ms as max delay in execution
+        assertTrue(delay < setDelay && delay > setDelay - 100);
+    }
+
+    private long setupAlarmForDelay(int setDelay) {
+        mQnsTimer.registerTimer(mMessage, setDelay);
+
+        waitForDelayedHandlerAction(mQnsTimer.mHandler, 10, 200);
+        ArgumentCaptor<Long> capture = ArgumentCaptor.forClass(Long.class);
+        verify(mAlarmManager)
+                .setExactAndAllowWhileIdle(anyInt(), capture.capture(), isA(PendingIntent.class));
+        return capture.getValue() - SystemClock.elapsedRealtime();
+    }
+
+    @Test
+    public void testAlarmInCallActiveState() {
+        mQnsTimer.updateCallState(QnsConstants.CALL_TYPE_VOICE);
+        int setDelay = 4000;
+        when(mPowerManager.isDeviceIdleMode()).thenReturn(true);
+        mBroadcastReceiver.onReceive(
+                sMockContext, new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+        long delay = setupAlarmForDelay(setDelay);
+
+        // assume 100ms as max delay in execution
+        assertTrue(delay < setDelay && delay > setDelay - 100);
+    }
+}
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QualifiedNetworksServiceImplTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QualifiedNetworksServiceImplTest.java
index 87eb761..9a1d559 100644
--- a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QualifiedNetworksServiceImplTest.java
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/QualifiedNetworksServiceImplTest.java
@@ -90,6 +90,7 @@
                     mMockQnsProvisioningListener,
                     mMockQnsTelephonyListener,
                     mMockQnsCallStatusTracker,
+                    mMockQnsTimer,
                     mMockWifiBm,
                     mMockWifiQm,
                     mMockQnsMetrics,
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/RestrictManagerTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/RestrictManagerTest.java
index f897366..ddc193a 100644
--- a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/RestrictManagerTest.java
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/RestrictManagerTest.java
@@ -46,7 +46,10 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.when;
 
@@ -74,7 +77,9 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
+import org.mockito.stubbing.Answer;
 
+import java.util.HashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -93,6 +98,8 @@
     private QnsImsManager mQnsImsManager;
 
     protected TestLooper mTestLooper;
+    int mId = 0;
+    HashMap<Integer, Message> mMessageHashMap = new HashMap<>();
 
     HandlerThread mHandlerThread =
             new HandlerThread("") {
@@ -123,6 +130,24 @@
         when(mMockQnsConfigManager.getWaitingTimerForPreferredTransportOnPowerOn(
                         AccessNetworkConstants.TRANSPORT_TYPE_WWAN))
                 .thenReturn(0);
+        when(mMockQnsTimer.registerTimer(isA(Message.class), anyLong())).thenAnswer(
+                (Answer<Integer>) invocation -> {
+                    Message msg = (Message) invocation.getArguments()[0];
+                    long delay = (long) invocation.getArguments()[1];
+                    msg.getTarget().sendMessageDelayed(msg, delay);
+                    mMessageHashMap.put(++mId, msg);
+                    return mId;
+                });
+
+        doAnswer(invocation -> {
+            int timerId = (int) invocation.getArguments()[0];
+            Message msg = mMessageHashMap.get(timerId);
+            if (msg != null && msg.getTarget() != null) {
+                msg.getTarget().removeMessages(msg.what, msg.obj);
+            }
+            return null;
+        }).when(mMockQnsTimer).unregisterTimer(anyInt());
+
         mTestLooper = new TestLooper();
         mHandlerThread.start();
 
@@ -140,6 +165,7 @@
                         mMockQnsProvisioningListener,
                         mTelephonyListener,
                         mMockQnsCallStatusTracker,
+                        mMockQnsTimer,
                         mMockWifiBm,
                         mMockWifiQm,
                         mMockQnsMetrics,
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiBackhaulMonitorTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiBackhaulMonitorTest.java
index 3f81fb1..b05d3c1 100644
--- a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiBackhaulMonitorTest.java
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiBackhaulMonitorTest.java
@@ -17,9 +17,11 @@
 package com.android.telephony.qns;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.telephony.qns.QnsConstants.INVALID_ID;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.anyInt;
@@ -76,6 +78,7 @@
     private StaticMockitoSession mMockitoSession;
     private LinkProperties mLinkProperties = new LinkProperties();
     private String mServerAddress;
+    private QnsTimer mQnsTimer;
     private int[] mRttConfigs;
 
     HandlerThread mHt =
@@ -85,7 +88,11 @@
                     super.onLooperPrepared();
                     mWbm =
                             new WifiBackhaulMonitor(
-                                    sMockContext, mMockQnsConfigManager, mMockQnsImsManager, 0);
+                                    sMockContext,
+                                    mMockQnsConfigManager,
+                                    mMockQnsImsManager,
+                                    mQnsTimer,
+                                    0);
                     setReady(true);
                 }
             };
@@ -121,6 +128,7 @@
         mRttConfigs = null;
         mLinkProperties.setInterfaceName("iwlan0");
         mLatch = new CountDownLatch(1);
+        mQnsTimer = new QnsTimer(sMockContext);
 
         mMockitoSession =
                 mockitoSession()
@@ -282,7 +290,7 @@
                                 null))
                 .sendToTarget();
         waitForDelayedHandlerAction(mRttHandler, 100, 100);
-        assertTrue(mRttHandler.hasMessages(EVENT_START_RTT_CHECK));
+        assertNotEquals(mWbm.getRttTimerId(), INVALID_ID);
     }
 
     @Test
diff --git a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiQualityMonitorTest.java b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiQualityMonitorTest.java
index 75d8db7..8e51d2c 100644
--- a/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiQualityMonitorTest.java
+++ b/services/QualifiedNetworksService/tests/src/com/android/telephony/qns/WifiQualityMonitorTest.java
@@ -53,8 +53,10 @@
 @RunWith(JUnit4.class)
 public class WifiQualityMonitorTest extends QnsTest {
 
+    private static final int EVENT_QNS_TIMER_EXPIRED = 1;
     Context mContext;
     @Mock ConnectivityManager mConnectivityManager;
+    QnsTimer mQnsTimer;
     @Mock WifiManager mWifiManager;
     @Mock NetworkCapabilities mNetworkCapabilityManager;
     @Mock private Network mMockNetwork;
@@ -96,7 +98,8 @@
         mWifiInfo = new WifiInfo.Builder().setRssi(mSetRssi).build();
         mLatch = new CountDownLatch(1);
         mThresholdListener = new ThresholdListener(mExecutor);
-        mWifiQualityMonitor = new WifiQualityMonitor(mContext);
+        mQnsTimer = new QnsTimer(mContext);
+        mWifiQualityMonitor = new WifiQualityMonitor(mContext, mQnsTimer);
     }
 
     @Test
@@ -232,7 +235,7 @@
     }
 
     @Test
-    public void testBackhaulTimer() throws InterruptedException {
+    public void testBackhaulTimer() {
         mSetRssi = -65;
         mLatch = new CountDownLatch(1);
         mWifiInfo = new WifiInfo.Builder().setRssi(mSetRssi).build();
@@ -277,8 +280,9 @@
 
         mWifiQualityMonitor.mHandler.obtainMessage(EVENT_WIFI_RSSI_CHANGED, -65, 0).sendToTarget();
         waitForDelayedHandlerAction(mWifiQualityMonitor.mHandler, 1000, 200);
-        assertTrue(mWifiQualityMonitor.mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED));
+        assertTrue(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
         waitForDelayedHandlerAction(mWifiQualityMonitor.mHandler, 4000, 200);
+        assertFalse(mQnsTimer.mHandler.hasMessages(EVENT_QNS_TIMER_EXPIRED));
         assertFalse(mWifiQualityMonitor.mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED));
     }
 
@@ -329,23 +333,6 @@
         isWifiRssiChangedHandlerNotPosted();
     }
 
-    @Test
-    public void testSkipValidateWqmStatus_WithBackhaulInProgress() {
-        mSetRssi = -65;
-        mLatch = new CountDownLatch(1);
-        mWifiInfo = new WifiInfo.Builder().setRssi(mSetRssi).build();
-        when(mWifiManager.getConnectionInfo()).thenReturn(mWifiInfo);
-
-        setWqmThreshold();
-        mWifiQualityMonitor.validateWqmStatus(-65);
-
-        waitForDelayedHandlerAction(mWifiQualityMonitor.mHandler, 1000, 200);
-        assertTrue(mWifiQualityMonitor.mHandler.hasMessages(EVENT_WIFI_NOTIFY_TIMER_EXPIRED));
-
-        mWifiQualityMonitor.validateWqmStatus(-68);
-        assertFalse(mWifiQualityMonitor.mHandler.hasMessages(EVENT_WIFI_RSSI_CHANGED));
-    }
-
     private void setWqmThreshold() {
         mThs1[0] =
                 new Threshold(