package com.example.android.nearbybeacons; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothManager; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanRecord; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.ParcelUuid; import android.util.ArrayMap; import android.util.Log; import java.util.ArrayList; import java.util.List; /** * This class is Deprecated! * It is only needed if you are targeting Play Services BEFORE 8.4 * * Background service to scan for Eddystone beacons and notify the user of * proximity. This service exists because Nearby Messages should not (and * can not) be run from the background. We will fire up that API once the * user decides to engage with the application. */ @Deprecated public class EddystoneScannerService extends Service { private static final String TAG = EddystoneScannerService.class.getSimpleName(); private static boolean sRunning; public static boolean isRunning() { return sRunning; } // Action to track notification dismissal public static final String ACTION_DISMISS = "EddystoneScannerService.ACTION_DISMISS"; // …if you feel like making the log a bit noisier… private static boolean DEBUG_SCAN = false; // Eddystone service uuid (0xfeaa) private static final ParcelUuid UID_SERVICE = ParcelUuid.fromString("0000feaa-0000-1000-8000-00805f9b34fb"); /** * ENTER ALL EDDYSTONE NAMESPACES YOU WANT TO SCAN HERE * e.g. "d89bed6e130ee5cf1ba1" */ private static final String[] NAMESPACE_IDS = { "YOUR_NAMESPACES_HERE" }; private static byte[] getNamespaceFilter(String namespaceId) { byte[] filter = new byte[18]; int len = namespaceId.length(); int index = 2; //Skip frame type + TX power bytes for (int i = 0; i < len; i += 2) { filter[index++] = (byte) ((Character.digit(namespaceId.charAt(i), 16) << 4) + Character.digit(namespaceId.charAt(i+1), 16)); } return filter; } // Filter that forces frame type and namespace id to match private static final byte[] NAMESPACE_FILTER_MASK = { (byte)0xFF, 0x00, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; private static String getNamespaceIdFromScan(byte[] scanData) { StringBuilder sb = new StringBuilder(); for (int i=2; i < 12; i++) { sb.append(String.format("%02x", scanData[i])); } return sb.toString(); } // Eddystone frame types private static final byte TYPE_UID = 0x00; private static final byte TYPE_URL = 0x10; private static final byte TYPE_TLM = 0x20; private static final int NOTIFICATION_ID = 42; private NotificationManager mNotificationManager; private BluetoothLeScanner mBluetoothLeScanner; private ArrayMap<String, Boolean> mDetectedBeacons; @Override public void onCreate() { super.onCreate(); sRunning = true; mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); mBluetoothLeScanner = manager.getAdapter().getBluetoothLeScanner(); mDetectedBeacons = new ArrayMap<>(); startScanning(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (ACTION_DISMISS.equals(intent.getAction())) { //Mark all currently discovered beacons as "seen" markAllRead(); //Hide the notification, if visible mNotificationManager.cancel(NOTIFICATION_ID); } return START_NOT_STICKY; } @Override public void onDestroy() { super.onDestroy(); sRunning = false; stopScanning(); } @Override public IBinder onBind(Intent intent) { return null; } /* Handle user notifications */ private void postScanResultNotification(int count) { Intent contentAction = new Intent(this, MainActivity.class); contentAction.setAction(ACTION_DISMISS); PendingIntent content = PendingIntent.getActivity(this, -1, contentAction, 0); Intent deleteAction = new Intent(this, EddystoneScannerService.class); deleteAction.setAction(ACTION_DISMISS); PendingIntent delete = PendingIntent.getService(this, -1, deleteAction, 0); Notification note = new Notification.Builder(this) .setContentTitle("Beacons Detected") .setContentText(String.format("%d New Beacons In Range", count)) .setSmallIcon(R.drawable.ic_stat_scan) .setContentIntent(content) .setDeleteIntent(delete) .build(); mNotificationManager.notify(NOTIFICATION_ID, note); } /* Begin scanning for Eddystone advertisers */ private void startScanning() { List<ScanFilter> filters = new ArrayList<>(); //Filter on just our requested namespaces for (String namespace : NAMESPACE_IDS) { ScanFilter beaconFilter = new ScanFilter.Builder() .setServiceUuid(UID_SERVICE) .setServiceData(UID_SERVICE, getNamespaceFilter(namespace), NAMESPACE_FILTER_MASK) .build(); filters.add(beaconFilter); } //Run in background mode ScanSettings settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) .build(); mBluetoothLeScanner.startScan(filters, settings, mScanCallback); if (DEBUG_SCAN) Log.d(TAG, "Scanning started…"); } /* Terminate scanning */ private void stopScanning() { mBluetoothLeScanner.stopScan(mScanCallback); if (DEBUG_SCAN) Log.d(TAG, "Scanning stopped…"); } /* Handle UID packet discovery on the main thread */ private void processUidPacket(String deviceAddress, int rssi, byte[] packet) { if (DEBUG_SCAN) { String id = getNamespaceIdFromScan(packet); Log.d(TAG, "Eddystone(" + deviceAddress + ") id = " + id); } if (!mDetectedBeacons.containsKey(deviceAddress)) { mDetectedBeacons.put(deviceAddress, false); int unreadCount = getUnreadCount(); if (unreadCount > 0) { postScanResultNotification(unreadCount); } } } private void markAllRead() { for (String key : mDetectedBeacons.keySet()) { mDetectedBeacons.put(key, true); } } private int getUnreadCount() { int count = 0; for (Boolean marker : mDetectedBeacons.values()) { if (!marker) count++; } return count; } /* Process each unique BLE scan result */ private ScanCallback mScanCallback = new ScanCallback() { private Handler mCallbackHandler = new Handler(Looper.getMainLooper()); @Override public void onScanResult(int callbackType, ScanResult result) { processResult(result); } @Override public void onScanFailed(int errorCode) { Log.w(TAG, "Scan Error Code: " + errorCode); } @Override public void onBatchScanResults(List<ScanResult> results) { for (ScanResult result : results) { processResult(result); } } private void processResult(ScanResult result) { ScanRecord record = result.getScanRecord(); if (record == null) { Log.w(TAG, "Invalid scan record."); return; } final byte[] data = record.getServiceData(UID_SERVICE); if (data == null) { Log.w(TAG, "Invalid Eddystone scan result."); return; } final String deviceAddress = result.getDevice().getAddress(); final int rssi = result.getRssi(); byte frameType = data[0]; switch (frameType) { case TYPE_UID: mCallbackHandler.post(new Runnable() { @Override public void run() { processUidPacket(deviceAddress, rssi, data); } }); break; case TYPE_TLM: case TYPE_URL: //Do nothing, ignoring these return; default: Log.w(TAG, "Invalid Eddystone scan result."); } } }; }