The Data Transfer example application demonstrates how to use the peer-to-peer data connection feature of BlackBerry Spark Communications Services. The data connection API allows arbitrary data to be sent securely through a familiar stream interface. This example builds on the Quick Start example that demonstrates how you can authenticate with the SDK with user authentication disabled while using the BlackBerry Key Management Service.
The Data Transfer example allows the user to do the following:
- Start a data connection
- Send and receive files over the connection
This example requires the Spark Communications SDK, which you can find along with related resources at the locations below.
- Instructions to Download and Configure the SDK.
- Android Getting Started instructions in the Developer Guide.
- API Reference
Getting started video
By default, this example application is configured to work in a domain with user authentication disabled and the BlackBerry Key Management Service enabled. See the Download & Configure section of the Developer Guide to get started configuring a domain in the sandbox.
Once you have a domain in the sandbox, edit DataTransfer's app.properties
file
to configure the example with your domain ID.
# Your Spark Domain ID
user_domain="your_spark_domain"
When you run the Data Transfer application, it will prompt you for a user ID and a password. Since you've configured your domain to have user authentication disabled, you can enter any string you like for the user ID and an SDK identity will be created for it. Other applications that you run in the same domain will be able to find this identity by this user ID. The password is used to protected the keys stored in the BlackBerry Key Management Service.
Notes:
- To complete a release build you must create your own signing key. To create your own signing key, visit https://developer.android.com/studio/publish/app-signing.html.
- After creating your signing key set the key store password, key password, key alias and path to the keystore file in the 'app.properties' file.
- This application has been built using gradle 6.1.1 (newer versions have not been validated).
Follow this guide for a walkthrough explaining how the Spark SDK is used to share data over a secure peer-to-peer data connection.
You can start a new data connection using startDataConnection
. When starting a data connection you can include a metadata string which is passed to the participant. The meta data can be used to describe the intent or content of the connection.
/**
* Starts a data connection with the provided regid.
*/
public void startDataConnection(final long regId, final String metaData) {
//Ask the media service to start a connection with the specified regId and include an observer to be notified of the result
BBMEnterprise.getInstance().getMediaManager().startDataConnection(regId, metaData, new BBMEDataConnectionCreatedObserver() {
@Override
public void onConnectionCreationSuccess(int connectionId) {
mDataConnectionId = connectionId;
mConnectionMonitor.activate();
}
@Override
public void onConnectionCreationFailure(@NonNull BBMEMediaManager.Error error) {
//The connection wasn't created. Display an error
Toast.makeText(MainActivity.this, getString(R.string.error_creating_connection, error.name()), Toast.LENGTH_LONG).show();
}
});
}
MainActivity.java
The state
of the data connection can be observed by getting
the data connection from the media manager
. When the state is connected we show the send file button. When the state is disconnected we hide the button. You can also check the failure reason
when the connection is disconnected.
//Monitor to observe the data connection
ObservableMonitor mConnectionMonitor = new ObservableMonitor() {
@Override
protected void run() {
BBMEDataConnection connection = BBMEnterprise.getInstance().getMediaManager().getDataConnection(mDataConnectionId).get();
switch (connection.getState()) {
case CONNECTED:
mConnectionStatusTextView.setText(getString(R.string.connection_status, "Connected"));
//Show the send button when connected
sendFileFab.setVisibility(View.VISIBLE);
break;
case CONNECTING:
mConnectionStatusTextView.setText(getString(R.string.connection_status, "Connecting"));
//Clear the list when we are starting a new connection
mTransfers.clear();
mDataChannelsAdapter.notifyDataSetChanged();
break;
case OFFERING:
mConnectionStatusTextView.setText(getString(R.string.connection_status, "Offering"));
break;
case DISCONNECTED:
mConnectionStatusTextView.setText(getString(R.string.connection_status, "Disconnected"));
mConnectionStatusTextView.setBackgroundColor(getResources().getColor(R.color.disconnected_color));
mStartStopConnectionButton.setText(R.string.create_connection);
//Hide the send button when disconnected
sendFileFab.setVisibility(View.GONE);
connection.setDataChannelCreatedObserver(null);
//Display a connection error if one exists
if (connection.getFailureReason() != BBMEDataConnection.FailReason.NO_FAILURE) {
mConnectionErrorTextView.setVisibility(View.VISIBLE);
mConnectionErrorTextView.setText(getString(R.string.connection_error, connection.getFailureReason().toString()));
}
break;
}
if (connection.getState() != BBMEDataConnection.ConnectionState.DISCONNECTED) {
mConnectionErrorTextView.setVisibility(View.GONE);
mConnectionStatusTextView.setBackgroundColor(getResources().getColor(R.color.connected_color));
mStartStopConnectionButton.setText(R.string.disconnect);
connection.setDataChannelCreatedObserver(MainActivity.this);
}
}
};
MainActivity.java
To monitor for incoming data connections, you register a BBMEIncomingDataConnectionObserver
.
//Add incoming connection observer
mConnectionObserver = new IncomingConnectionObserver(DataTransferApplication.this);
BBMEnterprise.getInstance().getMediaManager().addIncomingDataConnectionObserver(mConnectionObserver);
DataTransferApplication.java
The observer is triggered every time a new connection request arrives. After accepting the connection we launch a simple activity to prompt the user to accept or decline the connection.
/**
* The IncomingConnectionObserver will be notified when a new call has arrived.
*/
public class IncomingConnectionObserver implements BBMEIncomingDataConnectionObserver {
private Context mContext;
public static final String INCOMING_CONNECTION_ID = "INCOMING_CONNECTION_ID";
public IncomingConnectionObserver(Context context) {
mContext = context;
}
@Override
public void onIncomingDataConnection(final int connectionId) {
//Accept the data connection
BBMEnterprise.getInstance().getMediaManager().acceptDataConnection(connectionId);
//Launch the incoming data connection activity
Intent incomingCallIntent = new Intent(mContext, IncomingDataConnectionActivity.class);
incomingCallIntent.putExtra(IncomingConnectionObserver.INCOMING_CONNECTION_ID, connectionId);
incomingCallIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(incomingCallIntent);
}
}
IncomingConnectionObserver.java
To complete the connection, you must call
openDataConnection
. The
connection should transition to
connected
and you can begin writing data into the connection.
//Open the data connection, this will transition the connection to "connected"
BBMEnterprise.getInstance().getMediaManager().openDataConnection(mDataConnectionId);
IncomingDataConnectionActivity.java
Sending and receiving data in a connection is done through a DataChannel
. A data channel is a stream with a specified name and type
. The type and name define the expected data for the receiver. File
and Data
types have a defined expected size. The Stream
type should be used when the size of the transfer is undefined.
Each time the user selects a file to send, we'll create a new data channel using the createDataChannel
method. We add a transfer item to the adapter to display the status of the file transfer to the user.
//Create a new data channel of type "File" specifying the name and size. The channel id we are providing is a random string.
final BBMEDataChannelSender sender = connection.createDataChannel(
UUID.randomUUID().toString(),
fileName,
fileSize,
BBMEDataConnection.ChannelType.FILE
);
//Add the channel to the list and notify the adapter
final DataChannelsAdapter.TransferItem transferItem = new DataChannelsAdapter.TransferItem(fileUri, sender);
mTransfers.add(transferItem);
mDataChannelsAdapter.notifyDataSetChanged();
MainActivity.java
To be notified when a new channel has been created in a connection use the BBMEDataChannelCreatedObserver
. When a new channel arrives a BBMEDataChannelReceiver
is provided.
connection.setDataChannelCreatedObserver(MainActivity.this);
@Override
public void onDataChannelCreated(String s, BBMEDataChannelReceiver receiver) {
//Add a new "transfer item" to the data channels adapter
DataChannelsAdapter.TransferItem transferItem = new DataChannelsAdapter.TransferItem(null, receiver);
mTransfers.add(transferItem);
mDataChannelsAdapter.notifyDataSetChanged();
writeFile(receiver, transferItem);
}
Data can be sent into the channel through an OutputStream
in the BBMEDataChannelSender
. In our example we copy a file into the channel output stream. Reading and writing from a data channel stream will block when required to send and receive from the underlying connection. Accessing data in the stream should always be performed on a background thread to avoid blocking the main activity.
//Start a background task to write the file to the data channel
AsyncTask.execute(new Runnable() {
@Override
public void run() {
OutputStream dataChannelOutputStream = sender.getOutputStream();
InputStream fileInputStream = null;
try {
fileInputStream = getContentResolver().openInputStream(fileUri);
//Copy the data from the file to the data channel output stream
IOUtils.copy(fileInputStream, dataChannelOutputStream);
} catch (IOException e) {
//If an error occurred then mark the transfer as failed and display an error to the user
Logger.e(e);
mMainHandler.post(new Runnable() {
@Override
public void run() {
transferItem.mError = true;
mDataChannelsAdapter.notifyDataSetChanged();
Toast.makeText(MainActivity.this, "IOException occurred while sending " + fileName, Toast.LENGTH_LONG).show();
}
});
} finally {
IOUtils.safeClose(fileInputStream);
IOUtils.safeClose(dataChannelOutputStream);
}
}
});
Data is read from a BBMEDataChannelReceiver
through an InputStream
. All incoming channels are written into the /data_transfer_example directory.
private void writeFile(final BBMEDataChannelReceiver receiver, final DataChannelsAdapter.TransferItem transferItem) {
//Create an asynctask to copy the data from the channel into a file
AsyncTask.execute(new Runnable() {
@Override
public void run() {
//Create a folder to write the incoming data to
String outputDirectoryPath = Environment.getExternalStorageDirectory() + "/data_transfer_example";
File outputDir = new File(outputDirectoryPath);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
//Create a file using the "name" provided in the data channel
final File outputFile = new File(outputDirectoryPath + "/" + receiver.getName());
OutputStream fileOutputStream = null;
//Get the input stream from the data channel
InputStream dataChannelInputStream = receiver.getInputStream();
try {
fileOutputStream = new FileOutputStream(outputFile);
//Read data from the input stream and copy it to the file
IOUtils.copy(dataChannelInputStream, fileOutputStream);
} catch (IOException e) {
//If an error occurred then mark the transfer as failed and display an error to the user
Logger.e(e);
mMainHandler.post(new Runnable() {
@Override
public void run() {
transferItem.mError = true;
mDataChannelsAdapter.notifyDataSetChanged();
Toast.makeText(MainActivity.this, "IOException when writing received file " + receiver.getName(), Toast.LENGTH_LONG).show();
}
});
} finally {
IOUtils.safeClose(dataChannelInputStream);
IOUtils.safeClose(fileOutputStream);
}
if (!transferItem.mError) {
//Set the file uri in the transfer item
transferItem.mFileUri = FileProvider.getUriForFile(
MainActivity.this,
getApplicationContext().getPackageName() + ".fileprovider",
outputFile
);
}
}
});
}
To display a progress bar, you ask the DataChannel
for the progress
. The progress value is a provided as an observable value integer between 0 and 100. The progress is only available for channel types where the expected size is known. We also display the number of bytes transferred using getBytesTransferred()
. If the bytes transferred or progress values change the ObservableMonitor
is triggered and the UI is updated.
public ObservableMonitor mProgressObserver = new ObservableMonitor() {
@Override
protected void run() {
mProgressBarView.setProgress(mItem.mDataChannel.getProgress().get());
long bytesTransferred = mItem.mDataChannel.getBytesTransferred().get() / 1024;
long expectedSize = mItem.mDataChannel.getExpectedSize() / 1024;
mSizeView.setText(mSizeView.getContext().getString(R.string.transfer_size, bytesTransferred, expectedSize));
}
};
DataChannelsAdapter.java
These examples are released as Open Source and licensed under the Apache 2.0 License.
The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
This page includes icons from: https://material.io/icons/ used under the Apache 2.0 License.
If you find an issue in one of the Samples or have a Feature Request, simply file an issue.