diff --git a/.idea/encodings.xml b/.idea/encodings.xml index e206d70d..f7589596 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,5 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index e2258041..c75a99ee 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -5,6 +5,7 @@ - - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 59436c98..fc2d64b2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,8 +3,31 @@ + + + + - - + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/app.iml b/app/app.iml index 87f544ec..ea4ba425 100644 --- a/app/app.iml +++ b/app/app.iml @@ -71,8 +71,9 @@ - - + + + @@ -88,15 +89,18 @@ - + - + - + + - - + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6c4ba7eb..462efc49 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,14 +1,14 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 22 - buildToolsVersion '22.0.1' + compileSdkVersion 23 + buildToolsVersion '23.0.0' defaultConfig { applicationId "no.nordicsemi.android.nrftoolbox" minSdkVersion 18 - targetSdkVersion 22 - versionCode 36 - versionName "1.14.3" + targetSdkVersion 23 + versionCode 37 + versionName "1.15.0" } buildTypes { release { @@ -20,8 +20,13 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:22.2.1' - compile 'com.android.support:design:22.2.1' + compile 'com.android.support:appcompat-v7:23.0.0' + compile 'com.android.support:design:23.0.0' + compile 'no.nordicsemi.android.support.v18:scanner:0.1.1' + compile('org.simpleframework:simple-xml:2.7.1') { + exclude group: 'stax', module: 'stax-api' + exclude group: 'xpp3', module: 'xpp3' + } compile project(':dfu') compile files('libs/achartengine-1.1.0.jar') compile files('libs/nrf-logger-v2.0.jar') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5805dcfb..9d4ca83d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,17 +22,12 @@ --> - - + android:installLocation="auto"> + diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/PermissionRationaleFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/PermissionRationaleFragment.java new file mode 100644 index 00000000..6b1c5c1a --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/PermissionRationaleFragment.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.nrftoolbox; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; + +public class PermissionRationaleFragment extends DialogFragment { + private static final String ARG_PERMISSION = "ARG_PERMISSION"; + private static final String ARG_TEXT = "ARG_TEXT"; + + private PermissionDialogListener mListener; + + public interface PermissionDialogListener { + public void onRequestPermission(final String permission); + } + + @Override + public void onAttach(final Context context) { + super.onAttach(context); + + if (context instanceof PermissionDialogListener) { + mListener = (PermissionDialogListener) context; + } else { + throw new IllegalArgumentException("The parent activity must impelemnt PermissionDialogListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + public static PermissionRationaleFragment getInstance(final int aboutResId, final String permission) { + final PermissionRationaleFragment fragment = new PermissionRationaleFragment(); + + final Bundle args = new Bundle(); + args.putInt(ARG_TEXT, aboutResId); + args.putString(ARG_PERMISSION, permission); + fragment.setArguments(args); + + return fragment; + } + + @Override + @NonNull + public Dialog onCreateDialog(final Bundle savedInstanceState) { + final Bundle args = getArguments(); + final StringBuilder text = new StringBuilder(getString(args.getInt(ARG_TEXT))); + return new AlertDialog.Builder(getActivity()).setTitle(R.string.permission_title).setMessage(text) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + mListener.onRequestPermission(args.getString(ARG_PERMISSION)); + } + }).create(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java index 58497033..89017f35 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java @@ -32,6 +32,7 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.NotificationCompat; import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; @@ -138,7 +139,7 @@ public class CSCService extends BleProfileService implements CSCManagerCallbacks return; if (mLastWheelRevolutions >= 0) { - float timeDifference = 0; + float timeDifference; if (lastWheelEventTime < mLastWheelEventTime) timeDifference = (65535 + lastWheelEventTime - mLastWheelEventTime) / 1024.0f; // [s] else @@ -167,7 +168,7 @@ public class CSCService extends BleProfileService implements CSCManagerCallbacks return; if (mLastCrankRevolutions >= 0) { - float timeDifference = 0; + float timeDifference; if (lastCrankEventTime < mLastCrankEventTime) timeDifference = (65535 + lastCrankEventTime - mLastCrankEventTime) / 1024.0f; // [s] else @@ -206,11 +207,12 @@ public class CSCService extends BleProfileService implements CSCManagerCallbacks // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); - final Notification.Builder builder = new Notification.Builder(this).setContentIntent(pendingIntent); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentIntent(pendingIntent); builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); builder.setSmallIcon(R.drawable.ic_stat_notify_csc); builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction)); final Notification notification = builder.build(); final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java index 04ac855e..41760162 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java @@ -21,6 +21,7 @@ */ package no.nordicsemi.android.nrftoolbox.dfu; +import android.Manifest; import android.app.ActivityManager; import android.app.ActivityManager.RunningServiceInfo; import android.app.LoaderManager.LoaderCallbacks; @@ -38,10 +39,11 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.preference.PreferenceManager; import android.provider.MediaStore; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.DialogFragment; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; @@ -58,15 +60,13 @@ import android.widget.TextView; import android.widget.Toast; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; import no.nordicsemi.android.dfu.DfuProgressListener; import no.nordicsemi.android.dfu.DfuProgressListenerAdapter; -import no.nordicsemi.android.dfu.DfuServiceListenerHelper; import no.nordicsemi.android.dfu.DfuServiceInitiator; +import no.nordicsemi.android.dfu.DfuServiceListenerHelper; import no.nordicsemi.android.nrftoolbox.AppHelpFragment; +import no.nordicsemi.android.nrftoolbox.PermissionRationaleFragment; import no.nordicsemi.android.nrftoolbox.R; import no.nordicsemi.android.nrftoolbox.dfu.adapter.FileBrowserAppsAdapter; import no.nordicsemi.android.nrftoolbox.dfu.fragment.UploadCancelFragment; @@ -74,7 +74,7 @@ import no.nordicsemi.android.nrftoolbox.dfu.fragment.ZipInfoFragment; import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsActivity; import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsFragment; import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; +import no.nordicsemi.android.nrftoolbox.utility.FileHelper; /** * DfuActivity is the main DFU activity It implements DFUManagerCallbacks to receive callbacks from DFUManager class It implements @@ -82,12 +82,9 @@ import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; * landscape orientations */ public class DfuActivity extends AppCompatActivity implements LoaderCallbacks, ScannerFragment.OnDeviceSelectedListener, - UploadCancelFragment.CancelFragmentListener { + UploadCancelFragment.CancelFragmentListener, PermissionRationaleFragment.PermissionDialogListener { private static final String TAG = "DfuActivity"; - private static final String PREFS_SAMPLES_VERSION = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_SAMPLES_VERSION"; - private static final int CURRENT_SAMPLES_VERSION = 4; - 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_TYPE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_TYPE"; @@ -104,6 +101,7 @@ public class DfuActivity extends AppCompatActivity implements LoaderCallbacks 1) mTextUploading.setText(getString(R.string.dfu_status_uploading_part, currentPart, partsTotal)); else @@ -228,7 +226,15 @@ public class DfuActivity extends AppCompatActivity implements LoaderCallbacks 0) - fos.write(buf, 0, read); - } finally { - is.close(); - fos.close(); - } - } catch (final IOException e) { - DebugLogger.e(TAG, "Error while copying HEX file " + e.toString()); - } - } - @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.settings_and_about, menu); @@ -841,7 +710,7 @@ public class DfuActivity extends AppCompatActivity implements LoaderCallbackstrue if devices must have one of those flags set in their advertisement packets - */ - protected boolean isDiscoverableRequired() { - return true; - } - /** * Shows the scanner fragment. * * @param filter * the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their * services - * @param discoverableRequired - * true if devices must have GENERAL_DISCOVERABLE or LIMITED_DISCOVERABLE flags set in their advertisement packet * @see #getFilterUUID() */ - private void showDeviceScanningDialog(final UUID filter, final boolean discoverableRequired) { + private void showDeviceScanningDialog(final UUID filter) { runOnUiThread(new Runnable() { @Override public void run() { - final ScannerFragment dialog = ScannerFragment.getInstance(BleProfileActivity.this, filter, discoverableRequired); + final ScannerFragment dialog = ScannerFragment.getInstance(filter); dialog.show(getSupportFragmentManager(), "scan_fragment"); } }); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java index edddbeb2..b543b9f9 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java @@ -185,7 +185,7 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct if (isBLEEnabled()) { if (!mDeviceConnected) { setDefaultUI(); - showDeviceScanningDialog(getFilterUUID(), isDiscoverableRequired()); + showDeviceScanningDialog(getFilterUUID()); } else { mBleManager.disconnect(); } @@ -222,8 +222,9 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct mLogSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name); } } + mDeviceName = name; mBleManager.setLogger(mLogSession); - mDeviceNameView.setText(mDeviceName = name); + mDeviceNameView.setText(name != null ? name : getString(R.string.not_available)); mConnectButton.setText(R.string.action_disconnect); mBleManager.connect(device); } @@ -396,28 +397,18 @@ public abstract class BleProfileExpandableListActivity extends ExpandableListAct */ protected abstract UUID getFilterUUID(); - /** - * Whether the scanner must search only for devices with GENERAL_DISCOVERABLE or LIMITER_DISCOVERABLE flag set. - * - * @return true if devices must have one of those flags set in their advertisement packets - */ - protected boolean isDiscoverableRequired() { - return true; - } - /** * Shows the scanner fragment. * * @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their * services - * @param discoverableRequired true if devices must have GENERAL_DISCOVERABLE or LIMITED_DISCOVERABLE flags set in their advertisement packet * @see #getFilterUUID() */ - private void showDeviceScanningDialog(final UUID filter, final boolean discoverableRequired) { + private void showDeviceScanningDialog(final UUID filter) { runOnUiThread(new Runnable() { @Override public void run() { - final ScannerFragment dialog = ScannerFragment.getInstance(BleProfileExpandableListActivity.this, filter, discoverableRequired); + final ScannerFragment dialog = ScannerFragment.getInstance(filter); dialog.show(getSupportFragmentManager(), "scan_fragment"); } }); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java index 3f0c1612..dd182783 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java @@ -218,7 +218,8 @@ public abstract class BleProfileServiceReadyActivityOtherwise it is null. */ - protected final void onViewCreated(final Bundle savedInstanceState) { + protected void onViewCreated(final Bundle savedInstanceState) { + // empty default implementation + } + + /** + * Called after the view and the toolbar has been created. + */ + protected final void setUpView() { // set GUI getSupportActionBar().setDisplayHomeAsUpEnabled(true); mConnectButton = (Button) findViewById(R.id.action_connect); @@ -395,7 +403,7 @@ public abstract class BleProfileServiceReadyActivitytrue if devices must have one of those flags set in their advertisement packets - */ - protected boolean isDiscoverableRequired() { - return true; - } - /** * Shows the scanner fragment. * * @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their * services - * @param discoverableRequired true if devices must have GENERAL_DISCOVERABLE or LIMITED_DISCOVERABLE flags set in their advertisement packet * @see #getFilterUUID() */ - private void showDeviceScanningDialog(final UUID filter, final boolean discoverableRequired) { - final ScannerFragment dialog = ScannerFragment.getInstance(BleProfileServiceReadyActivity.this, filter, discoverableRequired); + private void showDeviceScanningDialog(final UUID filter) { + final ScannerFragment dialog = ScannerFragment.getInstance(filter); dialog.show(getSupportFragmentManager(), "scan_fragment"); } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java index 84f53aba..a7a7e921 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java @@ -32,6 +32,7 @@ import android.media.AudioManager; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; +import android.support.v7.app.NotificationCompat; import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; @@ -227,13 +228,14 @@ public class ProximityService extends BleProfileService implements ProximityMana // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); - final Notification.Builder builder = new Notification.Builder(this).setContentIntent(pendingIntent); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentIntent(pendingIntent); builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); builder.setSmallIcon(R.drawable.ic_stat_notify_proximity); builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(R.drawable.ic_action_bluetooth, getString(R.string.proximity_notification_action_disconnect), disconnectAction); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.proximity_notification_action_disconnect), disconnectAction)); if (isConnected()) - builder.addAction(R.drawable.ic_stat_notify_proximity, getString(isImmediateAlertOn ? R.string.proximity_action_silentme : R.string.proximity_action_findme), secondAction); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity, getString(isImmediateAlertOn ? R.string.proximity_action_silentme : R.string.proximity_action_findme), secondAction)); final Notification notification = builder.build(); final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java index 47fc12ef..9e36a3ad 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.NotificationCompat; import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; @@ -186,11 +187,12 @@ public class RSCService extends BleProfileService implements RSCManagerCallbacks // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); - final Notification.Builder builder = new Notification.Builder(this).setContentIntent(pendingIntent); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentIntent(pendingIntent); builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); builder.setSmallIcon(R.drawable.ic_stat_notify_rsc); builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(R.drawable.ic_action_bluetooth, getString(R.string.rsc_notification_action_disconnect), disconnectAction); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.rsc_notification_action_disconnect), disconnectAction)); final Notification notification = builder.build(); final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java index c9079a34..a38289c4 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java @@ -21,6 +21,7 @@ */ package no.nordicsemi.android.nrftoolbox.scanner; +import android.bluetooth.BluetoothDevice; import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -30,8 +31,11 @@ import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; +import java.util.List; +import java.util.Set; import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.support.v18.scanner.ScanResult; /** * DeviceListAdapter class is list adapter for showing scanned Devices name, address and RSSI image based on RSSI values. @@ -44,56 +48,48 @@ public class DeviceListAdapter extends BaseAdapter { private final ArrayList mListBondedValues = new ArrayList<>(); private final ArrayList mListValues = new ArrayList<>(); private final Context mContext; - private final ExtendedBluetoothDevice.AddressComparator comparator = new ExtendedBluetoothDevice.AddressComparator(); public DeviceListAdapter(Context context) { mContext = context; } - public void addBondedDevice(ExtendedBluetoothDevice device) { - mListBondedValues.add(device); + /** + * Sets a list of bonded devices. + * @param devices list of bonded devices. + */ + public void addBondedDevices(final Set devices) { + final List bondedDevices = mListBondedValues; + for (BluetoothDevice device : devices) { + bondedDevices.add(new ExtendedBluetoothDevice(device)); + } notifyDataSetChanged(); } /** - * Looks for the device with the same address as given one in the list of bonded devices. If the device has been found it updates its RSSI value. - * - * @param address - * the device address - * @param rssi - * the RSSI of the scanned device + * Updates the list of not bonded devices. + * @param results list of results from the scanner */ - public void updateRssiOfBondedDevice(String address, int rssi) { - comparator.address = address; - final int indexInBonded = mListBondedValues.indexOf(comparator); - if (indexInBonded >= 0) { - ExtendedBluetoothDevice previousDevice = mListBondedValues.get(indexInBonded); - previousDevice.rssi = rssi; - notifyDataSetChanged(); + public void update(final List results) { + for (final ScanResult result : results) { + final ExtendedBluetoothDevice device = findDevice(result); + if (device == null) { + mListValues.add(new ExtendedBluetoothDevice(result)); + } else { + device.name = result.getScanRecord() != null ? result.getScanRecord().getDeviceName() : null; + device.rssi = result.getRssi(); + } } + notifyDataSetChanged(); } - /** - * If such device exists on the bonded device list, this method does nothing. If not then the device is updated (rssi value) or added. - * - * @param device - * the device to be added or updated - */ - public void addOrUpdateDevice(ExtendedBluetoothDevice device) { - final boolean indexInBonded = mListBondedValues.contains(device); - if (indexInBonded) { - return; - } - - final int indexInNotBonded = mListValues.indexOf(device); - if (indexInNotBonded >= 0) { - ExtendedBluetoothDevice previousDevice = mListValues.get(indexInNotBonded); - previousDevice.rssi = device.rssi; - notifyDataSetChanged(); - return; - } - mListValues.add(device); - notifyDataSetChanged(); + private ExtendedBluetoothDevice findDevice(final ScanResult result) { + for (final ExtendedBluetoothDevice device : mListBondedValues) + if (device.matches(result)) + return device; + for (final ExtendedBluetoothDevice device : mListValues) + if (device.matches(result)) + return device; + return null; } public void clearDevices() { @@ -115,7 +111,7 @@ public class DeviceListAdapter extends BaseAdapter { final int bondedCount = mListBondedValues.size() + 1; // 1 for the title if (mListBondedValues.isEmpty()) { if (position == 0) - return R.string.scanner_subtitle__not_bonded; + return R.string.scanner_subtitle_not_bonded; else return mListValues.get(position - 1); } else { @@ -124,7 +120,7 @@ public class DeviceListAdapter extends BaseAdapter { if (position < bondedCount) return mListBondedValues.get(position - 1); if (position == bondedCount) - return R.string.scanner_subtitle__not_bonded; + return R.string.scanner_subtitle_not_bonded; return mListValues.get(position - bondedCount - 1); } } @@ -197,7 +193,7 @@ public class DeviceListAdapter extends BaseAdapter { final String name = device.name; holder.name.setText(name != null ? name : mContext.getString(R.string.not_available)); holder.address.setText(device.device.getAddress()); - if (!device.isBonded || device.rssi != ScannerFragment.NO_RSSI) { + if (!device.isBonded || device.rssi != ExtendedBluetoothDevice.NO_RSSI) { final int rssiPercent = (int) (100.0f * (127.0f + device.rssi) / (127.0f + 20.0f)); holder.rssi.setImageLevel(rssiPercent); holder.rssi.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java index 35899ab4..7a248b58 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java @@ -23,43 +23,31 @@ package no.nordicsemi.android.nrftoolbox.scanner; import android.bluetooth.BluetoothDevice; +import no.nordicsemi.android.support.v18.scanner.ScanResult; + public class ExtendedBluetoothDevice { + /* package */ static final int NO_RSSI = -1000; public final BluetoothDevice device; /** The name is not parsed by some Android devices, f.e. Sony Xperia Z1 with Android 4.3 (C6903). It needs to be parsed manually. */ public String name; public int rssi; public boolean isBonded; - public ExtendedBluetoothDevice(BluetoothDevice device, String name, int rssi, boolean isBonded) { + public ExtendedBluetoothDevice(final ScanResult scanResult) { + this.device = scanResult.getDevice(); + this.name = scanResult.getScanRecord() != null ? scanResult.getScanRecord().getDeviceName() : null; + this.rssi = scanResult.getRssi(); + this.isBonded = false; + } + + public ExtendedBluetoothDevice(final BluetoothDevice device) { this.device = device; - this.name = name; - this.rssi = rssi; - this.isBonded = isBonded; + this.name = device.getName(); + this.rssi = NO_RSSI; + this.isBonded = true; } - @Override - public boolean equals(Object o) { - if (o instanceof ExtendedBluetoothDevice) { - final ExtendedBluetoothDevice that = (ExtendedBluetoothDevice) o; - return device.getAddress().equals(that.device.getAddress()); - } - return super.equals(o); - } - - /** - * Class used as a temporary comparator to find the device in the List of {@link ExtendedBluetoothDevice}s. This must be done this way, because List#indexOf and List#contains use the parameter's - * equals method, not the object's from list. See {@link DeviceListAdapter#updateRssiOfBondedDevice(String, int)} for example - */ - public static class AddressComparator { - public String address; - - @Override - public boolean equals(Object o) { - if (o instanceof ExtendedBluetoothDevice) { - final ExtendedBluetoothDevice that = (ExtendedBluetoothDevice) o; - return address.equals(that.device.getAddress()); - } - return super.equals(o); - } + public boolean matches(final ScanResult scanResult) { + return device.getAddress().equals(scanResult.getDevice().getAddress()); } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java index 2b758fab..c5e5366d 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java @@ -21,6 +21,7 @@ */ package no.nordicsemi.android.nrftoolbox.scanner; +import android.Manifest; import android.app.Activity; import android.app.Dialog; import android.bluetooth.BluetoothAdapter; @@ -28,61 +29,65 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.DialogInterface; +import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.ParcelUuid; import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.DialogFragment; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; import android.widget.AdapterView; import android.widget.Button; import android.widget.ListView; +import android.widget.Toast; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.UUID; import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; +import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat; +import no.nordicsemi.android.support.v18.scanner.ScanCallback; +import no.nordicsemi.android.support.v18.scanner.ScanFilter; +import no.nordicsemi.android.support.v18.scanner.ScanResult; +import no.nordicsemi.android.support.v18.scanner.ScanSettings; /** - * ScannerFragment class scan required BLE devices and shows them in a list. This class scans and filter devices with standard BLE Service UUID and devices with custom BLE Service UUID It contains a - * list and a button to scan/cancel. There is a interface {@link OnDeviceSelectedListener} which is implemented by activity in order to receive selected device. The scanning will continue for 5 - * seconds and then stop + * ScannerFragment class scan required BLE devices and shows them in a list. This class scans and filter devices with standard BLE Service UUID and devices with custom BLE Service UUID. It contains a + * list and a button to scan/cancel. There is a interface {@link OnDeviceSelectedListener} which is implemented by activity in order to receive selected device. The scanning will continue to scan + * for 5 seconds and then stop. */ public class ScannerFragment extends DialogFragment { private final static String TAG = "ScannerFragment"; private final static String PARAM_UUID = "param_uuid"; - private final static String DISCOVERABLE_REQUIRED = "discoverable_required"; private final static long SCAN_DURATION = 5000; + private final static int REQUEST_PERMISSION_REQ_CODE = 34; // any 8-bit number + private BluetoothAdapter mBluetoothAdapter; private OnDeviceSelectedListener mListener; private DeviceListAdapter mAdapter; private final Handler mHandler = new Handler(); private Button mScanButton; - private boolean mDiscoverableRequired; - private UUID mUuid; + private View mPermissionRationale; + + private ParcelUuid mUuid; private boolean mIsScanning = false; - private static final boolean DEVICE_IS_BONDED = true; - private static final boolean DEVICE_NOT_BONDED = false; - /* package */static final int NO_RSSI = -1000; - - /** - * Static implementation of fragment so that it keeps data when phone orientation is changed For standard BLE Service UUID, we can filter devices using normal android provided command - * startScanLe() with required BLE Service UUID For custom BLE Service UUID, we will use class ScannerServiceParser to filter out required device. - */ - public static ScannerFragment getInstance(final Context context, final UUID uuid, final boolean discoverableRequired) { + public static ScannerFragment getInstance(final UUID uuid) { final ScannerFragment fragment = new ScannerFragment(); final Bundle args = new Bundle(); - args.putParcelable(PARAM_UUID, new ParcelUuid(uuid)); - args.putBoolean(DISCOVERABLE_REQUIRED, discoverableRequired); + if (uuid != null) + args.putParcelable(PARAM_UUID, new ParcelUuid(uuid)); fragment.setArguments(args); return fragment; } @@ -127,10 +132,8 @@ public class ScannerFragment extends DialogFragment { final Bundle args = getArguments(); if (args.containsKey(PARAM_UUID)) { - final ParcelUuid pu = args.getParcelable(PARAM_UUID); - mUuid = pu.getUuid(); + mUuid = args.getParcelable(PARAM_UUID); } - mDiscoverableRequired = args.getBoolean(DISCOVERABLE_REQUIRED); final BluetoothManager manager = (BluetoothManager) getActivity().getSystemService(Context.BLUETOOTH_SERVICE); mBluetoothAdapter = manager.getAdapter(); @@ -142,9 +145,6 @@ public class ScannerFragment extends DialogFragment { super.onDestroyView(); } - /** - * When dialog is created then set AlertDialog with list and button views. - */ @NonNull @Override public Dialog onCreateDialog(final Bundle savedInstanceState) { @@ -167,6 +167,8 @@ public class ScannerFragment extends DialogFragment { } }); + mPermissionRationale = dialogView.findViewById(R.id.permission_rationale); // this is not null only on API23+ + mScanButton = (Button) dialogView.findViewById(R.id.action_cancel); mScanButton.setOnClickListener(new View.OnClickListener() { @Override @@ -194,16 +196,54 @@ public class ScannerFragment extends DialogFragment { mListener.onDialogCanceled(); } + @Override + public void onRequestPermissionsResult(final int requestCode, final @NonNull String[] permissions, final @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_PERMISSION_REQ_CODE: { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // We have been granted the Manifest.permission.ACCESS_COARSE_LOCATION permission. Now we may proceed with scanning. + startScan(); + } else { + mPermissionRationale.setVisibility(View.VISIBLE); + Toast.makeText(getActivity(), R.string.no_required_permission, Toast.LENGTH_SHORT).show(); + } + break; + } + } + } + /** * Scan for 5 seconds and then stop scanning when a BluetoothLE device is found then mLEScanCallback is activated This will perform regular scan for custom BLE Service UUID and then filter out. * using class ScannerServiceParser */ private void startScan() { + // Since Android 6.0 we need to obtain either Manifest.permission.ACCESS_COARSE_LOCATION or Manifest.permission.ACCESS_FINE_LOCATION to be able to scan for + // Bluetooth LE devices. This is related to beacons as proximity devices. + // On API older than Marshmallow the following code does nothing. + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // When user pressed Deny and still wants to use this functionality, show the rationale + if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) && mPermissionRationale.getVisibility() == View.GONE) { + mPermissionRationale.setVisibility(View.VISIBLE); + return; + } + + requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_PERMISSION_REQ_CODE); + return; + } + + // Hide the rationale message, we don't need it anymore. + if (mPermissionRationale != null) + mPermissionRationale.setVisibility(View.GONE); + mAdapter.clearDevices(); mScanButton.setText(R.string.scanner_action_cancel); - // Samsung Note II with Android 4.3 build JSS15J.N7100XXUEMK9 is not filtering by UUID at all. We must parse UUIDs manually - mBluetoothAdapter.startLeScan(mLEScanCallback); + final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); + final ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(1000).setUseHardwareBatchingIfSupported(false).build(); + final List filters = new ArrayList<>(); + filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build()); + scanner.startScan(filters, settings, scanCallback); mIsScanning = true; mHandler.postDelayed(new Runnable() { @@ -217,68 +257,38 @@ public class ScannerFragment extends DialogFragment { } /** - * Stop scan if user tap Cancel button. + * Stop scan if user tap Cancel button */ private void stopScan() { if (mIsScanning) { mScanButton.setText(R.string.scanner_action_scan); - mBluetoothAdapter.stopLeScan(mLEScanCallback); + + final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); + scanner.stopScan(scanCallback); + mIsScanning = false; } } - private void addBondedDevices() { - final Set devices = mBluetoothAdapter.getBondedDevices(); - for (BluetoothDevice device : devices) { - mAdapter.addBondedDevice(new ExtendedBluetoothDevice(device, device.getName(), NO_RSSI, DEVICE_IS_BONDED)); - } - } - - /** - * if scanned device already in the list then update it otherwise add as a new device - */ - private void addScannedDevice(final BluetoothDevice device, final String name, final int rssi, final boolean isBonded) { - if (getActivity() != null) - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - mAdapter.addOrUpdateDevice(new ExtendedBluetoothDevice(device, name, rssi, isBonded)); - } - }); - } - - /** - * if scanned device already in the list then update it otherwise add as a new device. - */ - private void updateScannedDevice(final BluetoothDevice device, final int rssi) { - if (getActivity() != null) - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - mAdapter.updateRssiOfBondedDevice(device.getAddress(), rssi); - } - }); - } - - /** - * Callback for scanned devices class {@link ScannerServiceParser} will be used to filter devices with custom BLE service UUID then the device will be added in a list. - */ - private final BluetoothAdapter.LeScanCallback mLEScanCallback = new BluetoothAdapter.LeScanCallback() { + private ScanCallback scanCallback = new ScanCallback() { @Override - public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) { - if (device != null) { - updateScannedDevice(device, rssi); - try { - if (ScannerServiceParser.decodeDeviceAdvData(scanRecord, mUuid, mDiscoverableRequired)) { - // On some devices device.getName() is always null. We have to parse the name manually :( - // This bug has been found on Sony Xperia Z1 (C6903) with Android 4.3. - // https://devzone.nordicsemi.com/index.php/cannot-see-device-name-in-sony-z1 - addScannedDevice(device, ScannerServiceParser.decodeDeviceName(scanRecord), rssi, DEVICE_NOT_BONDED); - } - } catch (Exception e) { - DebugLogger.e(TAG, "Invalid data in Advertisement packet " + e.toString()); - } - } + public void onScanResult(final int callbackType, final ScanResult result) { + // do nothing + } + + @Override + public void onBatchScanResults(final List results) { + mAdapter.update(results); + } + + @Override + public void onScanFailed(final int errorCode) { + // should never be called } }; + + private void addBondedDevices() { + final Set devices = mBluetoothAdapter.getBondedDevices(); + mAdapter.addBondedDevices(devices); + } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerServiceParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerServiceParser.java deleted file mode 100644 index 6d903f48..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerServiceParser.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2015, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package no.nordicsemi.android.nrftoolbox.scanner; - -import android.bluetooth.BluetoothDevice; -import android.util.Log; - -import java.io.UnsupportedEncodingException; -import java.util.UUID; - -/** - * ScannerServiceParser is responsible to parse scanning data and it check if scanned device has required service in it. - */ -public class ScannerServiceParser { - private static final String TAG = "ScannerServiceParser"; - - private static final int FLAGS_BIT = 0x01; - private static final int SERVICES_MORE_AVAILABLE_16_BIT = 0x02; - private static final int SERVICES_COMPLETE_LIST_16_BIT = 0x03; - private static final int SERVICES_MORE_AVAILABLE_32_BIT = 0x04; - private static final int SERVICES_COMPLETE_LIST_32_BIT = 0x05; - private static final int SERVICES_MORE_AVAILABLE_128_BIT = 0x06; - private static final int SERVICES_COMPLETE_LIST_128_BIT = 0x07; - private static final int SHORTENED_LOCAL_NAME = 0x08; - private static final int COMPLETE_LOCAL_NAME = 0x09; - - private static final byte LE_LIMITED_DISCOVERABLE_MODE = 0x01; - private static final byte LE_GENERAL_DISCOVERABLE_MODE = 0x02; - - /** - * Checks if device is connectible (as Android cannot get this information directly we just check if it has GENERAL DISCOVERABLE or LIMITED DISCOVERABLE flag set) and has required service UUID in - * the advertising packet. The service UUID may be null. - *

- * For further details on parsing BLE advertisement packet data see https://developer.bluetooth.org/Pages/default.aspx Bluetooth Core Specifications Volume 3, Part C, and Section 8 - *

- */ - public static boolean decodeDeviceAdvData(byte[] data, UUID requiredUUID, boolean discoverableRequired) { - final String uuid = requiredUUID != null ? requiredUUID.toString() : null; - if (data != null) { - boolean connectible = !discoverableRequired; - boolean valid = uuid == null; - if (connectible && valid) - return true; - int fieldLength, fieldName; - int packetLength = data.length; - for (int index = 0; index < packetLength; index++) { - fieldLength = data[index]; - if (fieldLength == 0) { - return connectible && valid; - } - fieldName = data[++index]; - - if (uuid != null) { - if (fieldName == SERVICES_MORE_AVAILABLE_16_BIT || fieldName == SERVICES_COMPLETE_LIST_16_BIT) { - for (int i = index + 1; i < index + fieldLength - 1; i += 2) - valid = valid || decodeService16BitUUID(uuid, data, i, 2); - } else if (fieldName == SERVICES_MORE_AVAILABLE_32_BIT || fieldName == SERVICES_COMPLETE_LIST_32_BIT) { - for (int i = index + 1; i < index + fieldLength - 1; i += 4) - valid = valid || decodeService32BitUUID(uuid, data, i, 4); - } else if (fieldName == SERVICES_MORE_AVAILABLE_128_BIT || fieldName == SERVICES_COMPLETE_LIST_128_BIT) { - for (int i = index + 1; i < index + fieldLength - 1; i += 16) - valid = valid || decodeService128BitUUID(uuid, data, i, 16); - } - } - if (!connectible && fieldName == FLAGS_BIT) { - int flags = data[index + 1]; - connectible = (flags & (LE_GENERAL_DISCOVERABLE_MODE | LE_LIMITED_DISCOVERABLE_MODE)) > 0; - } - index += fieldLength - 1; - } - return connectible && valid; - } - return false; - } - - /** - * Decodes the device name from Complete Local Name or Shortened Local Name field in Advertisement packet. Usually if should be done by {@link BluetoothDevice#getName()} method but some phones - * skips that, f.e. Sony Xperia Z1 (C6903) with Android 4.3 where getName() always returns null. In order to show the device name correctly we have to parse it manually :( - */ - public static String decodeDeviceName(byte[] data) { - String name = null; - int fieldLength, fieldName; - int packetLength = data.length; - for (int index = 0; index < packetLength; index++) { - fieldLength = data[index]; - if (fieldLength == 0) - break; - fieldName = data[++index]; - - if (fieldName == COMPLETE_LOCAL_NAME || fieldName == SHORTENED_LOCAL_NAME) { - name = decodeLocalName(data, index + 1, fieldLength - 1); - break; - } - index += fieldLength - 1; - } - return name; - } - - /** - * Decodes the local name - */ - public static String decodeLocalName(final byte[] data, final int start, final int length) { - try { - return new String(data, start, length, "UTF-8"); - } catch (final UnsupportedEncodingException e) { - Log.e(TAG, "Unable to convert the complete local name to UTF-8", e); - return null; - } catch (final IndexOutOfBoundsException e) { - Log.e(TAG, "Error when reading complete local name", e); - return null; - } - } - - /** - * check for required Service UUID inside device - */ - private static boolean decodeService16BitUUID(String uuid, byte[] data, int startPosition, int serviceDataLength) { - String serviceUUID = Integer.toHexString(decodeUuid16(data, startPosition)); - String requiredUUID = uuid.substring(4, 8); - - return serviceUUID.equals(requiredUUID); - } - - /** - * check for required Service UUID inside device - */ - private static boolean decodeService32BitUUID(String uuid, byte[] data, int startPosition, int serviceDataLength) { - String serviceUUID = Integer.toHexString(decodeUuid16(data, startPosition + serviceDataLength - 4)); - String requiredUUID = uuid.substring(4, 8); - - return serviceUUID.equals(requiredUUID); - } - - /** - * check for required Service UUID inside device - */ - private static boolean decodeService128BitUUID(String uuid, byte[] data, int startPosition, int serviceDataLength) { - String serviceUUID = Integer.toHexString(decodeUuid16(data, startPosition + serviceDataLength - 4)); - String requiredUUID = uuid.substring(4, 8); - - return serviceUUID.equals(requiredUUID); - } - - private static int decodeUuid16(final byte[] data, final int start) { - final int b1 = data[start] & 0xff; - final int b2 = data[start + 1] & 0xff; - - return (b2 << 8 | b1); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java index 6b0ba5d4..8150586b 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java @@ -30,6 +30,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.NotificationCompat; import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.FeaturesActivity; @@ -141,11 +142,12 @@ public class TemplateService extends BleProfileService implements TemplateManage // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); - final Notification.Builder builder = new Notification.Builder(this).setContentIntent(pendingIntent); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentIntent(pendingIntent); builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); builder.setSmallIcon(R.drawable.ic_stat_notify_template); builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(R.drawable.ic_action_bluetooth, getString(R.string.template_notification_action_disconnect), disconnectAction); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.template_notification_action_disconnect), disconnectAction)); final Notification notification = builder.build(); final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java index 800b909b..db731f4e 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java @@ -22,38 +22,131 @@ package no.nordicsemi.android.nrftoolbox.uart; +import android.Manifest; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.TransitionDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.DialogFragment; import android.support.v4.widget.SlidingPaneLayout; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.NotificationCompat; +import android.util.Log; +import android.view.Menu; import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.Toast; +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.core.Persister; +import org.simpleframework.xml.strategy.Strategy; +import org.simpleframework.xml.strategy.Type; +import org.simpleframework.xml.strategy.Visitor; +import org.simpleframework.xml.strategy.VisitorStrategy; +import org.simpleframework.xml.stream.Format; +import org.simpleframework.xml.stream.HyphenStyle; +import org.simpleframework.xml.stream.InputNode; +import org.simpleframework.xml.stream.NodeMap; +import org.simpleframework.xml.stream.OutputNode; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.StringWriter; import java.util.UUID; import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.dfu.adapter.FileBrowserAppsAdapter; import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; +import no.nordicsemi.android.nrftoolbox.uart.database.DatabaseHelper; +import no.nordicsemi.android.nrftoolbox.uart.domain.Command; +import no.nordicsemi.android.nrftoolbox.uart.domain.UartConfiguration; +import no.nordicsemi.android.nrftoolbox.utility.FileHelper; +import no.nordicsemi.android.nrftoolbox.widget.ClosableSpinner; -public class UARTActivity extends BleProfileServiceReadyActivity implements UARTControlFragment.ControlFragmentListener, UARTInterface { +public class UARTActivity extends BleProfileServiceReadyActivity implements UARTInterface, + UARTNewConfigurationDialogFragment.NewConfigurationDialogListener, UARTConfigurationsAdapter.ActionListener, AdapterView.OnItemSelectedListener { + private final static String TAG = "UARTActivity"; + + private final static String PREFS_BUTTON_ENABLED = "prefs_uart_enabled_"; + private final static String PREFS_BUTTON_COMMAND = "prefs_uart_command_"; + private final static String PREFS_BUTTON_ICON = "prefs_uart_icon_"; + private final static String PREFS_CONFIGURATION = "configuration_id"; private final static String SIS_EDIT_MODE = "sis_edit_mode"; + private final static int SELECT_FILE_REQ = 2678; // random + private final static int PERMISSION_REQ = 24; // random, 8-bit + + /** The current configuration. */ + private UartConfiguration mConfiguration; + private DatabaseHelper mDatabaseHelper; + private SharedPreferences mPreferences; + private UARTConfigurationsAdapter mConfigurationsAdapter; + private ClosableSpinner mConfigurationSpinner; private SlidingPaneLayout mSlider; private UARTService.UARTBinder mServiceBinder; + private ConfigurationListener mConfigurationListener; private boolean mEditMode; + public interface ConfigurationListener { + public void onConfigurationModified(); + public void onConfigurationChanged(final UartConfiguration configuration); + public void setEditMode(final boolean editMode); + } + + public void setConfigurationListener(final ConfigurationListener listener) { + mConfigurationListener = listener; + } + @Override protected Class getServiceClass() { return UARTService.class; } + @Override + protected int getLoggerProfileTitle() { + return R.string.uart_feature_title; + } + + @Override + protected Uri getLocalAuthorityLogger() { + return UARTLocalLogContentProvider.AUTHORITY_URI; + } + + @Override + protected void setDefaultUI() { + // empty + } + @Override protected void onServiceBinded(final UARTService.UARTBinder binder) { mServiceBinder = binder; @@ -64,6 +157,14 @@ public class UARTActivity extends BleProfileServiceReadyActivity 1); + return super.onCreateOptionsMenu(menu); + } + + @Override + protected boolean onOptionsItemSelected(int itemId) { + final String name = mConfiguration.getName(); + switch (itemId) { + case R.id.action_configure: + setEditMode(!mEditMode); + return true; + case R.id.action_show_log: + mSlider.openPane(); + return true; + case R.id.action_share: { + final String xml = mDatabaseHelper.getConfiguration(mConfigurationSpinner.getSelectedItemId()); + + final Intent intent = new Intent(Intent.ACTION_SEND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setType("text/xml"); + intent.putExtra(Intent.EXTRA_TEXT, xml); + intent.putExtra(Intent.EXTRA_SUBJECT, mConfiguration.getName()); + try { + startActivity(intent); + } catch (final ActivityNotFoundException e) { + Toast.makeText(this, R.string.no_uri_application, Toast.LENGTH_SHORT).show(); + } + return true; + } + case R.id.action_export: { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + exportConfiguration(); + } else { + ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, PERMISSION_REQ); + } + return true; + } + case R.id.action_rename: { + final DialogFragment fragment = UARTNewConfigurationDialogFragment.getInstance(name, false); + fragment.show(getSupportFragmentManager(), null); + // onNewConfiguration(name, false) will be called when user press OK + return true; + } + case R.id.action_duplicate: { + final DialogFragment fragment = UARTNewConfigurationDialogFragment.getInstance(name, true); + fragment.show(getSupportFragmentManager(), null); + // onNewConfiguration(name, true) will be called when user press OK + return true; + } + case R.id.action_remove: { + mDatabaseHelper.removeDeletedServerConfigurations(); // just to be sure nothing has left + mDatabaseHelper.deleteConfiguration(name); + refreshConfigurations(); + + final Snackbar snackbar = Snackbar.make(mSlider, R.string.uart_configuration_deleted, Snackbar.LENGTH_INDEFINITE).setAction(R.string.uart_action_undo, new View.OnClickListener() { + @Override + public void onClick(final View v) { + mDatabaseHelper.restoreDeletedServerConfigurations(); + refreshConfigurations(); + } + }); + snackbar.setDuration(5000); // This is not an error + snackbar.show(); + return true; + } + } + return false; + } + + @Override + public void onRequestPermissionsResult(final int requestCode, final @NonNull String[] permissions, final @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case PERMISSION_REQ: { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // We have been granted the Manifest.permission.WRITE_EXTERNAL_STORAGE permission. Now we may proceed with exporting. + exportConfiguration(); + } else { + Toast.makeText(this, R.string.no_required_permission, Toast.LENGTH_SHORT).show(); + } + break; + } + } + } + + @Override + public void onItemSelected(final AdapterView parent, final View view, final int position, final long id) { + if (position > 0) { // FIXME this is called twice after rotation. + try { + final String xml = mDatabaseHelper.getConfiguration(id); + final Format format = new Format(new HyphenStyle()); + final Serializer serializer = new Persister(format); + mConfiguration = serializer.read(UartConfiguration.class, xml); + mConfigurationListener.onConfigurationChanged(mConfiguration); + } catch (final Exception e) { + Log.e(TAG, "Selecting configuration failed", e); + + String message; + if (e.getLocalizedMessage() != null) + message = e.getLocalizedMessage(); + else if (e.getCause() != null && e.getCause().getLocalizedMessage() != null) + message = e.getCause().getLocalizedMessage(); + else + message = "Unknown error"; + final String msg = message; + Snackbar.make(mSlider, R.string.uart_configuration_loading_failed, Snackbar.LENGTH_INDEFINITE).setAction(R.string.uart_action_details, new View.OnClickListener() { + @Override + public void onClick(final View v) { + new AlertDialog.Builder(UARTActivity.this).setMessage(msg).setTitle(R.string.uart_action_details).setPositiveButton(R.string.ok, null).show(); + } + }).show(); + return; + } + + mPreferences.edit().putLong(PREFS_CONFIGURATION, id).apply(); + } + } + + @Override + public void onNothingSelected(final AdapterView parent) { + // do nothing + } + + @Override + public void onNewConfigurationClick() { + // No item has been selected. We must close the spinner manually. + mConfigurationSpinner.close(); + + // Open the dialog + final DialogFragment fragment = UARTNewConfigurationDialogFragment.getInstance(null, false); + fragment.show(getSupportFragmentManager(), null); + + // onNewConfiguration(null, false) will be called when user press OK + } + + @Override + public void onImportClick() { + // No item has been selected. We must close the spinner manually. + mConfigurationSpinner.close(); + + final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("text/xml"); + 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(R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + dialog.dismiss(); + } + }).setPositiveButton(R.string.yes, 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(); + } + } + + @Override + protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode == Activity.RESULT_CANCELED) + return; + + switch (requestCode) { + case SELECT_FILE_REQ: { + // clear previous data + 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 with use of a 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(); + try { + final FileInputStream fis = new FileInputStream(path); + loadConfiguration(fis); + } catch (final FileNotFoundException e) { + Toast.makeText(this, R.string.uart_configuration_load_error, Toast.LENGTH_LONG).show(); + } + } else if (uri.getScheme().equals("content")) { + // An Uri has been returned + Uri u = uri; + + // If application returned Uri for streaming, let's us it. Does it works? + final Bundle extras = data.getExtras(); + if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) + u = extras.getParcelable(Intent.EXTRA_STREAM); + + try { + final InputStream is = getContentResolver().openInputStream(u); + loadConfiguration(is); + } catch (final FileNotFoundException e) { + Toast.makeText(this, R.string.uart_configuration_load_error, Toast.LENGTH_LONG).show(); + } + } + break; + } + } + } + + public void onCommandChanged(final int index, final String message, final boolean active, final int iconIndex) { + final Command command = mConfiguration.getCommands()[index]; + + command.setCommand(message); + command.setActive(active); + command.setIconIndex(iconIndex); + mConfigurationListener.onConfigurationModified(); + saveConfiguration(); + } + + @Override + public void onNewConfiguration(final String name, final boolean duplicate) { + final boolean exists = mDatabaseHelper.configurationExists(name); + if (exists) { + Toast.makeText(this, R.string.uart_configuration_name_already_taken, Toast.LENGTH_LONG).show(); + return; + } + + UartConfiguration configuration = mConfiguration; + if (!duplicate) + configuration = new UartConfiguration(); + configuration.setName(name); + + try { + final Format format = new Format(new HyphenStyle()); + final Strategy strategy = new VisitorStrategy(new CommentVisitor()); + final Serializer serializer = new Persister(strategy, format); + final StringWriter writer = new StringWriter(); + serializer.write(configuration, writer); + final String xml = writer.toString(); + + final long id = mDatabaseHelper.addConfiguration(name, xml); + refreshConfigurations(); + selectConfiguration(mConfigurationsAdapter.getItemPosition(id)); + } catch (final Exception e) { + Log.e(TAG, "Error while creating a new configuration", e); + } + } + + @Override + public void onRenameConfiguration(final String newName) { + final boolean exists = mDatabaseHelper.configurationExists(newName); + if (exists) { + Toast.makeText(this, R.string.uart_configuration_name_already_taken, Toast.LENGTH_LONG).show(); + return; + } + + final String oldName = mConfiguration.getName(); + mConfiguration.setName(newName); + + try { + final Format format = new Format(new HyphenStyle()); + final Strategy strategy = new VisitorStrategy(new CommentVisitor()); + final Serializer serializer = new Persister(strategy, format); + final StringWriter writer = new StringWriter(); + serializer.write(mConfiguration, writer); + final String xml = writer.toString(); + + mDatabaseHelper.renameConfiguration(oldName, newName, xml); + refreshConfigurations(); + } catch (final Exception e) { + Log.e(TAG, "Error while renaming configuration", e); + } + } + + private void refreshConfigurations() { + mConfigurationsAdapter.swapCursor(mDatabaseHelper.getServerConfigurationsNames()); + mConfigurationsAdapter.notifyDataSetChanged(); + invalidateOptionsMenu(); + } + + private void selectConfiguration(final int position) { + mConfigurationSpinner.setSelection(position); + } + /** * Updates the ActionBar background color depending on whether we are in edit mode or not. * @@ -195,6 +575,7 @@ public class UARTActivity extends BleProfileServiceReadyActivity node) throws Exception { + // do nothing + } + + @Override + public void write(final Type type, final NodeMap node) throws Exception { + if (type.getType().equals(Command[].class)) { + OutputNode element = node.getNode(); + + StringBuilder builder = new StringBuilder("A configuration must have 9 commands, one for each button.\n Possible icons are:"); + for (Command.Icon icon : Command.Icon.values()) + builder.append("\n - ").append(icon.toString()); + element.setComment(builder.toString()); + } + } + } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java index ad314ce6..7a25d8a1 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java @@ -22,9 +22,6 @@ package no.nordicsemi.android.nrftoolbox.uart; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,21 +29,15 @@ import android.widget.BaseAdapter; import android.widget.ImageView; import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.uart.domain.Command; +import no.nordicsemi.android.nrftoolbox.uart.domain.UartConfiguration; public class UARTButtonAdapter extends BaseAdapter { - public final static String PREFS_BUTTON_ENABLED = "prefs_uart_enabled_"; - public final static String PREFS_BUTTON_COMMAND = "prefs_uart_command_"; - public final static String PREFS_BUTTON_ICON = "prefs_uart_icon_"; - - private final SharedPreferences mPreferences; - private final int[] mIcons; - private final boolean[] mEnableFlags; + private UartConfiguration mConfiguration; private boolean mEditMode; - public UARTButtonAdapter(final Context context) { - mPreferences = PreferenceManager.getDefaultSharedPreferences(context); - mIcons = new int[9]; - mEnableFlags = new boolean[9]; + public UARTButtonAdapter(final UartConfiguration configuration) { + mConfiguration = configuration; } public void setEditMode(final boolean editMode) { @@ -54,24 +45,19 @@ public class UARTButtonAdapter extends BaseAdapter { notifyDataSetChanged(); } - @Override - public void notifyDataSetChanged() { - final SharedPreferences preferences = mPreferences; - for (int i = 0; i < mIcons.length; ++i) { - mIcons[i] = preferences.getInt(PREFS_BUTTON_ICON + i, -1); - mEnableFlags[i] = preferences.getBoolean(PREFS_BUTTON_ENABLED + i, false); - } - super.notifyDataSetChanged(); + public void setConfiguration(final UartConfiguration configuration) { + mConfiguration = configuration; + notifyDataSetChanged(); } @Override public int getCount() { - return mIcons.length; + return mConfiguration != null ? mConfiguration.getCommands().length : 0; } @Override public Object getItem(final int position) { - return mIcons[position]; + return mConfiguration.getCommands()[position]; } @Override @@ -91,7 +77,8 @@ public class UARTButtonAdapter extends BaseAdapter { @Override public boolean isEnabled(int position) { - return mEditMode || mEnableFlags[position]; + final Command command = (Command) getItem(position); + return mEditMode || (command != null && command.isActive()); } @Override @@ -105,9 +92,11 @@ public class UARTButtonAdapter extends BaseAdapter { view.setActivated(mEditMode); // Update image + final Command command = (Command) getItem(position); final ImageView image = (ImageView) view; - final int icon = mIcons[position]; - if (mEnableFlags[position] && icon != -1) { + final boolean active = command != null && command.isActive(); + if (active) { + final int icon = command.getIconIndex(); image.setImageResource(R.drawable.uart_button); image.setImageLevel(icon); } else diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java new file mode 100644 index 00000000..7ec704c8 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java @@ -0,0 +1,138 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ + +package no.nordicsemi.android.nrftoolbox.uart; + +import android.content.Context; +import android.database.Cursor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import no.nordicsemi.android.nrftoolbox.R; + +public class UARTConfigurationsAdapter extends CursorAdapter { + final Context mContext; + final ActionListener mListener; + + public interface ActionListener { + public void onNewConfigurationClick(); + public void onImportClick(); + } + + public UARTConfigurationsAdapter(final Context context, final ActionListener listener, final Cursor c) { + super(context, c, 0); + mContext = context; + mListener = listener; + } + + @Override + public int getCount() { + return super.getCount() + 1; // One for buttons at the top + } + + @Override + public boolean isEmpty() { + return super.getCount() == 0; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(final int position) { + if (position > 0) + return super.getItemId(position - 1); + return 0; + } + + public int getItemPosition(final long id) { + final Cursor cursor = getCursor(); + if (cursor == null) + return 1; + + if (cursor.moveToFirst()) + do { + if (cursor.getLong(0 /* _ID */) == id) + return cursor.getPosition() + 1; + } while (cursor.moveToNext()); + return 1; // should never happen + } + + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + if (position == 0) { + // This empty view should never be visible. Only positions 1+ are valid. Position 0 is reserved for action buttons. + // It is only created temporally when activity is created. + return LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_spinner_item, parent, false); + } + return super.getView(position - 1, convertView, parent); + } + + @Override + public View getDropDownView(final int position, final View convertView, final ViewGroup parent) { + if (position == 0) { + return newToolbarView(mContext, parent); + } + if (convertView instanceof ViewGroup) + return super.getDropDownView(position - 1, null, parent); + return super.getDropDownView(position - 1, convertView, parent); + } + + @Override + public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { + return LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_spinner_item, parent, false); + } + + @Override + public View newDropDownView(final Context context, final Cursor cursor, final ViewGroup parent) { + return LayoutInflater.from(mContext).inflate(R.layout.feature_uart_dropdown_item, parent, false); + } + + public View newToolbarView(final Context context, final ViewGroup parent) { + final View view = LayoutInflater.from(context).inflate(R.layout.feature_uart_dropdown_title, parent, false); + view.findViewById(R.id.action_add).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + mListener.onNewConfigurationClick(); + } + }); + view.findViewById(R.id.action_import).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + mListener.onImportClick(); + } + }); + return view; + } + + @Override + public void bindView(final View view, final Context context, final Cursor cursor) { + final String name = cursor.getString(1 /* NAME */); + ((TextView) view).setText(name); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java index ae89a990..1435a4fb 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java @@ -23,6 +23,7 @@ package no.nordicsemi.android.nrftoolbox.uart; import android.app.Activity; +import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; @@ -38,48 +39,43 @@ import android.widget.AdapterView; import android.widget.GridView; import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.uart.domain.Command; +import no.nordicsemi.android.nrftoolbox.uart.domain.UartConfiguration; -public class UARTControlFragment extends Fragment implements GridView.OnItemClickListener { +public class UARTControlFragment extends Fragment implements GridView.OnItemClickListener, UARTActivity.ConfigurationListener { private final static String TAG = "UARTControlFragment"; private final static String SIS_EDIT_MODE = "sis_edit_mode"; - private ControlFragmentListener mListener; - private SharedPreferences mPreferences; + private UartConfiguration mConfiguration; private UARTButtonAdapter mAdapter; private boolean mEditMode; - public static interface ControlFragmentListener { - public void setEditMode(final boolean editMode); - } - @Override - public void onAttach(final Activity activity) { - super.onAttach(activity); + public void onAttach(final Context context) { + super.onAttach(context); try { - mListener = (ControlFragmentListener) activity; + ((UARTActivity)context).setConfigurationListener(this); } catch (final ClassCastException e) { Log.e(TAG, "The parent activity must implement EditModeListener"); } } - @Override - public void onDetach() { - super.onDetach(); - mListener = null; - } - @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - if (savedInstanceState != null) { mEditMode = savedInstanceState.getBoolean(SIS_EDIT_MODE); } } + @Override + public void onDestroy() { + super.onDestroy(); + ((UARTActivity)getActivity()).setConfigurationListener(null); + } + @Override public void onSaveInstanceState(final Bundle outState) { outState.putBoolean(SIS_EDIT_MODE, mEditMode); @@ -90,49 +86,42 @@ public class UARTControlFragment extends Fragment implements GridView.OnItemClic final View view = inflater.inflate(R.layout.fragment_feature_uart_control, container, false); final GridView grid = (GridView) view.findViewById(R.id.grid); - grid.setAdapter(mAdapter = new UARTButtonAdapter(getActivity())); + grid.setAdapter(mAdapter = new UARTButtonAdapter(mConfiguration)); grid.setOnItemClickListener(this); mAdapter.setEditMode(mEditMode); - setHasOptionsMenu(true); return view; } @Override public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { if (mEditMode) { - final UARTEditDialog dialog = UARTEditDialog.getInstance(position); + Command command = mConfiguration.getCommands()[position]; + if (command == null) + mConfiguration.getCommands()[position] = command = new Command(); + final UARTEditDialog dialog = UARTEditDialog.getInstance(position, command); dialog.show(getChildFragmentManager(), null); } else { + final String command = ((Command)mAdapter.getItem(position)).getCommand(); final UARTInterface uart = (UARTInterface) getActivity(); - uart.send(mPreferences.getString(UARTButtonAdapter.PREFS_BUTTON_COMMAND + position, "")); + uart.send(command); } } @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - inflater.inflate(mEditMode ? R.menu.uart_menu_config : R.menu.uart_menu, menu); + public void onConfigurationModified() { + mAdapter.notifyDataSetChanged(); } @Override - public boolean onOptionsItemSelected(final MenuItem item) { - final int itemId = item.getItemId(); - switch (itemId) { - case R.id.action_configure: - setEditMode(!mEditMode); - return true; - } - return false; + public void onConfigurationChanged(final UartConfiguration configuration) { + mConfiguration = configuration; + mAdapter.setConfiguration(configuration); } + @Override public void setEditMode(final boolean editMode) { mEditMode = editMode; mAdapter.setEditMode(mEditMode); - getActivity().invalidateOptionsMenu(); - mListener.setEditMode(mEditMode); - } - - public void onConfigurationChanged() { - mAdapter.notifyDataSetChanged(); } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java index 1ecb1629..4b04a5fe 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java @@ -43,21 +43,25 @@ import android.widget.GridView; import android.widget.ImageView; import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.uart.domain.Command; public class UARTEditDialog extends DialogFragment implements View.OnClickListener, GridView.OnItemClickListener { - private final static String TAG = "UARTEditDialog"; private final static String ARG_INDEX = "index"; + private final static String ARG_COMMAND = "command"; + private final static String ARG_ICON_INDEX = "iconIndex"; private int mActiveIcon; private EditText mField; private CheckBox mCheckBox; private IconAdapter mIconAdapter; - public static UARTEditDialog getInstance(final int index) { + public static UARTEditDialog getInstance(final int index, final Command command) { final UARTEditDialog fragment = new UARTEditDialog(); final Bundle args = new Bundle(); args.putInt(ARG_INDEX, index); + args.putString(ARG_COMMAND, command.getCommand()); + args.putInt(ARG_ICON_INDEX, command.getIconIndex()); fragment.setArguments(args); return fragment; @@ -66,15 +70,15 @@ public class UARTEditDialog extends DialogFragment implements View.OnClickListen @NonNull @Override public Dialog onCreateDialog(final Bundle savedInstanceState) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); final LayoutInflater inflater = LayoutInflater.from(getActivity()); // Read button configuration final Bundle args = getArguments(); final int index = args.getInt(ARG_INDEX); - final String command = preferences.getString(UARTButtonAdapter.PREFS_BUTTON_COMMAND + index, null); - final boolean active = true;//preferences.getBoolean(UARTButtonAdapter.PREFS_BUTTON_ENABLED + index, false); - mActiveIcon = preferences.getInt(UARTButtonAdapter.PREFS_BUTTON_ICON + index, 0); + final String command = args.getString(ARG_COMMAND); + final int iconIndex = args.getInt(ARG_ICON_INDEX); + final boolean active = true; // change to active by default + mActiveIcon = iconIndex; // Create view final View view = inflater.inflate(R.layout.feature_uart_dialog_edit, null); @@ -120,16 +124,9 @@ public class UARTEditDialog extends DialogFragment implements View.OnClickListen final Bundle args = getArguments(); final int index = args.getInt(ARG_INDEX); - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - final SharedPreferences.Editor editor = preferences.edit(); - editor.putString(UARTButtonAdapter.PREFS_BUTTON_COMMAND + index, command); - editor.putBoolean(UARTButtonAdapter.PREFS_BUTTON_ENABLED + index, active); - editor.putInt(UARTButtonAdapter.PREFS_BUTTON_ICON + index, mActiveIcon); - editor.apply(); - dismiss(); - final UARTControlFragment parent = (UARTControlFragment) getParentFragment(); - parent.onConfigurationChanged(); + final UARTActivity parent = (UARTActivity) getActivity(); + parent.onCommandChanged(index, command, active, mActiveIcon); } @Override @@ -167,6 +164,5 @@ public class UARTEditDialog extends DialogFragment implements View.OnClickListen image.setActivated(position == mActiveIcon && mCheckBox.isChecked()); return view; } - } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java index 1f72f65c..d0f6a43c 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java @@ -139,6 +139,10 @@ public class UARTManager extends BleManager { * @param text the text to be sent */ public void send(final String text) { + // Are we connected? + if (mRXCharacteristic == null) + return; + // An outgoing buffer may not be null if there is already another packet being sent. We do nothing in this case. if (!TextUtils.isEmpty(text) && mOutgoingBuffer == null) { final byte[] buffer = mOutgoingBuffer = text.getBytes(); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTNewConfigurationDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTNewConfigurationDialogFragment.java new file mode 100644 index 00000000..1c01e9a2 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTNewConfigurationDialogFragment.java @@ -0,0 +1,142 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ + +package no.nordicsemi.android.nrftoolbox.uart; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; + +import no.nordicsemi.android.nrftoolbox.R; + +public class UARTNewConfigurationDialogFragment extends DialogFragment implements View.OnClickListener { + private static final String NAME = "name"; + private static final String DUPLICATE = "duplicate"; + + private EditText mEditText; + + private NewConfigurationDialogListener mListener; + + public interface NewConfigurationDialogListener { + /** + * Creates a new configuration with given name. + * @param name the name + * @param duplicate true if configuration is to be duplicated + */ + public void onNewConfiguration(final String name, final boolean duplicate); + + /** + * Renames the current configuration with given name. + * @param newName the new name + */ + public void onRenameConfiguration(final String newName); + } + + @Override + public void onAttach(final Activity activity) { + super.onAttach(activity); + + if (activity instanceof NewConfigurationDialogListener) { + mListener = (NewConfigurationDialogListener) activity; + } else { + throw new IllegalArgumentException("The parent activity must implement NewConfigurationDialogListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + public static DialogFragment getInstance(final String name, final boolean duplicate) { + final DialogFragment dialog = new UARTNewConfigurationDialogFragment(); + + final Bundle args = new Bundle(); + args.putString(NAME, name); + args.putBoolean(DUPLICATE, duplicate); + dialog.setArguments(args); + + return dialog; + } + + @Override + @NonNull + public Dialog onCreateDialog(final Bundle savedInstanceState) { + final Context context = getActivity(); + + final Bundle args = getArguments(); + final String oldName = args.getString(NAME); + final boolean duplicate = args.getBoolean(DUPLICATE); + final int titleResId = duplicate || oldName == null ? R.string.uart_new_configuration_title : R.string.uart_rename_configuration_title; + + final LayoutInflater inflater = LayoutInflater.from(getActivity()); + final View view = inflater.inflate(R.layout.feature_uart_dialog_new_configuration, null); + final EditText editText = mEditText = (EditText) view.findViewById(R.id.name); + editText.setText(args.getString(NAME)); + final View actionClear = view.findViewById(R.id.action_clear); + actionClear.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + editText.setText(null); + } + }); + + final AlertDialog dialog = new AlertDialog.Builder(context).setTitle(titleResId).setView(view).setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok, null).setCancelable(false).show(); // this must be show() or the getButton() below will return null. + + final Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + okButton.setOnClickListener(this); + + return dialog; + } + + @Override + public void onClick(final View v) { + final String newName = mEditText.getText().toString().trim(); + if (TextUtils.isEmpty(newName)) { + mEditText.setError(getString(R.string.uart_empty_name_error)); + return; + } + + final String oldName = getArguments().getString(NAME); + final boolean duplicate = getArguments().getBoolean(DUPLICATE); + + if (duplicate || TextUtils.isEmpty(oldName)) + mListener.onNewConfiguration(newName, duplicate); + else { + mListener.onRenameConfiguration(newName); + } + dismiss(); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java index eb547808..be14311d 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java @@ -30,6 +30,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.NotificationCompat; import no.nordicsemi.android.log.ILogSession; import no.nordicsemi.android.log.Logger; @@ -158,11 +159,12 @@ public class UARTService extends BleProfileService implements UARTManagerCallbac // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); - final Notification.Builder builder = new Notification.Builder(this).setContentIntent(pendingIntent); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentIntent(pendingIntent); builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); builder.setSmallIcon(R.drawable.ic_stat_notify_uart); builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(R.drawable.ic_action_bluetooth, getString(R.string.uart_notification_action_disconnect), disconnectAction); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.uart_notification_action_disconnect), disconnectAction)); final Notification notification = builder.build(); final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/ConfigurationContract.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/ConfigurationContract.java new file mode 100644 index 00000000..d7898019 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/ConfigurationContract.java @@ -0,0 +1,38 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ +package no.nordicsemi.android.nrftoolbox.uart.database; + +import android.provider.BaseColumns; + +public class ConfigurationContract { + + protected interface ConfigurationColumns { + /** The XML with configuration. */ + public final static String XML = "xml"; + } + + public final class Configuration implements BaseColumns, NameColumns, ConfigurationColumns, UndoColumns { + private Configuration() { + // empty + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/DatabaseHelper.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/DatabaseHelper.java new file mode 100644 index 00000000..b0381ee6 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/DatabaseHelper.java @@ -0,0 +1,226 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ +package no.nordicsemi.android.nrftoolbox.uart.database; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; + +public class DatabaseHelper { + /** Database file name */ + private static final String DATABASE_NAME = "toolbox_uart.db"; + /** Database version */ + private static final int DATABASE_VERSION = 1; + + private interface Tables { + /** Configurations table. See {@link ConfigurationContract.Configuration} for column names. */ + public static final String CONFIGURATIONS = "configurations"; + } + + private static final String[] ID_PROJECTION = new String[] { BaseColumns._ID }; + private static final String[] NAME_PROJECTION = new String[] { BaseColumns._ID, NameColumns.NAME }; + private static final String[] XML_PROJECTION = new String[] { BaseColumns._ID, ConfigurationContract.Configuration.XML }; + + private static final String ID_SELECTION = BaseColumns._ID + "=?"; + private static final String NAME_SELECTION = NameColumns.NAME + "=?"; + private static final String DELETED_SELECTION = UndoColumns.DELETED + "=1"; + private static final String NOT_DELETED_SELECTION = UndoColumns.DELETED + "=0"; + + private static SQLiteHelper mDatabaseHelper; + private static SQLiteDatabase mDatabase; + private final ContentValues mValues = new ContentValues(); + private final String[] mSingleArg = new String[1]; + + public DatabaseHelper(final Context context) { + if (mDatabaseHelper == null) { + mDatabaseHelper = new SQLiteHelper(context); + mDatabase = mDatabaseHelper.getWritableDatabase(); + } + } + + /** + * Returns number of saved configurations. + */ + public int getConfigurationsCount() { + final Cursor cursor = mDatabase.query(Tables.CONFIGURATIONS, ID_PROJECTION, NOT_DELETED_SELECTION, null, null, null, null); + try { + return cursor.getCount(); + } finally { + cursor.close(); + } + } + + /** + * Returns the list of names of all saved configurations. + * @return cursor + */ + public Cursor getServerConfigurationsNames() { + return mDatabase.query(Tables.CONFIGURATIONS, NAME_PROJECTION, NOT_DELETED_SELECTION, null, null, null, ConfigurationContract.Configuration.NAME + " ASC"); + } + + /** + * Returns the XML wth the configuration by id. + * @param id the configuration id in the DB + * @return the XML with configuration or null + */ + public String getConfiguration(final long id) { + mSingleArg[0] = String.valueOf(id); + + final Cursor cursor = mDatabase.query(Tables.CONFIGURATIONS, XML_PROJECTION, ID_SELECTION, mSingleArg, null, null, null); + try { + if (cursor.moveToNext()) + return cursor.getString(1 /* XML */); + return null; + } finally { + cursor.close(); + } + } + + /** + * Adds new configuration to the database. + * @param name the configuration name + * @param configuration the XML + * @return the id or -1 if error occurred + */ + public long addConfiguration(final String name, final String configuration) { + final ContentValues values = mValues; + values.clear(); + values.put(ConfigurationContract.Configuration.NAME, name); + values.put(ConfigurationContract.Configuration.XML, configuration); + values.put(ConfigurationContract.Configuration.DELETED, 0); + return mDatabase.replace(Tables.CONFIGURATIONS, null, values); + } + + /** + * Updates the configuration with the given name with the new XML + * @param name the configuration name to be updated + * @param configuration the new XML with configuration + * @return number of rows updated + */ + public int updateConfiguration(final String name, final String configuration) { + mSingleArg[0] = name; + + final ContentValues values = mValues; + values.clear(); + values.put(ConfigurationContract.Configuration.XML, configuration); + values.put(ConfigurationContract.Configuration.DELETED, 0); + return mDatabase.update(Tables.CONFIGURATIONS, values, NAME_SELECTION, mSingleArg); + } + + /** + * Marks the configuration with given name as deleted. If may be restored or removed permanently afterwards. + * @param name the configuration name + * @return number of rows affected + */ + public int deleteConfiguration(final String name) { + mSingleArg[0] = name; + + final ContentValues values = mValues; + values.clear(); + values.put(ConfigurationContract.Configuration.DELETED, 1); + return mDatabase.update(Tables.CONFIGURATIONS, values, NAME_SELECTION, mSingleArg); + } + + public int removeDeletedServerConfigurations() { + return mDatabase.delete(Tables.CONFIGURATIONS, DELETED_SELECTION, null); + } + + public int restoreDeletedServerConfigurations() { + final ContentValues values = mValues; + values.clear(); + values.put(ConfigurationContract.Configuration.DELETED, 0); + return mDatabase.update(Tables.CONFIGURATIONS, values, null, null); + } + + /** + * Renames the server configuration and replaces its XML (name inside has changed). + * @param oldName the old name to look for + * @param newName the new configuration name + * @param configuration the new XML + * @return number of rows affected + */ + public int renameConfiguration(final String oldName, final String newName, final String configuration) { + mSingleArg[0] = oldName; + + final ContentValues values = mValues; + values.clear(); + values.put(ConfigurationContract.Configuration.NAME, newName); + values.put(ConfigurationContract.Configuration.XML, configuration); + return mDatabase.update(Tables.CONFIGURATIONS, values, NAME_SELECTION, mSingleArg); + } + + /** + * Returns true if a configuration with given name was found in the database. + * @param name the name to check + * @return true if such name exists, false otherwise + */ + public boolean configurationExists(final String name) { + mSingleArg[0] = name; + + final Cursor cursor = mDatabase.query(Tables.CONFIGURATIONS, NAME_PROJECTION, NAME_SELECTION + " AND " + NOT_DELETED_SELECTION, mSingleArg, null, null, null); + try { + return cursor.getCount() > 0; + } finally { + cursor.close(); + } + } + + private class SQLiteHelper extends SQLiteOpenHelper { + + /** + * The SQL code that creates the Server Configurations: + * + *
+		 * ----------------------------------------------------------------------------
+		 *                            CONFIGURATIONS                           |
+		 * ----------------------------------------------------------------------------
+		 * | _id (int, pk, auto increment) | name (text) | xml (text) | deleted (int) |
+		 * ----------------------------------------------------------------------------
+		 * 
+ */ + private static final String CREATE_CONFIGURATIONS = "CREATE TABLE " + Tables.CONFIGURATIONS+ "(" + ConfigurationContract.Configuration._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ConfigurationContract.Configuration.NAME + " TEXT UNIQUE NOT NULL, " + ConfigurationContract.Configuration.XML + " TEXT NOT NULL, " + ConfigurationContract.Configuration.DELETED +" INTEGER NOT NULL DEFAULT(0))"; + + private static final String DROP_IF_EXISTS = "DROP TABLE IF EXISTS "; + + public SQLiteHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(final SQLiteDatabase db) { + db.execSQL(CREATE_CONFIGURATIONS); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + // This method does nothing for now. + switch (oldVersion) { + case 1: + // do nothing + } + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/NameColumns.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/NameColumns.java new file mode 100644 index 00000000..a74678ec --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/NameColumns.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.uart.database; + +public interface NameColumns { + /** The name */ + public final static String NAME = "name"; +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/UndoColumns.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/UndoColumns.java new file mode 100644 index 00000000..eafacb32 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/UndoColumns.java @@ -0,0 +1,27 @@ +/************************************************************************************************************************************************* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ************************************************************************************************************************************************/ +package no.nordicsemi.android.nrftoolbox.uart.database; + +public interface UndoColumns { + /** The 'deleted' flag */ + public final static String DELETED = "deleted"; +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java new file mode 100644 index 00000000..6b4c13e1 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.uart.domain; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.Text; + +@Root +public class Command { + public enum Icon { + LEFT(0), + UP(1), + RIGHT(2), + DOWN(3), + SETTINGS(4), + REW(5), + PLAY(6), + PAUSE(7), + STOP(8), + FWD(9), + INFO(10), + NUMBER_1(11), + NUMBER_2(12), + NUMBER_3(13), + NUMBER_4(14), + NUMBER_5(15), + NUMBER_6(16), + NUMBER_7(17), + NUMBER_8(18), + NUMBER_9(19); + + public int index; + + private Icon(final int index) { + this.index = index; + } + } + + @Text(required = false) + private String command; + + @Attribute(required = false) + private boolean active = false; + + @Attribute(required = false) + private Icon icon = Icon.LEFT; + + /** + * Sets the command. + * @param command the command that will be sent to UART device + */ + public void setCommand(final String command) { + this.command = command; + } + + /** + * Sets whether the command is active. + * @param active true to make it active + */ + public void setActive(boolean active) { + this.active = active; + } + + /** + * Sets the icon index. + * @param index index of the icon. + */ + public void setIconIndex(final int index) { + this.icon = Icon.values()[index]; + } + + /** + * Returns the command that will be sent to UART device. + * @return the command + */ + public String getCommand() { + return command; + } + + /** + * Returns whether the icon is active. + * @return true if it's active + */ + public boolean isActive() { + return active; + } + + /** + * Returns the icon index. + * @return the icon index + */ + public int getIconIndex() { + return icon.index; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java new file mode 100644 index 00000000..981f48ce --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.uart.domain; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.ElementArray; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.core.PersistenceException; +import org.simpleframework.xml.core.Validate; + +@Root +public class UartConfiguration { + public static final int COMMANDS_COUNT = 9; + + @Attribute(required = false, empty = "Unnamed") + private String name; + + @ElementArray + private Command[] commands = new Command[COMMANDS_COUNT]; + + /** + * Returns the field name + * + * @return optional name + */ + public String getName() { + return name; + } + + /** + * Sets the name to specified value + * @param name the new name + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Returns the array of commands. There is always 9 of them. + * @return the commands array + */ + public Command[] getCommands() { + return commands; + } + + @Validate + private void validate() throws PersistenceException{ + if (commands == null || commands.length != COMMANDS_COUNT) + throw new PersistenceException("There must be always " + COMMANDS_COUNT + " commands in a configuration."); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/FileHelper.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/FileHelper.java new file mode 100644 index 00000000..ed1f9c20 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/FileHelper.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.utility; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import no.nordicsemi.android.nrftoolbox.R; + +public class FileHelper { + private static final String TAG = "FileHelper"; + + private static final String PREFS_SAMPLES_VERSION = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_SAMPLES_VERSION"; + private static final int CURRENT_SAMPLES_VERSION = 4; + + public static final String NORDIC_FOLDER = "Nordic Semiconductor"; + public static final String UART_FOLDER = "UART Configurations"; + public static final String BOARD_FOLDER = "Board"; + public static final String BOARD_NRF6310_FOLDER = "nrf6310"; + public static final String BOARD_PCA10028_FOLDER = "pca10028"; + public static final String BOARD_PCA10036_FOLDER = "pca10036"; + + public static boolean newSamplesAvailable(final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + final int version = preferences.getInt(PREFS_SAMPLES_VERSION, 0); + return version < CURRENT_SAMPLES_VERSION; + } + + public static void createSamples(final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + final int version = preferences.getInt(PREFS_SAMPLES_VERSION, 0); + if (version == CURRENT_SAMPLES_VERSION) + return; + + /* + * Copy example HEX files to the external storage. Files will be copied if the DFU Applications folder is missing + */ + final File root = new File(Environment.getExternalStorageDirectory(), "Nordic Semiconductor"); + if (!root.exists()) { + root.mkdir(); + } + final File board = new File(root, "Board"); + if (!board.exists()) { + board.mkdir(); + } + final File nrf6310 = new File(board, "nrf6310"); + if (!nrf6310.exists()) { + nrf6310.mkdir(); + } + final File pca10028 = new File(board, "pca10028"); + if (!pca10028.exists()) { + pca10028.mkdir(); + } + + // Remove old files. Those will be moved to a new folder structure + new File(root, "ble_app_hrs_s110_v6_0_0.hex").delete(); + new File(root, "ble_app_rscs_s110_v6_0_0.hex").delete(); + new File(root, "ble_app_hrs_s110_v7_0_0.hex").delete(); + new File(root, "ble_app_rscs_s110_v7_0_0.hex").delete(); + new File(root, "blinky_arm_s110_v7_0_0.hex").delete(); + new File(root, "dfu_2_0.bat").delete(); // This file has been migrated to 3.0 + new File(root, "dfu_3_0.bat").delete(); // This file has been migrated to 3.1 + new File(root, "dfu_2_0.sh").delete(); // This file has been migrated to 3.0 + new File(root, "dfu_3_0.sh").delete(); // This file has been migrated to 3.1 + new File(root, "README.txt").delete(); // This file has been modified to match v.3.0+ + + boolean oldCopied = false; + boolean newCopied = false; + + // nrf6310 files + File f = new File(nrf6310, "ble_app_hrs_s110_v6_0_0.hex"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_hrs_s110_v6_0_0, f); + oldCopied = true; + } + f = new File(nrf6310, "ble_app_rscs_s110_v6_0_0.hex"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_rscs_s110_v6_0_0, f); + oldCopied = true; + } + f = new File(nrf6310, "ble_app_hrs_s110_v7_0_0.hex"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_hrs_s110_v7_0_0, f); + oldCopied = true; + } + f = new File(nrf6310, "ble_app_rscs_s110_v7_0_0.hex"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_rscs_s110_v7_0_0, f); + oldCopied = true; + } + f = new File(nrf6310, "blinky_arm_s110_v7_0_0.hex"); + if (!f.exists()) { + copyRawResource(context, R.raw.blinky_arm_s110_v7_0_0, f); + oldCopied = true; + } + // PCA10028 files + f = new File(pca10028, "blinky_s110_v7_1_0.hex"); + if (!f.exists()) { + copyRawResource(context, R.raw.blinky_s110_v7_1_0, f); + oldCopied = true; + } + f = new File(pca10028, "blinky_s110_v7_1_0_ext_init.dat"); + if (!f.exists()) { + copyRawResource(context, R.raw.blinky_s110_v7_1_0_ext_init, f); + oldCopied = true; + } + f = new File(pca10028, "ble_app_hrs_dfu_s110_v7_1_0.hex"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_hrs_dfu_s110_v7_1_0, f); + oldCopied = true; + } + f = new File(pca10028, "ble_app_hrs_dfu_s110_v7_1_0_ext_init.dat"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_hrs_dfu_s110_v7_1_0_ext_init, f); + oldCopied = true; + } + new File(root, "ble_app_hrs_dfu_s110_v8_0_0.zip").delete(); // name changed + f = new File(pca10028, "ble_app_hrs_dfu_s110_v8_0_0_sdk_v8_0.zip"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_hrs_dfu_s110_v8_0_0_sdk_v8_0, f); + newCopied = true; + } + f = new File(pca10028, "ble_app_hrs_dfu_s110_v8_0_0_sdk_v9_0.zip"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_hrs_dfu_s110_v8_0_0_sdk_v9_0, f); + newCopied = true; + } + f = new File(pca10028, "ble_app_hrs_dfu_all_in_one_sdk_v9_0.zip"); + if (!f.exists()) { + copyRawResource(context, R.raw.ble_app_hrs_dfu_all_in_one_sdk_v9_0, f); + newCopied = true; + } + + if (oldCopied) + Toast.makeText(context, R.string.dfu_example_files_created, Toast.LENGTH_SHORT).show(); + else if (newCopied) + Toast.makeText(context, R.string.dfu_example_new_files_created, Toast.LENGTH_SHORT).show(); + + // Scripts + newCopied = false; + f = new File(root, "dfu_3_1.bat"); + if (!f.exists()) { + copyRawResource(context, R.raw.dfu_win_3_1, f); + newCopied = true; + } + f = new File(root, "dfu_3_1.sh"); + if (!f.exists()) { + copyRawResource(context, R.raw.dfu_mac_3_1, f); + newCopied = true; + } + f = new File(root, "README.txt"); + if (!f.exists()) { + copyRawResource(context, R.raw.readme, f); + } + if (newCopied) + Toast.makeText(context, R.string.dfu_scripts_created, Toast.LENGTH_SHORT).show(); + + // Save the current version + preferences.edit().putInt(PREFS_SAMPLES_VERSION, CURRENT_SAMPLES_VERSION).apply(); + } + + /** + * Copies the file from res/raw with given id to given destination file. If dest does not exist it will be created. + * + * @param context activity context + * @param rawResId the resource id + * @param dest destination file + */ + private static void copyRawResource(final Context context, final int rawResId, final File dest) { + try { + final InputStream is = context.getResources().openRawResource(rawResId); + final FileOutputStream fos = new FileOutputStream(dest); + + final byte[] buf = new byte[1024]; + int read; + try { + while ((read = is.read(buf)) > 0) + fos.write(buf, 0, read); + } finally { + is.close(); + fos.close(); + } + } catch (final IOException e) { + DebugLogger.e(TAG, "Error while copying HEX file " + e.toString()); + } + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ClosableSpinner.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ClosableSpinner.java new file mode 100644 index 00000000..30a94d89 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ClosableSpinner.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Spinner; + +public class ClosableSpinner extends Spinner { + public ClosableSpinner(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void close() { + super.onDetachedFromWindow(); + } +} diff --git a/app/src/main/res/drawable-hdpi/item_background_light_n.9.png b/app/src/main/res/drawable-hdpi/item_background_light_n.9.png deleted file mode 100644 index 0c8a9451..00000000 Binary files a/app/src/main/res/drawable-hdpi/item_background_light_n.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/item_background_light_p.9.png b/app/src/main/res/drawable-hdpi/item_background_light_p.9.png deleted file mode 100644 index 74947f36..00000000 Binary files a/app/src/main/res/drawable-hdpi/item_background_light_p.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-v21/ic_icon_button_background.xml b/app/src/main/res/drawable-v21/ic_icon_button_background.xml new file mode 100644 index 00000000..6b1e883b --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_icon_button_background.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable-xhdpi/item_background_light_n.9.png b/app/src/main/res/drawable-xhdpi/item_background_light_n.9.png deleted file mode 100644 index 0c8a9451..00000000 Binary files a/app/src/main/res/drawable-xhdpi/item_background_light_n.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/item_background_light_p.9.png b/app/src/main/res/drawable-xhdpi/item_background_light_p.9.png deleted file mode 100644 index 74947f36..00000000 Binary files a/app/src/main/res/drawable-xhdpi/item_background_light_p.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add_normal.png b/app/src/main/res/drawable-xxhdpi/ic_action_add_normal.png new file mode 100644 index 00000000..9e16b1f7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_add_normal.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add_pressed.png b/app/src/main/res/drawable-xxhdpi/ic_action_add_pressed.png new file mode 100644 index 00000000..e4b6217c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_add_pressed.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_clear_normal.png b/app/src/main/res/drawable-xxhdpi/ic_action_clear_normal.png new file mode 100644 index 00000000..c03cb879 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_clear_normal.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_clear_pressed.png b/app/src/main/res/drawable-xxhdpi/ic_action_clear_pressed.png new file mode 100644 index 00000000..ed70b4e6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_clear_pressed.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_download_normal.png b/app/src/main/res/drawable-xxhdpi/ic_action_download_normal.png new file mode 100644 index 00000000..bde2379d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_download_normal.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_download_pressed.png b/app/src/main/res/drawable-xxhdpi/ic_action_download_pressed.png new file mode 100644 index 00000000..2d91bc3f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_download_pressed.png differ diff --git a/app/src/main/res/values-sw600dp-land-v21/dimens.xml b/app/src/main/res/drawable/ic_action_add.xml similarity index 73% rename from app/src/main/res/values-sw600dp-land-v21/dimens.xml rename to app/src/main/res/drawable/ic_action_add.xml index 5b0dcaf3..b5f0f258 100644 --- a/app/src/main/res/values-sw600dp-land-v21/dimens.xml +++ b/app/src/main/res/drawable/ic_action_add.xml @@ -1,4 +1,5 @@ - - + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> + - - 56dp - 0dp + + - + diff --git a/app/src/main/res/values-land-v21/dimens.xml b/app/src/main/res/drawable/ic_action_clear.xml similarity index 73% rename from app/src/main/res/values-land-v21/dimens.xml rename to app/src/main/res/drawable/ic_action_clear.xml index 5b9851b3..88047bc2 100644 --- a/app/src/main/res/values-land-v21/dimens.xml +++ b/app/src/main/res/drawable/ic_action_clear.xml @@ -1,5 +1,5 @@ - - - + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> + - - 6dp - 42dp - - + + + + diff --git a/app/src/main/res/drawable/ic_action_download.xml b/app/src/main/res/drawable/ic_action_download.xml new file mode 100644 index 00000000..e0953c2b --- /dev/null +++ b/app/src/main/res/drawable/ic_action_download.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/layout-land/activity_feature_bpm.xml b/app/src/main/res/layout-land/activity_feature_bpm.xml index f6cefee9..6d6e34bf 100644 --- a/app/src/main/res/layout-land/activity_feature_bpm.xml +++ b/app/src/main/res/layout-land/activity_feature_bpm.xml @@ -20,286 +20,287 @@ ~ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".BPMActivity"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - - + + - + - + - + - - - - + + + + - + - + - + - + - + - + - + - - - + + + - + - + - + - - - - + + + + -