diff options
Diffstat (limited to 'samples/browseable/BluetoothAdvertisements/src')
5 files changed, 701 insertions, 0 deletions
diff --git a/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/AdvertiserFragment.java b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/AdvertiserFragment.java new file mode 100644 index 000000000..f8daefb04 --- /dev/null +++ b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/AdvertiserFragment.java @@ -0,0 +1,190 @@ +package com.example.android.bluetoothadvertisements; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.le.AdvertiseCallback; +import android.bluetooth.le.AdvertiseData; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.BluetoothLeAdvertiser; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Switch; +import android.widget.Toast; + +/** + * Allows user to start & stop Bluetooth LE Advertising of their device. + */ +public class AdvertiserFragment extends Fragment { + + private BluetoothAdapter mBluetoothAdapter; + + private BluetoothLeAdvertiser mBluetoothLeAdvertiser; + + private AdvertiseCallback mAdvertiseCallback; + + private Switch mSwitch; + + /** + * Must be called after object creation by MainActivity. + * + * @param btAdapter the local BluetoothAdapter + */ + public void setBluetoothAdapter(BluetoothAdapter btAdapter) { + this.mBluetoothAdapter = btAdapter; + mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.fragment_advertiser, container, false); + + mSwitch = (Switch) view.findViewById(R.id.advertise_switch); + mSwitch.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onSwitchClicked(v); + } + }); + + return view; + } + + @Override + public void onStop() { + super.onStop(); + + if(mAdvertiseCallback != null){ + stopAdvertising(); + } + } + + /** + * Called when switch is toggled - starts or stops advertising. + * + * @param view is the Switch View object + */ + public void onSwitchClicked(View view) { + + // Is the toggle on? + boolean on = ((Switch) view).isChecked(); + + if (on) { + startAdvertising(); + } else { + stopAdvertising(); + } + } + + /** + * Starts BLE Advertising. + */ + private void startAdvertising() { + + mAdvertiseCallback = new SampleAdvertiseCallback(); + + if (mBluetoothLeAdvertiser != null) { + mBluetoothLeAdvertiser.startAdvertising(buildAdvertiseSettings(), buildAdvertiseData(), + mAdvertiseCallback); + } else { + mSwitch.setChecked(false); + Toast.makeText(getActivity(), getString(R.string.bt_null), Toast.LENGTH_LONG).show(); + } + } + + /** + * Stops BLE Advertising. + */ + private void stopAdvertising() { + + if (mBluetoothLeAdvertiser != null) { + + mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback); + mAdvertiseCallback = null; + + } else { + mSwitch.setChecked(false); + Toast.makeText(getActivity(), getString(R.string.bt_null), Toast.LENGTH_LONG).show(); + } + } + + /** + * Returns an AdvertiseData object which includes the Service UUID and Device Name. + */ + private AdvertiseData buildAdvertiseData() { + + // Note: There is a strict limit of 31 Bytes on packets sent over BLE Advertisements. + // This includes everything put into AdvertiseData including UUIDs, device info, & + // arbitrary service or manufacturer data. + // Attempting to send packets over this limit will result in a failure with error code + // AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE. Catch this error in the + // onStartFailure() method of an AdvertiseCallback implementation. + + AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder(); + dataBuilder.addServiceUuid(Constants.Service_UUID); + dataBuilder.setIncludeDeviceName(true); + + return dataBuilder.build(); + } + + /** + * Returns an AdvertiseSettings object set to use low power (to help preserve battery life). + */ + private AdvertiseSettings buildAdvertiseSettings() { + AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder(); + settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER); + + return settingsBuilder.build(); + } + + /** + * Custom callback after Advertising succeeds or fails to start. + */ + private class SampleAdvertiseCallback extends AdvertiseCallback { + + @Override + public void onStartFailure(int errorCode) { + super.onStartFailure(errorCode); + + mSwitch.setChecked(false); + + String errorMessage = getString(R.string.start_error_prefix); + switch (errorCode) { + case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED: + errorMessage += " " + getString(R.string.start_error_already_started); + break; + case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE: + errorMessage += " " + getString(R.string.start_error_too_large); + break; + case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED: + errorMessage += " " + getString(R.string.start_error_unsupported); + break; + case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR: + errorMessage += " " + getString(R.string.start_error_internal); + break; + case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: + errorMessage += " " + getString(R.string.start_error_too_many); + break; + } + + Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_LONG).show(); + + } + + @Override + public void onStartSuccess(AdvertiseSettings settingsInEffect) { + super.onStartSuccess(settingsInEffect); + // Don't need to do anything here, advertising successfully started. + } + } + +} diff --git a/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/Constants.java b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/Constants.java new file mode 100644 index 000000000..d3941e2ab --- /dev/null +++ b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/Constants.java @@ -0,0 +1,22 @@ +package com.example.android.bluetoothadvertisements; + +import android.os.ParcelUuid; + +/** + * Constants for use in the Bluetooth Advertisements sample + */ +public class Constants { + + /** + * UUID identified with this app - set as Service UUID for BLE Advertisements. + * + * Bluetooth requires a certain format for UUIDs associated with Services. + * The official specification can be found here: + * {@link https://www.bluetooth.org/en-us/specification/assigned-numbers/service-discovery} + */ + public static final ParcelUuid Service_UUID = ParcelUuid + .fromString("0000b81d-0000-1000-8000-00805f9b34fb"); + + public static final int REQUEST_ENABLE_BT = 1; + +} diff --git a/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/MainActivity.java b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/MainActivity.java new file mode 100644 index 000000000..f0044a3e8 --- /dev/null +++ b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/MainActivity.java @@ -0,0 +1,130 @@ +/* +* Copyright 2013 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.example.android.bluetoothadvertisements; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.widget.TextView; +import android.widget.Toast; + +/** + * Setup display fragments and ensure the device supports Bluetooth. + */ +public class MainActivity extends FragmentActivity { + + private BluetoothAdapter mBluetoothAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + setTitle(R.string.activity_main_title); + + if (savedInstanceState == null ) { + + mBluetoothAdapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)) + .getAdapter(); + + // Is Bluetooth supported on this device? + if (mBluetoothAdapter != null) { + + // Is Bluetooth turned on? + if (mBluetoothAdapter.isEnabled()) { + + // Are Bluetooth Advertisements supported on this device? + if (mBluetoothAdapter.isMultipleAdvertisementSupported()) { + + // Everything is supported and enabled, load the fragments. + setupFragments(); + + } else { + + // Bluetooth Advertisements are not supported. + showErrorText(R.string.bt_ads_not_supported); + } + } else { + + // Prompt user to turn on Bluetooth (logic continues in onActivityResult()). + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableBtIntent, Constants.REQUEST_ENABLE_BT); + } + } else { + + // Bluetooth is not supported. + showErrorText(R.string.bt_not_supported); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case Constants.REQUEST_ENABLE_BT: + + if (resultCode == RESULT_OK) { + + // Bluetooth is now Enabled, are Bluetooth Advertisements supported on + // this device? + if (mBluetoothAdapter.isMultipleAdvertisementSupported()) { + + // Everything is supported and enabled, load the fragments. + setupFragments(); + + } else { + + // Bluetooth Advertisements are not supported. + showErrorText(R.string.bt_ads_not_supported); + } + } else { + + // User declined to enable Bluetooth, exit the app. + Toast.makeText(this, R.string.bt_not_enabled_leaving, + Toast.LENGTH_SHORT).show(); + finish(); + } + + default: + super.onActivityResult(requestCode, resultCode, data); + } + } + + private void setupFragments() { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + + ScannerFragment scannerFragment = new ScannerFragment(); + scannerFragment.setBluetoothAdapter(mBluetoothAdapter); + transaction.replace(R.id.scanner_fragment_container, scannerFragment); + + AdvertiserFragment advertiserFragment = new AdvertiserFragment(); + advertiserFragment.setBluetoothAdapter(mBluetoothAdapter); + transaction.replace(R.id.advertiser_fragment_container, advertiserFragment); + + transaction.commit(); + } + + private void showErrorText(int messageId) { + + TextView view = (TextView) findViewById(R.id.error_textview); + view.setText(getString(messageId)); + } +}
\ No newline at end of file diff --git a/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/ScanResultAdapter.java b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/ScanResultAdapter.java new file mode 100644 index 000000000..0f905ea7a --- /dev/null +++ b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/ScanResultAdapter.java @@ -0,0 +1,147 @@ +package com.example.android.bluetoothadvertisements; + +import android.bluetooth.le.ScanResult; +import android.content.Context; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * Holds and displays {@link ScanResult}s, used by {@link ScannerFragment}. + */ +public class ScanResultAdapter extends BaseAdapter { + + private ArrayList<ScanResult> mArrayList; + + private Context mContext; + + private LayoutInflater mInflater; + + ScanResultAdapter(Context context, LayoutInflater inflater) { + super(); + mContext = context; + mInflater = inflater; + mArrayList = new ArrayList<>(); + } + + @Override + public int getCount() { + return mArrayList.size(); + } + + @Override + public Object getItem(int position) { + return mArrayList.get(position); + } + + @Override + public long getItemId(int position) { + return mArrayList.get(position).getDevice().getAddress().hashCode(); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + + // Reuse an old view if we can, otherwise create a new one. + if (view == null) { + view = mInflater.inflate(R.layout.listitem_scanresult, null); + } + + TextView deviceNameView = (TextView) view.findViewById(R.id.device_name); + TextView deviceAddressView = (TextView) view.findViewById(R.id.device_address); + TextView lastSeenView = (TextView) view.findViewById(R.id.last_seen); + + ScanResult scanResult = mArrayList.get(position); + + deviceNameView.setText(scanResult.getDevice().getName()); + deviceAddressView.setText(scanResult.getDevice().getAddress()); + lastSeenView.setText(getTimeSinceString(mContext, scanResult.getTimestampNanos())); + + return view; + } + + /** + * Search the adapter for an existing device address and return it, otherwise return -1. + */ + private int getPosition(String address) { + int position = -1; + for (int i = 0; i < mArrayList.size(); i++) { + if (mArrayList.get(i).getDevice().getAddress().equals(address)) { + position = i; + break; + } + } + return position; + } + + + /** + * Add a ScanResult item to the adapter if a result from that device isn't already present. + * Otherwise updates the existing position with the new ScanResult. + */ + public void add(ScanResult scanResult) { + + int existingPosition = getPosition(scanResult.getDevice().getAddress()); + + if (existingPosition >= 0) { + // Device is already in list, update its record. + mArrayList.set(existingPosition, scanResult); + } else { + // Add new Device's ScanResult to list. + mArrayList.add(scanResult); + } + } + + /** + * Clear out the adapter. + */ + public void clear() { + mArrayList.clear(); + } + + /** + * Takes in a number of nanoseconds and returns a human-readable string giving a vague + * description of how long ago that was. + */ + public static String getTimeSinceString(Context context, long timeNanoseconds) { + String lastSeenText = context.getResources().getString(R.string.last_seen) + " "; + + long timeSince = SystemClock.elapsedRealtimeNanos() - timeNanoseconds; + long secondsSince = TimeUnit.SECONDS.convert(timeSince, TimeUnit.NANOSECONDS); + + if (secondsSince < 5) { + lastSeenText += context.getResources().getString(R.string.just_now); + } else if (secondsSince < 60) { + lastSeenText += secondsSince + " " + context.getResources() + .getString(R.string.seconds_ago); + } else { + long minutesSince = TimeUnit.MINUTES.convert(secondsSince, TimeUnit.SECONDS); + if (minutesSince < 60) { + if (minutesSince == 1) { + lastSeenText += minutesSince + " " + context.getResources() + .getString(R.string.minute_ago); + } else { + lastSeenText += minutesSince + " " + context.getResources() + .getString(R.string.minutes_ago); + } + } else { + long hoursSince = TimeUnit.HOURS.convert(minutesSince, TimeUnit.MINUTES); + if (hoursSince == 1) { + lastSeenText += hoursSince + " " + context.getResources() + .getString(R.string.hour_ago); + } else { + lastSeenText += hoursSince + " " + context.getResources() + .getString(R.string.hours_ago); + } + } + } + + return lastSeenText; + } +} diff --git a/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/ScannerFragment.java b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/ScannerFragment.java new file mode 100644 index 000000000..b9ad4d966 --- /dev/null +++ b/samples/browseable/BluetoothAdvertisements/src/com.example.android.bluetoothadvertisements/ScannerFragment.java @@ -0,0 +1,212 @@ +package com.example.android.bluetoothadvertisements; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.ListFragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + + +/** + * Scans for Bluetooth Low Energy Advertisements matching a filter and displays them to the user. + */ +public class ScannerFragment extends ListFragment { + + private static final String TAG = ScannerFragment.class.getSimpleName(); + + /** + * Stops scanning after 5 seconds. + */ + private static final long SCAN_PERIOD = 5000; + + private BluetoothAdapter mBluetoothAdapter; + + private BluetoothLeScanner mBluetoothLeScanner; + + private ScanCallback mScanCallback; + + private ScanResultAdapter mAdapter; + + private Handler mHandler; + + /** + * Must be called after object creation by MainActivity. + * + * @param btAdapter the local BluetoothAdapter + */ + public void setBluetoothAdapter(BluetoothAdapter btAdapter) { + this.mBluetoothAdapter = btAdapter; + mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + setRetainInstance(true); + + // Use getActivity().getApplicationContext() instead of just getActivity() because this + // object lives in a fragment and needs to be kept separate from the Activity lifecycle. + // + // We could get a LayoutInflater from the ApplicationContext but it messes with the + // default theme, so generate it from getActivity() and pass it in separately. + mAdapter = new ScanResultAdapter(getActivity().getApplicationContext(), + LayoutInflater.from(getActivity())); + mHandler = new Handler(); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + final View view = super.onCreateView(inflater, container, savedInstanceState); + + setListAdapter(mAdapter); + + return view; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + getListView().setDivider(null); + getListView().setDividerHeight(0); + + setEmptyText(getString(R.string.empty_list)); + + // Trigger refresh on app's 1st load + startScanning(); + + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.scanner_menu, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + case R.id.refresh: + startScanning(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Start scanning for BLE Advertisements (& set it up to stop after a set period of time). + */ + public void startScanning() { + if (mScanCallback == null) { + Log.d(TAG, "Starting Scanning"); + + // Will stop the scanning after a set time. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + stopScanning(); + } + }, SCAN_PERIOD); + + // Kick off a new scan. + mScanCallback = new SampleScanCallback(); + mBluetoothLeScanner.startScan(buildScanFilters(), buildScanSettings(), mScanCallback); + + String toastText = getString(R.string.scan_start_toast) + " " + + TimeUnit.SECONDS.convert(SCAN_PERIOD, TimeUnit.MILLISECONDS) + " " + + getString(R.string.seconds); + Toast.makeText(getActivity(), toastText, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(getActivity(), R.string.already_scanning, Toast.LENGTH_SHORT); + } + } + + /** + * Stop scanning for BLE Advertisements. + */ + public void stopScanning() { + Log.d(TAG, "Stopping Scanning"); + + // Stop the scan, wipe the callback. + mBluetoothLeScanner.stopScan(mScanCallback); + mScanCallback = null; + + // Even if no new results, update 'last seen' times. + mAdapter.notifyDataSetChanged(); + } + + /** + * Return a List of {@link ScanFilter} objects to filter by Service UUID. + */ + private List<ScanFilter> buildScanFilters() { + List<ScanFilter> scanFilters = new ArrayList<>(); + + ScanFilter.Builder builder = new ScanFilter.Builder(); + builder.setServiceUuid(Constants.Service_UUID); + scanFilters.add(builder.build()); + + return scanFilters; + } + + /** + * Return a {@link ScanSettings} object set to use low power (to preserve battery life). + */ + private ScanSettings buildScanSettings() { + ScanSettings.Builder builder = new ScanSettings.Builder(); + builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER); + return builder.build(); + } + + /** + * Custom ScanCallback object - adds to adapter on success, displays error on failure. + */ + private class SampleScanCallback extends ScanCallback { + + @Override + public void onBatchScanResults(List<ScanResult> results) { + super.onBatchScanResults(results); + + for (ScanResult result : results) { + mAdapter.add(result); + } + mAdapter.notifyDataSetChanged(); + } + + @Override + public void onScanResult(int callbackType, ScanResult result) { + super.onScanResult(callbackType, result); + + mAdapter.add(result); + mAdapter.notifyDataSetChanged(); + } + + @Override + public void onScanFailed(int errorCode) { + super.onScanFailed(errorCode); + Toast.makeText(getActivity(), "Scan failed with error: " + errorCode, Toast.LENGTH_LONG) + .show(); + } + } +} |