no.nordicsemi.android.nrftoolbox.dfu.DfuActivity.java Source code

Java tutorial

Introduction

Here is the source code for no.nordicsemi.android.nrftoolbox.dfu.DfuActivity.java

Source

/*******************************************************************************
 * Copyright (c) 2013 Nordic Semiconductor. All Rights Reserved.
 * 
 * The information contained herein is property of Nordic Semiconductor ASA. Terms and conditions of usage are described in detail in NORDIC SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT.
 * Licensees are granted free, non-transferable use of the information. NO WARRANTY of ANY KIND is provided. This heading must NOT be removed from the file.
 ******************************************************************************/

/*
 * NORDIC SEMICONDUTOR EXAMPLE CODE AND LICENSE AGREEMENT
 *
 * You are receiving this document because you have obtained example code ("Software") 
 * from Nordic Semiconductor ASA * ("Licensor"). The Software is protected by copyright 
 * laws and international treaties. All intellectual property rights related to the 
 * Software is the property of the Licensor. This document is a license agreement governing 
 * your rights and obligations regarding usage of the Software. Any variation to the terms 
 * of this Agreement shall only be valid if made in writing by the Licensor.
 * 
 * == Scope of license rights ==
 * 
 * You are hereby granted a limited, non-exclusive, perpetual right to use and modify the 
 * Software in order to create your own software. You are entitled to distribute the 
 * Software in original or modified form as part of your own software.
 *
 * If distributing your software in source code form, a copy of this license document shall 
 * follow with the distribution.
 *   
 * The Licensor can at any time terminate your rights under this license agreement.
 * 
 * == Restrictions on license rights ==
 * 
 * You are not allowed to distribute the Software on its own, without incorporating it into 
 * your own software.  
 * 
 * You are not allowed to remove, alter or destroy any proprietary, 
 * trademark or copyright markings or notices placed upon or contained with the Software.
 *     
 * You shall not use Licensor's name or trademarks without Licensor's prior consent.
 * 
 * == Disclaimer of warranties and limitation of liability ==
 * 
 * YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT USE OF THE SOFTWARE IS AT YOUR OWN RISK AND THAT THE 
 * SOFTWARE IS PROVIDED *AS IS" WITHOUT ANY WARRANTIES OR CONDITIONS WHATSOEVER. NORDIC SEMICONDUCTOR ASA 
 * DOES NOT WARRANT THAT THE FUNCTIONS OF THE SOFTWARE WILL MEET YOUR REQUIREMENTS OR THAT THE 
 * OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR FREE. YOU ASSUME RESPONSIBILITY FOR 
 * SELECTING THE SOFTWARE TO ACHIEVE YOUR INTENDED RESULTS, AND FOR THE *USE AND THE RESULTS 
 * OBTAINED FROM THE SOFTWARE.
 * 
 * NORDIC SEMICONDUCTOR ASA DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
 * TO WARRANTIES RELATED TO: NON-INFRINGEMENT, LACK OF VIRUSES, ACCURACY OR COMPLETENESS OF RESPONSES 
 * OR RESULTS, IMPLIED  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 * 
 * IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL OR 
 * CONSEQUENTIAL DAMAGES OR FOR ANY DAMAGES WHATSOEVER (INCLUDING BUT NOT LIMITED TO DAMAGES FOR 
 * LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, PERSONAL INJURY, 
 * LOSS OF PRIVACY OR OTHER PECUNIARY OR OTHER LOSS WHATSOEVER) ARISING OUT OF USE OR INABILITY TO 
 * USE THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 * 
 * REGARDLESS OF THE FORM OF ACTION, NORDIC SEMICONDUCTOR ASA AGGREGATE LIABILITY ARISING OUT OF 
 * OR RELATED TO THIS AGREEMENT SHALL NOT EXCEED THE TOTAL AMOUNT PAYABLE BY YOU UNDER THIS AGREEMENT. 
 * THE FOREGOING LIMITATIONS, EXCLUSIONS AND DISCLAIMERS SHALL APPLY TO THE MAXIMUM EXTENT ALLOWED BY 
 * APPLICABLE LAW.
 * 
 * == Dispute resolution and legal venue ==
 * 
 * Any and all disputes arising out of the rights and obligations in this license agreement shall be 
 * submitted to ordinary court proceedings. You accept the Oslo City Court as legal venue under this agreement.
 * 
 * This license agreement shall be governed by Norwegian law.
 * 
 * == Contact information ==
 * 
 * All requests regarding the Software or the API shall be directed to: 
 * Nordic Semiconductor ASA, P.O. Box 436, Skyen, 0213 Oslo, Norway.
 * 
 * http://www.nordicsemi.com/eng/About-us/Contact-us
 */
package no.nordicsemi.android.nrftoolbox.dfu;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.UUID;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import com.mbientlab.metawear.api.GATT;
import com.mbientlab.metawear.app.ModuleActivity;
import com.mbientlab.metawear.app.popup.FirmwareVersionSelector;
import com.mbientlab.metawear.app.R;

import no.nordicsemi.android.nrftoolbox.AppHelpFragment;
import no.nordicsemi.android.nrftoolbox.dfu.adapter.FileBrowserAppsAdapter;
import no.nordicsemi.android.nrftoolbox.dfu.fragment.UploadCancelFragment;
import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsActivity;
import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment;
import no.nordicsemi.android.nrftoolbox.utility.GattError;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.Loader;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.content.CursorLoader;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

/**
 * DfuActivity is the main DFU activity It implements DFUManagerCallbacks to receive callbacks from DFUManager class It implements 
 * DeviceScannerFragment.OnDeviceSelectedListener callback to receive
 * callback when device is selected from scanning dialog The activity supports portrait and landscape orientations
 */
public class DfuActivity extends FragmentActivity
        implements LoaderCallbacks<Cursor>, ScannerFragment.OnDeviceSelectedListener,
        UploadCancelFragment.CancelFragmetnListener, FirmwareVersionSelector.FirmwareConfiguration {
    private static final String TAG = "DfuActivity";

    private static final String PREFS_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_DEVICE_NAME";
    private static final String PREFS_FILE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_NAME";
    private static final String PREFS_FILE_SIZE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_SIZE";

    private static final String DATA_FILE_PATH = "file_path";
    private static final String DATA_FILE_STREAM = "file_stream";
    private static final String DATA_STATUS = "status";

    public static final String EXTRA_DEVICE_ADDRESS = "EXTRA_DEVICE_ADDRESS";
    public static final String EXTRA_DEVICE_NAME = "EXTRA_DEVICE_NAME";
    public static final String EXTRA_PROGRESS = "EXTRA_PROGRESS";
    public static final String EXTRA_LOG_URI = "EXTRA_LOG_URI";

    private static final String EXTRA_URI = "uri";

    private static final int SELECT_FILE_REQ = 1;
    static final int REQUEST_ENABLE_BT = 2;

    private TextView mDeviceNameView;
    private TextView mFileNameView;
    private TextView mFileSizeView;
    private TextView mFileStatusView;
    private TextView mTextPercentage;
    private TextView mTextUploading;
    private ProgressBar mProgressBar;

    private Button mUploadButton;
    private Button[] firmwareButtons;

    private BluetoothDevice mSelectedDevice;
    private String mFilePath;
    private Uri mFileStreamUri;
    private boolean mStatusOk;

    private final BroadcastReceiver mDfuUpdateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            // DFU is in progress or an error occurred 
            final String action = intent.getAction();

            if (DfuService.BROADCAST_PROGRESS.equals(action)) {
                final int progress = intent.getIntExtra(DfuService.EXTRA_DATA, 0);
                updateProgressBar(progress, false);
            } else if (DfuService.BROADCAST_ERROR.equals(action)) {
                final int error = intent.getIntExtra(DfuService.EXTRA_DATA, 0);
                updateProgressBar(error, true);

                // We have to wait a bit before canceling notification. This is called before DfuService creates the last notification.
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        // if this activity is still open and upload process was completed, cancel the notification
                        final NotificationManager manager = (NotificationManager) getSystemService(
                                Context.NOTIFICATION_SERVICE);
                        manager.cancel(DfuService.NOTIFICATION_ID);
                    }
                }, 200);
            }
        }
    };

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_feature_dfu);
        isBLESupported();
        setGUI();

        // restore saved state
        if (savedInstanceState != null) {
            mFilePath = savedInstanceState.getString(DATA_FILE_PATH);
            mFileStreamUri = savedInstanceState.getParcelable(DATA_FILE_STREAM);
            mStatusOk = savedInstanceState.getBoolean(DATA_STATUS);
            mUploadButton.setEnabled(mStatusOk);
        }
        mSelectedDevice = getIntent().getParcelableExtra(ModuleActivity.EXTRA_BLE_DEVICE);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(DATA_FILE_PATH, mFilePath);
        outState.putParcelable(DATA_FILE_STREAM, mFileStreamUri);
        outState.putBoolean(DATA_STATUS, mStatusOk);
    }

    private void setGUI() {
        final ActionBar actionBar = getActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);

        mDeviceNameView = (TextView) findViewById(R.id.device_name);
        mFileNameView = (TextView) findViewById(R.id.file_name);
        mFileSizeView = (TextView) findViewById(R.id.file_size);
        mFileStatusView = (TextView) findViewById(R.id.file_status);

        firmwareButtons = new Button[3];
        firmwareButtons[0] = (Button) findViewById(R.id.action_select_file);
        firmwareButtons[1] = (Button) findViewById(R.id.action_choose_version);
        firmwareButtons[2] = (Button) findViewById(R.id.action_update_latest);

        mUploadButton = (Button) findViewById(R.id.action_upload);
        mTextPercentage = (TextView) findViewById(R.id.textviewProgress);
        mTextUploading = (TextView) findViewById(R.id.textviewUploading);
        mProgressBar = (ProgressBar) findViewById(R.id.progressbar_file);

        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        final boolean uploadInProgress = preferences.getBoolean(DfuService.PREFS_DFU_IN_PROGRESS, false);
        if (uploadInProgress) {
            // Restore image file information
            mDeviceNameView.setText(preferences.getString(PREFS_DEVICE_NAME, ""));
            mFileNameView.setText(preferences.getString(PREFS_FILE_NAME, ""));
            mFileSizeView.setText(preferences.getString(PREFS_FILE_SIZE, ""));
            mFileStatusView.setText(R.string.dfu_file_status_ok);
            showProgressBar();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        // We are using LocalBroadcastReceiver instead of normal BroadcastReceiver for optimization purposes
        final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
        broadcastManager.registerReceiver(mDfuUpdateReceiver, makeDfuUpdateIntentFilter());
    }

    @Override
    protected void onPause() {
        super.onPause();

        final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
        broadcastManager.unregisterReceiver(mDfuUpdateReceiver);
    }

    private static IntentFilter makeDfuUpdateIntentFilter() {
        final IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(DfuService.BROADCAST_PROGRESS);
        intentFilter.addAction(DfuService.BROADCAST_ERROR);
        intentFilter.addAction(DfuService.BROADCAST_LOG);
        return intentFilter;
    }

    private void isBLESupported() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            showToast(R.string.no_ble);
            finish();
        }
    }

    private void showToast(final int messageResId) {
        Toast.makeText(this, messageResId, Toast.LENGTH_LONG).show();
    }

    private void showToast(final String message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }

    @Override
    public boolean onCreateOptionsMenu(final Menu menu) {
        getMenuInflater().inflate(R.menu.dfu_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_connect:
            final FragmentManager fm = getSupportFragmentManager();
            final ScannerFragment dialog = ScannerFragment.getInstance(this,
                    new UUID[] { GATT.GATTService.METAWEAR.uuid(), DfuService.DFU_SERVICE_UUID }, true);
            dialog.show(fm, "scan_fragment");
            break;
        case android.R.id.home:
            onBackPressed();
            break;
        case R.id.action_about:
            final AppHelpFragment fragment = AppHelpFragment.getInstance(R.string.dfu_about_text);
            fragment.show(getSupportFragmentManager(), "help_fragment");
            break;
        case R.id.action_settings:
            final Intent intent = new Intent(this, SettingsActivity.class);
            startActivity(intent);
            break;
        }
        return true;
    }

    @Override
    protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
        if (resultCode != RESULT_OK)
            return;

        switch (requestCode) {
        case SELECT_FILE_REQ:
            // clear previous data
            mFilePath = null;
            mFileStreamUri = null;

            // and read new one
            final Uri uri = data.getData();
            /*
             * The URI returned from application may be in 'file' or 'content' schema.
             * 'File' schema allows us to create a File object and read details from if directly.
             * 
             * Data from 'Content' schema must be read by Content Provider. To do that we are using a Loader.
             */
            if (uri.getScheme().equals("file")) {
                // the direct path to the file has been returned
                final String path = uri.getPath();
                final File file = new File(path);
                mFilePath = path;

                mFileNameView.setText(file.getName());
                mFileSizeView.setText(getString(R.string.dfu_file_size_text, file.length()));
                final boolean isHexFile = mStatusOk = MimeTypeMap.getFileExtensionFromUrl(path)
                        .equalsIgnoreCase("HEX");
                mFileStatusView.setText(isHexFile ? R.string.dfu_file_status_ok : R.string.dfu_file_status_invalid);
                mUploadButton.setEnabled(mSelectedDevice != null && isHexFile);
            } else if (uri.getScheme().equals("content")) {
                // an Uri has been returned
                mFileStreamUri = uri;
                // if application returned Uri for streaming, let's us it. Does it works?
                // FIXME both Uris works with Google Drive app. Why both? What's the difference? How about other apps like DropBox?
                final Bundle extras = data.getExtras();
                if (extras != null && extras.containsKey(Intent.EXTRA_STREAM))
                    mFileStreamUri = extras.getParcelable(Intent.EXTRA_STREAM);

                // file name and size must be obtained from Content Provider
                final Bundle bundle = new Bundle();
                bundle.putParcelable(EXTRA_URI, uri);
                getSupportLoaderManager().restartLoader(0, bundle, this);
            }
            break;
        default:
            break;
        }
    }

    @Override
    public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
        final Uri uri = args.getParcelable(EXTRA_URI);
        /*
         * Some apps, f.e. Google Drive allow to select file that is not on the device. There is no "_data" column handled by that provider. Let's try to obtain all columns and than check
         * which columns are present.
         */
        //final String[] projection = new String[] { MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.DATA };
        return new CursorLoader(this, uri, null /*all columns, instead of projection*/, null, null, null);
    }

    @Override
    public void onLoaderReset(final Loader<Cursor> loader) {
        mFileNameView.setText(null);
        mFileSizeView.setText(null);
        mFilePath = null;
        mFileStreamUri = null;
        mStatusOk = false;
    }

    @Override
    public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
        if (data.moveToNext()) {
            /*
             * Here we have to check the column indexes by name as we have requested for all. The order may be different.
             */
            final String fileName = data
                    .getString(data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)/* 0 DISPLAY_NAME */);
            final int fileSize = data.getInt(data.getColumnIndex(MediaStore.MediaColumns.SIZE) /* 1 SIZE */);
            String filePath = null;
            final int dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA);
            if (dataIndex != -1)
                filePath = data.getString(dataIndex /*2 DATA */);
            if (!TextUtils.isEmpty(filePath))
                mFilePath = filePath;

            mFileNameView.setText(fileName);
            mFileSizeView.setText(getString(R.string.dfu_file_size_text, fileSize));
            final boolean isHexFile = mStatusOk = MimeTypeMap.getFileExtensionFromUrl(fileName)
                    .equalsIgnoreCase("HEX");
            mFileStatusView.setText(isHexFile ? R.string.dfu_file_status_ok : R.string.dfu_file_status_invalid);
            mUploadButton.setEnabled(mSelectedDevice != null && isHexFile);
        }
    }

    /**
     * Called when the question mark was pressed
     * 
     * @param view
     *            a button that was pressed
     */
    public void onSelectFileHelpClicked(final View view) {
        new AlertDialog.Builder(this).setTitle(R.string.dfu_help_title).setMessage(R.string.dfu_help_message)
                .setPositiveButton(android.R.string.ok, null).show();
    }

    /**
     * Called when Select File was pressed
     * 
     * @param view
     *            a button that was pressed
     */
    public void onSelectFileClicked(final View view) {
        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("application/octet-stream");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        if (intent.resolveActivity(getPackageManager()) != null) {
            // file browser has been found on the device
            startActivityForResult(intent, SELECT_FILE_REQ);
        } else {
            // there is no any file browser app, let's try to download one
            final View customView = getLayoutInflater().inflate(R.layout.app_file_browser, null);
            final ListView appsList = (ListView) customView.findViewById(android.R.id.list);
            appsList.setAdapter(new FileBrowserAppsAdapter(this));
            appsList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            appsList.setItemChecked(0, true);
            new AlertDialog.Builder(this).setTitle(R.string.dfu_alert_no_filebrowser_title).setView(customView)
                    .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(final DialogInterface dialog, final int which) {
                            dialog.dismiss();
                        }
                    }).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(final DialogInterface dialog, final int which) {
                            final int pos = appsList.getCheckedItemPosition();
                            if (pos >= 0) {
                                final String query = getResources()
                                        .getStringArray(R.array.dfu_app_file_browser_action)[pos];
                                final Intent storeIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(query));
                                startActivity(storeIntent);
                            }
                        }
                    }).show();
        }
    }

    private class CheckFilesTask extends AsyncTask<Uri, Integer, Long> {
        private Uri uri;

        /* (non-Javadoc)
         * @see android.os.AsyncTask#doInBackground(java.lang.Object[])
         */
        @Override
        protected Long doInBackground(Uri... params) {
            long len;
            HttpResponse response = null;
            uri = params[0];
            try {
                HttpClient httpclient = new DefaultHttpClient();
                HttpGet httpget = new HttpGet(uri.toString());
                response = httpclient.execute(httpget);
                HttpEntity entity = response.getEntity();
                len = entity.getContentLength();
                mStatusOk = true;
            } catch (IOException e) {
                len = -1;
            }

            return len;
        }

        protected void onPostExecute(final Long result) {
            mFileNameView.setText(uri.toString());
            if (result != -1) {
                mFileSizeView.setText(getString(R.string.dfu_file_size_text, result));
                mFileStatusView.setText(R.string.dfu_file_status_ok);
                mUploadButton.setEnabled(mSelectedDevice != null);
            } else {
                mFileSizeView.setText("N/A");
                mFileStatusView.setText("File unavailable");
                mUploadButton.setEnabled(false);
            }
        }

    };

    public void onUpdateLatestClicked(final View view) {
        mFileStreamUri = Uri.parse("http://releases.mbientlab.com/metawear/vanilla/latest/firmware.hex");
        new CheckFilesTask().execute(mFileStreamUri);
    }

    private String artifactPattern;
    private String[] versions;

    private class GetVersionsTask extends AsyncTask<URL, Integer, String[]> {
        @Override
        protected String[] doInBackground(URL... params) {
            try {
                boolean inDesiredArtifact = false;
                String module = "metawear", build = "vanilla", artifact = "", ext = "hex", pattern = "";
                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
                factory.setNamespaceAware(true);
                XmlPullParser xpp = factory.newPullParser();
                xpp.setInput(params[0].openStream(), "UTF-8");

                ArrayList<String> versions = new ArrayList<>();

                int eventType = xpp.getEventType();
                while (eventType != XmlPullParser.END_DOCUMENT) {
                    if (eventType == XmlPullParser.START_TAG) {
                        switch (xpp.getName()) {
                        case "publications":
                            pattern = xpp.getAttributeValue(null, "artifactPattern");
                            break;
                        case "artifact":
                            inDesiredArtifact = xpp.getAttributeValue(null, "build").equals(build)
                                    && xpp.getAttributeValue(null, "ext").equals(ext);
                            if (inDesiredArtifact) {
                                artifact = xpp.getAttributeValue(null, "name");
                            }
                            break;
                        case "release":
                            if (inDesiredArtifact) {
                                versions.add(xpp.getAttributeValue(null, "version"));
                            }
                            break;

                        }
                    }
                    eventType = xpp.next();
                }
                artifactPattern = pattern.replace("[module]", module).replace("[build]", build)
                        .replace("[artifact]", artifact).replace("[ext]", ext);

                String[] versionArray = new String[versions.size()];
                versions.toArray(versionArray);
                return versionArray;
            } catch (XmlPullParserException | IOException e) {
                return null;
            }

        }

        protected void onPostExecute(final String[] result) {
            if (result != null) {
                versions = result;

                final FragmentManager fm = getSupportFragmentManager();
                final FirmwareVersionSelector dialog = new FirmwareVersionSelector();
                dialog.show(fm, "firmware_version_selector");
            }
        }
    }

    @Override
    public void versionSelected(int index) {
        mFileStreamUri = Uri.parse(String.format("http://releases.mbientlab.com/%s",
                artifactPattern.replace("[version]", versions[index])));
        new CheckFilesTask().execute(mFileStreamUri);
    }

    @Override
    public String[] availableVersions() {
        return versions;
    }

    public void onChooseVersionClicked(final View view) throws XmlPullParserException, IOException {
        URL modulesXml = new URL("http://releases.mbientlab.com/metawear/module.xml");
        new GetVersionsTask().execute(modulesXml);
    }

    /**
     * Callback of UPDATE/CANCEL button on DfuActivity
     */
    public void onUploadClicked(final View view) {
        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        final boolean dfuInProgress = preferences.getBoolean(DfuService.PREFS_DFU_IN_PROGRESS, false);
        if (dfuInProgress) {
            showUploadCancelDialog();
            return;
        }

        // check whether the selected file is a HEX file (we are just checking the extension)
        if (!mStatusOk) {
            Toast.makeText(this, R.string.dfu_file_status_invalid_message, Toast.LENGTH_LONG).show();
            return;
        }

        // Save current state in order to restore it if user quit the Activity
        final SharedPreferences.Editor editor = preferences.edit();
        editor.putString(PREFS_DEVICE_NAME, mSelectedDevice.getName());
        editor.putString(PREFS_FILE_NAME, mFileNameView.getText().toString());
        editor.putString(PREFS_FILE_SIZE, mFileSizeView.getText().toString());
        editor.commit();

        showProgressBar();

        final Intent service = new Intent(this, DfuService.class);
        service.putExtra(DfuService.EXTRA_DEVICE_ADDRESS, mSelectedDevice.getAddress());
        service.putExtra(DfuService.EXTRA_DEVICE_NAME, mSelectedDevice.getName());
        service.putExtra(DfuService.EXTRA_FILE_PATH, mFilePath);
        service.putExtra(DfuService.EXTRA_FILE_URI, mFileStreamUri);
        startService(service);
    }

    /**
      * Callback of CONNECT/DISCONNECT button on DfuActivity
      */
    public void onConnectClicked(final View view) {
        if (mSelectedDevice == null) {
            final FragmentManager fm = getSupportFragmentManager();
            final ScannerFragment dialog = ScannerFragment.getInstance(DfuActivity.this,
                    new UUID[] { GATT.GATTService.METAWEAR.uuid(), DfuService.DFU_SERVICE_UUID }, true);
            dialog.show(fm, "scan_fragment");
        }
    }

    private void showUploadCancelDialog() {
        final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
        final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION);
        pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_PAUSE);
        manager.sendBroadcast(pauseAction);

        UploadCancelFragment fragment = UploadCancelFragment.getInstance();
        fragment.show(getFragmentManager(), TAG);
    }

    private void showProgressBar() {
        mProgressBar.setVisibility(View.VISIBLE);
        mTextPercentage.setVisibility(View.VISIBLE);
        mTextUploading.setVisibility(View.VISIBLE);

        for (Button button : firmwareButtons) {
            button.setEnabled(false);
        }

        mUploadButton.setEnabled(true);
        mUploadButton.setText(R.string.dfu_action_upload_cancel);
    }

    private void updateProgressBar(final int progress, final boolean error) {
        switch (progress) {
        case DfuService.PROGRESS_CONNECTING:
            mProgressBar.setIndeterminate(true);
            mTextPercentage.setText(R.string.dfu_status_connecting);
            break;
        case DfuService.PROGRESS_STARTING:
            mProgressBar.setIndeterminate(true);
            mTextPercentage.setText(R.string.dfu_status_starting);
            break;
        case DfuService.PROGRESS_VALIDATING:
            mProgressBar.setIndeterminate(true);
            mTextPercentage.setText(R.string.dfu_status_validating);
            break;
        case DfuService.PROGRESS_DISCONNECTING:
            mProgressBar.setIndeterminate(true);
            mTextPercentage.setText(R.string.dfu_status_disconnecting);
            break;
        case DfuService.PROGRESS_COMPLETED:
            mTextPercentage.setText(R.string.dfu_status_completed);
            // let's wait a bit until we reconnect to the device again. Mainly because of the notification. When canceled immediately it will be recreated by service again.
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    showFileTransferSuccessMessage();

                    // if this activity is still open and upload process was completed, cancel the notification
                    final NotificationManager manager = (NotificationManager) getSystemService(
                            Context.NOTIFICATION_SERVICE);
                    manager.cancel(DfuService.NOTIFICATION_ID);
                }
            }, 200);
            break;
        default:
            mProgressBar.setIndeterminate(false);
            if (error) {
                showErrorMessage(progress);
            } else {
                mProgressBar.setProgress(progress);
                mTextPercentage.setText(getString(R.string.progress, progress));
            }
            break;
        }
    }

    private void showFileTransferSuccessMessage() {
        clearUI();
        showToast("Application has been transfered successfully.");
    }

    @Override
    public void onUploadCanceled() {
        clearUI();
        showToast("Uploading of the application has been canceled.");
    }

    private void showErrorMessage(final int code) {
        clearUI();
        showToast("Upload failed: " + GattError.parse(code) + " (" + code + ")");
    }

    private void clearUI() {
        mProgressBar.setVisibility(View.INVISIBLE);
        mTextPercentage.setVisibility(View.INVISIBLE);
        mTextUploading.setVisibility(View.INVISIBLE);

        for (Button button : firmwareButtons) {
            button.setEnabled(true);
        }

        mUploadButton.setEnabled(false);
        mDeviceNameView.setText(R.string.dfu_default_name);
        mUploadButton.setText(R.string.dfu_action_upload);
    }

    @Override
    public void onDeviceSelected(final BluetoothDevice device, final String name) {
        mSelectedDevice = device;
        mUploadButton.setEnabled(mStatusOk);
        mDeviceNameView.setText(name);
    }

    @Override
    public void onDialogCanceled() {
        // do nothing
    }

    @Override
    public void onBackPressed() {
        Intent result = new Intent();
        result.putExtra(ModuleActivity.EXTRA_BLE_DEVICE, mSelectedDevice);
        setResult(RESULT_OK, result);
        super.onBackPressed();
    }
}