Slide 1

Slide 1 text

Android Wear Get your app on your wrist Maria Neumayer @marianeum

Slide 2

Slide 2 text

What does Android Wear do? • Glanceable information • Short interactions • Companion to your phone • Shows the time

Slide 3

Slide 3 text

How to get started?

Slide 4

Slide 4 text

Notifications • Appear automatically on Wear • Use WearableExtender for Wear only features • Add custom layouts on separate Wear notifications

Slide 5

Slide 5 text

Android Wear is Android But… • Different form factor • No device specific customisations • No internet access

Slide 6

Slide 6 text

minSdkVersion 21

Slide 7

Slide 7 text

minSdkVersion 21

Slide 8

Slide 8 text

minSdkVersion 21 22

Slide 9

Slide 9 text

Wear UI elements

Slide 10

Slide 10 text

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'
 }

Slide 11

Slide 11 text

round vs square

Slide 12

Slide 12 text

BoxInsetLayout 
 
 
 


Slide 13

Slide 13 text

BoxInsetLayout

Slide 14

Slide 14 text

WearableFrameLayout 
 
 
 


Slide 15

Slide 15 text

WearableFrameLayout

Slide 16

Slide 16 text

Custom Inset public class InsetView extends TextView { 
 @Override
 protected void onAttachedToWindow() {
 super.onAttachedToWindow();
 requestApplyInsets();
 } 
 }

Slide 17

Slide 17 text

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;
 }

Slide 18

Slide 18 text

Custom Intent

Slide 19

Slide 19 text

WatchViewStub

Slide 20

Slide 20 text

values-round?

Slide 21

Slide 21 text

GridViewPager • ViewPager with vertical and horizontal paging • Variable number of columns per row

Slide 22

Slide 22 text

WearableListView • RecyclerView with item snapping support • Use OnCenterProximityListener to style 
 selected item

Slide 23

Slide 23 text

ConfirmationActivity Intent intent = new Intent(this, ConfirmationActivity.class);
 intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,
 ConfirmationActivity.SUCCESS_ANIMATION);
 intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,
 getString(R.string.message_sent)); 
 startActivity(intent);

Slide 24

Slide 24 text

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) {}
 }

Slide 25

Slide 25 text

WearableFrameLayout ActionPage CircledImageView CircularButton CardFrame CardScrollView DismissOverlayView ActionLabel CardFragment CrossfadeDrawable DotsPageIndicator ProgressSpinner

Slide 26

Slide 26 text

Ambient mode • Low battery mode • Only update infrequently • Always show current time

Slide 27

Slide 27 text

Ambient mode 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'
 }

Slide 28

Slide 28 text

Ambient mode public class AmbientActivity extends WearableActivity {
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 setAmbientEnabled();
 }
 }

Slide 29

Slide 29 text

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);
 }

Slide 30

Slide 30 text

Ambient mode @Override public void onUpdateAmbient() {
 super.onUpdateAmbient();
 
 timeView.setText(time);
 }

Slide 31

Slide 31 text

Battery life • User needs to get through the day • Only send data when necessary • Stop regular updates when screen is off

Slide 32

Slide 32 text

Communication between phone and watch

Slide 33

Slide 33 text

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'
 }

Slide 34

Slide 34 text

Communication between phone and watch GoogleApiClient googleApiClient =
 new GoogleApiClient.Builder(this)
 .addApi(Wearable.API)
 .build();
 
 googleApiClient.connect();

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

WearableListenerService • Receive data in the background and wake up app • Lifecycle handled by the system • Only one per application allowed 
 
 
 


Slide 37

Slide 37 text

Nodes • Each connected device is a Node • Phone can be connected with multiple wearables • Nodes can define capabilities

Slide 38

Slide 38 text

Nodes res/values/wear.xml 
 
 internet
 


Slide 39

Slide 39 text

Nodes private Collection getNodesWithInternetCapability() { 
 CapabilityApi.GetCapabilityResult result =
 Wearable.CapabilityApi.getCapability(
 googleApiClient, INTERNET_CAPABILITY,
 CapabilityApi.FILTER_REACHABLE) .await();
 
 return result.getCapability().getNodes();
 }

Slide 40

Slide 40 text

Nodes private String getBestNodeId(Collection nodes) {
 String nodeId = null;
 for (Node node : nodes) {
 if (node.isNearby()) {
 return node.getId();
 }
 nodeId = node.getId();
 }
 return nodeId;
 }

Slide 41

Slide 41 text

Nodes private Collection getConnectedNodes() { 
 NodeApi.GetConnectedNodesResult nodesResult =
 Wearable.NodeApi .getConnectedNodes(googleApiClient) .await(); Collection nodeIds = new HashSet<>();
 for (Node node : nodesResult.getNodes()) {
 nodeIds.add(node.getId());
 } return nodeIds;
 }

Slide 42

Slide 42 text

Message • Use for small and short lived data • One way communication • Sent to a specified Node

Slide 43

Slide 43 text

Send a message private static final String MESSAGE_PATH = "/message";
 
 private void sendMessage(String nodeId, String message) { 
 PendingResult pendingResult =
 Wearable.MessageApi.sendMessage(googleApiClient, nodeId, MESSAGE_PATH, message.getBytes());
 }

Slide 44

Slide 44 text

Send a message pendingResult.setResultCallback(new ResultCallback() { 
 @Override public void onResult(MessageApi.SendMessageResult result) { if (!result.getStatus().isSuccess()) {
 // sending message failed
 } } });

Slide 45

Slide 45 text

Success doesn’t mean the message was delivered

Slide 46

Slide 46 text

Receiving a message @Override
 public void onMessageReceived(MessageEvent messageEvent) { 
 if (messageEvent.getPath().equals(MESSAGE_PATH)) {
 byte[] data = messageEvent.getData();
 String message = new String(data);
 } 
 }

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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);
 }

Slide 49

Slide 49 text

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);
 }
 }
 }
 }

Slide 50

Slide 50 text

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);
 }

Slide 51

Slide 51 text

Sync Assets • Items larger than 100KB • Send Files or Bitmaps

Slide 52

Slide 52 text

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);
 }

Slide 53

Slide 53 text

Retrieve Asset private InputStream getIsFromDataItem(DataMapItem dataItem) {
 Asset asset = dataItem.getDataMap().getAsset(KEY_IMAGE);
 
 return Wearable.DataApi.getFdForAsset(googleApiClient, asset).await().getInputStream();
 }

Slide 54

Slide 54 text

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);
 }

Slide 55

Slide 55 text

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
 }
 }
 }
 }

Slide 56

Slide 56 text

Sync data • Check cache for mandatory data • onDataChanged() is also called for the local Node • Sending the same data twice won’t trigger onDataChanged()

Slide 57

Slide 57 text

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 result = Wearable.DataApi.getDataItem(googleApiClient, uri); 
 return result.await().getDataItem();
 }

Slide 58

Slide 58 text

Check local Node private String getLocalNode() {
 return Wearable.NodeApi.getLocalNode(googleApiClient) .await().getNode().getId();
 }

Slide 59

Slide 59 text

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);
 }

Slide 60

Slide 60 text

Publishing Wear apps

Slide 61

Slide 61 text

Packaging the app Use the same • Package name • Version number • Keystore

Slide 62

Slide 62 text

Permissions • Phone app needs to include all Wear permissions • If the phone app targets >=23 dangerous permissions need to be approved before first run

Slide 63

Slide 63 text

Logging • Don’t forget to log crashes! • Use DataLayer to sync to phone

Slide 64

Slide 64 text

Questions? Maria Neumayer @marianeum We’re hiring citymapper.com/jobs