We are using RxJava in Android a lot, with good reasons. However, we still need to use code that is not built with RxJava, so let’s wrap them.
Synchronous APIs
For very simple synchronous APIs, you can use Observable.just() to wrap them. e.g. you can use Observable.just(1, 2, 3, 4, 5) to emit an integer sequence from 1 to 5.
If the API is blocking, you can use Observable.defer() to wrap them:
For advanced RxJava users, who don’t need to read this article, you can use Observable.create() to wrap it, and fullfil the contract by yourself. But for us, we can easily use Observable.fromEmitter() to handle the case, and let the framework to help us:
Observable<Location>observable=Observable.fromEmitter(newAction1<AsyncEmitter<Location>>(){@Overridepublicvoidcall(finalAsyncEmitter<Location>emitter){finalLocationListenerlocationListener=newLocationListener(){@OverridepublicvoidonLocationChanged(Locationlocation){// emits locationemitter.onNext(location);}...};emitter.setCancellation(newAsyncEmitter.Cancellable(){@Overridepublicvoidcancel()throwsException{// stops location updates when unsubscribedlocationManager.removeUpdates(locationListener);});locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,1000L,10.0F,locationListener);// if you also emit onError() or onComplete(),// the framework will make sure the Observable// contract is fullfilled}// let the framework to worry about backpressure},AsyncEmitter.BackpressureMode.BUFFER);
It’s extremely easy to share with Intent on Android. However, there are some apps that capture the ACTION_SEND intents, but doesn’t allow the app to pre-fill text set with EXTRA_TEXT, resulting in poor user experience.
With the following code, we can easily exclude some unwanted apps from the chooser intent:
@NullableprivatestaticIntentcreateChooserExcludingPackage(Contextcontext,StringpackageToExclude,Stringtext){// 1) gets all activities that can handle the sharing intentfinalIntentsendIntent=newIntent(Intent.ACTION_SEND).setType("text/plain");finalPackageManagerpm=context.getPackageManager();finalList<ResolveInfo>resolveInfoList=pm.queryIntentActivities(sendIntent,0);finalintsize=resolveInfoList.size();if(size==0){returnnull;}// 2) now let's filter by package namefinalArrayList<Intent>filteredIntents=newArrayList<>(size);for(inti=0;i<size;++i){finalResolveInforesolveInfo=resolveInfoList.get(i);finalStringpackageName=resolveInfo.activityInfo.packageName;if(!packageToExclude.equals(packageName)){// creates a LabeledIntent with custom icon and textfinalLabeledIntentlabeledIntent=newLabeledIntent(packageName,resolveInfo.loadLabel(pm),resolveInfo.getIconResource());labeledIntent.setAction(Intent.ACTION_SEND).setPackage(packageName).setComponent(newComponentName(packageName,resolveInfo.activityInfo.name)).setType("text/plain").putExtra(Intent.EXTRA_TEXT,text);filteredIntents.add(labeledIntent);}}// 3) creates new chooser intentfinalIntentchooserIntent=Intent.createChooser(filteredIntents.remove(0),context.getText(R.string.text_share_with));finalintextraIntents=filteredIntents.size();if(extraIntents>0){chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,filteredIntents.toArray(newParcelable[extraIntents]));}returnchooserIntent;}
The above code is taken from here. Enjoy and happy coding!
Osmosis is a command line application for processing Open Street Map data, with the souce code available here. The tool provides e.g. ability to generate and read data dumps, extract data inside a bounding box, etc. You can also easily write your own plugin to convert OSM data to e.g. formats your application understands.
Now, let’s start.
Write a Simple Plugin
An osmosis plugin is basically a normal jar, plus a plugin.xml file that describes how to load the plugin as shown below:
<?xml version="1.0" ?><!DOCTYPE plugin PUBLIC "-//JPF//Java Plug-in Manifest 1.0"
"http://jpf.sourceforge.net/plugin_1_0.dtd"><!-- Here, provides a unique ID and version for your plugin. --><pluginid="MyPlugin"version="1.0"><requires><importplugin-id="org.openstreetmap.osmosis.core.plugin.Core"reverse-lookup="false"/></requires><runtime><libraryid="mycode"path="/"type="code"/></runtime><!-- Describes where the plugin plugs into. --><extensionid="MyPlugin"plugin-id="org.openstreetmap.osmosis.core.plugin.Core"point-id="Task"><parameterid="name"value="MyPlugin"/><!-- Here, give the fully qualified name for your class loader. --><parameterid="class"value="net.zionsoft.sample.osmosisplugin.MyPluginLoader"/></extension></plugin>
As we can see, the plugin is loaded by MyPluginLoader:
publicclassMyPluginLoaderimplementsPluginLoader{@OverridepublicMap<String,TaskManagerFactory>loadTaskFactories(){// the map describes how to load the plugin// with the following statement, you can load the task// created by MyFactory using:// osmosis --read-pbf latest.osm.pbf --my-plugin// you can also create multiple factories that create// different tasks if neededHashMap<String,TaskManagerFactory>map=newHashMap<>();map.put("my-plugin",newMyFactory());returnmap;}}
The following shows MyFactory that creates the real tasks:
publicclassMyFactoryextendsTaskManagerFactory{@OverrideprotectedTaskManagercreateTaskManagerImpl(TaskConfigurationtaskConfiguration){// the provided configuration includes the argument you pass to osmosis,// which can be used to config the task to be created// e.g. if you started osmosis like this:// osmosis --read-pbf latest.osm.pbf --my-plugin key=value// you can get fetch the passed argument like this:// String value = getStringArgument(taskConfiguration, "key", null);MyTaskmyTask=newMyTask();returnnewSinkManager(taskConfiguration.getId(),myTask,taskConfiguration.getPipeArgs());}}
Now, a simple task that does nothing but print some messages:
publicclassMyTaskimplementsSink{@Overridepublicvoidinitialize(Map<String,Object>map){// initializes resources you needSystem.out.println("initialize()");}@Overridepublicvoidprocess(EntityContainerentityContainer){// processes each entityEntityentity=entityContainer.getEntity();EntityTypeentityType=entity.getType();System.out.println("process(): "+entityType);switch(entityType){caseBound:Boundbound=(Bound)entity;System.out.println(bound.toString());break;caseNode:Nodenode=(Node)entity;System.out.println(node.toString());break;caseWay:Wayway=(Way)entity;System.out.println(way.toString());break;caseRelation:Relationrelation=(Relation)entity;System.out.println(relation.toString());break;}}@Overridepublicvoidcomplete(){// makes sure all info is fully persistedSystem.out.println("complete()");}@Overridepublicvoidrelease(){// releases resourcesSystem.out.println("release()");}}
Installation
Just copy the generated jar to ~/.openstreetmap/osmosis/plugins folder.
Use the Plugin
You can now use your plugin: osmosis --read-pbf latest.osm.pbf --my-plugin, and you will get some output like below:
Oct 13, 2015 9:05:39 PM org.openstreetmap.osmosis.core.Osmosis run
INFO: Osmosis Version 0.42-6-gf39a160-dirty
Oct 13, 2015 9:05:40 PM org.openstreetmap.osmosis.core.Osmosis run
INFO: Preparing pipeline.
Oct 13, 2015 9:05:40 PM org.openstreetmap.osmosis.core.Osmosis run
INFO: Launching pipeline execution.
initialize()
Oct 13, 2015 9:05:40 PM org.openstreetmap.osmosis.core.Osmosis run
INFO: Pipeline executing, waiting for completion.
process(): Bound
Bound(top=60.2359955, bottom=60.2358955, left=24.7136472, right=24.7186472)
process(): Node
Node(id=506188159, #tags=0)
process(): Node
Node(id=518411565, #tags=0)
process(): Node
Node(id=2009423577, #tags=0)
process(): Way
Way(id=23123806, #tags=2)complete()
release()
Oct 13, 2015 9:05:53 PM org.openstreetmap.osmosis.core.Osmosis run
INFO: Pipeline complete.
Oct 13, 2015 9:05:53 PM org.openstreetmap.osmosis.core.Osmosis run
INFO: Total execution time: 13088 milliseconds.
Yes, it’s that simple to create your own osmosis plugin, and time to hack now!
To scan e.g. heart rate monitors, you’re supposed to use code like this:
// scan for devices providing a certain servicebluetoothAdapter.startLeScan(newUUID[]{serviceUuid},newBluetoothAdapter.LeScanCallback(){@OverridepublicvoidonLeScan(BluetoothDevicedevice,intrssi,byte[]scanRecord){// wow, it should work}});// or scan for all nearby BLE devicesbluetoothAdapter.startLeScan(newBluetoothAdapter.LeScanCallback(){@OverridepublicvoidonLeScan(BluetoothDevicedevice,intrssi,byte[]scanRecord){// yeah, it must work, right?}});
Here, the service UUID is to describe the service the peripheral devices provide, e.g. 0000180d-0000-1000-8000-00805f9b34fb is the service UUID for heart rate monitors.
Well, the reality is, the above two approaches may or may not work, depending on the specific device and OS version (e.g. my Samsung Galaxy S3 with Android 4.3 doesn’t support the first filter-based approach). It’s a known issue, and only fixed in Android 5.0 (only checked with Nexus 5, and seems working fine).
So, in your code, you have to scan devices with both of the ways, and hope that one shall work. With the second approach, you have to check if the scanned device is your target device through device names or even connect to it and figure out the services it provides.
Sometimes, you might need to involve the users. First, ask your users to turn off WiFi and try again. Not working? Turn off Bluetooth and on again and give a try. Still not working? OK, the Bluetooth service is crashed and can’t be restarted unless you reboot the device. So, just reboot your device like you often do with your Windows PC, then you shouldTM be fine.
If you’ve paired with BLE devices, there’s a known bug that Android might forget the status, and you have to pair again.
BLE and Classic Bluetooth
So you want to connect to both BLE devices and classic Bluetooth devices at the same time? Obviously, you’re asking too much. My experience is, the classic Bluetooth devices are more likely to be disconnected, and they won’t be able to connect until you ask the users to act as described above.
I haven’t figured any robust ways to get them always working. Please let me know if you’ve found solutions.
Too Many BLE Devices
If the user has scanned too many devices, he / she will be notified Unfortunately, Bluetooth share has stopped. And of course it’s a system issue, and it sucks especially when there’re BLE beacons changing address all the time.
OK, there’s a non-perfect solution. Also, for non-rooted devices, the user can turn off the Bluetooth to postpone the issue from happening. Alternatively, the user can do a factory reset when shit hits the fan, and wait for it to happen the next time. For rooted devices, you can manually edit the bt_config.xml file.
Best solution? Get a device with Android 4.4.3 or later.
Concurrent Connections
There is some limitation on the number of BLE devices Android can connect to at the same time, and it seems the number is different on different versions. So better connect one by one.
Threading Issue
This is not an Android issue, but more like a Samsung issue (works fine on Nexus, but haven’t checked other vendors):
privatefinalBluetoothAdapter.LeScanCallbackleScanCallback=newBluetoothAdapter.LeScanCallback(){@OverridepublicvoidonLeScan(BluetoothDevicedevice,intrssi,byte[]scanRecord){// on certain devices, this won't work// you must connect in another threadBluetoothGattgatt=device.connectGatt(context,false,gattCallback);}}
Disconnect from GATT Server
So, hopefully, you have successfully connected to your GATT server, and after all the reading and writing, you need to disconnect:
gatt.disconnect();gatt.close();
Well, it may or may not work, and might throw exceptions, so better catch all Throwables.
Conclusion
Well, it’s a big mess in 4.3 and 4.4 (thanks to the new Bluedroid Bluetooth stack they introduced in 4.2), but luckily things seem to be more stable in 5.0. And hopefully, most devices could enjoy Lollipop somewhere in the future.
To use Android Wear, you’ll need a hosting device (e.g. a phone or a tablet) with Android 4.3 or above, with BLE support. To set up the Google Play services that is used for communication between phone and watch, please follow this tutorial.
Among others, you will need to add the wearable module to your build.gradle file for both wearable app project and phone app project:
Basically, you can run any Android application on your watch, though it has a small display and limited hardware support (e.g. some watch has no GPS support). You can also use the support library provided by Google for some common UI widgets:
The WatchViewStub view will load the corresponding layout based on the shape of the watch. Then in your activity:
@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);WatchViewStubwatchViewStub=(WatchViewStub)findViewById(R.id.watch_view_stub);watchViewStub.setOnLayoutInflatedListener(newWatchViewStub.OnLayoutInflatedListener(){@OverridepublicvoidonLayoutInflated(WatchViewStubstub){// the layout is fully inflated}});}
Send and sync data
There are two ways to share data between your phone and watch:
Send a specific message to a certain node using the MessageApi.
Share data among all nodes using the DataApi. With this API, the data sent will be synchronized across all connected devices, which means the data will be pushed to a disconnected device if it gets connected later.
With both methods, the data are private to the application, so the develop doesn’t need to worry about the privacy nor security.
Send data with MessageApi
Once you have a connected GoogleApiClient, you can use the following code to send a message to a connected node:
// first finds all the connected nodesWearable.NodeApi.getConnectedNodes(googleApiClient).setResultCallback(newResultCallback<NodeApi.GetConnectedNodesResult>(){@OverridepublicvoidonResult(NodeApi.GetConnectedNodesResultgetConnectedNodesResult){List<Node>nodes=getConnectedNodesResult.getNodes();if(nodes.size()==0){// no connected nodesreturn;}// sends a message to the specified path for the first connected nodeStringnodeId=nodes.get(0).getId();byte[]data=...;Wearable.MessageApi.sendMessage(googleApiClient,nodeId,"/some/random/path",data);}});
Sync data with DataApi
Once you have a connected GoogleApiClient, you can use the following code to sync data across all connected nodes:
PutDataMapRequestputDataMapRequest=PutDataMapRequest.create("/some/random/path");DataMapdataMap=putDataMapRequest.getDataMap();// you can put different data heredataMap.putLong("key 1",12345L);Wearable.DataApi.putDataItem(googleApiClient,putDataMapRequest.asPutDataRequest());
Receive data with WearableListenerService
To receive messages or data updates from other nodes, you must extend the WearableListenerService, whose life cycle is managed by the phone or the watch:
publicclassWearableListenerextendsWearableListenerService{@OverridepublicvoidonMessageReceived(MessageEventmessageEvent){// this is called when it receives one single message from// a connected node}@OverridepublicvoidonDataChanged(DataEventBufferdataEvents){// this is called when one or more data is created, updated// or deleted using the DataApi// note:// 1) if the same data is updated several times, you might// only be notified once, with the final state of the data// 2) it might contain more than one data events// 3) the provided buffer is only valid till the end of this// methodfor(DataEventdataEvent:dataEvents){DataItemdataItem=dataEvent.getDataItem();UridataItemUri=dataItem.getUri();// the path is the one provided when sync the data with DataApiStringpath=dataItemUri.getPath();if(!"/some/random/path".equals(path)){continue;}switch(dataEvent.getType()){caseDataEvent.TYPE_CHANGED:// the data is created or updated// note that if it is updated several times with the// same data, this method won't be calledbreak;caseDataEvent.TYPE_DELETED:// the data is deleted// note that if the data is not deleted, it will be// "persisted" until the application is removedbreak;}}}}
The two permissions allow you to control the accuracy of the requested locations, and you don’t have to request both for your app. If you only request the coarse location permission, the fetched location will be obfuscated. However, if you want to use the geofencing feature, you must request the ACCESS_FINE_LOCATION permission.
Connect Location Client
With the new GoogleApiClient class, you can connect all needed services at once, and Google Play services will handle all the permission requests, etc.:
publicclassMainActivityextendsActivityimplementsGoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener{privateGoogleApiClientmGoogleClient;@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);// you can also add more APIs and scopes heremGoogleClient=newGoogleApiClient.Builder(this,this,this).addApi(LocationServices.API).build();}@OverrideprotectedvoidonStart(){super.onStart();mGoogleClient.connect();}@OverrideprotectedvoidonStop(){mGoogleClient.disconnect();super.onStop();}@OverridepublicvoidonConnected(BundleconnectionHint){// this callback will be invoked when all specified services are connected}@OverridepublicvoidonConnectionSuspended(intcause){// this callback will be invoked when the client is disconnected// it might happen e.g. when Google Play service crashes// when this happens, all requests are canceled,// and you must wait for it to be connected again}@OverridepublicvoidonConnectionFailed(ConnectionResultconnectionResult){// this callback will be invoked when the connection attempt failsif(connectionResult.hasResolution()){// Google Play services can fix the issue// e.g. the user needs to enable it, updates to latest version// or the user needs to grant permissions to ittry{connectionResult.startResolutionForResult(this,0);}catch(IntentSender.SendIntentExceptione){// it happens if the resolution intent has been canceled,// or is no longer able to execute the request}}else{// Google Play services has no idea how to fix the issue}}}
Access Current Location
Once connected, you can easily fetch the current location:
// fetch the current locationLocationlocation=LocationServices.FusedLocationApi.getLastLocation(mGoogleClient);
Note that this getLastLocation() method might return null in case location is not available, though this happens very rarely. Also, it might return a location that is a bit old, so the client should check it manually.
Listen to Location Updates
Let’s extend the above code snippet:
publicclassMainActivityextendsActivityimplementsGoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener,LocationListener{...@OverridepublicvoidonConnected(BundledataBundle){...// start listening to location updates// this is suitable for foreground listening,// with the onLocationChanged() invoked for location updatesLocationRequestlocationRequest=LocationRequest.create().setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY).setFastestInterval(5000L).setInterval(10000L).setSmallestDisplacement(75.0F);LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleClient,locationRequest,this);}@OverridepublicvoidonLocationChanged(Locationlocation){// this callback is invoked when location updates}...}
The difference between setFastestInterval() and setInterval() is:
If the location updates is retrieved by other apps (or other location request in your app), your onLocationChanged() callback here won’t be called more frequently than the time set by setFastestInterval().
On the other hand, the location client will actively try to get location updates at the interval set by setInterval(), which has a direct impact on the power consumption of your app.
Then how about background location tracking? Do I need to implement a long-running Service myself? The answer is simply, no.
With this, your listener (it can be an IntentService, or a BroadcastReceiver) as defined in the PendingIntent will be triggered even if your app is killed by the system. The location updated will be sent with key FusedLocationProviderApi.KEY_LOCATION_CHANGED and a Location object as the value in the Intent:
publicclassMyLocationHandlerextendsIntentService{publicMyLocationHandler(){super("net.zionsoft.example.MyLocationHandler");}@OverrideprotectedvoidonHandleIntent(Intentintent){finalLocationlocation=intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);// happy playing with your location}}
Geofencing
With geofencing, your app can be notified when the device enters, stays in, or exits a defined area. Please note that geofencing requires ACCESS_FINE_LOCATION. Again, let’s keep extending the above sample:
Here, the loitering delay means the GEOFENCE_TRANSITION_DWELL will be notified 30 seconds after the device enters the area. There’re also limitations on the number of geofences (100 per app) and pending intents (5 per app) enforced.
When a geofence transition is triggered, you can find more details easily e.g. in an IntentService:
To distinguish if the location is a mock one, a key of FusedLocationProviderApi.KEY_MOCK_LOCATION in the location object’s bundle extra will be set to true.
Once done, please remember to set the mock mode to false. If you forget that, the system will set it to false when your location client is disconnected.
That’s it for today. Happy coding and keep reading.
If you haven’t set up Google Play services SDK yet, please follow this tutorial. Today, we demonstrate how to use the cloud messaging / push notification.
Enable Cloud Messaging API
First, create a project in Google Developers Console for your app, and enable the Google Cloud Messaging for Android API. The project number will be used as the GCM sender ID.
Then, create a server key in Public API access with your server’s IP address (for testing purposes, you can use e.g. 0.0.0.0/0 to allow anybody to send messages with the generated API key). The generated API key will be used to authenticate your server.
Note: For devices running Android older than 4.0.4, it requires users to set up a Google account before using GCM.
Update AndroidManifest.xml
Now, update your AndroidManifest.xml file, specifying the required permissions:
The following snippet shows how to register a client for cloud messaging:
// this might invoke network calls, so call me in a worker threadprivatevoidregisterCloudMessaging(){// repeated calls to this method will return the same token// a new registration is needed if the app is updated or backup & restore happensfinalStringtoken=InstanceID.getInstance(this).getToken("my-GCM-sender-ID",GoogleCloudMessaging.INSTANCE_ID_SCOPE,null);// then uploads the token to your server// or, if the client wants to subscribe to certain topics, use this methodGcmPubSub.getInstance(this).subscribe(token,"/topics/your_topic_name",null);}
Note that Google’s GCM server doesn’t handle localization nor message scheduling, so the client might also need to upload e.g. the preferred language and timezone to the server to improve user experiences.
publicclassMyPushNotificationReceiverextendsBroadcastReceiver{@OverridepublicvoidonReceive(Contextcontext,Intentintent){// a null or "gcm" message type indicates a regular message,// and you can get the message details as described in section 5// "deleted_messages" indicates some pending messages are deleted by serverStringmessageType=intent.getStringExtra("message_type");// indicates the sender of the message, or the topic nameStringfrom=intent.getStringExtra("from");}}
Send a Message
To send a push message, you can simply send a POST request to Google’s GCM server. The URL of the server is: https://gcm-http.googleapis.com/gcm/send.
The HTTP header must contain the following two:
Authorization:key=
Content-Type: application/json
The HTTP body is a JSON object, something like this:
The fields of the data object represent the key-value pairs of the message’s payload data. With the above example, the client will receive an intent with an extra of key key_1 and value value_1, and another extra of key key_2 and value value_2.
The priority can be either normal (default) or high. The messages marked as high priority will be sent immediately, even when the device is in Doze mode. For normal priority messages, they will be batched for devices in Doze mode, and will be discarded if the message expires while the device is in Doze mode.
For a complete list of allowed fields in the posted JSON, check here.
The response can contain the following status code:
200: The message is successfully processed.
400: The request JSON object is malformed.
401: The sender fails to authenticate itself.
5xx: Server error, the sender should respect the Retry-After and retry later.
More details of how to parse the response messages can be found here.
That’s it for today. Happy hacking and keep reading!