Share
Embed
Android Wear - Get your app on your wrist
Slides for my talk at Droidcon London 2015
Maria Neumayer
October 30, 2015
Transcript
64 slides
-
What does Android Wear do? • Glanceable information • Short
interactions • Companion to your phone • Shows the time -
Notifications • Appear automatically on Wear • Use WearableExtender for
Wear only features • Add custom layouts on separate Wear notifications -
Android Wear is Android But… • Different form factor •
No device specific customisations • No internet access -
WearableFrameLayout <android.support.wearable.view.WearableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <TextView android:layout_margin="8dp" app:layout_marginRound="16dp" android:text="@string/hello_wear"/>
</android.support.wearable.view.WearableFrameLayout> -
Custom Inset public class InsetView extends TextView { @Override
protected void onAttachedToWindow() { super.onAttachedToWindow(); requestApplyInsets(); } } -
Custom Inset @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { super.onApplyWindowInsets(insets);
if (insets.hasSystemWindowInsets()) { ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams(); params.bottom += insets.getSystemWindowInsetBottom(); setLayoutParams(layoutParams); } return insets; } -
GridViewPager • ViewPager with vertical and horizontal paging • Variable
number of columns per row -
WearableListView • RecyclerView with item snapping support • Use OnCenterProximityListener
to style selected item -
DelayedConfirmationView public class ConfirmationActivity extends Activity implements DelayedConfirmationView.DelayedConfirmationListener {
private DelayedConfirmationView confirmationView; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); confirmationView.setListener(this); confirmationView.setTotalTimeMs(1000); confirmationView.start(); } public void onTimerFinished(View view) {} public void onTimerSelected(View view) {} } -
Ambient mode • Low battery mode • Only update infrequently
• Always show current time -
Ambient mode public class AmbientActivity extends WearableActivity { @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setAmbientEnabled(); } } -
Ambient mode @Override public void onEnterAmbient(Bundle ambientDetails) { super.onEnterAmbient(ambientDetails);
timeView.setTextColor(Color.WHITE); contentView.setBackgroundColor(Color.BLACK); } @Override public void onExitAmbient() { super.onExitAmbient(); timeView.setTextColor(Color.BLACK); contentView.setBackgroundColor(Color.WHITE); } -
Battery life • User needs to get through the day
• Only send data when necessary • Stop regular updates when screen is off -
Communication between phone and watch dependencies { compile 'com.google.android.support:wearable:1.3.0' provided
'com.google.android.wearable:wearable:1.0.0' compile 'com.google.android.gms:play-services-wearable:8.1.0' } -
Communication between phone and watch GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API) .build(); googleApiClient.connect(); -
Listener Activity • Receive data when the app is in
the foreground • Different listener per event type Wearable.DataApi.addListener(googleApiClient, this); MessageApi NodeApi CapabilityApi ChannelApi -
WearableListenerService • Receive data in the background and wake up
app • Lifecycle handled by the system • Only one per application allowed <service android:name=".DataLayerListenerService"> <intent-filter> <action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> </intent-filter> </service> -
Nodes • Each connected device is a Node • Phone
can be connected with multiple wearables • Nodes can define capabilities -
Nodes private Collection<Node> getNodesWithInternetCapability() { CapabilityApi.GetCapabilityResult result = Wearable.CapabilityApi.getCapability(
googleApiClient, INTERNET_CAPABILITY, CapabilityApi.FILTER_REACHABLE) .await(); return result.getCapability().getNodes(); } -
Nodes private String getBestNodeId(Collection<Node> nodes) { String nodeId = null;
for (Node node : nodes) { if (node.isNearby()) { return node.getId(); } nodeId = node.getId(); } return nodeId; } -
Nodes private Collection<String> getConnectedNodes() { NodeApi.GetConnectedNodesResult nodesResult = Wearable.NodeApi
.getConnectedNodes(googleApiClient) .await(); Collection<String> nodeIds = new HashSet<>(); for (Node node : nodesResult.getNodes()) { nodeIds.add(node.getId()); } return nodeIds; } -
Message • Use for small and short lived data •
One way communication • Sent to a specified Node -
Send a message private static final String MESSAGE_PATH = "/message";
private void sendMessage(String nodeId, String message) { PendingResult<MessageApi.SendMessageResult> pendingResult = Wearable.MessageApi.sendMessage(googleApiClient, nodeId, MESSAGE_PATH, message.getBytes()); } -
Send a message pendingResult.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() { @Override public void
onResult(MessageApi.SendMessageResult result) { if (!result.getStatus().isSuccess()) { // sending message failed } } }); -
Receiving a message @Override public void onMessageReceived(MessageEvent messageEvent) {
if (messageEvent.getPath().equals(MESSAGE_PATH)) { byte[] data = messageEvent.getData(); String message = new String(data); } } -
Sync data • DataItem syncs across all devices • Data
will be queued up and delivered once Node connects • Used for small, longer lived data - max 100KB -
Sync DataItem private static final String DATA_PATH = "/data"; private
void syncData(String data) { PutDataRequest request = PutDataRequest.create(DATA_PATH); request.setData(data.getBytes()); Wearable.DataApi.putDataItem(googleApiClient, request); } -
Receive DataItem @Override public void onDataChanged(DataEventBuffer dataEvents) { for (DataEvent
event : dataEvents) { if (event.getType() == DataEvent.TYPE_CHANGED) { DataItem item = event.getDataItem(); if (item.getUri().getPath().equals(DATA_PATH)) { byte[] byteData = item.getData(); String data = new String(byteData); } } } } -
Sync DataMapItem private static final String NOTIFICATION_PATH = "/notification"; private
static final String KEY_TITLE = "title"; private static final String KEY_SUBTITLE = "subTitle"; private void syncNotification(String title, String subTitle) { PutDataMapRequest mapRequest = PutDataMapRequest.create(NOTIFICATION_PATH); mapRequest.getDataMap().putString(KEY_TITLE, title); mapRequest.getDataMap().putString(KEY_SUBTITLE, subTitle); PutDataRequest request = mapRequest.asPutDataRequest(); Wearable.DataApi.putDataItem(googleApiClient, request); } -
Sync Asset private static final String ASSET_PATH = "/asset"; private
static final String KEY_IMAGE = "image"; private void sendAsset(byte[] bytes) { Asset asset = Asset.createFromBytes(bytes); PutDataRequest request = PutDataRequest.create(ASSET_PATH); request.putAsset(KEY_IMAGE, asset); Wearable.DataApi.putDataItem(googleApiClient, request); } -
Retrieve Asset private InputStream getIsFromDataItem(DataMapItem dataItem) { Asset asset =
dataItem.getDataMap().getAsset(KEY_IMAGE); return Wearable.DataApi.getFdForAsset(googleApiClient, asset).await().getInputStream(); } -
Delete item private void deleteItem(String path) { Wearable.DataApi.deleteDataItems(googleApiClient, new Uri.Builder()
.scheme(PutDataRequest.WEAR_URI_SCHEME) .path(path) .build(), DataApi.FILTER_LITERAL); } -
Delete item @Override public void onDataChanged(DataEventBuffer dataEventBuffer) { for (DataEvent
event : dataEventBuffer) { if (event.getType() == DataEvent.TYPE_DELETED) { DataItem item = event.getDataItem(); if (item.getUri().getPath().equals(DATA_PATH)) { // delete data } } } } -
Sync data • Check cache for mandatory data • onDataChanged()
is also called for the local Node • Sending the same data twice won’t trigger onDataChanged() -
Get cached DataItem private DataItem getCachedDataItem(String nodeId, String path) {
Uri uri = new Uri.Builder() .scheme(PutDataRequest.WEAR_URI_SCHEME) .authority(nodeId).path(path).build(); PendingResult<DataApi.DataItemResult> result = Wearable.DataApi.getDataItem(googleApiClient, uri); return result.await().getDataItem(); } -
Check local Node @Override public void onDataChanged(DataEventBuffer dataEvents) { for
(DataEvent event : dataEvents) { DataItem item = event.getDataItem(); if (isFromSelf(item.getUri().getHost())) { continue; } } } private boolean isFromSelf(String sourceNodeId) { return localNode.equals(sourceNodeId); } -
Packaging the app Use the same • Package name •
Version number • Keystore -
Permissions • Phone app needs to include all Wear permissions
• If the phone app targets >=23 dangerous permissions need to be approved before first run -
Logging • Don’t forget to log crashes! • Use DataLayer
to sync to phone