Historically in Android, if we wanted to share information like photos, music or other files, we needed to use Bluetooth or some kind of cloud or messaging service.

In 2014 Android Beam appeared as a solution to make it easier to share data with other devices, and it actually worked pretty good, but it didn’t reach as many users as I think Google anticipated. This technology relied on NFC to pair the two devices that were about to share something and then the data would be sent using the same technology, or Bluetooth if the file size was big enough. In 2019, Android Beam stopped being part of the operative system, so newer devices couldn’t use this technology, but that was because a new functionality was coming around.

In 2020 Google released Nearby Share, a new way to share information between devices that aimed to be the AirDrop of Android, compatible with Android 6.0 or later.

Nearby allows devices to connect following the subscription-publication protocol, achieving a high bandwidth connection, with low latency and encryption using simultaneously Bluetooth and Wi-Fi access points.

It’s also important to know that Nearby can be configured to work with different strategies, something that in Android Beam was restricted to just two devices. These are the three strategies that can be followed:

  1. Cluster: creates a network of connected devices following an M-N topology. Every device in the net can be a transmitter and a receptor.
  2. Star: in this case, the network is going to follow a 1-N topology. Every device can work as an access point and a higher bandwidth is allowed in comparison with the cluster configuration.
  3. Point to point: this configuration only allows 1–1 topology, so only two devices can be connected. In this case, we can use all the available bandwidth, so if you need the fastest transmission speeds, this is the strategy to use.

It’s also important to remember that Nearby is limited to devices in a 100 meter radius, regardless of the strategy that has been configured.

Let’s continue with some code

For this tutorial, I’m going to use as sample, a recipe app that I developed for myself, following the point to point strategy for Nearby.

First, we are going to add the next dependency to our Gradle module and sync the project:

implementation 'com.google.android.gms:play-services-nearby:18.0.0

After that, we need to implement how a device can work as publisher and how the other one is going to be the subscriber. To act as a publisher, we just require the following code, where the strategy to follow is specified.

fun startAdvertising() {
    val advertisingOptions =
        AdvertisingOptions.Builder().setStrategy(Strategy.P2P_POINT_TO_POINT).build()
    client.startAdvertising(
        android.os.Build.MODEL, SERVICE_ID, ConnectingProcessCallback(), advertisingOptions
    )
        .addOnSuccessListener {
            Log.d(GENERAL, "advertising...")
        }
        .addOnFailureListener {
            Log.d(GENERAL, "failure advertising...")
        }
}
Kotlin

After one of the devices runs the code above, another one is going to try to discover it, to subscribe to whatever the transmitter is going to send. For that, we require the next code.


fun startDiscovering() {
    val discoveryOptions =
        DiscoveryOptions.Builder().setStrategy(Strategy.P2P_POINT_TO_POINT).build()
    client
        .startDiscovery(SERVICE_ID, object : EndpointDiscoveryCallback() {
            override fun onEndpointFound(endPointId: String, info: DiscoveredEndpointInfo) {
                client.requestConnection(android.os.Build.MODEL, endPointId, ConnectingProcessCallback())
                    .addOnSuccessListener {

                    }
                    .addOnFailureListener {
                    }
            }

            override fun onEndpointLost(endPointId: String) {
                Log.d(GENERAL, "endpoint lost")
            }
        }, discoveryOptions)
        .addOnSuccessListener { Log.d(GENERAL, "discovering...") }
        .addOnFailureListener {
            Log.d(GENERAL, "failure discovering...")
        }
}
Kotlin

The last thing to do, is setting up the callbacks for when the information is fully transmitted in the phone that is acting as a receiver and for when we are pairing the devices. When we are pairing devices, we need to implement two functions, one to do something whenever a new device is discovered, and another to do something when the connection is successful. In the following example, I’m showing a dialogue to accept the connection in both devices, and after the pairing is complete, I’m sharing some information.


 private inner class ConnectingProcessCallback : ConnectionLifecycleCallback() {
    override fun onConnectionInitiated(endPointId: String, info: ConnectionInfo) {
        MaterialAlertDialogBuilder(context)
            .setTitle(Strings.get(R.string.accept_connection, info.endpointName))
            .setMessage(Strings.get(R.string.confirm_code, info.authenticationDigits))
            .setPositiveButton(Strings.get(R.string.accept)) { _: DialogInterface, _: Int ->
                Nearby.getConnectionsClient(context)
                    .acceptConnection(endPointId, DataReceivedCallback())
            }
            .setNegativeButton(Strings.get(R.string.cancel)) { _: DialogInterface, _: Int ->
                Nearby.getConnectionsClient(context).rejectConnection(endPointId)
            }
            .setIcon(R.drawable.outline_warning_24)
            .show()
    }

    override fun onConnectionResult(endPointId: String, result: ConnectionResolution) {
        when (result.status.statusCode) {
            ConnectionsStatusCodes.STATUS_OK -> {
                if (emitter) {
                    val gson = Gson()
                    val rec = viewModel.recipeWithIng.value
                    rec?.let {
                        // Changing the id so the receiver saves it as a new recipe and not an updated one
                        val jsonRecipe = gson.toJson(
                            RecipeWithIng(
                                Recipe(
                                    -1,
                                    rec.domainRecipe.name,
                                    rec.domainRecipe.timeToCook,
                                    rec.domainRecipe.dishType,
                                    rec.domainRecipe.dietType,
                                    rec.domainRecipe.instructions,
                                    rec.domainRecipe.image,
                                    rec.domainRecipe.description
                                ), rec.ings
                            )
                        )
                        sendText(jsonRecipe, endPointId)
                        viewModel.recipeWithIng.value?.domainRecipe?.image?.let { uri ->
                            Log.d(GENERAL, "enviando foto")
                            sendImage(uri, endPointId)
                        }
                    }
                }
            }
            ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED -> {
                Log.d(GENERAL, "conexion rechazada")
            }
            ConnectionsStatusCodes.STATUS_ERROR -> {
                Log.d(GENERAL, "DESCONEXION")
            }
        }
    }

    override fun onDisconnected(endPointId: String) {
        Log.d(GENERAL, "DESCONEXION OK")
    }

}
view rawpairing.kt hosted with ❤ by GitHub
Kotlin

In the receiver, we need to implement a callback so that the system knows what to do depending on the type of file received. Then, another callback that it’s going to give us different statuses during the process so that we can, for example, notify the user about how the transmission is going.


private inner class DataReceivedCallback : PayloadCallback() {
    private val incomingFilePayloads = SimpleArrayMap<Long, Payload>()
    private val completedFilePayloads = SimpleArrayMap<Long, Payload>()
    private var filePayloadFilename: String = ""

    override fun onPayloadReceived(endPointId: String, payload: Payload) {
        when (payload.type) {
            Payload.Type.BYTES -> {
                val gson = Gson()
                val recipe =
                    gson.fromJson(String(payload.asBytes()!!), RecipeWithIng::class.java)
                filePayloadFilename = recipe.domainRecipe.image
                viewModel.setRecipeReceivedWithNearby(recipe)
                Log.d(GENERAL, recipe.toString())
            }
            Payload.Type.FILE -> {
                incomingFilePayloads.put(payload.id, payload)
            }
        }
    }

    override fun onPayloadTransferUpdate(endPointId: String, update: PayloadTransferUpdate) {
        if (update.status == PayloadTransferUpdate.Status.SUCCESS) {
            val payload = incomingFilePayloads.remove(update.payloadId)
            completedFilePayloads.put(update.payloadId, payload)
            if (payload?.type == Payload.Type.FILE) {
                val filePayload = completedFilePayloads[update.payloadId]
                if (filePayload != null && filePayloadFilename != "") {
                    completedFilePayloads.remove(update.payloadId)
                    // Get the received file (which will be in the Downloads folder)
                    // Because of https://developer.android.com/preview/privacy/scoped-storage, we are not
                    // allowed to access filepaths from another process directly. Instead, we must open the
                    // uri using our ContentResolver.
                    val uri = filePayload.asFile()!!.asUri()
                    try {
                        // Copy the file to a new location.
                        val input: InputStream? = context.contentResolver.openInputStream(uri!!)
                        if (input != null) {
                            val storageDir: File? =
                                context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
                            val file = File(storageDir, filePayloadFilename.split("/").last())
                            copyStream(input, FileOutputStream(file))
                            viewModel.updateWithRecipeReceivedWithNearby()
                        }
                    } catch (e: IOException) {
                        Log.d(GENERAL, "error al guardar el fichero", e)
                    } finally {
                        // Delete the original file.
                        context.contentResolver.delete(uri!!, null, null)
                    }
                }
            }
        }
    }
}
view rawreceiver.kt hosted with ❤ by GitHub
Kotlin

That’s all!

Now you can use Nearby to share information between devices that your app may need. In my case, I used it to share recipes between users in the recipe app. You can check the full code in the GitHub repository that it’s linked below.

https://github.com/molidev8/Recipe-Vault

Featured image by Google


If you want to read more content like this and support me, don’t forget to check the rest of the bolg or subscribe here to get an email every time I publish new content.

Categorized in: