Skip to content
This repository has been archived by the owner on Jan 11, 2024. It is now read-only.

Latest commit

 

History

History
377 lines (309 loc) · 20.7 KB

File metadata and controls

377 lines (309 loc) · 20.7 KB

BlackBerry Spark Communications Services

Data Transfer for Android

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.

Features

The Data Transfer example allows the user to do the following:

  • Start a data connection
  • Send and receive files over the connection

Getting Started

This example requires the Spark Communications SDK, which you can find along with related resources at the locations below.

YouTube Getting Started Video

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).

Walkthrough

Follow this guide for a walkthrough explaining how the Spark SDK is used to share data over a secure peer-to-peer data connection.

Creating a 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

Observing the connection

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

Receive a connection

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

Data channels

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

Observe a new incoming channel

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

Send and receive data

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

Reading data from a channel

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

Observing a channel

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

License

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.

Reporting Issues and Feature Requests

If you find an issue in one of the Samples or have a Feature Request, simply file an issue.