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