Stop unfiltered BLE scanning on screen off

Stop unfiltered BLE scans when the screen goes off.
These scans will be resumed when the screen turns on
again unless these scans are stopped by application
itself before the screen is turned on.

While screen is off, new scans are allowed to be
created but scan instances will not receive results
until the screen is turned on.

Stats in bluetooth_manager dumpsys are updated to show
suspended time as below:

  LE scans (started/stopped)         : 11 / 10
  Scan time in ms (min/max/avg/total): 1107 / 15003 / 7623 / 83859
  Total time suspended               : 4257ms
  Total number of results            : 14
  Last 5 scans                       :
    2017/07/26 15:41:50 - 14993ms Filter 0 results (6)
    2017/07/26 15:42:05 - 5021ms 0 results (6)
    2017/07/26 15:42:10 - 9908ms Filter 0 results (6)
    2017/07/26 15:42:20 - 5011ms 11 results (6)
      └ Suspended Time: 1193ms
    2017/07/26 15:42:25 - 14990ms Filter 0 results (6)
  Ongoing scans                      :
    2017/07/26 15:42:40 - 1342ms Suspended 0 results (6)
      └ Suspended Time: 1239ms

BUG: 62264269
Test: Tested BLE scanning applications.

Change-Id: I017f0cdc0fc52a0a30d6cf870d7e688e5ff18df9
diff --git a/src/com/android/bluetooth/gatt/AppScanStats.java b/src/com/android/bluetooth/gatt/AppScanStats.java
index 22cbd8a..36c7d1d 100644
--- a/src/com/android/bluetooth/gatt/AppScanStats.java
+++ b/src/com/android/bluetooth/gatt/AppScanStats.java
@@ -50,6 +50,9 @@
 
     class LastScan {
         long duration;
+        long suspendDuration;
+        long suspendStartTime;
+        boolean isSuspended;
         long timestamp;
         boolean opportunistic;
         boolean timeout;
@@ -67,6 +70,9 @@
             this.filtered = filtered;
             this.results = 0;
             this.scannerId = scannerId;
+            this.suspendDuration = 0;
+            this.suspendStartTime = 0;
+            this.isSuspended = false;
         }
     }
 
@@ -90,6 +96,7 @@
     long maxScanTime = 0;
     long mScanStartTime = 0;
     long mTotalScanTime = 0;
+    long mTotalSuspendTime = 0;
     List<LastScan> lastScans = new ArrayList<LastScan>(NUM_SCAN_DURATIONS_KEPT);
     HashMap<Integer, LastScan> ongoingScans = new HashMap<Integer, LastScan>();
     long startTime = 0;
@@ -168,6 +175,10 @@
         stopTime = SystemClock.elapsedRealtime();
         long scanDuration = stopTime - scan.timestamp;
         scan.duration = scanDuration;
+        if (scan.isSuspended) {
+            scan.suspendDuration += stopTime - scan.suspendStartTime;
+            mTotalSuspendTime += scan.suspendDuration;
+        }
         ongoingScans.remove(scannerId);
         if (lastScans.size() >= NUM_SCAN_DURATIONS_KEPT) {
             lastScans.remove(0);
@@ -194,6 +205,26 @@
         }
     }
 
+    synchronized void recordScanSuspend(int scannerId) {
+        LastScan scan = getScanFromScannerId(scannerId);
+        if (scan == null || scan.isSuspended) {
+            return;
+        }
+        scan.suspendStartTime = SystemClock.elapsedRealtime();
+        scan.isSuspended = true;
+    }
+
+    synchronized void recordScanResume(int scannerId) {
+        LastScan scan = getScanFromScannerId(scannerId);
+        if (scan == null || !scan.isSuspended) {
+            return;
+        }
+        scan.isSuspended = false;
+        stopTime = SystemClock.elapsedRealtime();
+        scan.suspendDuration += stopTime - scan.suspendStartTime;
+        mTotalSuspendTime += scan.suspendDuration;
+    }
+
     synchronized void setScanTimeout(int scannerId) {
         if (!isScanning()) return;
 
@@ -281,6 +312,9 @@
                   maxScan + " / " +
                   avgScan + " / " +
                   totalScanTime + "\n");
+        if (mTotalSuspendTime != 0) {
+            sb.append("  Total time suspended             : " + mTotalSuspendTime + "ms\n");
+        }
         sb.append("  Total number of results            : " +
                   results + "\n");
 
@@ -301,6 +335,10 @@
                 sb.append(scan.results + " results");
                 sb.append(" (" + scan.scannerId + ")");
                 sb.append("\n");
+                if (scan.suspendDuration != 0) {
+                    sb.append("      └"
+                            + " Suspended Time: " + scan.suspendDuration + "ms\n");
+                }
             }
         }
 
@@ -315,9 +353,16 @@
                 if (scan.background) sb.append("Back ");
                 if (scan.timeout) sb.append("Forced ");
                 if (scan.filtered) sb.append("Filter ");
+                if (scan.isSuspended) sb.append("Suspended ");
                 sb.append(scan.results + " results");
                 sb.append(" (" + scan.scannerId + ")");
                 sb.append("\n");
+                if (scan.suspendStartTime != 0) {
+                    long duration = scan.suspendDuration
+                            + (scan.isSuspended ? (elapsedRt - scan.suspendStartTime) : 0);
+                    sb.append("      └"
+                            + " Suspended Time: " + duration + "ms\n");
+                }
             }
         }
 
diff --git a/src/com/android/bluetooth/gatt/ScanManager.java b/src/com/android/bluetooth/gatt/ScanManager.java
index f7dc9f3..7131714 100644
--- a/src/com/android/bluetooth/gatt/ScanManager.java
+++ b/src/com/android/bluetooth/gatt/ScanManager.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -34,6 +35,7 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.util.Log;
+import android.view.Display;
 
 import com.android.bluetooth.Utils;
 import com.android.bluetooth.btservice.AdapterService;
@@ -69,7 +71,8 @@
     private static final int MSG_STOP_BLE_SCAN = 1;
     private static final int MSG_FLUSH_BATCH_RESULTS = 2;
     private static final int MSG_SCAN_TIMEOUT = 3;
-
+    private static final int MSG_SUSPEND_SCANS = 4;
+    private static final int MSG_RESUME_SCANS = 5;
     private static final String ACTION_REFRESH_BATCHED_SCAN =
             "com.android.bluetooth.gatt.REFRESH_BATCHED_SCAN";
 
@@ -89,15 +92,22 @@
 
     private Set<ScanClient> mRegularScanClients;
     private Set<ScanClient> mBatchClients;
+    private Set<ScanClient> mSuspendedScanClients;
 
     private CountDownLatch mLatch;
 
+    private DisplayManager mDm;
+
     ScanManager(GattService service) {
         mRegularScanClients = Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>());
         mBatchClients = Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>());
+        mSuspendedScanClients =
+                Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>());
         mService = service;
         mScanNative = new ScanNative();
         curUsedTrackableAdvertisements = 0;
+        mDm = (DisplayManager) mService.getSystemService(Context.DISPLAY_SERVICE);
+        mDm.registerDisplayListener(mDisplayListener, null);
     }
 
     void start() {
@@ -109,6 +119,7 @@
     void cleanup() {
         mRegularScanClients.clear();
         mBatchClients.clear();
+        mSuspendedScanClients.clear();
         mScanNative.cleanup();
 
         if (mHandler != null) {
@@ -215,6 +226,12 @@
                 case MSG_SCAN_TIMEOUT:
                     mScanNative.regularScanTimeout(client);
                     break;
+                case MSG_SUSPEND_SCANS:
+                    handleSuspendScans();
+                    break;
+                case MSG_RESUME_SCANS:
+                    handleResumeScans();
+                    break;
                 default:
                     // Shouldn't happen.
                     Log.e(TAG, "received an unkown message : " + msg.what);
@@ -223,6 +240,7 @@
 
         void handleStartScan(ScanClient client) {
             Utils.enforceAdminPermission(mService);
+            boolean isFiltered = (client.filters != null) && !client.filters.isEmpty();
             if (DBG) Log.d(TAG, "handling starting scan");
 
             if (!isScanSupported(client)) {
@@ -234,6 +252,15 @@
                 Log.e(TAG, "Scan already started");
                 return;
             }
+
+            if (!mScanNative.isOpportunisticScanClient(client) && !isScreenOn() && !isFiltered) {
+                Log.e(TAG,
+                        "Cannot start unfiltered scan in screen-off. This scan will be resumed later: "
+                                + client.scannerId);
+                mSuspendedScanClients.add(client);
+                return;
+            }
+
             // Begin scan operations.
             if (isBatchClient(client)) {
                 mBatchClients.add(client);
@@ -258,6 +285,10 @@
             Utils.enforceAdminPermission(mService);
             if (client == null) return;
 
+            if (mSuspendedScanClients.contains(client)) {
+                mSuspendedScanClients.remove(client);
+            }
+
             if (mRegularScanClients.contains(client)) {
                 mScanNative.stopRegularScan(client);
 
@@ -305,6 +336,30 @@
             return settings.getCallbackType() == ScanSettings.CALLBACK_TYPE_ALL_MATCHES &&
                     settings.getReportDelayMillis() == 0;
         }
+
+        void handleSuspendScans() {
+            for (ScanClient client : mRegularScanClients) {
+                if (!mScanNative.isOpportunisticScanClient(client)
+                        && (client.filters == null || client.filters.isEmpty())) {
+                    /*Suspend unfiltered scans*/
+                    if (client.stats != null) {
+                        client.stats.recordScanSuspend(client.scannerId);
+                    }
+                    handleStopScan(client);
+                    mSuspendedScanClients.add(client);
+                }
+            }
+        }
+
+        void handleResumeScans() {
+            for (ScanClient client : mSuspendedScanClients) {
+                if (client.stats != null) {
+                    client.stats.recordScanResume(client.scannerId);
+                }
+                handleStartScan(client);
+            }
+            mSuspendedScanClients.clear();
+        }
     }
 
     /**
@@ -1163,4 +1218,38 @@
 
         private native void gattClientReadScanReportsNative(int client_if, int scan_type);
     }
+
+    private boolean isScreenOn() {
+        Display[] displays = mDm.getDisplays();
+
+        if (displays == null) {
+            return false;
+        }
+
+        for (Display display : displays) {
+            if (display.getState() == Display.STATE_ON) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private final DisplayManager.DisplayListener mDisplayListener =
+            new DisplayManager.DisplayListener() {
+                @Override
+                public void onDisplayAdded(int displayId) {}
+
+                @Override
+                public void onDisplayRemoved(int displayId) {}
+
+                @Override
+                public void onDisplayChanged(int displayId) {
+                    if (isScreenOn()) {
+                        sendMessage(MSG_RESUME_SCANS, null);
+                    } else {
+                        sendMessage(MSG_SUSPEND_SCANS, null);
+                    }
+                }
+            };
 }