1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.bluetoothadvertisements; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.le.BluetoothLeScanner; 21 import android.bluetooth.le.ScanCallback; 22 import android.bluetooth.le.ScanFilter; 23 import android.bluetooth.le.ScanResult; 24 import android.bluetooth.le.ScanSettings; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.support.v4.app.ListFragment; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.Menu; 31 import android.view.MenuInflater; 32 import android.view.MenuItem; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.widget.Toast; 36 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.concurrent.TimeUnit; 40 41 42 /** 43 * Scans for Bluetooth Low Energy Advertisements matching a filter and displays them to the user. 44 */ 45 public class ScannerFragment extends ListFragment { 46 47 private static final String TAG = ScannerFragment.class.getSimpleName(); 48 49 /** 50 * Stops scanning after 5 seconds. 51 */ 52 private static final long SCAN_PERIOD = 5000; 53 54 private BluetoothAdapter mBluetoothAdapter; 55 56 private BluetoothLeScanner mBluetoothLeScanner; 57 58 private ScanCallback mScanCallback; 59 60 private ScanResultAdapter mAdapter; 61 62 private Handler mHandler; 63 64 /** 65 * Must be called after object creation by MainActivity. 66 * 67 * @param btAdapter the local BluetoothAdapter 68 */ setBluetoothAdapter(BluetoothAdapter btAdapter)69 public void setBluetoothAdapter(BluetoothAdapter btAdapter) { 70 this.mBluetoothAdapter = btAdapter; 71 mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); 72 } 73 74 @Override onCreate(Bundle savedInstanceState)75 public void onCreate(Bundle savedInstanceState) { 76 super.onCreate(savedInstanceState); 77 setHasOptionsMenu(true); 78 setRetainInstance(true); 79 80 // Use getActivity().getApplicationContext() instead of just getActivity() because this 81 // object lives in a fragment and needs to be kept separate from the Activity lifecycle. 82 // 83 // We could get a LayoutInflater from the ApplicationContext but it messes with the 84 // default theme, so generate it from getActivity() and pass it in separately. 85 mAdapter = new ScanResultAdapter(getActivity().getApplicationContext(), 86 LayoutInflater.from(getActivity())); 87 mHandler = new Handler(); 88 89 } 90 91 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)92 public View onCreateView(LayoutInflater inflater, ViewGroup container, 93 Bundle savedInstanceState) { 94 95 final View view = super.onCreateView(inflater, container, savedInstanceState); 96 97 setListAdapter(mAdapter); 98 99 return view; 100 } 101 102 @Override onViewCreated(View view, Bundle savedInstanceState)103 public void onViewCreated(View view, Bundle savedInstanceState) { 104 super.onViewCreated(view, savedInstanceState); 105 106 getListView().setDivider(null); 107 getListView().setDividerHeight(0); 108 109 setEmptyText(getString(R.string.empty_list)); 110 111 // Trigger refresh on app's 1st load 112 startScanning(); 113 114 } 115 116 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)117 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 118 super.onCreateOptionsMenu(menu, inflater); 119 inflater.inflate(R.menu.scanner_menu, menu); 120 } 121 122 @Override onOptionsItemSelected(MenuItem item)123 public boolean onOptionsItemSelected(MenuItem item) { 124 125 switch (item.getItemId()) { 126 case R.id.refresh: 127 startScanning(); 128 return true; 129 default: 130 return super.onOptionsItemSelected(item); 131 } 132 } 133 134 /** 135 * Start scanning for BLE Advertisements (& set it up to stop after a set period of time). 136 */ startScanning()137 public void startScanning() { 138 if (mScanCallback == null) { 139 Log.d(TAG, "Starting Scanning"); 140 141 // Will stop the scanning after a set time. 142 mHandler.postDelayed(new Runnable() { 143 @Override 144 public void run() { 145 stopScanning(); 146 } 147 }, SCAN_PERIOD); 148 149 // Kick off a new scan. 150 mScanCallback = new SampleScanCallback(); 151 mBluetoothLeScanner.startScan(buildScanFilters(), buildScanSettings(), mScanCallback); 152 153 String toastText = getString(R.string.scan_start_toast) + " " 154 + TimeUnit.SECONDS.convert(SCAN_PERIOD, TimeUnit.MILLISECONDS) + " " 155 + getString(R.string.seconds); 156 Toast.makeText(getActivity(), toastText, Toast.LENGTH_LONG).show(); 157 } else { 158 Toast.makeText(getActivity(), R.string.already_scanning, Toast.LENGTH_SHORT); 159 } 160 } 161 162 /** 163 * Stop scanning for BLE Advertisements. 164 */ stopScanning()165 public void stopScanning() { 166 Log.d(TAG, "Stopping Scanning"); 167 168 // Stop the scan, wipe the callback. 169 mBluetoothLeScanner.stopScan(mScanCallback); 170 mScanCallback = null; 171 172 // Even if no new results, update 'last seen' times. 173 mAdapter.notifyDataSetChanged(); 174 } 175 176 /** 177 * Return a List of {@link ScanFilter} objects to filter by Service UUID. 178 */ buildScanFilters()179 private List<ScanFilter> buildScanFilters() { 180 List<ScanFilter> scanFilters = new ArrayList<>(); 181 182 ScanFilter.Builder builder = new ScanFilter.Builder(); 183 // Comment out the below line to see all BLE devices around you 184 builder.setServiceUuid(Constants.Service_UUID); 185 scanFilters.add(builder.build()); 186 187 return scanFilters; 188 } 189 190 /** 191 * Return a {@link ScanSettings} object set to use low power (to preserve battery life). 192 */ buildScanSettings()193 private ScanSettings buildScanSettings() { 194 ScanSettings.Builder builder = new ScanSettings.Builder(); 195 builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER); 196 return builder.build(); 197 } 198 199 /** 200 * Custom ScanCallback object - adds to adapter on success, displays error on failure. 201 */ 202 private class SampleScanCallback extends ScanCallback { 203 204 @Override onBatchScanResults(List<ScanResult> results)205 public void onBatchScanResults(List<ScanResult> results) { 206 super.onBatchScanResults(results); 207 208 for (ScanResult result : results) { 209 mAdapter.add(result); 210 } 211 mAdapter.notifyDataSetChanged(); 212 } 213 214 @Override onScanResult(int callbackType, ScanResult result)215 public void onScanResult(int callbackType, ScanResult result) { 216 super.onScanResult(callbackType, result); 217 218 mAdapter.add(result); 219 mAdapter.notifyDataSetChanged(); 220 } 221 222 @Override onScanFailed(int errorCode)223 public void onScanFailed(int errorCode) { 224 super.onScanFailed(errorCode); 225 Toast.makeText(getActivity(), "Scan failed with error: " + errorCode, Toast.LENGTH_LONG) 226 .show(); 227 } 228 } 229 } 230