Merge "Bluetooth 5 periodic scan (1/3)"
diff --git a/jni/com_android_bluetooth_gatt.cpp b/jni/com_android_bluetooth_gatt.cpp
index bf8d48b..06a11a4 100644
--- a/jni/com_android_bluetooth_gatt.cpp
+++ b/jni/com_android_bluetooth_gatt.cpp
@@ -199,12 +199,20 @@
 static jmethodID method_onPeriodicAdvertisingEnabled;
 
 /**
+ * Periodic scanner callback methods
+ */
+static jmethodID method_onSyncLost;
+static jmethodID method_onSyncReport;
+static jmethodID method_onSyncStarted;
+
+/**
  * Static variables
  */
 
 static const btgatt_interface_t* sGattIf = NULL;
 static jobject mCallbacksObj = NULL;
 static jobject mAdvertiseCallbacksObj = NULL;
+static jobject mPeriodicScanCallbacksObj = NULL;
 
 /**
  * BTA client callbacks
@@ -1938,6 +1946,83 @@
       base::Bind(&enablePeriodicSetCb, advertiser_id, enable));
 }
 
+static void periodicScanClassInitNative(JNIEnv* env, jclass clazz) {
+  method_onSyncStarted =
+      env->GetMethodID(clazz, "onSyncStarted", "(IIIILjava/lang/String;III)V");
+  method_onSyncReport = env->GetMethodID(clazz, "onSyncReport", "(IIII[B)V");
+  method_onSyncLost = env->GetMethodID(clazz, "onSyncLost", "(I)V");
+}
+
+static void periodicScanInitializeNative(JNIEnv* env, jobject object) {
+  if (mPeriodicScanCallbacksObj != NULL) {
+    ALOGW("Cleaning up periodic scan callback object");
+    env->DeleteGlobalRef(mPeriodicScanCallbacksObj);
+    mPeriodicScanCallbacksObj = NULL;
+  }
+
+  mPeriodicScanCallbacksObj = env->NewGlobalRef(object);
+}
+
+static void periodicScanCleanupNative(JNIEnv* env, jobject object) {
+  if (mPeriodicScanCallbacksObj != NULL) {
+    env->DeleteGlobalRef(mPeriodicScanCallbacksObj);
+    mPeriodicScanCallbacksObj = NULL;
+  }
+}
+
+static void onSyncStarted(int reg_id, uint8_t status, uint16_t sync_handle,
+                          uint8_t sid, uint8_t address_type,
+                          bt_bdaddr_t address, uint8_t phy, uint16_t interval) {
+  CallbackEnv sCallbackEnv(__func__);
+  if (!sCallbackEnv.valid()) return;
+
+  sCallbackEnv->CallVoidMethod(mPeriodicScanCallbacksObj, method_onSyncStarted,
+                               reg_id, sync_handle, sid, address_type, address,
+                               phy, interval, status);
+}
+
+static void onSyncReport(uint16_t sync_handle, int8_t tx_power, int8_t rssi,
+                         uint8_t data_status, std::vector<uint8_t> data) {
+  CallbackEnv sCallbackEnv(__func__);
+  if (!sCallbackEnv.valid()) return;
+
+  ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(),
+                                sCallbackEnv->NewByteArray(data.size()));
+  sCallbackEnv->SetByteArrayRegion(jb.get(), 0, data.size(),
+                                   (jbyte*)data.data());
+
+  sCallbackEnv->CallVoidMethod(mPeriodicScanCallbacksObj, method_onSyncReport,
+                               sync_handle, tx_power, rssi, data_status,
+                               jb.get());
+}
+
+static void onSyncLost(uint16_t sync_handle) {
+  CallbackEnv sCallbackEnv(__func__);
+  if (!sCallbackEnv.valid()) return;
+
+  sCallbackEnv->CallVoidMethod(mPeriodicScanCallbacksObj, method_onSyncLost,
+                               sync_handle);
+}
+
+static void startSyncNative(JNIEnv* env, jobject object, jint sid,
+                            jstring address, jint skip, jint timeout,
+                            jint reg_id) {
+  if (!sGattIf) return;
+
+  bt_bdaddr_t tmp;
+  jstr2bdaddr(env, &tmp, address);
+
+  sGattIf->scanner->StartSync(
+      sid, tmp, skip, timeout, base::Bind(&onSyncStarted, reg_id),
+      base::Bind(&onSyncReport), base::Bind(&onSyncLost));
+}
+
+static void stopSyncNative(int sync_handle) {
+  if (!sGattIf) return;
+
+  sGattIf->scanner->StopSync(sync_handle);
+}
+
 static void gattTestNative(JNIEnv* env, jobject object, jint command,
                            jlong uuid1_lsb, jlong uuid1_msb, jstring bda1,
                            jint p1, jint p2, jint p3, jint p4, jint p5) {
@@ -1989,6 +2074,15 @@
      (void*)setPeriodicAdvertisingEnableNative},
 };
 
+// JNI functions defined in PeriodicScanManager class.
+static JNINativeMethod sPeriodicScanMethods[] = {
+    {"classInitNative", "()V", (void*)periodicScanClassInitNative},
+    {"initializeNative", "()V", (void*)periodicScanInitializeNative},
+    {"cleanupNative", "()V", (void*)periodicScanCleanupNative},
+    {"startSyncNative", "(ILjava/lang/String;III)V", (void*)startSyncNative},
+    {"stopSyncNative", "(I)V", (void*)stopSyncNative},
+};
+
 // JNI functions defined in ScanManager class.
 static JNINativeMethod sScanMethods[] = {
     {"registerScannerNative", "(JJ)V", (void*)registerScannerNative},
@@ -2100,6 +2194,9 @@
   register_success &= jniRegisterNativeMethods(
       env, "com/android/bluetooth/gatt/AdvertiseManager", sAdvertiseMethods,
       NELEM(sAdvertiseMethods));
+  register_success &= jniRegisterNativeMethods(
+      env, "com/android/bluetooth/gatt/PeriodicScanManager",
+      sPeriodicScanMethods, NELEM(sPeriodicScanMethods));
   return register_success &
          jniRegisterNativeMethods(env, "com/android/bluetooth/gatt/GattService",
                                   sMethods, NELEM(sMethods));
diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java
index 8a43198..990957c 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -142,6 +142,7 @@
             new HashMap<Integer, List<BluetoothGattService>>();
 
     private AdvertiseManager mAdvertiseManager;
+    private PeriodicScanManager mPeriodicScanManager;
     private ScanManager mScanManager;
     private AppOpsManager mAppOps;
 
@@ -172,6 +173,9 @@
         mScanManager = new ScanManager(this);
         mScanManager.start();
 
+        mPeriodicScanManager = new PeriodicScanManager(AdapterService.getAdapterService());
+        mPeriodicScanManager.start();
+
         return true;
     }
 
@@ -184,6 +188,7 @@
         mReliableQueue.clear();
         if (mAdvertiseManager != null) mAdvertiseManager.cleanup();
         if (mScanManager != null) mScanManager.cleanup();
+        if (mPeriodicScanManager != null) mPeriodicScanManager.cleanup();
         return true;
     }
 
@@ -192,6 +197,7 @@
         cleanupNative();
         if (mAdvertiseManager != null) mAdvertiseManager.cleanup();
         if (mScanManager != null) mScanManager.cleanup();
+        if (mPeriodicScanManager != null) mPeriodicScanManager.cleanup();
         return true;
     }
 
@@ -1487,12 +1493,12 @@
     void registerSync(
             ScanResult scanResult, int skip, int timeout, IPeriodicAdvertisingCallback callback) {
         enforceAdminPermission();
-        // TODO(jpawlowski): implement
+        mPeriodicScanManager.startSync(scanResult, skip, timeout, callback);
     }
 
     void unregisterSync(IPeriodicAdvertisingCallback callback) {
         enforceAdminPermission();
-        // TODO(jpawlowski): implement
+        mPeriodicScanManager.stopSync(callback);
     }
 
     /**************************************************************************
diff --git a/src/com/android/bluetooth/gatt/PeriodicScanManager.java b/src/com/android/bluetooth/gatt/PeriodicScanManager.java
new file mode 100644
index 0000000..591018b
--- /dev/null
+++ b/src/com/android/bluetooth/gatt/PeriodicScanManager.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2017 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.bluetooth.gatt;
+
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertisingSetParameters;
+import android.bluetooth.le.IPeriodicAdvertisingCallback;
+import android.bluetooth.le.PeriodicAdvertisingParameters;
+import android.bluetooth.le.PeriodicAdvertisingReport;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+import android.util.Log;
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Manages Bluetooth LE Periodic scans
+ *
+ * @hide
+ */
+class PeriodicScanManager {
+    private static final boolean DBG = GattServiceConfig.DBG;
+    private static final String TAG = GattServiceConfig.TAG_PREFIX + "SyncManager";
+
+    private final AdapterService mAdapterService;
+    Map<IBinder, SyncInfo> mSyncs = Collections.synchronizedMap(new HashMap<>());
+    static int sTempRegistrationId = -1;
+
+    /**
+     * Constructor of {@link SyncManager}.
+     */
+    PeriodicScanManager(AdapterService adapterService) {
+        logd("advertise manager created");
+        mAdapterService = adapterService;
+    }
+
+    void start() {
+        initializeNative();
+    }
+
+    void cleanup() {
+        logd("cleanup()");
+        cleanupNative();
+        mSyncs.clear();
+        sTempRegistrationId = -1;
+    }
+
+    class SyncInfo {
+        /* When id is negative, the registration is ongoing. When the registration finishes, id
+         * becomes equal to sync_handle */
+        public Integer id;
+        public SyncDeathRecipient deathRecipient;
+        public IPeriodicAdvertisingCallback callback;
+
+        SyncInfo(Integer id, SyncDeathRecipient deathRecipient,
+                IPeriodicAdvertisingCallback callback) {
+            this.id = id;
+            this.deathRecipient = deathRecipient;
+            this.callback = callback;
+        }
+    }
+
+    IBinder toBinder(IPeriodicAdvertisingCallback e) {
+        return ((IInterface) e).asBinder();
+    }
+
+    class SyncDeathRecipient implements IBinder.DeathRecipient {
+        IPeriodicAdvertisingCallback callback;
+
+        public SyncDeathRecipient(IPeriodicAdvertisingCallback callback) {
+            this.callback = callback;
+        }
+
+        @Override
+        public void binderDied() {
+            if (DBG) Log.d(TAG, "Binder is dead - unregistering advertising set");
+            stopSync(callback);
+        }
+    }
+
+    Map.Entry<IBinder, SyncInfo> findSync(int sync_handle) {
+        Map.Entry<IBinder, SyncInfo> entry = null;
+        for (Map.Entry<IBinder, SyncInfo> e : mSyncs.entrySet()) {
+            if (e.getValue().id == sync_handle) {
+                entry = e;
+                break;
+            }
+        }
+        return entry;
+    }
+
+    void onSyncStarted(int reg_id, int sync_handle, int sid, int address_type, String address,
+            int phy, int interval, int status) throws Exception {
+        logd("onSyncStarted() - reg_id=" + reg_id + ", sync_handle=" + sync_handle + ", status="
+                + status);
+
+        Map.Entry<IBinder, SyncInfo> entry = findSync(reg_id);
+        if (entry == null) {
+            Log.i(TAG, "onSyncStarted() - no callback found for reg_id " + reg_id);
+            // Sync was stopped before it was properly registered.
+            stopSyncNative(sync_handle);
+            return;
+        }
+
+        IPeriodicAdvertisingCallback callback = entry.getValue().callback;
+        if (status == 0) {
+            entry.setValue(new SyncInfo(sync_handle, entry.getValue().deathRecipient, callback));
+        } else {
+            IBinder binder = entry.getKey();
+            binder.unlinkToDeath(entry.getValue().deathRecipient, 0);
+            mSyncs.remove(binder);
+        }
+
+        // TODO: fix callback arguments
+        // callback.onSyncStarted(sync_handle, tx_power, status);
+    }
+
+    void onSyncReport(int sync_handle, int tx_power, int rssi, int data_status, byte[] data)
+            throws Exception {
+        logd("onSyncReport() - sync_handle=" + sync_handle);
+
+        Map.Entry<IBinder, SyncInfo> entry = findSync(sync_handle);
+        if (entry == null) {
+            Log.i(TAG, "onSyncReport() - no callback found for sync_handle " + sync_handle);
+            return;
+        }
+
+        IPeriodicAdvertisingCallback callback = entry.getValue().callback;
+        PeriodicAdvertisingReport report = new PeriodicAdvertisingReport(
+                sync_handle, tx_power, rssi, data_status, ScanRecord.parseFromBytes(data));
+        callback.onPeriodicAdvertisingReport(report);
+    }
+
+    void onSyncLost(int sync_handle) throws Exception {
+        logd("onSyncLost() - sync_handle=" + sync_handle);
+
+        Map.Entry<IBinder, SyncInfo> entry = findSync(sync_handle);
+        if (entry == null) {
+            Log.i(TAG, "onSyncLost() - no callback found for sync_handle " + sync_handle);
+            return;
+        }
+
+        IPeriodicAdvertisingCallback callback = entry.getValue().callback;
+        mSyncs.remove(entry);
+        callback.onSyncLost(sync_handle);
+    }
+
+    void startSync(
+            ScanResult scanResult, int skip, int timeout, IPeriodicAdvertisingCallback callback) {
+        SyncDeathRecipient deathRecipient = new SyncDeathRecipient(callback);
+        IBinder binder = toBinder(callback);
+        try {
+            binder.linkToDeath(deathRecipient, 0);
+        } catch (RemoteException e) {
+            throw new IllegalArgumentException("Can't link to periodic scanner death");
+        }
+
+        String address = scanResult.getDevice().getAddress();
+        int sid = scanResult.getAdvertisingSid();
+
+        int cb_id = --sTempRegistrationId;
+        mSyncs.put(binder, new SyncInfo(cb_id, deathRecipient, callback));
+
+        logd("startSync() - reg_id=" + cb_id + ", callback: " + binder);
+        startSyncNative(sid, address, skip, timeout, cb_id);
+    }
+
+    void stopSync(IPeriodicAdvertisingCallback callback) {
+        IBinder binder = toBinder(callback);
+        logd("stopSync() " + binder);
+
+        SyncInfo sync = mSyncs.remove(binder);
+        if (sync == null) {
+            Log.e(TAG, "stopSync() - no client found for callback");
+            return;
+        }
+
+        Integer sync_handle = sync.id;
+        binder.unlinkToDeath(sync.deathRecipient, 0);
+
+        if (sync_handle < 0) {
+            Log.i(TAG, "stopSync() - not finished registration yet");
+            // Sync will be freed once initiated in onSyncStarted()
+            return;
+        }
+
+        stopSyncNative(sync_handle);
+    }
+
+    private void logd(String s) {
+        if (DBG) {
+            Log.d(TAG, s);
+        }
+    }
+
+    static {
+        classInitNative();
+    }
+
+    private native static void classInitNative();
+    private native void initializeNative();
+    private native void cleanupNative();
+    private native void startSyncNative(int sid, String address, int skip, int timeout, int reg_id);
+    private native void stopSyncNative(int sync_handle);
+}