Transferring data between Android and Wear OS

One of the best things about Android development is how easy it is to ship apps for different experiences like Android TV, Android Auto and Wear OS(smartwatches). We all know that providing a seamless experience is crucial for retaining our users and on some occasions it's a great idea to take advantage of the integration between phone and watch.

In this tutorial, we'll walk through the process of synergizing your Android app with Wear OS creating a channel for transferring data.

1 - Install Smartwatch Wear OS by Google

The first step is to install this app(Smartwatch Wear OS by Google) in your emulator.
It is necessary to access some Wear OS exclusive libs and for pairing the Wear OS emulator.
Be aware that you'll need a Google account to access the Play Store.

2 - Pair the emulated devices

After creating both the phone and watch emulator head over to Device Manager in Android Studio make sure they're both running and select the Pair Wearable option in the More Actions(three vertical dots) dropdown.

3 - Create/Add Wear OS Application module in Android Studio

The steps:

  • File

  • New

  • New Project

  • Select the Wear OS template

  • Select Empty Wear App

  • Check Pair with Empty Phone app

  • Finish

4 - Create the view model on your wear module

Now create the view model that'll be responsible for receiving the picture taken by the app's phone module and loading it on our Wear OS device.

class MainDataViewModel(
    application: Application
) :
    AndroidViewModel(application),
    DataClient.OnDataChangedListener {

    var image by mutableStateOf<Bitmap?>(null)
        private set

    private var loadPhotoJob: Job = Job().apply { complete() }

    @SuppressLint("VisibleForTests")
    override fun onDataChanged(dataEvents: DataEventBuffer) {
        dataEvents.forEach { dataEvent ->
            when (dataEvent.type) {
                DataEvent.TYPE_CHANGED -> {
                    when (dataEvent.dataItem.uri.path) {
                        IMAGE_PATH -> {
                            loadPhotoJob.cancel()
                            loadPhotoJob = viewModelScope.launch {
                                image = loadBitmap(
                                    DataMapItem.fromDataItem(dataEvent.dataItem)
                                        .dataMap
                                        .getAsset(IMAGE_KEY)
                                )
                            }
                        }
                    }
                }
            }
        }
    }

    private suspend fun loadBitmap(asset: Asset?): Bitmap? {
        if (asset == null) return null
        val response =
            Wearable.getDataClient(getApplication<Application>()).getFdForAsset(asset).await()
        return response.inputStream.use { inputStream ->
            withContext(Dispatchers.IO) {
                BitmapFactory.decodeStream(inputStream)
            }
        }
    }

    companion object {
        const val IMAGE_PATH = "/image"
        const val IMAGE_KEY = "photo"
    }
}

We need to implement DataClient.OnDataChangedListener to keep an eye on any data received from the phone. After receiving the data we unload it as an image to further displaying it.

5 - Create the layout and set up the activity for displaying the picture in the Wear module

class MainActivity : ComponentActivity() {

    private val dataClient by lazy { Wearable.getDataClient(this) }

    private val mainDataViewModel by viewModels<MainDataViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainApp(image = mainDataViewModel.image)
        }
    }

    override fun onResume() {
        super.onResume()
        dataClient.addListener(mainDataViewModel)
    }

    override fun onPause() {
        super.onPause()
        dataClient.removeListener(mainDataViewModel)
    }
}
@Composable
fun MainApp(
    image: Bitmap?
) {
    val scalingLazyListState = rememberScalingLazyListState()

    Scaffold(
        vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
        positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyListState) },
        timeText = { TimeText() }
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(1f)
                .padding(32.dp)
        ) {
            if (image == null) {
                Image(
                    painterResource(id = R.drawable.photo_placeholder),
                    contentDescription = stringResource(
                        id = R.string.placeholder
                    ),
                    modifier = Modifier.fillMaxSize()
                )
            } else {
                Image(
                    image.asImageBitmap(),
                    contentDescription = stringResource(
                        id = R.string.received_from_phone
                    ),
                    modifier = Modifier.fillMaxSize()
                )
            }
        }

    }
}

6 - Create the view model in your phone's module

class MainViewModel :
    ViewModel() {

    var image by mutableStateOf<Bitmap?>(null)
        private set

    fun onPictureTaken(bitmap: Bitmap?) {
        image = bitmap ?: return
    }
}

7 - Create the activity in your phone's module

class MainActivity : ComponentActivity() {

    private val dataClient by lazy { Wearable.getDataClient(this) }

    private val isCameraSupported by lazy {
        packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
    }

    private val mainViewModel by viewModels<MainViewModel>()

    private val takePhotoLauncher = registerForActivityResult(
        ActivityResultContracts.TakePicturePreview()
    ) { bitmap ->
        mainViewModel.onPictureTaken(bitmap = bitmap)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                MainApp(
                    image = mainViewModel.image,
                    isCameraSupported = isCameraSupported,
                    onTakePhotoClick = ::takePhoto,
                    onSendPhotoClick = ::sendPhoto
                )
            }
        }
    }

    private fun takePhoto() {
        if (!isCameraSupported) return
        takePhotoLauncher.launch(null)
    }

    private fun sendPhoto() {
        lifecycleScope.launch {
            try {
                val image = mainViewModel.image ?: return@launch
                val imageAsset = image.toAsset()
                val request = PutDataMapRequest.create(IMG_PATH).apply {
                    dataMap.putAsset(IMAGE_KEY, imageAsset)
                }
                    .asPutDataRequest()
                    .setUrgent()
                val result = dataClient.putDataItem(request).await()
                Log.d(TAG, "PutDataItem result: $result")
            } catch (exception: Exception) {
                Log.d(TAG, "PutDataItem exception: $exception")
            }
        }
    }

    private suspend fun Bitmap.toAsset(): Asset =
        withContext(Dispatchers.Default) {
            ByteArrayOutputStream().use { byteStream ->
                compress(Bitmap.CompressFormat.PNG, 100, byteStream)
                Asset.createFromBytes(byteStream.toByteArray())
            }
        }

    companion object {
        private const val TAG = "MainActivity"

        private const val IMG_PATH = "/image"
        private const val IMAGE_KEY = "photo"
    }
}

In the sendPhoto() function we retrieve the captured picture stored in the view model and build the request with PutDataMapRequest.create passing the path and key to be retrieved in the Wear OS module.
The statement dataClient.putDataItem(request).await() is responsible for sending the image as a request.

8 - Run

Now run each module in its compatible emulated device, take a picture on your phone hit send and watch as the captured is displayed in the Wear OS emulator.
And congratulations you have fully created an in-app connection between Android and Wear OS.

0
Subscribe to my newsletter

Read articles from Eduardo Munstein directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Eduardo Munstein
Eduardo Munstein