original source : https://developer.android.com/training/wearables/wearable-sounds.html


Using Speakers on Wearables

Detect the Speaker

the app uses the getDevices() method in conjunction with the value of FEATURE_AUDIO_OUTPUT to confirm that the device is equipped with a speaker.

PackageManager packageManager = context.getPackageManager();
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

// Check whether the device has a speaker.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
   // Check FEATURE_AUDIO_OUTPUT to guard against false positives.
   if (!packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {
       return false;
   }

   AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
   for (AudioDeviceInfo device : devices) {
       if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
           return true;
       }
   }
}
return false;

Play Sounds

the process for playing sound on Android Wear is the same as for a handset or other device. For more information, see Media Playback.

If you also want to record audio from the microphone on the wearable, your app must also get permission to use the microphone. To learn more, seePermissions on Android Wear.

original source : https://developer.android.com/training/wearables/data-layer/messages.html

Sending and Receiving Messages

You send messages using the MessageApi and attach the following items to the message:

  • An arbitrary payload (optional)
  • A path that uniquely identifies the message’s action

syncing 은 양방향이라면 message는 일방이며 

remote procedure calls (RPC)에 유용하다. 예를 들어 sending a message to the wearable to start an activity.

Multiple wearable devices can be connected to a user’s handheld device. Each connected device in the network is considered a node. 그러므로 message를 보내는 경우 목적지를 명확하게 해야 한다. google play 7.3 이전에는 하나의 mobile에 하나의 wearable기기만 연결할수 있으나 그 이후 버전에는 여러개를 연결해서 사용가능하므로 새로운 기능에대한 업데이트를 잘못하면 제대로 전달되지 않는 문제가 발생할수 있다.

Send a Message

       

Advertise capabilities

To launch an activity on a handheld device from a wearable device, use the MessageApi class to send the request.

the wearable app needs to determine that a connected node is capable of launching the activity. wearable app에서 작업을 수행할수 있는지 확인 하는 과정에서 작업을 수행할수 있는 기기의 app에서는 wearable app에게 본기기가 작업수행이 가능하다는 것을 알려야 하는 데 그방법은 아래와 같다.

  1. Create an XML configuration file in the res/values/ directory of your project and name it wear.xml.
  2. Add a resource named android_wear_capabilities to wear.xml.
  3. Define capabilities that the device provides.

Note: Capabilities are custom strings that you define and must be unique within your app.

<resources>
   <string-array name="android_wear_capabilities">
       <item>voice_transcription</item>
   </string-array>
</resources>

        Retrieve the nodes with the required capabilities (어느 app이 작업을 처리가능한지 확인하는 과정)

CapabilityApi.getCapability() method 를 이용한다.

private static final String
       VOICE_TRANSCRIPTION_CAPABILITY_NAME = "voice_transcription";

private GoogleApiClient mGoogleApiClient;

...

private void setupVoiceTranscription() {
   CapabilityApi.GetCapabilityResult result =
           Wearable.CapabilityApi.getCapability(
                   mGoogleApiClient, VOICE_TRANSCRIPTION_CAPABILITY_NAME,
                   CapabilityApi.FILTER_REACHABLE).await();

   updateTranscriptionCapability(result.getCapability());
}

To detect capable nodes as they connect to the wearable device, register a CapabilityApi.CapabilityListener() instance to your GoogleApiClient.

private void setupVoiceTranscription() {
   ...

   CapabilityApi.CapabilityListener capabilityListener =
           new CapabilityApi.CapabilityListener() {
               @Override
               public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
                   updateTranscriptionCapability(capabilityInfo);
               }
           };

   Wearable.CapabilityApi.addCapabilityListener(
           mGoogleApiClient,
           capabilityListener,
           VOICE_TRANSCRIPTION_CAPABILITY_NAME);
}

Note: If you create a service that extends WearableListenerService to detect capability changes, you may want to override the onConnectedNodes()method to listen to finer-grained connectivity details, such as when a wearable device switches from Wi-Fi to a Bluetooth connection to the handset. For more information on how to listen for important events, see Listen for Data Layer Events.

After detecting the capable nodes, determine where to send the message. You should pick a node that is in close proximity to your wearable device to minimize message routing through multiple nodes. A nearby node is defined as one that is directly connected to the device. To determine if a node is nearby, call the Node.isNearby() method.

private String transcriptionNodeId = null;

private void updateTranscriptionCapability(CapabilityInfo capabilityInfo) {
   Set<Node> connectedNodes = capabilityInfo.getNodes();

   transcriptionNodeId = pickBestNodeId(connectedNodes);
}

private String pickBestNodeId(Set<Node> nodes) {
   String bestNodeId = null;
   // Find a nearby node or pick one arbitrarily
   for (Node node : nodes) {
       if (node.isNearby()) {
           return node.getId();
        }
        bestNodeId = node.getId();
   }
   return bestNodeId;
}

       Deliver the message

Once you’ve identified the best node to use, send the message using the MessageApi class.

Verify that the node is available before you attempt to send the message. This call is synchronous and blocks processing until the system queues the message for delivery.

Note: A successful result code does not guarantee delivery of the message. If your app requires data reliability, consider using DataItem objects or the ChannelApi class to send data between devices.

public static final String VOICE_TRANSCRIPTION_MESSAGE_PATH = "/voice_transcription";

private void requestTranscription(byte[] voiceData) {
   if (transcriptionNodeId != null) {
       Wearable.MessageApi.sendMessage(googleApiClient, transcriptionNodeId,
           VOICE_TRANSCRIPTION_MESSAGE_PATH, voiceData).setResultCallback(
                 new ResultCallback() {
                     @Override
                     public void onResult(SendMessageResult sendMessageResult) {
                         if (!sendMessageResult.getStatus().isSuccess()) {
                             // Failed to send message
                         }
                     }
                 }
           );
   } else {
       // Unable to retrieve node with transcription capability
   }
}

Note: To learn more about asynchronous and synchronous calls to Google Play services and when to use each, see Communicate with Google Play Services.

You can also broadcast messages to all connected nodes.

private Collection<String> getNodes() {
   HashSet <String>results = new HashSet<String>();
   NodeApi.GetConnectedNodesResult nodes =
           Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await();
   for (Node node : nodes.getNodes()) {
       results.add(node.getId());
   }
   return results;
}

Receive a Message

To be notified of received messages, implement the MessageListener interface to provide a listener for message events. Then, register the listener with the MessageApi.addListener() method.

@Override
public void onMessageReceived(MessageEvent messageEvent) {
   if (messageEvent.getPath().equals(VOICE_TRANSCRIPTION_MESSAGE_PATH)) {
       Intent startIntent = new Intent(this, MainActivity.class);
       startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
       startIntent.putExtra("VOICE_DATA", messageEvent.getData());
       startActivity(startIntent);
   }
}

original source : https://developer.android.com/training/wearables/data-layer/assets.html

Transferring Assets

image와 같이 크기가 큰 data를 전달하는 경우, you can attach an Asset to a data item and the put the data item into the replicated data store.

data item의 경우 최대 크기가 100kb 이나 assets의 경우에는 그 제한이 없다.

Transfer an Asset

Create the asset using one of the create...() methods in the Asset class.    

private static Asset createAssetFromBitmap(Bitmap bitmap) {

final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream);
    return Asset.createFromBytes(byteStream.toByteArray());
}

Using PutDataRequest

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
Asset asset = createAssetFromBitmap(bitmap);
PutDataRequest request = PutDataRequest.create("/image");
request.putAsset("profileImage", asset);
Wearable.DataApi.putDataItem(mGoogleApiClient, request);

Using PutDataMapRequest

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
Asset asset = createAssetFromBitmap(bitmap);
PutDataMapRequest dataMap = PutDataMapRequest.create("/image");
dataMap.getDataMap().putAsset("profileImage", asset)
PutDataRequest request = dataMap.asPutDataRequest();
PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi
       .putDataItem(mGoogleApiClient, request);



Receive assets

@Override
public void onDataChanged(DataEventBuffer dataEvents) {
 for (DataEvent event : dataEvents) {
   if (event.getType() == DataEvent.TYPE_CHANGED &&
       event.getDataItem().getUri().getPath().equals("/image")) {
     DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
     Asset profileAsset = dataMapItem.getDataMap().getAsset("profileImage");
     Bitmap bitmap = loadBitmapFromAsset(profileAsset);
     // Do something with the bitmap
   }
 }
}

public Bitmap loadBitmapFromAsset(Asset asset) {
   if (asset == null) {
       throw new IllegalArgumentException("Asset must be non-null");
   }
   ConnectionResult result =
          mGoogleApiClient.blockingConnect(TIMEOUT_MS, TimeUnit.MILLISECONDS);
   if (!result.isSuccess()) {
       return null;
   }
   // convert asset into a file descriptor and block until it's ready
   InputStream assetInputStream = Wearable.DataApi.getFdForAsset(
           mGoogleApiClient, asset).await().getInputStream();
           mGoogleApiClient.disconnect();

   if (assetInputStream == null) {
       Log.w(TAG, "Requested an unknown Asset.");
       return null;
   }
   // decode the stream into a bitmap
   return BitmapFactory.decodeStream(assetInputStream);
}

original source: https://developer.android.com/training/wearables/data-layer/data-items.html

Syncing Data Items

A DataItem defines the data interface that the system uses to synchronize data between handhelds and wearables.

Payload – A byte array, which you can set with whatever data you wish. 크기는  100kb

Path – A unique string that must start with a forward slash

normally don’t implement DataItem directly. Instead, you do the following:

  1. Create a PutDataRequest object, specifying a string path to uniquely identify the item.
  2. Call setData() to set the payload.
  3. If a delay in syncing would negatively impact user experience, call setUrgent().
  4. Call DataApi.putDataItem() to request the system to create the data item.

Sync Data with a Data Map

use the DataMap class. This approach lets you work with data items in the form of an Android Bundle, so the system does object serialization and deserialization for you, and you can manipulate data with key-value pairs.

  1. Create a PutDataMapRequest object, setting the path of the data item.
  2. Call PutDataMapRequest.getDataMap() to obtain a data map that you can set values on.
  3. Set any desired values for the data map using the put...() methods, such as putString().
  4. If a delay in syncing would negatively impact user experience, call setUrgent().
  5. Call PutDataMapRequest.asPutDataRequest() to obtain a PutDataRequest object.
  6. Call DataApi.putDataItem() to request the system to create the data item.

Note: If the handset and wearable devices are disconnected, the data is buffered and synced when the connection is re-established.

ex)

public class MainActivity extends Activity implements
       DataApi.DataListener,
       GoogleApiClient.ConnectionCallbacks,
       GoogleApiClient.OnConnectionFailedListener {

   private static final String COUNT_KEY = "com.example.key.count";

   private GoogleApiClient mGoogleApiClient;
   private int count = 0;

   ...

   // Create a data map and put data in it
   private void increaseCounter() {
       PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/count");
       putDataMapReq.getDataMap().putInt(COUNT_KEY, count++);
       PutDataRequest putDataReq = putDataMapReq.asPutDataRequest();
       PendingResult<DataApi.DataItemResult> pendingResult =
               Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq);
   }

   ...
}

Set DataItem priority

In Google Play services 8.3 and later, the DataApi interface allows urgent requests for syncing of DataItems.
setUrgent() 을 설정한다.

Listen for Data Item Events

public class MainActivity extends Activity implements
       DataApi.DataListener,
       GoogleApiClient.ConnectionCallbacks,
       GoogleApiClient.OnConnectionFailedListener {

   private static final String COUNT_KEY = "com.example.key.count";

   private GoogleApiClient mGoogleApiClient;
   private int count = 0;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       mGoogleApiClient = new GoogleApiClient.Builder(this)
               .addApi(Wearable.API)
               .addConnectionCallbacks(this)
               .addOnConnectionFailedListener(this)
               .build();
   }

   @Override
   protected void onResume() {
       super.onResume();
       mGoogleApiClient.connect();
   }

   @Override
   public void onConnected(Bundle bundle) {
       Wearable.DataApi.addListener(mGoogleApiClient, this);
   }

   @Override
   protected void onPause() {
       super.onPause();
       Wearable.DataApi.removeListener(mGoogleApiClient, this);
       mGoogleApiClient.disconnect();
   }

   @Override
   public void onDataChanged(DataEventBuffer dataEvents) {
       for (DataEvent event : dataEvents) {
           if (event.getType() == DataEvent.TYPE_CHANGED) {
               // DataItem changed
               DataItem item = event.getDataItem();
               if (item.getUri().getPath().compareTo("/count") == 0) {
                   DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
                   updateCount(dataMap.getInt(COUNT_KEY));
               }
           } else if (event.getType() == DataEvent.TYPE_DELETED) {
               // DataItem deleted
           }
       }
   }

   // Our method to update the count
   private void updateCount(int c) { ... }

   ...
}

original source: 

https://developer.android.com/training/wearables/data-layer/accessing.html

Accessing the Wearable Data Layer

To call the Data Layer API, create an instance of GoogleApiClient .

Note: A Wear app can communicate with a phone app using the Data Layer API, but connecting to a network using this API is discouraged.

Here is a minimal GoogleApiClient:

GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(this)
       .addConnectionCallbacks(new ConnectionCallbacks() {
               @Override
               public void onConnected(Bundle connectionHint) {
                   Log.d(TAG, "onConnected: " + connectionHint);
                   // Now you can use the Data Layer API
               }
               @Override
               public void onConnectionSuspended(int cause) {
                   Log.d(TAG, "onConnectionSuspended: " + cause);
               }
       })
       .addOnConnectionFailedListener(new OnConnectionFailedListener() {
               @Override
               public void onConnectionFailed(ConnectionResult result) {
                   Log.d(TAG, "onConnectionFailed: " + result);
               }
           })
       // Request access only to the Wearable API
       .addApi(Wearable.API)
       .build();

Important: If you are adding multiple APIs to a GoogleApiClient, you may run into client connection errors on devices that do not have the Android Wear app installed. To avoid connection errors, call the addApiIfAvailable() method and pass in the Wearable API to indicate that your client should gracefully handle the missing API. For more information, see Access the Wearable API.

Before you use the data layer API, start a connection on your client by calling the connect() method, as described in Start a Connection. When the system invokes the onConnected() callback for your client, you’re ready to use the Data Layer API.

original source : https://developer.android.com/training/wearables/data-layer/network-access.html

Network Access and Syncing

With Android Wear 2.0, a watch can communicate with a network directly.

This direct network access replaces the use (in Wear 1.x) of the Data Layer API for connecting to a network .

Network Access

watch’s network traffic generally is proxied through the phone. But when a phone is unavailable, Wi-Fi and cellular networks are used, depending on the hardware. The Wear platform handles transitions between networks.

You can use protocols such as HTTP, TCP, and UDP. However, the android.webkit APIs (including the CookieManager class) are not available. You can use cookies by reading and writing headers on requests and responses.

참고사항

  • The JobScheduler API for asynchronous jobs, including polling at regular intervals (described below)
  • Multi-networking APIs if you need to connect to specific network types; see Multiple Network Connections

High-bandwidth Network Access

The Android Wear platform manages network connectivity.

The platform chooses the default, active network by balancing two factors:

  • The need for long battery life
  • The need for network bandwidth

            Acquiring a High-Bandwidth Network

high-bandwidth network가 항상 사용가능한게 아니므로

high-bandwidth network 가 필요한 경우아래의 과정을 거쳐야 한다. 

  1. Check for an active network, and if there is one, check its bandwidth.
  2. If there isn’t an active network, or its bandwidth is insufficient, request access to an unmetered Wi-Fi or cellular network.

ConnectivityManager class 를 사용하여 네트워크가 있는지 밴드크기 어떤지 확인가능하다. 

int MIN_BANDWIDTH_KBPS = 320;
mConnectivityManager =
 (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
Network activeNetwork = mConnectivityManager.getActiveNetwork();

if (activeNetwork != null) {
 int bandwidth =
   mConnectivityManager.getNetworkCapabilities(activeNetwork).getLinkDownstreamBandwidthKbps();

 if (bandwidth < MIN_BANDWIDTH_KBPS) {
   // Request a high-bandwidth network
 }
} else {
 // You already are on a high-bandwidth network, so start your network request
}

You can request an unmetered, high-bandwidth network using the ConnectivityManager.

When the network is ready (e.g., the device’s Wi-Fi radio connects to a saved network), the onAvailable() method of your NetworkCallback instance is called. If a suitable network is not found, the onAvailable() method is not called. Therefore, you should time-out your request manually; see Waiting for Network Availability.

mNetworkCallback = new ConnectivityManager.NetworkCallback() {
 @Override
 public void onAvailable(Network network) {
   if (bindProcessToNetwork(network)) {
     // socket connections will now use this network
   } else {
     // app doesn't have android.permission.INTERNET permission
   }
 }
};

NetworkRequest request = new NetworkRequest.Builder()
 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
 .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
 .build();

mConnectivityManager.requestNetwork(request, mNetworkCallback);

       Releasing the Network

사용이 끝난 network는 release해야 한다. 

mConnectivityManager.bindProcessToNetwork(null);
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);

또 이작업은 

activity’s onStop() method 안에서도 실행되어야 한다.

       Waiting for Network Availability

if a watch cannot connect to a network, the onAvailable() method of your NetworkCallback instance is not called. Therefore, you should time-out the request after a predetermined length of time and release any associated resources.

int MESSAGE_CONNECTIVITY_TIMEOUT = 1;
long NETWORK_CONNECTIVITY_TIMEOUT_MS = 10000

mHandler = new Handler() {
 @Override
 public void handleMessage(Message msg) {
   switch (msg.what) {
     case MESSAGE_CONNECTIVITY_TIMEOUT:
       // unregister the network
       break;
   }
 }
};

mNetworkCallback = new ConnectivityManager.NetworkCallback() {
 @Override
 public void onAvailable(Network network) {
   mHandler.removeMessages(MESSAGE_CONNECTIVITY_TIMEOUT);
   ...
 }
};

mConnectivityManager.requestNetwork(request, mNetworkCallback);

mHandler.sendMessageDelayed(
 mHandler.obtainMessage(MESSAGE_CONNECTIVITY_TIMEOUT),
 NETWORK_CONNECTIVITY_TIMEOUT_MS);

       

Monitoring the Network State

mNetworkCallback = ConnectivityManager.NetworkCallback {
 @Override
 public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
   int bandwidth =
     mConnectivityManager.getNetworkCapabilities(network).getLinkDownstreamBandwidthKbps();

     if (bandwidth < MIN_BANDWIDTH.KBPS) {
       // handle insufficient network bandwidth
     }
 }

 @Override
 public void onLost(Network network) {
   // handle network loss
 }
}

       

Launching the Wi-Fi Settings Activity

사용자가 저장한 wi-fi가 없거나 접근 불가능한 경우 사용자를 wi-fi 설정 activity로 이동시킬수 있다.

context.startActivity(new Intent("com.google.android.clockwork.settings.connectivity.wifi.ADD_NETWORK_SETTINGS"));

Cloud Messaging

For sending notifications, apps can directly use Firebase Cloud Messaging (FCM)

By default, notifications are bridged (shared) from a phone app to a watch. If you have a standalone Wear app and a corresponding phone app, duplicate notifications can occur.

Using Background Services

You should schedule jobs with the JobScheduler API, which enables your app to register for Doze-safe code execution.

Jobs should use a JobInfo.Builder object to provide constraints and metadata.

  • To schedule a task that requires networking, use setRequiredNetworkType(int networkType), specifying NETWORK_TYPE_ANY or NETWORK_TYPE_UNMETERED; note that NETWORK_TYPE_UNMETERED is for large data transfers while NETWORK_TYPE_ANY is for small transfers
  • To schedule a task while charging, use setRequiresCharging(boolean requiresCharging)
  • To specify that a device is idle for a task, use setRequiresDeviceIdle(boolean requiresDeviceIdle); this method is useful for lower-priority background work or synchronization, especially when used with setRequiresCharging

Note that some low-bandwidth networks, such as Bluetooth LE, are considered meter

Scheduling with constraints

You can use the builder method setExtras to attach a bundle of app-specific metadata to the job request. When your job executes, this bundle is provided to your job service. Note the MY_JOB_ID value passed to the JobInfo.Builder constructor. This MY_JOB_ID value is an app-provided identifier. Subsequent calls to cancel, and subsequent jobs created with that same value, will update the existing job.

JobInfo jobInfo = new JobInfo.Builder(MY_JOB_ID,
       new ComponentName(this, MyJobService.class))
       .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
       .setRequiresCharging(true)
       .setExtras(extras)
       .build();
((JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE))
       .schedule(jobInfo);

아래는 위의 작업을 서비스하는 부분설명.

JobParameters object is passed into the onStartJobmethod. The JobParameters object enables you to get the job ID value along with any extras bundle provided when scheduling the job.

The onStartJobmethod is called on the main application thread, and therefore any expensive logic should be run from a separate thread .

When work is complete, you would call the jobFinished method to notify JobScheduler that the task is done.

public class MyJobService extends JobService {
   @Override public boolean onStartJob(JobParameters params) {
       new JobAsyncTask().execute(params);
       return true;
   }

   private class JobAsyncTask extends AsyncTask