Bluetooth Low Energy (BLE) Beacons are one-way transmitters that mark important places and objects in a way that users' devices understand. The Google beacon platform provides a set of resources and APIs to make interacting with beacons power-efficient and useful. This allows developers to create context aware and proximity based applications using the high quality signal provided by BLE beacons.
Location and Proximity Superpowers: Eddystone + Google Beacon Platform - Google I/O 2016 |
In this codelab you'll create a simple app that uses the Nearby Messages API to fetch beacon attachments, blobs of useful data associated with beacons in the cloud. Along the way, you'll learn about beacons, the Eddystone beacon format, the Google beacon platform, and pick up tips on how to provision beacons and register them with Google. Nearby takes care of beacon scanning for you, so that you can focus on attachment data (the meaning you associated with a beacon) and building awesome features!
Creating a simple, useable beacon-aware app of the kind we'll build in this codelab can be broken down into these steps:
This codelab is intended for Android developers who want to learn about beacons, understand how to set up beacons, and write an context-aware apps that subscribes to and fetches attachments on those beacons.
No prior knowledge of beacons, the Google beacon platform, or Nearby Messages is required. But we do assume the following:
You can clone the repo that contains the code used in this codelab:
$ git clone https://github.com/googlecodelabs/hello-beacons.git
Or you can download the code by clicking the following button and unpacking the contents:
Change into the root directory and you'll see that the code is organized in the following sub-directories:
Launch Android Studio by clicking the Studio icon:
Select the Import project (Eclipse ADT, Gradle, etc.) option:
Navigate to the location where you unzipped the source, and select HelloBeacons-Start.
Your Android studio screen should now look like this:
Expose the directory structure of the application using the following steps:
All source files are located under app/java, and all layout files under app/res/layout.
To launch the HelloBeacons app, first make sure your device is connected to the computer. Then, press the green Play icon from the menu bar towards the top of the screen.
The app has only a single activity, MainActivity, and if everything goes well, the app should open with MainActivity visible.
Here's a list of files that you'll be modifying in this codelab:
Here are the contents of MainActivity.java:
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private GoogleApiClient mGoogleApiClient;
private RelativeLayout mContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mContainer = (RelativeLayout) findViewById(R.id.main_activity_container);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
}
The code is pretty barebones at the moment and it contains the following:
In this step, we'll set up GoogleApiClient and implement the logic to connect and disconnect GoogleApiClient.
Modify MainActivity so that it implements GoogleApiClient.ConnectionCallbacks and GoogleApiClient.OnConnectionFailedListener:
public class MainActivity extends AppCompatActivity
implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
}
GoogleApiClient.ConnectionCallbacks provides callbacks that are called when GoogleApiClient connects or disconnects. Because MainActivity now implements GoogleApiClient.ConnectionCallbacks, you need to implement the onConnected(Bundle) and onConnectionSuspended(int) callbacks.
GoogleApiClient.OnConnectionFailedListener provides callbacks for handling scenarios where the GoogleApiClient connection fails. Because MainActivity now implements GoogleApiClient.OnConnectionFailedListener, you need to implement the onConnectionFailed(ConnectionResult) callback.
Add the needed callbacks to MainActivity.java:
public class MainActivity extends AppCompatActivity
implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
if (mContainer != null) {
Snackbar.make(mContainer,
"Exception while connecting to Google Play services: " +
connectionResult.getErrorMessage(),
Snackbar.LENGTH_INDEFINITE).show();
}
}
@Override
public void onConnectionSuspended(int i) {
Log.w(TAG, "Connection suspended. Error code: " + i);
}
@Override
public void onConnected(@Nullable Bundle bundle) {
Log.i(TAG, "GoogleApiClient connected");
}
}
The implementation of these callbacks is pretty minimal:
You'll be using the Nearby.Messages.subscribe() method to set up the subscription that allows HelloBeacons to scan for beacons. The Nearby.Messages.subscribe() method requires a connected GoogleApiClient, and you'll be calling that method later in this codelab inside onConnected().
With the ConnectionCallbacks and the OnConnectionFailedListener callbacks out of the way, we can now start building GoogleApiClient. Add the following method to MainActivity:
public class MainActivity extends AppCompatActivity
implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
private synchronized void buildGoogleApiClient() {
if (mGoogleApiClient == null) {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Nearby.MESSAGES_API, new MessagesOptions.Builder()
.setPermissions(NearbyPermissions.BLE).build())
.addConnectionCallbacks(this)
.enableAutoManage(this, this)
.build();
}
}
}
We won't use this method yet (that part comes up soon in the next step), but there are a few things to note:
MainActivity.java should now look like this:
public class MainActivity extends AppCompatActivity
implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
private static final String TAG = MainActivity.class.getSimpleName();
private GoogleApiClient mGoogleApiClient;
private RelativeLayout mContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mContainer = (RelativeLayout) findViewById(R.id.main_activity_container);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
if (mContainer != null) {
Snackbar.make(mContainer,
"Exception while connecting to Google Play services: " +
connectionResult.getErrorMessage(),
Snackbar.LENGTH_INDEFINITE).show();
}
}
@Override
public void onConnectionSuspended(int i) {
Log.w(TAG, "Connection suspended. Error code: " + i);
}
@Override
public void onConnected(@Nullable Bundle bundle) {
Log.i(TAG, "GoogleApiClient connected");
}
private synchronized void buildGoogleApiClient() {
if (mGoogleApiClient == null) {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Nearby.MESSAGES_API, new MessagesOptions.Builder()
.setPermissions(NearbyPermissions.BLE).build())
.addConnectionCallbacks(this)
.enableAutoManage(this, this)
.build();
}
}
}
Using the green Play icon from the Android Studio menu, run your program to make sure everything works as intended. There's nothing to display yet, so you'll still get an empty screen. But you should not see any error messages.
Close the current Android Studio project (go to File > Close Project).
The Nearby Messages API takes care of scanning for beacons for you, fetches beacon attachments and delivers them to your app as a message object. As well as an API in Google Play Services, there is a CocoaPod for iOS devices. You can specify the Namespace and Type of attachments that your app is interested in. By default, Nearby will fetch all beacon attachments associated with namespaces that your project owns. You can subscribe to attachments in others' namespaces, provided that those namespaces have appropriately set visibility.
Nearby handles beacon scanning on your behalf, so that you don't have to schedule scans or think about beacon IDs when creating your subscription.. You can subscribe to fetch beacon attachments In the foreground (in response to a user action or event) or in the background (when your app is not running). Foreground subscriptions maintain continuous beacon scans, which can affect users' battery if overused. Nearby handles background beacon scanning in a battery efficient way, waking your app only when an interesting beacon is close by, and you'll be using this method in HelloBeacons.
When your app subscribes to beacon messages in the background, low-power scans are triggered at screen-on events, or when the screen is off if other services are performing scans, even when your app is not currently active. You will use these scan notifications to "wake up" your app in response to a particular message.
The full Nearby Messages API can be used for a variety of device-to-device interactions, as well as beacon scanning. For this codelab, you can initiate a beacon-only background subscription by calling Nearby.Messages.subscribe() and setting the Strategy option to BLE_ONLY. Before creating a subscription, we need to make sure that the user has granted all the necessary permissions.
Using the Nearby Messages API requires user consent. Prior to subscribing, your app should check to see whether the user has consented, and present them with a consent dialog if they have not.
Using the Import Project (Eclipse ADT, Gradle, etc.), open HelloBeacons-Nearby-Setup. The code in this step picks up where Step 1 left off.
Using the Nearby Messages API requires user consent. Open app/src/main/AndroidManifest.xml to see the permissions necessary for HelloBeacons to function:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.gms.nearby.messages.samples.hellobeacons">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
</manifest>
The ACCESS_FINE_LOCATION permission is required and Nearby provides a handy permissions dialog for getting the user's consent. You'll implement the code that allows the user to grant permissions to HelloBeacons using this dialog.
In MainActivity.java, create a PERMISSIONS_REQUEST_CODE constant. We'll use this constant as part of the permissions workflow for Nearby:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
private static final String TAG = ...
private static final int PERMISSIONS_REQUEST_CODE = 1111;
...
}
Add the havePermissions() and requestPermissions() methods to MainActivity:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
private synchronized void buildGoogleApiClient() {
...
}
private boolean havePermissions() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermissions() {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSIONS_REQUEST_CODE);
}
The havePermissions() method checks if you have been granted the ACCESS_FINE_LOCATION permission. The requestPermissions() method requests that permission.
Next, use these two methods in the activity lifecycle. Modify the onCreate() and the onResume() methods as shown:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mContainer = (RelativeLayout) findViewById(...);
if (!havePermissions()) {
Log.i(TAG, "Requesting permissions needed for this app.");
requestPermissions();
}
}
@Override
protected void onResume() {
super.onResume();
if (havePermissions()) {
buildGoogleApiClient();
}
}
...
}
You check permission in onCreate() and request permissions if necessary. You check permissions again in onResume() in case the user has gone to Settings and granted permissions there (thus bypassing the Permissions Dialog flow).
Once the apps permissions requirements are met, you call buildGoogleApiClient(), which you implemented this earlier in the codelab. This kicks off the process of building and connecting GoogleApiClient.
Using the green Play icon from the Android Studio menu, run the app. Your screen should look this:
Don't press "Deny" or "Allow" just yet. We still have a little more work to do. If you accidentally pressed "Allow", delete the app from the device. That way, you'll be forced to go through the permission workflow all over again.
A user is presented with the Permissions dialog shown above, can take any of these three actions:
Add the showLinkToSettingsSnackBar() and showRequestPermissionsSnackBar() methods to MainActivity.java. These methods are responsible for displaying Snackbars when a user does not grant permission:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
private requestPermissions() {
...
}
private void showLinkToSettingsSnackbar() {
if (mContainer == null) {
return;
}
Snackbar.make(mContainer,
R.string.permission_denied_explanation,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.settings, new View.OnClickListener() {
@Override
public void onClick(View view) {
// Build intent that displays the App settings screen.
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package",
BuildConfig.APPLICATION_ID, null);
intent.setData(uri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}).show();
}
private void showRequestPermissionsSnackbar() {
if (mContainer == null) {
return;
}
Snackbar.make(mContainer, R.string.permission_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
// Request permission.
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
PERMISSIONS_REQUEST_CODE);
}
}).show();
}
}
We have now handled the flow for the Permissions Dialog. Once a user engages with Dialog, the onRequestPermissionsResult() callback fires. Add the code for this callback to MainActivity.java:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[]permissions, @NonNull int[] grantResults) {
if (requestCode != PERMISSIONS_REQUEST_CODE) {
return;
}
for (int i = 0; i < permissions.length; i++) {
String permission = permissions[i];
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
if (shouldShowRequestPermissionRationale(permission)) {
Log.i(TAG, "Permission denied without 'NEVER ASK AGAIN': "
+ permission);
showRequestPermissionsSnackbar();
} else {
Log.i(TAG, "Permission denied with 'NEVER ASK AGAIN': "
+ permission);
showLinkToSettingsSnackbar();
}
} else {
Log.i(TAG, "Permission granted, building GoogleApiClient");
buildGoogleApiClient();
}
}
}
}
Note that once the user grants permissions, we call buildGoogleApiClient(), which triggers the following flow:
Using the green Play icon from the Android Studio menu, run the app. When the Permissions Dialog opens, press "Allow". Or, to test the workflow, press "Deny", retrigger the permissions flow by tapping "OK" on the Snackbar, and then press "Allow".
That's all for this step. We're done with all the boilerplate code related to permissions. In the next step we'll add code to the onConnected() callback to start scanning for beacons!
Close the current Android Studio project (go to File > Close Project).
Using the Import Project (Eclipse ADT, Gradle, etc.), open HelloBeacons-Subscribe.
In this step, you'll implement the functionality to subscribe to beacon attachments using Nearby Messages API. But for the background scanning to actually work, you need to add an API KEY in AndroidManifest.xml.
Using the Nearby Messages API requires an API key to ensure that your app receives the right attachments.
Beacon attachments always have an associated Namespace and Type. Namespaces contain a visibility field that allows beacon deployers to control which other projects have access to the information associated with their beacons. By default, attachment Namespaces are ‘unlisted', meaning that attachment data is only returned for calls that contain an API key issued by the same project that owns the Namespace. Note that the API key is the only check for Proximity Beacon API's serving endpoints: no end-user credentials are involved. For this reason, do not store personal or sensitive information in beacon attachments.
If beacons are intended to be used in a public beacon network, attachment Namespace visibility can be set to ‘public', meaning that attachment data is returned for any request regardless of the API key provided. Nevertheless, an API key is required.
If you are registering beacons and adding attachments from scratch, you'll need to set up a project in Google API Console, and associate the beacons with that project. Then, you can generate an API KEY and enter that in the AndroidManifest.xml.
In this codelab, we'll subscribe when MainActivity launches, but not every time the device rotates. We'll find a way to retain state, and when a subscription is active (i.e., a call to Nearby.Messages.subscribe() has succeeded), we won't bother replacing that subscription with a new one.
To track the subscription state, create a private boolean field in MainActivity.java:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
private boolean mSubscribed = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
}
...
}
Create a static variable that you'll use in saving the subscription state to the bundle:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
private static final int PERMISSIONS_REQUEST_CODE = ...;
private static final String KEY_SUBSCRIBED = "subscribed";
...
}
To retain the state of the mSubscribed variable, add the following lines to onCreate():
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mContainer = ...;
if (savedInstanceState != null) {
mSubscribed = savedInstanceState.getBoolean(KEY_SUBSCRIBED, false);
}
if (!havePermissions()) {...}
}
}
Then, to save the current value of mSubscribed in the Bundle, modify the onSaveInstanceState() method so it looks like this:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_SUBSCRIBED, mSubscribed);
}
}
Add the subscribe(), getPendingIntnet(), and getBackgroundServiceIntent() method to MainActivity.java (we'll talk about what these methods do in a moment):
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
private void subscribe() {
if (mSubscribed) {
Log.i(TAG, "Already subscribed.");
return;
}
SubscribeOptions options = new SubscribeOptions.Builder()
.setStrategy(Strategy.BLE_ONLY)
// Note: If no filter is specified, Nearby will return all of your
// attachments regardless of type. You must use a filter to specify
// a particular set of attachments (by type) or to fetch attachments
// in a namespace other than your project's default.
.setFilter(new MessageFilter.Builder()
.includeNamespacedType("some_namespace", "some_type")
.build();
Nearby.Messages.subscribe(mGoogleApiClient, getPendingIntent(), options)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(@NonNull Status status) {
if (status.isSuccess()) {
Log.i(TAG, "Subscribed successfully.");
startService(getBackgroundSubscribeServiceIntent());
} else {
Log.e(TAG, "Operation failed. Error: " +
NearbyMessagesStatusCodes.getStatusCodeString(
status.getStatusCode()));
}
}
});
}
private PendingIntent getPendingIntent() {
return PendingIntent.getService(this, 0,
getBackgroundSubscribeServiceIntent(), PendingIntent.FLAG_UPDATE_CURRENT);
}
private Intent getBackgroundSubscribeServiceIntent() {
return new Intent(this, BackgroundSubscribeIntentService.class);
}
...
}
The subscribe() method calls the Nearby.Messages.subscribe() method to set up a background subscription to scan for attachments from nearby beacons. The Nearby.Messages.subscribe() method takes three arguments:
SubscribeOptions options = new SubscribeOptions.Builder()
.setStrategy(Strategy.BLE_ONLY)
.setFilter(new MessageFilter.Builder()
.includeNamespacedType("some_namespace", "some_type")
.build();
Now, use the subscribe() method you just created by modifying the code in the onConnected() callback:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
...
@Override
public void onConnected(@Nullable Bundle bundle) {
Log.i(TAG, "GoogleApiClient connected");
subscribe();
}
}
We attached a ResultCallback to the Nearby.Messages.subscribe() call and Nearby will let us know if you subscribed successfully or not.
Using the green Play icon from the Android Studio menu, run the app.
If everything goes as planned, you should see a "Subscribed successfully" message in the logs. If the subscription failed, you should see an error message.
Close the current Android Studio project (go to File > Close Project).
In the previous step, you called Nearby.Messages.subscribe(), using a PendingIntent as one of the arguments. Nearby uses that PendingIntent to inform your app inside BackgroundSubscribeIntentService about nearby beacon attachments. When Nearby detects a new beacon, it reads the beacon attachment and transmits that attachment to your app as Nearby Message. When Nearby detects that a previously scanned beacon is no longer in range, it also informs your app that that message has been lost.
You are now ready to write the code to process the beacon scans that Nearby reports to your app.
Using the Import Project (Eclipse ADT, Gradle, etc.), open HelloBeacons-Display-Messages.
Open BackgroundSubscribeIntentService.java, and modify the onHandleIntent() method so it looks like this:
public class BackgroundSubscribeIntentService extends IntentService {
...
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
Nearby.Messages.handleIntent(intent, new MessageListener() {
@Override
public void onFound(Message message) {
Log.i(TAG, "found message = " + message);
}
@Override
public void onLost(Message message) {
Log.i(TAG, "lost message = " + message);
}
});
}
}
}
You call Nearby.Messages.handleIntent() to process the information sent to you by Nearby, using a MessageListener object. There are two important methods whose function you should understand:
The Message that Nearby transmits is available inside onFound() and onLost().
Using the green Play icon from the Android Studio menu, run your app.
Look in the logs to make sure you subscription worked, and wait for the log messages from BackgroundSubscribeIntentService to tell you if any beacons were found.
Close the current Android Studio project (go to File > Close Project).
If you succeeded in scanning for beacon attachments in the previous step, your app already works. Congratulations!!!
In this step, we'll spruce up the UI a bit and display beacon attachments found by Nearby in a list on MainActivity. We'll also add a persistent notification so that you can know about scanned beacons even when your app is not in use (or even when you screen is locked).
Using the Import Project (Eclipse ADT, Gradle, etc.), open HelloBeacons-Display-Messages.
In the previous step, we logged the beacon attachment provided to HelloBeacons in BackgroundSubscribeIntentService.
Here's a summary and survey of the new code you'll discover in HelloBeacons-Display-Messages:
In Utils.java, we've added code for getting messages that were saved to SharedPreferences, and saving a found message and removing a lost message from SharedPreferences.
public final class Utils {
...
static List<String> getCachedMessages(...) {
...
}
static void saveFoundMessage(...) {
...
}
static void removeLostMessage(...) {
...
}
static SharedPreferences getSharedPreferences(Context context) {
...
}
}
In BackgroundSubscribeIntentService.java, we've added code to manage the persistent notification, and to formulate it nicely with a title and body:
private void updateNotification() {
...
}
private String getContentTitle(List<String> messages) {
...
}
private String getContentText(List<String> messages) {
...
}
We've made numerous changes to MainActivity.java to display the attachment messages.
We've added an ArrayAdapter and a backing ArrayList to store messages from Nearby.
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
SharedPreferences.OnSharedPreferenceChangeListener {
...
private ArrayAdapter<String> mNearbyMessagesArrayAdapter;
private List<String> mNearbyMessagesList = new ArrayList<>();
...
}
MainActivity.java now implements SharedPreferences.OnSharedPreferenceChangeListener and we call registerOnSharedPreferenceChangeListener(this) in onResume() and unregisterOnSharedPreferenceChangeListener(this) in onPause(). In the onSharedPreferenceChanged() callback, we've added code to display the list of messages found by Nearby. This callback is triggered every time the app adds or removes a message to SharedPreferences, and when that happens, we update the list of messages and ask the ArrayAdapter to refresh the contents on the screen.
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (TextUtils.equals(key, Utils.KEY_CACHED_MESSAGES)) {
mNearbyMessagesList.clear();
mNearbyMessagesList.addAll(Utils.getCachedMessages(this));
mNearbyMessagesArrayAdapter.notifyDataSetChanged();
}
}
You need to make only a few small changes to complete the new UI for displaying messages.
In MainActivity.java, add the following lines at the bottom of the onCreate() method:
public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
SharedPreferences.OnSharedPreferenceChangeListener {
. . .
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
. . .
final List<String> cachedMessages = Utils.getCachedMessages(this);
if (cachedMessages != null) {
mNearbyMessagesList.addAll(cachedMessages);
}
final ListView nearbyMessagesListView = (ListView) findViewById(
R.id.nearby_messages_list_view);
mNearbyMessagesArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1,
mNearbyMessagesList);
if (nearbyMessagesListView != null) {
nearbyMessagesListView.setAdapter(mNearbyMessagesArrayAdapter);
}
}
. . .
}
In BackgroundSubscribeIntenentService.java, find the onHandleIntent() method and modify the code inside onFound() and onLost() to save content to SharedPreferences and update the persistent notification:
public class BackgroundSubscribeIntentService extends IntentService {
. . .
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
Nearby.Messages.handleIntent(intent, new MessageListener() {
@Override
public void onFound(Message message) {
Utils.saveFoundMessage(getApplicationContext(), message);
updateNotification();
}
@Override
public void onLost(Message message) {
Utils.removeLostMessage(getApplicationContext(), message);
updateNotification();
}
});
}
}
}
Using the green Play icon from the Android Studio menu, run your app.
Your screen should now look something like this:
In this codelab, we assumed that beacons had already been provisioned, registered with the beacon platform and had attachments added. If you want to do all that yourself, here are the steps:
Go to https://console.developers.google.com/ and select Create a project....
Remember the project name, which determines the default namespace for your attachments. When you register your beacons with Google, you'll be linking them to this project. Further, this project will own all the beacon and attachment data that you store in the service.
Open the side nav and select API Manager.
On the API Manager screen do the following:
You'll be asked to set up credentials.
Click on "Go to Credentials" and on the Credentials screen, do the following:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
You should see a popup with your API Key.
Open HelloBeacons-Finished in Android Studio and add the API key to AndroidManifest.xml.
Beacons can be provisioned using instructions provided by the beacon manufacturer. Many vendors provide Android apps to help with provisioning. For example:
When provisioning your beacon, follow these general guidelines to set the beacon's key parameters (broadcast ID, advertising interval or broadcast rate, broadcast power):
The Google Beacon Tools app, which lets you register your beacons with the Google Beacon Registry and create small attachments for them.
With the beacon you have just registered in hand, launch Beacon Tools and go through the following steps:
Using the green Play icon from the Android Studio menu, run HelloBeacons.
HelloBeacons should be able to find the beacons you set up, and it should display the attachment data on the screen.