Merge "Implement main functions to consolidate signal strength request"
diff --git a/src/java/com/android/internal/telephony/DeviceStateMonitor.java b/src/java/com/android/internal/telephony/DeviceStateMonitor.java
index 94de555..204b7d6 100644
--- a/src/java/com/android/internal/telephony/DeviceStateMonitor.java
+++ b/src/java/com/android/internal/telephony/DeviceStateMonitor.java
@@ -398,7 +398,7 @@
      *
      * @return True if the response update should be enabled.
      */
-    private boolean shouldEnableHighPowerConsumptionIndications() {
+    public boolean shouldEnableHighPowerConsumptionIndications() {
         // We should enable indications reports if one of the following condition is true.
         // 1. The device is charging.
         // 2. When the screen is on.
@@ -477,6 +477,7 @@
      */
     private void onUpdateDeviceState(int eventType, boolean state) {
         final boolean shouldEnableBarringInfoReportsOld = shouldEnableBarringInfoReports();
+        final boolean wasHighPowerEnabled = shouldEnableHighPowerConsumptionIndications();
         switch (eventType) {
             case EVENT_SCREEN_STATE_CHANGED:
                 if (mIsScreenOn == state) return;
@@ -512,6 +513,11 @@
                 return;
         }
 
+        final boolean isHighPowerEnabled = shouldEnableHighPowerConsumptionIndications();
+        if (wasHighPowerEnabled != isHighPowerEnabled) {
+            mPhone.notifyDeviceIdleStateChanged(!isHighPowerEnabled /*isIdle*/);
+        }
+
         final int newCellInfoMinInterval = computeCellInfoMinInterval();
         if (mCellInfoMinInterval != newCellInfoMinInterval) {
             mCellInfoMinInterval = newCellInfoMinInterval;
diff --git a/src/java/com/android/internal/telephony/GsmCdmaPhone.java b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
index 812a0c6..c983c1f 100644
--- a/src/java/com/android/internal/telephony/GsmCdmaPhone.java
+++ b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
@@ -4054,13 +4054,18 @@
     @Override
     public void setSignalStrengthReportingCriteria(
             int signalStrengthMeasure, int[] thresholds, int ran, boolean isEnabled) {
+        int[] consolidatedThresholds = mSST.getConsolidatedSignalThresholds(
+                ran,
+                signalStrengthMeasure,
+                mSST.shouldHonorSystemThresholds() ? thresholds : new int[]{},
+                REPORTING_HYSTERESIS_DB);
         mCi.setSignalStrengthReportingCriteria(
                 new SignalThresholdInfo.Builder()
                         .setRadioAccessNetworkType(ran)
                         .setSignalMeasurementType(signalStrengthMeasure)
                         .setHysteresisMs(REPORTING_HYSTERESIS_MILLIS)
                         .setHysteresisDb(REPORTING_HYSTERESIS_DB)
-                        .setThresholds(thresholds)
+                        .setThresholdsUnlimited(consolidatedThresholds)
                         .setIsEnabled(isEnabled)
                         .build(),
                 ran, null);
diff --git a/src/java/com/android/internal/telephony/Phone.java b/src/java/com/android/internal/telephony/Phone.java
index 7d64a46..1948ac9 100644
--- a/src/java/com/android/internal/telephony/Phone.java
+++ b/src/java/com/android/internal/telephony/Phone.java
@@ -4600,6 +4600,36 @@
     }
 
     /**
+     * Check if device is idle. Device is idle when it is not in high power consumption mode.
+     *
+     * @see DeviceStateMonitor#shouldEnableHighPowerConsumptionIndications()
+     *
+     * @return true if device is idle
+     */
+    public boolean isDeviceIdle() {
+        DeviceStateMonitor dsm = getDeviceStateMonitor();
+        if (dsm == null) {
+            Rlog.e(LOG_TAG, "isDeviceIdle: DeviceStateMonitor is null");
+            return false;
+        }
+        return !dsm.shouldEnableHighPowerConsumptionIndications();
+    }
+
+    /**
+     * Get notified when device idleness state has changed
+     *
+     * @param isIdle true if the new state is idle
+     */
+    public void notifyDeviceIdleStateChanged(boolean isIdle) {
+        ServiceStateTracker sst = getServiceStateTracker();
+        if (sst == null) {
+            Rlog.e(LOG_TAG, "notifyDeviceIdleStateChanged: SST is null");
+            return;
+        }
+        sst.onDeviceIdleStateChanged(isIdle);
+    }
+
+    /**
      * Returns a list of the equivalent home PLMNs (EF_EHPLMN) from the USIM app.
      *
      * @return A list of equivalent home PLMNs. Returns an empty list if EF_EHPLMN is empty or
diff --git a/src/java/com/android/internal/telephony/ServiceStateTracker.java b/src/java/com/android/internal/telephony/ServiceStateTracker.java
index ce37488..66cc005 100755
--- a/src/java/com/android/internal/telephony/ServiceStateTracker.java
+++ b/src/java/com/android/internal/telephony/ServiceStateTracker.java
@@ -44,11 +44,13 @@
 import android.os.BaseBundle;
 import android.os.Build;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Message;
 import android.os.Parcel;
 import android.os.PersistableBundle;
 import android.os.Registrant;
 import android.os.RegistrantList;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.TimestampedValue;
@@ -77,6 +79,7 @@
 import android.telephony.ServiceState;
 import android.telephony.ServiceState.RilRadioTechnology;
 import android.telephony.SignalStrength;
+import android.telephony.SignalStrengthUpdateRequest;
 import android.telephony.SignalThresholdInfo;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
@@ -125,9 +128,11 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -293,6 +298,9 @@
     protected static final int EVENT_CELL_LOCATION_RESPONSE            = 56;
     protected static final int EVENT_CARRIER_CONFIG_CHANGED            = 57;
     private static final int EVENT_POLL_STATE_REQUEST                  = 58;
+    private static final int EVENT_SET_SIGNAL_STRENGTH_UPDATE_REQUEST = 59;
+    private static final int EVENT_CLEAR_SIGNAL_STRENGTH_UPDATE_REQUEST = 60;
+    private static final int EVENT_ON_DEVICE_IDLE_STATE_CHANGED        = 61;
 
     /**
      * The current service state.
@@ -643,6 +651,9 @@
     private final Object mLteRsrpBoostLock = new Object();
     private static final int INVALID_LTE_EARFCN = -1;
 
+    // @GuardedBy("mSignalRequestRecords")
+    private final List<SignalRequestRecord> mSignalRequestRecords = new ArrayList<>();
+
     public ServiceStateTracker(GsmCdmaPhone phone, CommandsInterface ci) {
         mNitzState = TelephonyComponentFactory.getInstance()
                 .inject(NitzStateMachine.class.getName())
@@ -1752,6 +1763,76 @@
                 pollStateInternal(false);
                 break;
 
+            case EVENT_SET_SIGNAL_STRENGTH_UPDATE_REQUEST: {
+                Pair<SignalRequestRecord, Message> pair =
+                        (Pair<SignalRequestRecord, Message>) msg.obj;
+                SignalRequestRecord record = pair.first;
+                Message onCompleted = pair.second;
+                AsyncResult ret = AsyncResult.forMessage(onCompleted);
+
+                // TODO(b/177956310): Check subId to filter out old request until a better solution
+                boolean dupRequest = mSignalRequestRecords.stream().anyMatch(
+                        srr -> srr.mCallingUid == record.mCallingUid
+                                && srr.mSubId == record.mSubId);
+                if (dupRequest) {
+                    ret.exception = new IllegalStateException(
+                            "setSignalStrengthUpdateRequest called again with same subId");
+                    onCompleted.sendToTarget();
+                    break;
+                }
+
+                try {
+                    record.mRequest.getLiveToken().linkToDeath(record, 0);
+                } catch (RemoteException | NullPointerException ex) {
+                    ret.exception = new IllegalStateException(
+                            "Signal request client is already dead.");
+                    onCompleted.sendToTarget();
+                    break;
+                }
+
+                synchronized (mSignalRequestRecords) {
+                    mSignalRequestRecords.add(record);
+                }
+
+                updateAlwaysReportSignalStrength();
+                updateReportingCriteria(getCarrierConfig());
+
+                onCompleted.sendToTarget();
+                break;
+            }
+
+            case EVENT_CLEAR_SIGNAL_STRENGTH_UPDATE_REQUEST: {
+                Pair<SignalRequestRecord, Message> pair =
+                        (Pair<SignalRequestRecord, Message>) msg.obj;
+                SignalRequestRecord record = pair.first;
+                Message onCompleted = pair.second;
+
+                synchronized (mSignalRequestRecords) {
+                    // for loop with removal may cause ConcurrentModificationException
+                    Iterator<SignalRequestRecord> it = mSignalRequestRecords.iterator();
+                    while (it.hasNext()) {
+                        SignalRequestRecord srr = it.next();
+                        if (srr.mRequest.getLiveToken().equals(record.mRequest.getLiveToken())) {
+                            it.remove();
+                        }
+                    }
+                }
+
+                updateAlwaysReportSignalStrength();
+                updateReportingCriteria(getCarrierConfig());
+
+                if (onCompleted != null) {
+                    AsyncResult ret = AsyncResult.forMessage(onCompleted);
+                    onCompleted.sendToTarget();
+                }
+                break;
+            }
+
+            case EVENT_ON_DEVICE_IDLE_STATE_CHANGED: {
+                updateReportingCriteria(getCarrierConfig());
+                break;
+            }
+
             default:
                 log("Unhandled message with number: " + msg.what);
                 break;
@@ -5953,4 +6034,123 @@
         values.put(SERVICE_STATE, p.marshall());
         return values;
     }
+
+    /**
+     * Set a new request to update the signal strength thresholds.
+     */
+    public void setSignalStrengthUpdateRequest(int subId, int callingUid,
+            SignalStrengthUpdateRequest request, @NonNull Message onCompleted) {
+        SignalRequestRecord record = new SignalRequestRecord(subId, callingUid, request);
+        sendMessage(obtainMessage(EVENT_SET_SIGNAL_STRENGTH_UPDATE_REQUEST,
+                new Pair<SignalRequestRecord, Message>(record, onCompleted)));
+    }
+
+    /**
+     * Clear the previously set request.
+     */
+    public void clearSignalStrengthUpdateRequest(int subId, int callingUid,
+            SignalStrengthUpdateRequest request, @Nullable Message onCompleted) {
+        SignalRequestRecord record = new SignalRequestRecord(subId, callingUid, request);
+        sendMessage(obtainMessage(EVENT_CLEAR_SIGNAL_STRENGTH_UPDATE_REQUEST,
+                new Pair<SignalRequestRecord, Message>(record, onCompleted)));
+    }
+
+    /**
+     * Align all the qualified thresholds set from applications to the {@code systemThresholds}
+     * and consolidate a new thresholds array, follow rules below:
+     * 1. All threshold values (whose interval is guaranteed to be larger than hysteresis) in
+     *    {@code systemThresholds} will keep as it.
+     * 2. Any threshold from apps that has interval less than hysteresis from any threshold in
+     *    {@code systemThresholds} will be removed.
+     * 3. The target thresholds will be {@code systemThresholds} + all qualified thresholds from
+     *    apps, sorted in ascending order.
+     */
+    int[] getConsolidatedSignalThresholds(int ran, int measurement,
+            int[] systemThresholds, int hysteresis) {
+
+        // TreeSet with comparator that will filter element with interval less than hysteresis
+        // from any current element
+        Set<Integer> target = new TreeSet<>((x, y) -> {
+            if (y >= x - hysteresis && y <= x + hysteresis) {
+                return 0;
+            }
+            return Integer.compare(x, y);
+        });
+
+        for (int systemThreshold : systemThresholds) {
+            target.add(systemThreshold);
+        }
+
+        final boolean isDeviceIdle = mPhone.isDeviceIdle();
+        final int curSubId = mPhone.getSubId();
+        synchronized (mSignalRequestRecords) {
+            // The total number of record is small (10~15 tops). With each request has at most 5
+            // SignalThresholdInfo which has at most 8 thresholds arrays. So the nested loop should
+            // not be a concern here.
+            for (SignalRequestRecord record : mSignalRequestRecords) {
+                if (curSubId != record.mSubId
+                        || (isDeviceIdle && !record.mRequest.isReportingRequestedWhileIdle())) {
+                    continue;
+                }
+                for (SignalThresholdInfo info : record.mRequest.getSignalThresholdInfos()) {
+                    if (ran == info.getRadioAccessNetworkType()
+                            && measurement == info.getSignalMeasurementType()) {
+                        for (int appThreshold : info.getThresholds()) {
+                            target.add(appThreshold);
+                        }
+                    }
+                }
+            }
+        }
+
+        int[] targetArray = new int[target.size()];
+        int i = 0;
+        for (int element : target) {
+            targetArray[i++] = element;
+        }
+        return targetArray;
+    }
+
+    boolean shouldHonorSystemThresholds() {
+        if (!mPhone.isDeviceIdle()) {
+            return true;
+        }
+
+        final int curSubId = mPhone.getSubId();
+        return mSignalRequestRecords.stream().anyMatch(
+                srr -> curSubId == srr.mSubId
+                        && srr.mRequest.isSystemThresholdReportingRequestedWhileIdle());
+    }
+
+    void onDeviceIdleStateChanged(boolean isDeviceIdle) {
+        sendMessage(obtainMessage(EVENT_ON_DEVICE_IDLE_STATE_CHANGED, isDeviceIdle));
+    }
+
+    private class SignalRequestRecord implements IBinder.DeathRecipient {
+        final int mSubId; // subId the request originally applied to
+        final int mCallingUid;
+        final SignalStrengthUpdateRequest mRequest;
+
+        SignalRequestRecord(int subId, int uid, SignalStrengthUpdateRequest request) {
+            this.mCallingUid = uid;
+            this.mSubId = subId;
+            this.mRequest = request;
+        }
+
+        @Override
+        public void binderDied() {
+            clearSignalStrengthUpdateRequest(mSubId, mCallingUid, mRequest, null /*onCompleted*/);
+        }
+    }
+
+    private void updateAlwaysReportSignalStrength() {
+        final int curSubId = mPhone.getSubId();
+        boolean alwaysReport = mSignalRequestRecords.stream().anyMatch(
+                srr -> srr.mSubId == curSubId && (srr.mRequest.isReportingRequestedWhileIdle()
+                        || srr.mRequest.isSystemThresholdReportingRequestedWhileIdle()));
+
+        // TODO(b/177924721): TM#setAlwaysReportSignalStrength will be removed and we will not
+        // worry about unset flag which was set by other client.
+        mPhone.setAlwaysReportSignalStrength(alwaysReport);
+    }
 }